Android: static popping during playback #135

Closed
opened 2026-08-19 16:42:32 +00:00 by logan · 5 comments
Collaborator

Report

Audio on Android has a static/popping noise while a track is playing. Reported
from the device; not reproduced on desktop.

Findings

I have not reproduced this — it needs the phone — so everything below is read
from the source rather than measured, and the last section says what would
settle it. Three things in the audio path are candidates, and the first is the
one whose audible signature is exactly this symptom.

1. An underrun is rendered as silence, not as a stall. When the ring buffer
is empty, BufferedStreamer.Stream zeroes the caller's buffer and returns
ok:

if bs.count == 0 {
    ...
    for i := range samples {
        samples[i] = [2]float64{}
    }
    return len(samples), true
}

A run of zeros spliced into a waveform is a discontinuity at both edges, and a
step discontinuity is what a click or pop is. So an underrun here does not
sound like a gap or a stutter — it sounds like a pop, and a series of short
underruns sounds like static. Everything that makes an underrun likelier is
worse on a phone than on a desktop: slower storage, a CPU governor that parks
cores, background work, GC.

Nothing counts or logs it. starved/starvedSince exist (from the stall
fix in #122) but they only feed the 3-second give-up threshold; a hundred
20 ms underruns a minute are invisible to the log, to the UI and to every test
tier. That is why this is a report rather than a measurement, and it is the
cheapest thing to change first.

2. The output rate is hardcoded to 44100, which is not a phone's native
rate.

var speakerSampleRate = beep.SampleRate(44100)
// TODO: allow user to change buffer size and speaker sample rate.

Every file is resampled to it (beep.Resample(4, sr, speakerSampleRate, ...))
and the speaker is opened at it on every platform — there is no Android-specific
audio code at all (backend/player has no build tags). Android's native output
is 48000 on essentially every device, so the platform resamples a second
time on the way out, and asking Oboe for a non-native rate is also what stops it
taking the fast/low-latency path. Two resamplings do not by themselves make
popping, but the second one is happening in the layer whose buffer is being
missed.

3. The device buffer is ~100 ms, not the 200 ms the comment claims. The
comment at InitSpeaker says "Speaker buffer is 200ms", and beep then splits it
in half:

driverBufferSize := bufferSize / 2   // -> oto NewContextOptions.BufferSize
playerBufferSize := bufferSize / 2   // -> player.SetBufferSize

So Oboe gets ~100 ms. oto's own documentation for that option says, in as many
words, to raise it "if you want to adjust latency or reduce noises". On
Android oto drives Oboe (oboe.Play(sampleRate, channelCount, ..., bufferSizeInBytes) in driver_android.go), so this is the device-level buffer
and it has never been tuned for a phone.

A distant fourth, and only if the popping correlates with notifications:
SetDuck applies its attenuation instantaneously with no ramp, so each duck and
un-duck is a step change in gain — one click per event, not continuous static.
It only fires below API 26.

Direction

Measure before changing anything, because all three candidates above are
plausible and the fix for each is different.

  1. Count the underruns and log them. BufferedStreamer already tracks a
    starvation run for the stall threshold; add a cumulative counter and a debug
    log, and read it off the device with make android-inspect / logcat while
    the popping is audible. If the count is zero while it pops, candidates 1 and
    3 are both out and it is the resampling or something below us.
  2. If underruns are the cause, the levers in order of cost: raise the
    speaker buffer (the TODO at InitSpeaker is exactly this knob, and it
    should be per-platform rather than a single constant — a phone can afford
    latency a desktop player cannot); raise the 2 s read-ahead; and consider
    whether an underrun should hold the last sample rather than jump to zero,
    which turns a pop into a much less audible artifact.
  3. Ask the device for its own numbers. The standard Android recipe is
    AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE and
    PROPERTY_OUTPUT_FRAMES_PER_BUFFER, read on the Java side
    (build/android/.../WailsForegroundService.java is where our Java lives) and
    handed to InitSpeaker. Opening at the device's native rate also removes the
    second resampling for free.

Worth knowing while working on this: make android-inspect forwards the
WebView's devtools socket, but this is entirely below the WebView — the signal
is in logcat, not in the page. No test tier here can see it either: CI's
audio device is a PulseAudio null sink chosen because it keeps time, not
because it reproduces a phone's scheduling.

**Report** Audio on Android has a static/popping noise while a track is playing. Reported from the device; not reproduced on desktop. **Findings** I have not reproduced this — it needs the phone — so everything below is read from the source rather than measured, and the last section says what would settle it. Three things in the audio path are candidates, and the first is the one whose *audible signature is exactly this symptom*. **1. An underrun is rendered as silence, not as a stall.** When the ring buffer is empty, `BufferedStreamer.Stream` zeroes the caller's buffer and returns `ok`: ```go if bs.count == 0 { ... for i := range samples { samples[i] = [2]float64{} } return len(samples), true } ``` A run of zeros spliced into a waveform is a discontinuity at both edges, and a step discontinuity is what a click or pop *is*. So an underrun here does not sound like a gap or a stutter — it sounds like a pop, and a series of short underruns sounds like static. Everything that makes an underrun likelier is worse on a phone than on a desktop: slower storage, a CPU governor that parks cores, background work, GC. **Nothing counts or logs it.** `starved`/`starvedSince` exist (from the stall fix in #122) but they only feed the 3-second give-up threshold; a hundred 20 ms underruns a minute are invisible to the log, to the UI and to every test tier. That is why this is a report rather than a measurement, and it is the cheapest thing to change first. **2. The output rate is hardcoded to 44100, which is not a phone's native rate.** ```go var speakerSampleRate = beep.SampleRate(44100) // TODO: allow user to change buffer size and speaker sample rate. ``` Every file is resampled to it (`beep.Resample(4, sr, speakerSampleRate, ...)`) and the speaker is opened at it on every platform — there is no Android-specific audio code at all (`backend/player` has no build tags). Android's native output is **48000** on essentially every device, so the platform resamples a second time on the way out, and asking Oboe for a non-native rate is also what stops it taking the fast/low-latency path. Two resamplings do not by themselves make popping, but the second one is happening in the layer whose buffer is being missed. **3. The device buffer is ~100 ms, not the 200 ms the comment claims.** The comment at `InitSpeaker` says "Speaker buffer is 200ms", and beep then splits it in half: ```go driverBufferSize := bufferSize / 2 // -> oto NewContextOptions.BufferSize playerBufferSize := bufferSize / 2 // -> player.SetBufferSize ``` So Oboe gets ~100 ms. oto's own documentation for that option says, in as many words, to raise it "if you want to adjust latency or **reduce noises**". On Android oto drives **Oboe** (`oboe.Play(sampleRate, channelCount, ..., bufferSizeInBytes)` in `driver_android.go`), so this is the device-level buffer and it has never been tuned for a phone. **A distant fourth, and only if the popping correlates with notifications:** `SetDuck` applies its attenuation instantaneously with no ramp, so each duck and un-duck is a step change in gain — one click per event, not continuous static. It only fires below API 26. **Direction** Measure before changing anything, because all three candidates above are plausible and the fix for each is different. 1. **Count the underruns and log them.** `BufferedStreamer` already tracks a starvation run for the stall threshold; add a cumulative counter and a debug log, and read it off the device with `make android-inspect` / `logcat` while the popping is audible. If the count is zero while it pops, candidates 1 and 3 are both out and it is the resampling or something below us. 2. **If underruns are the cause**, the levers in order of cost: raise the speaker buffer (the `TODO` at `InitSpeaker` is exactly this knob, and it should be per-platform rather than a single constant — a phone can afford latency a desktop player cannot); raise the 2 s read-ahead; and consider whether an underrun should hold the last sample rather than jump to zero, which turns a pop into a much less audible artifact. 3. **Ask the device for its own numbers.** The standard Android recipe is `AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE` and `PROPERTY_OUTPUT_FRAMES_PER_BUFFER`, read on the Java side (`build/android/.../WailsForegroundService.java` is where our Java lives) and handed to `InitSpeaker`. Opening at the device's native rate also removes the second resampling for free. Worth knowing while working on this: `make android-inspect` forwards the WebView's devtools socket, but this is entirely below the WebView — the signal is in `logcat`, not in the page. No test tier here can see it either: CI's audio device is a PulseAudio null sink chosen because it *keeps time*, not because it reproduces a phone's scheduling.
logan added the Area/PlayerKind/BugPlatform/Android
Priority
High
2
labels 2026-08-19 16:42:32 +00:00
logan self-assigned this 2026-08-21 20:18:45 +00:00
logan added the
Status
In Progress
label 2026-08-21 20:18:45 +00:00
Author
Collaborator

Taking this. Branch 135-android-underrun-instrumentation, stacked on
160-android-slog-logcat because it depends on it: this issue's own
Direction is "count the underruns and log them ... read it off the
device with logcat", and until #160 every slog line on Android went to
/dev/null. Instrumenting and then reading nothing is the failure mode
worth avoiding.

Following the Direction as written rather than jumping to a fix: a
cumulative counter plus a bounded log first, then listen on the
device, and only then choose between the three candidates. If the
count is zero while it pops, candidates 1 and 3 are both out.

Taking this. Branch `135-android-underrun-instrumentation`, stacked on `160-android-slog-logcat` because it depends on it: this issue's own Direction is "count the underruns and log them ... read it off the device with logcat", and until #160 every slog line on Android went to /dev/null. Instrumenting and then reading nothing is the failure mode worth avoiding. Following the Direction as written rather than jumping to a fix: a cumulative counter plus a bounded log first, then listen on the device, and only then choose between the three candidates. If the count is zero while it pops, candidates 1 and 3 are both out.
Author
Collaborator

Measured on the device. The ring buffer is not underrunning, and the
Findings' first candidate is out.

Instrument in PR #191 (feat(player): count what the ring buffer misses), following this issue's Direction step 1 rather than jumping
to a fix. It is stacked on #160 for the reason argued on #73: until
that landed, "read it off the device with logcat" was not a thing that
could be done.

Setup. TLP301, Android 14, arm64, debug build, real 1,577-track
library on /sdcard/Music/t8-library. Test signal is a 4-minute
440 Hz sine
(ffmpeg -f lavfi -i sine=frequency=440:duration=240),
because the generated fixtures are ~2 s and a pop in a pure tone is
unmistakable where it hides in a mix. Index build stopped before each
measurement and confirmed stopped via jobs.Service.GetJobs.

Result: zero underruns, in every condition tried.

condition duration runs calls silence
steady playback, nothing else running ~45 s 0 0 0 ms
playback during a full catalog index build ~60 s 0 0 0 ms
playback across 6 seeks (each of which flushes the ring) ~14 s 0 0 0 ms

Playback was genuinely running throughout, not merely reported as
running: PlaybackPositionChanged arrived once a second with a
monotonic position (130,131,132,133,134,135), and dumpsys audio
showed our pid with an OpenSL ES AudioPlayer (Buffer Queue) in
state:started.

The instrument is live, and that had to be checked separately. A
counter that cannot move reports zero for the wrong reason, and "zero
underruns" from an unwired instrument is exactly the conclusion worth
not drawing. So a temporary build logged unconditionally rather than
only on a change, and produced the line at 1 Hz with runs=0 calls=0 silenceMs=0 — the path is reached, the count is genuinely zero. The
shipped build is silent while playback is healthy, as intended.

So candidates 1 and 3 are out, which is what this issue's own
Direction says to conclude: "If the count is zero while it pops,
candidates 1 and 3 are both out and it is the resampling or something
below us."
Nothing is being spliced into the waveform by
BufferedStreamer, and a device buffer that were too small would show
up here as underruns.

And candidate 2 has independent device evidence now. From the same
dumpsys audio, for our own pid:

AudioPlaybackConfiguration piid:839 deviceId:3
  type:OpenSL ES AudioPlayer (Buffer Queue) u/pid:10191/13280
  state:started ... FormatInfo{... channelMask=0x3, sampleRate=48000}

The device's output stream is 48000 Hz, and speakerSampleRate is
hardcoded to beep.SampleRate(44100). So every file is resampled to
44100 by us and then resampled again to 48000 below us, which is the
Findings' candidate 2 confirmed on hardware rather than inferred. Worth
noting it is OpenSL ES, not AAudio — so the "asking for a
non-native rate stops Oboe taking the fast path" reasoning applies with
a different mechanism than the Findings assumed, and that is worth
re-reading before acting on it.

What this does not establish, and I want to be plain about it: I
could not hear the phone.
These measurements say the ring buffer did
not underrun during them; they do not say the popping was audible at
the time, because nothing here can listen. So the honest statement is
"the cheapest hypothesis does not fire under load, seeks, or steady
playback", not "the popping is gone" or "the popping is the sample
rate".

Next, and it now has evidence behind it rather than being one of
three guesses:

  1. Take the device's own rate instead of assuming 44100. The standard
    recipe is AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE and
    PROPERTY_OUTPUT_FRAMES_PER_BUFFER read on the Java side and handed
    to InitSpeaker. That removes one of the two resamplings for free,
    and speakerSampleRate being a package var rather than a const
    suggests it was always meant to be settable.
  2. Whoever does that should re-run the table above afterwards — the
    instrument stays, so "did that change anything" is now a number
    rather than an opinion.
  3. If it still pops with the rate matched, the remaining suspects are
    below us: oto's OpenSL path and its buffer size, which is the other
    half of the TODO at InitSpeaker.

Unclaiming: the instrument is landing in #191 and the fix is a separate
piece of work that wants a device and someone who can hear it.

**Measured on the device. The ring buffer is not underrunning, and the Findings' first candidate is out.** Instrument in PR #191 (`feat(player): count what the ring buffer misses`), following this issue's Direction step 1 rather than jumping to a fix. It is stacked on #160 for the reason argued on #73: until that landed, "read it off the device with logcat" was not a thing that could be done. **Setup.** TLP301, Android 14, arm64, debug build, real 1,577-track library on `/sdcard/Music/t8-library`. Test signal is a **4-minute 440 Hz sine** (`ffmpeg -f lavfi -i sine=frequency=440:duration=240`), because the generated fixtures are ~2 s and a pop in a pure tone is unmistakable where it hides in a mix. Index build stopped before each measurement and confirmed stopped via `jobs.Service.GetJobs`. **Result: zero underruns, in every condition tried.** | condition | duration | runs | calls | silence | |---|---|---|---|---| | steady playback, nothing else running | ~45 s | 0 | 0 | 0 ms | | playback during a **full catalog index build** | ~60 s | 0 | 0 | 0 ms | | playback across **6 seeks** (each of which flushes the ring) | ~14 s | 0 | 0 | 0 ms | Playback was genuinely running throughout, not merely reported as running: `PlaybackPositionChanged` arrived once a second with a monotonic position (`130,131,132,133,134,135`), and `dumpsys audio` showed our pid with an `OpenSL ES AudioPlayer (Buffer Queue)` in `state:started`. **The instrument is live, and that had to be checked separately.** A counter that cannot move reports zero for the wrong reason, and "zero underruns" from an unwired instrument is exactly the conclusion worth not drawing. So a temporary build logged unconditionally rather than only on a change, and produced the line at 1 Hz with `runs=0 calls=0 silenceMs=0` — the path is reached, the count is genuinely zero. The shipped build is silent while playback is healthy, as intended. **So candidates 1 and 3 are out**, which is what this issue's own Direction says to conclude: *"If the count is zero while it pops, candidates 1 and 3 are both out and it is the resampling or something below us."* Nothing is being spliced into the waveform by `BufferedStreamer`, and a device buffer that were too small would show up here as underruns. **And candidate 2 has independent device evidence now.** From the same `dumpsys audio`, for our own pid: ``` AudioPlaybackConfiguration piid:839 deviceId:3 type:OpenSL ES AudioPlayer (Buffer Queue) u/pid:10191/13280 state:started ... FormatInfo{... channelMask=0x3, sampleRate=48000} ``` **The device's output stream is 48000 Hz**, and `speakerSampleRate` is hardcoded to `beep.SampleRate(44100)`. So every file is resampled to 44100 by us and then resampled again to 48000 below us, which is the Findings' candidate 2 confirmed on hardware rather than inferred. Worth noting it is **OpenSL ES**, not AAudio — so the "asking for a non-native rate stops Oboe taking the fast path" reasoning applies with a different mechanism than the Findings assumed, and that is worth re-reading before acting on it. **What this does not establish, and I want to be plain about it: I could not hear the phone.** These measurements say the ring buffer did not underrun during them; they do not say the popping was audible at the time, because nothing here can listen. So the honest statement is "the cheapest hypothesis does not fire under load, seeks, or steady playback", not "the popping is gone" or "the popping is the sample rate". **Next**, and it now has evidence behind it rather than being one of three guesses: 1. Take the device's own rate instead of assuming 44100. The standard recipe is `AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE` and `PROPERTY_OUTPUT_FRAMES_PER_BUFFER` read on the Java side and handed to `InitSpeaker`. That removes one of the two resamplings for free, and `speakerSampleRate` being a package `var` rather than a const suggests it was always meant to be settable. 2. Whoever does that should re-run the table above afterwards — the instrument stays, so "did that change anything" is now a number rather than an opinion. 3. If it still pops with the rate matched, the remaining suspects are below us: oto's OpenSL path and its buffer size, which is the other half of the `TODO` at `InitSpeaker`. Unclaiming: the instrument is landing in #191 and the fix is a separate piece of work that wants a device and someone who can hear it.
logan removed their assignment 2026-08-21 20:35:39 +00:00
logan removed the
Status
In Progress
label 2026-08-21 20:35:39 +00:00
Author
Collaborator

Listened to on the device by the reporter. The symptom does not
reproduce, in two conditions, and the instrument agrees.

Following the comment above, which counted the underruns and found
none: the missing half was that nothing here can hear the phone. That
was done directly this session.

condition signal underrun runs audible
steady playback 5 min 440 Hz sine, 48 kHz source 0 nothing
playback under a full catalog index build a real mp3 from the 1,577-track library 0 nothing

The second row is the one that matters. The Findings say "everything
that makes an underrun likelier is worse on a phone than on a desktop
-- slower storage, a CPU governor that parks cores, background work,
GC", so the test was built to supply exactly that: real music with a
full index build running against the same single-writer SQLite
connection, on the reference device (TLP301, Android 14). Nothing was
audible and the counter stayed at zero throughout.

A sine was used for the first row on purpose -- a pop in a pure tone is
unmistakable where it hides in a mix -- and the 48 kHz source was
chosen to be the worst case for candidate 2, since it is resampled to
44100 by us and back to 48000 by the platform.

Why it may have gone away, offered as a hypothesis rather than a
claim.
This was filed on 2026-08-19, four releases ago, and the
strongest candidate is not in the audio path at all: it is #190, fixed
earlier today. Before that, every launch ran the champion FTS
rebuild -- an INSERT ... SELECT over a 1,079,667-row index -- which
spilled to a temporary file, failed with SQLITE_IOERR_GETTEMPPATH,
and was retried on the next launch, forever. That is a large, repeated
database operation contending with playback for CPU, IO and the single
SQLite writer on a phone, which is a good shape for an intermittent
audible glitch. It now succeeds once, in 6.6 s, and stops.

That is not proven and cannot now be, since the fix is already in. It
is written down so that whoever meets this again has somewhere to
start.

Closing, because the person who saw it has now listened and says it
is not there.
That is the same standard #53 is being held to, with
the answer supplied rather than pending.

What stays is the instrument, which is the durable half of this
issue: audio underrun lines in logcat now say immediately whether the
ring buffer is the cause, with runs, calls and milliseconds of silence,
and they are silent while playback is healthy. If this returns it is a
number rather than a report.

And the sample-rate mismatch is real but is not this bug. The
device's output stream is 48000 Hz and speakerSampleRate is hardcoded
to 44100, so every file is resampled twice; dumpsys audio also shows
the stream opening as OpenSL ES rather than AAudio. Both are worth
fixing on their own terms and neither is a defect anyone can hear right
now, so changing the audio path on the strength of them would be
exactly what this issue's own Direction warns against -- "measure
before changing anything". Filed separately as #194.

Closes #135

**Listened to on the device by the reporter. The symptom does not reproduce, in two conditions, and the instrument agrees.** Following the comment above, which counted the underruns and found none: the missing half was that nothing here can hear the phone. That was done directly this session. | condition | signal | underrun runs | audible | |---|---|---|---| | steady playback | 5 min 440 Hz sine, 48 kHz source | 0 | nothing | | **playback under a full catalog index build** | a real mp3 from the 1,577-track library | 0 | nothing | The second row is the one that matters. The Findings say "everything that makes an underrun likelier is worse on a phone than on a desktop -- slower storage, a CPU governor that parks cores, background work, GC", so the test was built to supply exactly that: real music with a full index build running against the same single-writer SQLite connection, on the reference device (TLP301, Android 14). Nothing was audible and the counter stayed at zero throughout. A sine was used for the first row on purpose -- a pop in a pure tone is unmistakable where it hides in a mix -- and the 48 kHz source was chosen to be the *worst* case for candidate 2, since it is resampled to 44100 by us and back to 48000 by the platform. **Why it may have gone away, offered as a hypothesis rather than a claim.** This was filed on 2026-08-19, four releases ago, and the strongest candidate is not in the audio path at all: it is #190, fixed earlier today. Before that, **every launch** ran the champion FTS rebuild -- an `INSERT ... SELECT` over a 1,079,667-row index -- which spilled to a temporary file, failed with `SQLITE_IOERR_GETTEMPPATH`, and was retried on the next launch, forever. That is a large, repeated database operation contending with playback for CPU, IO and the single SQLite writer on a phone, which is a good shape for an intermittent audible glitch. It now succeeds once, in 6.6 s, and stops. That is not proven and cannot now be, since the fix is already in. It is written down so that whoever meets this again has somewhere to start. **Closing, because the person who saw it has now listened and says it is not there.** That is the same standard #53 is being held to, with the answer supplied rather than pending. **What stays is the instrument**, which is the durable half of this issue: `audio underrun` lines in logcat now say immediately whether the ring buffer is the cause, with runs, calls and milliseconds of silence, and they are silent while playback is healthy. If this returns it is a number rather than a report. **And the sample-rate mismatch is real but is not this bug.** The device's output stream is 48000 Hz and `speakerSampleRate` is hardcoded to 44100, so every file is resampled twice; `dumpsys audio` also shows the stream opening as **OpenSL ES** rather than AAudio. Both are worth fixing on their own terms and neither is a defect anyone can hear right now, so changing the audio path on the strength of them would be exactly what this issue's own Direction warns against -- "measure before changing anything". Filed separately as #194. Closes #135
Author
Collaborator

Closed on the evidence in the comment above: measured at zero underruns
in every condition tried, and listened to on the device by the reporter
under both steady playback and a full index build, with nothing
audible.

The instrument stays, which is the durable half. #194 carries the
sample-rate finding.

Closed on the evidence in the comment above: measured at zero underruns in every condition tried, and listened to on the device by the reporter under both steady playback and a full index build, with nothing audible. The instrument stays, which is the durable half. #194 carries the sample-rate finding.
logan closed this issue 2026-08-21 23:33:59 +00:00
Author
Collaborator

Successor: #203. The reporter has listened again on the most recent
release build and popping is still audible — less obvious than the
static this issue described, so the work here improved it without
settling it.

One fact from this issue needs qualifying rather than repeating, which
is why #203 exists instead of a reopen. The measurement above was
taken on a debug build of main, and the counter that produced it is
not in any release.
842fe47 landed after v0.5.0 was tagged:

v0.5.0 tagged   2026-08-21 16:47:46 +0000
842fe47         2026-08-21 20:30:32 +0000
$ git show v0.5.0:backend/player/buffered_streamer.go | grep -c UnderrunStats
0

So "zero underruns" is true of what it measured and says nothing about
the build people install — candidate 1 is not eliminated for that
build, it was never tested on it. The listening test has the same
qualification, plus the conditions it was necessarily run in: attached
to USB, charging, screen on, debuggable process. All three bear on a
real-time deadline.

Nothing needs to change for that to become answerable: the instrument
logs at Info from an untagged file, and make android-logs filters on
the fixed yellowjacket tag rather than the package id, so it reads
the release app too.

Leaving this closed. Its record — the instrument, two measured
conditions and a listening test — is the useful thing and stays intact.

**Successor: #203.** The reporter has listened again on the most recent **release** build and popping is still audible — less obvious than the static this issue described, so the work here improved it without settling it. One fact from this issue needs qualifying rather than repeating, which is why #203 exists instead of a reopen. **The measurement above was taken on a debug build of `main`, and the counter that produced it is not in any release.** `842fe47` landed after `v0.5.0` was tagged: ``` v0.5.0 tagged 2026-08-21 16:47:46 +0000 842fe47 2026-08-21 20:30:32 +0000 $ git show v0.5.0:backend/player/buffered_streamer.go | grep -c UnderrunStats 0 ``` So "zero underruns" is true of what it measured and says nothing about the build people install — candidate 1 is not eliminated for that build, it was never tested on it. The listening test has the same qualification, plus the conditions it was necessarily run in: attached to USB, charging, screen on, debuggable process. All three bear on a real-time deadline. Nothing needs to change for that to become answerable: the instrument logs at Info from an untagged file, and `make android-logs` filters on the fixed `yellowjacket` tag rather than the package id, so it reads the release app too. Leaving this closed. Its record — the instrument, two measured conditions and a listening test — is the useful thing and stays intact.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yonlu/yellowjacket#135