Files
mg4/app/src/main/java/com/evsuite/abrp/MusicTap.java
T
nilsandClaude Opus 5.5 9d84dc6d15 App 2.44.0: the pumpkin light follows the kick drum, behind a switch
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
2026-11-02 13:15:25 +01:00

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.
}
}
}
}
}