App 2.43.4: the Halloween pumpkin's light follows the music
The bass from Android's audio analyser drives the glow and brightens the carving on the beat, only while the style is on screen. Yellow and green and the embers keep their meaning; without a signal, as with the radio, the light stays as it was. Levels only. Tried on the emulator. Not tried on the car. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
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 bass 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). 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, 20f, 250f) : 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user