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