package com.evsuite.abrp; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.media.AudioManager; import android.media.audiofx.Visualizer; import android.media.session.MediaController; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; /** * The sound check on the Logs tab: whether Android's audio analyser ({@link Visualizer} on the * output mix, session 0) can hear what the head unit plays, before the Halloween pumpkin's light * follows the music. Only one tap starts it, and it listens for {@value #SECONDS} seconds and lets * go. It records levels only, never the sound, and uses no microphone. The report goes where a * signal recording goes ({@link SignalRecorder#saveCopies}): the app, a USB stick, the server. */ final class SoundProbe { static final String FILE = "sound_probe.txt"; private static final int SECONDS = 10, READS_PER_SECOND = 10; private static volatile boolean running; private static long startedMs; private static List report = new ArrayList<>(); private static String result; private SoundProbe() {} static synchronized boolean running() { return running; } static synchronized void start(Context context) { if (running) return; Context app = context.getApplicationContext(); running = true; startedMs = System.currentTimeMillis(); result = null; report = new ArrayList<>(); new Thread(() -> probe(app), "sound-probe").start(); } private static void probe(Context app) { List out = new ArrayList<>(); out.add("Sound probe, " + SignalRecording.time(startedMs) + ", app " + BuildConfig.VERSION_NAME); String saved; try { permission(app, Manifest.permission.RECORD_AUDIO, out); permission(app, Manifest.permission.MODIFY_AUDIO_SETTINGS, out); boolean plays = sessions(app, out); plays |= musicActive(app, out); SoundLevels.Summary all = listen(out); plays |= musicActive(app, out); out.add(""); out.add(SoundLevels.words(all.verdict(plays))); } catch (Throwable t) { out.add("probe stopped: " + t.getClass().getSimpleName()); } finally { String name = "sound-probe-" + new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US) .format(new Date(startedMs)) + ".txt"; saved = SignalRecorder.saveCopies(app, FILE, name, out); synchronized (SoundProbe.class) { report = out; result = saved; running = false; } } } private static void permission(Context app, String name, List out) { boolean granted = app.checkSelfPermission(name) == PackageManager.PERMISSION_GRANTED; out.add(name.substring(name.lastIndexOf('.') + 1) + ": " + (granted ? "granted" : "not granted")); } private static boolean musicActive(Context app, List out) { try { AudioManager am = (AudioManager) app.getSystemService(Context.AUDIO_SERVICE); boolean active = am != null && am.isMusicActive(); out.add("music active: " + active); return active; } catch (RuntimeException e) { out.add("music active: unreadable (" + e.getClass().getSimpleName() + ")"); return false; } } /** Which source is playing, for reading the report later; no song. True when one is. */ private static boolean sessions(Context app, List out) { boolean playing = false; try { MediaSessionManager msm = (MediaSessionManager) app.getSystemService(Context.MEDIA_SESSION_SERVICE); List controllers = msm == null ? null : msm.getActiveSessions(null); if (controllers == null || controllers.isEmpty()) out.add("sessions: none"); else for (MediaController c : controllers) { PlaybackState s = c.getPlaybackState(); out.add("session: " + MediaProbeText.clean(c.getPackageName()) + ", " + (s == null ? "no state" : MediaProbeText.playbackState(s.getState()))); if (s != null && s.getState() == PlaybackState.STATE_PLAYING) playing = true; } } catch (Throwable t) { out.add("sessions: not readable (" + t.getClass().getSimpleName() + ")"); } return playing; } /** Listens for the whole time, one report line a second; always lets the analyser go. */ private static SoundLevels.Summary listen(List out) throws InterruptedException { SoundLevels.Summary all = new SoundLevels.Summary(); Visualizer analyser = null; try { analyser = new Visualizer(0); int[] sizes = Visualizer.getCaptureSizeRange(); int code = analyser.setCaptureSize(sizes[1]); analyser.setScalingMode(Visualizer.SCALING_MODE_NORMALIZED); analyser.setMeasurementMode(Visualizer.MEASUREMENT_MODE_PEAK_RMS); int enabled = analyser.setEnabled(true); int size = analyser.getCaptureSize(), rateHz = analyser.getSamplingRate() / 1000; out.add("analyser: capture " + size + " at " + rateHz + " Hz, set size " + code + ", enable " + enabled); if (enabled != Visualizer.SUCCESS) return all; byte[] wave = new byte[size], fft = new byte[size]; Visualizer.MeasurementPeakRms measure = new Visualizer.MeasurementPeakRms(); for (int second = 0; second < SECONDS; second++) { SoundLevels.Summary now = new SoundLevels.Summary(); for (int read = 0; read < READS_PER_SECOND; read++) { Thread.sleep(1000 / READS_PER_SECOND); Float peak = analyser.getWaveForm(wave) == Visualizer.SUCCESS ? SoundLevels.peak(wave) : null; boolean spectrum = analyser.getFft(fft) == Visualizer.SUCCESS; Float bass = spectrum ? SoundLevels.band(fft, rateHz, 20f, 250f) : null; Float mid = spectrum ? SoundLevels.band(fft, rateHz, 250f, 2000f) : null; Float treble = spectrum ? SoundLevels.band(fft, rateHz, 2000f, 16000f) : null; Integer rms = analyser.getMeasurementPeakRms(measure) == Visualizer.SUCCESS ? measure.mRms : null; now.add(peak, bass, mid, treble, rms); all.add(peak, bass, mid, treble, rms); } out.add(MediaProbeText.duration(second * 1000L) + " " + now.line()); } } catch (RuntimeException e) { out.add("analyser not available: " + e.getClass().getSimpleName() + " " + MediaProbeText.clean(e.getMessage())); } finally { if (analyser != null) { try { analyser.setEnabled(false); } catch (RuntimeException ignored) { // Released next either way. } try { analyser.release(); } catch (RuntimeException e) { out.add("analyser not released: " + e.getClass().getSimpleName()); } } } return all; } /** What the Logs tab shows: the state, then the report. */ static synchronized List status(Context context) { List lines = new ArrayList<>(); if (startedMs == 0) return lines; if (running) { lines.add(context.getString(R.string.sound_probe_running)); return lines; } lines.add(context.getString(R.string.media_probe_done, SignalRecording.time(startedMs), result == null ? "…" : result)); String sent = LogOutbox.status(context); if (sent != null) lines.add(sent); lines.addAll(report.subList(Math.min(1, report.size()), report.size())); return lines; } }