From c04cd78ae9e901aa3ea90ae2bebcdfd82c22b0cd Mon Sep 17 00:00:00 2001 From: koka Date: Sun, 30 Aug 2026 02:16:30 +0200 Subject: [PATCH] test(ambiance): verify white disc playback cadence --- ambiance/build.gradle | 4 +- .../client/audio/LavaplayerAudioStream.java | 27 +++++++++++- .../client/audio/WhiteDiscPlaybackSmoke.java | 44 +++++++++++++------ 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/ambiance/build.gradle b/ambiance/build.gradle index c6ef258..3e38b43 100644 --- a/ambiance/build.gradle +++ b/ambiance/build.gradle @@ -361,7 +361,9 @@ tasks.register("verifyAmbiance") { || !whiteDiscAudioPlayer.contains('handle.execute(channel -> channel.stop())') || whiteDiscAudioPlayer.contains('.release()') || !whiteDiscPlaybackSmoke.contains('https://www.youtube.com/watch?v=Ue5ZBe-GzSM') - || !whiteDiscPlaybackSmoke.contains('Decoded audible PCM data') + || !whiteDiscPlaybackSmoke.contains('PCM cadence: 100%% speed') + || !whiteDiscPlaybackSmoke.contains('stream.decodedTimelineMillis() != 4_000L') + || !whiteDiscPlaybackSmoke.contains('stream.paddedSilenceBytes() != 0L') || !javaText.contains('SoundSource.RECORDS') || !javaText.contains('channel.linearAttenuation(ATTENUATION_DISTANCE)') || !javaText.contains('LONG_HOSTS = Set.of(') diff --git a/ambiance/src/main/java/fr/koka99cab/sanctuary26/ambiance/client/audio/LavaplayerAudioStream.java b/ambiance/src/main/java/fr/koka99cab/sanctuary26/ambiance/client/audio/LavaplayerAudioStream.java index 33e6865..95d8f0e 100644 --- a/ambiance/src/main/java/fr/koka99cab/sanctuary26/ambiance/client/audio/LavaplayerAudioStream.java +++ b/ambiance/src/main/java/fr/koka99cab/sanctuary26/ambiance/client/audio/LavaplayerAudioStream.java @@ -19,6 +19,11 @@ final class LavaplayerAudioStream implements AudioStream { private final AudioPlayer player; private byte[] pending = new byte[0]; private int pendingOffset; + private long decodedBytes; + private long paddedSilenceBytes; + private long firstFrameTimecode = -1L; + private long lastFrameTimecode = -1L; + private int decodedFrames; private boolean initialWaitAttempted; private volatile boolean closed; private volatile boolean ended; @@ -41,18 +46,25 @@ final class LavaplayerAudioStream implements AudioStream { int length = Math.min(output.remaining(), pending.length - pendingOffset); output.put(pending, pendingOffset, length); pendingOffset += length; + decodedBytes += length; continue; } AudioFrame frame = nextFrame(); if (frame == null) { if (ended) break; - while (output.hasRemaining()) output.put((byte) 0); + while (output.hasRemaining()) { + output.put((byte) 0); + paddedSilenceBytes++; + } break; } if (frame.isTerminator()) { ended = true; break; } + if (firstFrameTimecode < 0L) firstFrameTimecode = frame.getTimecode(); + lastFrameTimecode = frame.getTimecode(); + decodedFrames++; pending = frame.getData(); pendingOffset = 0; } @@ -86,6 +98,19 @@ final class LavaplayerAudioStream implements AudioStream { return player == expectedPlayer; } + long decodedBytes() { + return decodedBytes; + } + + long paddedSilenceBytes() { + return paddedSilenceBytes; + } + + long decodedTimelineMillis() { + if (decodedFrames == 0) return 0L; + return lastFrameTimecode - firstFrameTimecode + 20L; + } + @Override public void close() { if (closed) return; diff --git a/ambiance/src/test/java/fr/koka99cab/sanctuary26/ambiance/client/audio/WhiteDiscPlaybackSmoke.java b/ambiance/src/test/java/fr/koka99cab/sanctuary26/ambiance/client/audio/WhiteDiscPlaybackSmoke.java index b7ae0f8..e296197 100644 --- a/ambiance/src/test/java/fr/koka99cab/sanctuary26/ambiance/client/audio/WhiteDiscPlaybackSmoke.java +++ b/ambiance/src/test/java/fr/koka99cab/sanctuary26/ambiance/client/audio/WhiteDiscPlaybackSmoke.java @@ -30,29 +30,45 @@ public final class WhiteDiscPlaybackSmoke { AudioPlayer player = manager.createPlayer(); player.playTrack(track); LavaplayerAudioStream stream = new LavaplayerAudioStream(player); - int decodedBytes = 0; int totalBytes = 0; + long squaredSampleSum = 0L; + int sampleCount = 0; + int peakSample = 0; for (int index = 0; index < 4; index++) { ByteBuffer pcm = stream.read(LavaplayerAudioStream.SAMPLE_RATE * 2); if (pcm == null) break; - while (pcm.hasRemaining()) { - totalBytes++; - if (pcm.get() != 0) decodedBytes++; + totalBytes += pcm.remaining(); + while (pcm.remaining() >= 2) { + int low = Byte.toUnsignedInt(pcm.get()); + int high = pcm.get(); + short sample = (short) ((high << 8) | low); + int magnitude = Math.abs((int) sample); + peakSample = Math.max(peakSample, magnitude); + squaredSampleSum += (long) sample * sample; + sampleCount++; } } - if (decodedBytes == 0) { - throw new IllegalStateException("YouTube playback produced no audible PCM data"); - } - double audibleRatio = (double) decodedBytes / totalBytes; - if (totalBytes != LavaplayerAudioStream.SAMPLE_RATE * 2 * 4) { + int expectedBytes = LavaplayerAudioStream.SAMPLE_RATE * 2 * 4; + if (totalBytes != expectedBytes) { throw new IllegalStateException("YouTube playback did not fill four PCM seconds"); } - if (audibleRatio < 0.75) { - throw new IllegalStateException("YouTube PCM stream was mostly silence: " - + Math.round(audibleRatio * 100.0) + "% audible bytes"); + if (stream.decodedBytes() != expectedBytes || stream.paddedSilenceBytes() != 0L) { + throw new IllegalStateException("YouTube playback was not 100% decoded PCM: " + + stream.decodedBytes() + " decoded bytes and " + + stream.paddedSilenceBytes() + " padded bytes"); } - System.out.printf("Decoded audible PCM data: %.1f%% at %.0f Hz from %s%n", - audibleRatio * 100.0, stream.getFormat().getSampleRate(), track.getInfo().title); + if (stream.decodedTimelineMillis() != 4_000L) { + throw new IllegalStateException("PCM cadence mismatch: four seconds of samples cover " + + stream.decodedTimelineMillis() + " ms of the YouTube timeline"); + } + double rmsSample = Math.sqrt((double) squaredSampleSum / sampleCount); + if (rmsSample < 256.0 || peakSample < 2_048) { + throw new IllegalStateException("YouTube PCM signal is too quiet: RMS " + + Math.round(rmsSample) + ", peak " + peakSample); + } + System.out.printf( + "PCM cadence: 100%% speed, 4000 ms decoded into 4000 ms at %.0f Hz; RMS %.0f, peak %d from %s%n", + stream.getFormat().getSampleRate(), rmsSample, peakSample, track.getInfo().title); stream.close(); } finally { manager.shutdown();