MusicPulse reads 40-120 Hz and lights only on jumps of the bass, so held notes and bass lines stay dark. Settings gets "Pumpkin light follows the music" (Halloween only, off by default); off, the audio analyser never starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
92 lines
3.4 KiB
Java
92 lines
3.4 KiB
Java
package com.evsuite.abrp;
|
|
|
|
import android.Manifest;
|
|
import android.content.Context;
|
|
import android.content.pm.PackageManager;
|
|
import android.media.audiofx.Visualizer;
|
|
|
|
/**
|
|
* Listens to what the head unit plays for the Halloween pumpkin's light: Android's audio analyser
|
|
* on the output mix, its kick-drum band read 25 times a second on a thread of its own, made into a
|
|
* {@link MusicPulse}. Runs only while MainActivity says so (the style on screen, the app in
|
|
* front, and "Pumpkin light follows the music" on). Levels only: no sound is kept and no microphone used. Where the analyser hears nothing
|
|
* (the radio) or is not available, the pulse stays 0 and the pumpkin burns as usual.
|
|
*/
|
|
final class MusicTap {
|
|
|
|
private static final int READ_MS = 40, GIVE_UP_AFTER_FAILURES = 50;
|
|
|
|
private final Context app;
|
|
private volatile Thread worker;
|
|
private volatile float pulse;
|
|
|
|
MusicTap(Context context) {
|
|
app = context.getApplicationContext();
|
|
}
|
|
|
|
/** From 0 to 1; safe to read from any thread. */
|
|
float pulse() {
|
|
return pulse;
|
|
}
|
|
|
|
void setRunning(boolean run) {
|
|
if (run == (worker != null)) return;
|
|
if (!run) {
|
|
Thread stopping = worker;
|
|
worker = null;
|
|
pulse = 0f;
|
|
stopping.interrupt();
|
|
return;
|
|
}
|
|
if (app.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) return;
|
|
Thread thread = new Thread(this::listen, "music-tap");
|
|
thread.setDaemon(true);
|
|
worker = thread;
|
|
thread.start();
|
|
}
|
|
|
|
/** All analyser calls live here, off the main thread; it is always let go. */
|
|
private void listen() {
|
|
Thread me = Thread.currentThread();
|
|
Visualizer analyser = null;
|
|
try {
|
|
analyser = new Visualizer(0);
|
|
analyser.setCaptureSize(Visualizer.getCaptureSizeRange()[1]);
|
|
analyser.setScalingMode(Visualizer.SCALING_MODE_NORMALIZED);
|
|
if (analyser.setEnabled(true) != Visualizer.SUCCESS) return;
|
|
int rateHz = analyser.getSamplingRate() / 1000;
|
|
byte[] fft = new byte[analyser.getCaptureSize()];
|
|
MusicPulse music = new MusicPulse();
|
|
long last = System.nanoTime();
|
|
int failures = 0;
|
|
while (worker == me && failures < GIVE_UP_AFTER_FAILURES) {
|
|
Thread.sleep(READ_MS);
|
|
boolean read = analyser.getFft(fft) == Visualizer.SUCCESS;
|
|
failures = read ? 0 : failures + 1;
|
|
long now = System.nanoTime();
|
|
float value = music.update(read ? SoundLevels.band(fft, rateHz, MusicPulse.FROM_HZ, MusicPulse.TO_HZ) : null, (now - last) / 1e9f);
|
|
last = now;
|
|
if (worker == me) pulse = value;
|
|
}
|
|
} catch (InterruptedException e) {
|
|
// Told to stop.
|
|
} catch (RuntimeException e) {
|
|
// No analyser here: the pumpkin burns as usual.
|
|
} finally {
|
|
if (worker == me) pulse = 0f;
|
|
if (analyser != null) {
|
|
try {
|
|
analyser.setEnabled(false);
|
|
} catch (RuntimeException ignored) {
|
|
// Released next either way.
|
|
}
|
|
try {
|
|
analyser.release();
|
|
} catch (RuntimeException ignored) {
|
|
// Nothing more can be done from here.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|