fix(ambiance): preserve white disc native audio rate
This commit is contained in:
@@ -16,8 +16,12 @@ jukebox vanilla.
|
||||
libre ; il est envoyé uniquement aux clients qui suivent le chunk et annoncent le canal ;
|
||||
- chaque client ouvre lui-même la source YouTube, refuse les directs et les vidéos de plus de
|
||||
trente minutes, et limite les lectures simultanées à quatre ;
|
||||
- la lecture utilise le canal `RECORDS`, un flux PCM mono positionnel et une atténuation à
|
||||
64 blocs. Elle s’arrête lorsque le disque est éjecté, le chunk disparaît ou la session se ferme.
|
||||
- la source AAC stéréo est décodée à 48 kHz puis convertie explicitement en PCM mono 16 bits
|
||||
little-endian, échantillon gauche/droite par échantillon gauche/droite, afin de conserver la
|
||||
vitesse et la hauteur originales tout en restant positionnelle ;
|
||||
- OpenAL lit ce PCM à sa hauteur native `1.0` sur le canal `RECORDS`, avec une atténuation à
|
||||
64 blocs. La lecture s’arrête lorsque le disque est éjecté, le chunk disparaît ou la session se
|
||||
ferme.
|
||||
|
||||
Le disque ne crée aucune donnée persistante et ne demande aucune migration de sauvegarde.
|
||||
Ambiance conserve ses données v2 ; son protocole passe de 5 à 6.
|
||||
|
||||
@@ -102,7 +102,7 @@ tasks.named("test") {
|
||||
|
||||
tasks.register("verifyAmbiance") {
|
||||
group = "verification"
|
||||
description = "Checks Ambiance alpha.21, including corrected white-disc pitch and Alpha textures."
|
||||
description = "Checks Ambiance alpha.22, including native-rate white-disc audio and Alpha textures."
|
||||
inputs.files(fileTree("src/main/java"))
|
||||
inputs.files(fileTree("src/main/resources"))
|
||||
inputs.file(file("src/test/java/fr/koka99cab/sanctuary26/ambiance/client/audio/WhiteDiscPlaybackSmoke.java"))
|
||||
@@ -161,7 +161,7 @@ tasks.register("verifyAmbiance") {
|
||||
def whiteDiscPlaybackSmoke = file(
|
||||
"src/test/java/fr/koka99cab/sanctuary26/ambiance/client/audio/WhiteDiscPlaybackSmoke.java").text
|
||||
|
||||
if (project.version.toString() != "0.0.0-alpha.21"
|
||||
if (project.version.toString() != "0.0.0-alpha.22"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.125"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.126"
|
||||
|| migration.target?.modules?.ambiance != "0.0.0-alpha.1"
|
||||
@@ -364,15 +364,18 @@ tasks.register("verifyAmbiance") {
|
||||
|| !javaText.contains('tickPendingStarts(client)')
|
||||
|| !javaText.contains('client.level.dimension().equals(playback.dimension())')
|
||||
|| !javaText.contains('MAXIMUM_TRACK_MILLIS = 30L * 60L * 1_000L')
|
||||
|| !javaText.contains('new Pcm16AudioDataFormat(1, LavaplayerAudioStream.SAMPLE_RATE')
|
||||
|| !javaText.contains('new Pcm16AudioDataFormat(LavaplayerAudioStream.SOURCE_CHANNELS,')
|
||||
|| !javaText.contains('new YoutubeAudioSourceManager(false, new Ios())')
|
||||
|| !javaText.contains('player.addListener(new AudioEventAdapter()')
|
||||
|| !javaText.contains('failPlayback(client, key, failedPlayer)')
|
||||
|| !javaText.contains('initialWaitAttempted')
|
||||
|| !javaText.contains('CONTINUATION_FRAME_WAIT_MILLIS = 250L')
|
||||
|| !javaText.contains('player.provide(waitMillis, TimeUnit.MILLISECONDS)')
|
||||
|| !whiteDiscAudioPlayer.contains('PLAYBACK_PITCH = 4.0F / 3.0F')
|
||||
|| !whiteDiscAudioPlayer.contains('PLAYBACK_PITCH = 1.0F')
|
||||
|| whiteDiscAudioPlayer.count('channel.setPitch(PLAYBACK_PITCH)') != 2
|
||||
|| !javaText.contains('SOURCE_CHANNELS = 2')
|
||||
|| !javaText.contains('downmixStereoFrame(frame.getData())')
|
||||
|| !javaText.contains('short mixed = (short) (((int) left + right) / SOURCE_CHANNELS)')
|
||||
|| !whiteDiscAudioPlayer.contains('handle.execute(channel -> channel.stop())')
|
||||
|| whiteDiscAudioPlayer.contains('.release()')
|
||||
|| !whiteDiscPlaybackSmoke.contains('https://www.youtube.com/watch?v=Ue5ZBe-GzSM')
|
||||
@@ -472,7 +475,7 @@ tasks.register("verifyAlphaTextures") {
|
||||
include "**/*golden*days*.zip", "**/*golden*days*.jar"
|
||||
exclude "**/build/**", "**/.gradle/**", "**/.git/**"
|
||||
}.files
|
||||
if (project.version.toString() != "0.0.0-alpha.21"
|
||||
if (project.version.toString() != "0.0.0-alpha.22"
|
||||
|| migration.alpha_textures?.destination != "sanctuary:alpha"
|
||||
|| migration.alpha_textures?.client_only != true
|
||||
|| migration.alpha_textures?.bundled_assets != false
|
||||
|
||||
+26
-1
@@ -12,6 +12,10 @@ import net.minecraft.client.sounds.AudioStream;
|
||||
/** Adapts bounded mono PCM frames from Lavaplayer to Minecraft's streaming OpenAL channel. */
|
||||
final class LavaplayerAudioStream implements AudioStream {
|
||||
static final int SAMPLE_RATE = 48_000;
|
||||
static final int SOURCE_CHANNELS = 2;
|
||||
static final int FRAME_SAMPLES = 960;
|
||||
private static final int BYTES_PER_SAMPLE = Short.BYTES;
|
||||
private static final int SOURCE_FRAME_SIZE = SOURCE_CHANNELS * BYTES_PER_SAMPLE;
|
||||
private static final long INITIAL_FRAME_WAIT_MILLIS = 2_000L;
|
||||
private static final long CONTINUATION_FRAME_WAIT_MILLIS = 250L;
|
||||
private static final AudioFormat FORMAT =
|
||||
@@ -65,7 +69,7 @@ final class LavaplayerAudioStream implements AudioStream {
|
||||
if (firstFrameTimecode < 0L) firstFrameTimecode = frame.getTimecode();
|
||||
lastFrameTimecode = frame.getTimecode();
|
||||
decodedFrames++;
|
||||
pending = frame.getData();
|
||||
pending = downmixStereoFrame(frame.getData());
|
||||
pendingOffset = 0;
|
||||
}
|
||||
if (output.position() == 0 && ended) return null;
|
||||
@@ -73,6 +77,27 @@ final class LavaplayerAudioStream implements AudioStream {
|
||||
return output;
|
||||
}
|
||||
|
||||
private static byte[] downmixStereoFrame(byte[] stereo) throws IOException {
|
||||
if (stereo.length == 0 || stereo.length % SOURCE_FRAME_SIZE != 0) {
|
||||
throw new IOException("Invalid stereo PCM frame length: " + stereo.length);
|
||||
}
|
||||
byte[] mono = new byte[stereo.length / SOURCE_CHANNELS];
|
||||
for (int sourceOffset = 0, targetOffset = 0;
|
||||
sourceOffset < stereo.length;
|
||||
sourceOffset += SOURCE_FRAME_SIZE, targetOffset += BYTES_PER_SAMPLE) {
|
||||
short left = littleEndianSample(stereo, sourceOffset);
|
||||
short right = littleEndianSample(stereo, sourceOffset + BYTES_PER_SAMPLE);
|
||||
short mixed = (short) (((int) left + right) / SOURCE_CHANNELS);
|
||||
mono[targetOffset] = (byte) mixed;
|
||||
mono[targetOffset + 1] = (byte) (mixed >>> 8);
|
||||
}
|
||||
return mono;
|
||||
}
|
||||
|
||||
private static short littleEndianSample(byte[] pcm, int offset) {
|
||||
return (short) (Byte.toUnsignedInt(pcm[offset]) | pcm[offset + 1] << 8);
|
||||
}
|
||||
|
||||
private AudioFrame nextFrame() throws IOException {
|
||||
try {
|
||||
long waitMillis = initialWaitAttempted
|
||||
|
||||
+4
-3
@@ -36,8 +36,8 @@ public final class WhiteDiscAudioPlayer {
|
||||
private static final long MAXIMUM_TRACK_MILLIS = 30L * 60L * 1_000L;
|
||||
private static final long FINISHED_GRACE_NANOS = 5_000_000_000L;
|
||||
private static final float ATTENUATION_DISTANCE = 64.0F;
|
||||
/** Compensates Lavaplayer's low AAC cadence before PCM reaches OpenAL. */
|
||||
static final float PLAYBACK_PITCH = 4.0F / 3.0F;
|
||||
/** OpenAL must preserve the decoded music's native speed and pitch. */
|
||||
static final float PLAYBACK_PITCH = 1.0F;
|
||||
private static final DefaultAudioPlayerManager MANAGER = createManager();
|
||||
private static final Map<Long, PendingStart> PENDING_STARTS = new HashMap<>();
|
||||
private static final Map<Long, Long> REQUESTS = new HashMap<>();
|
||||
@@ -237,7 +237,8 @@ public final class WhiteDiscAudioPlayer {
|
||||
private static DefaultAudioPlayerManager createManager() {
|
||||
DefaultAudioPlayerManager manager = new DefaultAudioPlayerManager();
|
||||
manager.getConfiguration().setOutputFormat(
|
||||
new Pcm16AudioDataFormat(1, LavaplayerAudioStream.SAMPLE_RATE, 960, false));
|
||||
new Pcm16AudioDataFormat(LavaplayerAudioStream.SOURCE_CHANNELS,
|
||||
LavaplayerAudioStream.SAMPLE_RATE, LavaplayerAudioStream.FRAME_SAMPLES, false));
|
||||
manager.setFrameBufferDuration(5_000);
|
||||
manager.setItemLoaderThreadPoolSize(2);
|
||||
manager.registerSourceManager(new YoutubeAudioSourceManager(false, new Ios()));
|
||||
|
||||
+24
-10
@@ -7,7 +7,6 @@ import com.sedmelluq.discord.lavaplayer.track.playback.ImmutableAudioFrame;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -15,22 +14,27 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
/** Deterministic check for Minecraft's four one-second streaming buffers. */
|
||||
public final class LavaplayerAudioStreamSmoke {
|
||||
private static final int FRAME_SAMPLES = 960;
|
||||
private static final int FRAME_BYTES = FRAME_SAMPLES * 2;
|
||||
private static final int SOURCE_FRAME_BYTES = FRAME_SAMPLES
|
||||
* LavaplayerAudioStream.SOURCE_CHANNELS * Short.BYTES;
|
||||
private static final int FRAMES_PER_SECOND = LavaplayerAudioStream.SAMPLE_RATE / FRAME_SAMPLES;
|
||||
|
||||
private LavaplayerAudioStreamSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
if (Math.abs(WhiteDiscAudioPlayer.PLAYBACK_PITCH - 4.0F / 3.0F) > 0.0001F) {
|
||||
throw new IllegalStateException("The white-disc AAC pitch compensation changed");
|
||||
if (Math.abs(WhiteDiscAudioPlayer.PLAYBACK_PITCH - 1.0F) > 0.0001F) {
|
||||
throw new IllegalStateException("The white-disc playback pitch is not native");
|
||||
}
|
||||
Queue<AudioFrame> frames = new ArrayDeque<>();
|
||||
Pcm16AudioDataFormat format = new Pcm16AudioDataFormat(
|
||||
1, LavaplayerAudioStream.SAMPLE_RATE, FRAME_SAMPLES, false);
|
||||
LavaplayerAudioStream.SOURCE_CHANNELS, LavaplayerAudioStream.SAMPLE_RATE,
|
||||
FRAME_SAMPLES, false);
|
||||
for (int index = 0; index < FRAMES_PER_SECOND; index++) {
|
||||
byte[] data = new byte[FRAME_BYTES];
|
||||
Arrays.fill(data, (byte) (index + 1));
|
||||
byte[] data = new byte[SOURCE_FRAME_BYTES];
|
||||
for (int offset = 0; offset < data.length; offset += 4) {
|
||||
putLittleEndian(data, offset, (short) (1_000 + index));
|
||||
putLittleEndian(data, offset + 2, (short) (3_000 + index));
|
||||
}
|
||||
frames.add(new ImmutableAudioFrame(index * 20L, data, 100, format));
|
||||
}
|
||||
|
||||
@@ -63,9 +67,14 @@ public final class LavaplayerAudioStreamSmoke {
|
||||
if (pcm == null || pcm.remaining() != LavaplayerAudioStream.SAMPLE_RATE * 2) {
|
||||
throw new IllegalStateException("The stream did not fill Minecraft's one-second buffer");
|
||||
}
|
||||
while (pcm.hasRemaining()) {
|
||||
if (pcm.get() == 0) {
|
||||
throw new IllegalStateException("The stream padded available PCM with silence");
|
||||
for (int frame = 0; frame < FRAMES_PER_SECOND; frame++) {
|
||||
short expected = (short) (2_000 + frame);
|
||||
for (int sample = 0; sample < FRAME_SAMPLES; sample++) {
|
||||
int low = Byte.toUnsignedInt(pcm.get());
|
||||
short actual = (short) (low | pcm.get() << 8);
|
||||
if (actual != expected) {
|
||||
throw new IllegalStateException("Stereo PCM was not downmixed in sample order");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (timedProvides.get() != FRAMES_PER_SECOND || nonBlockingProvides.get() != 0) {
|
||||
@@ -80,4 +89,9 @@ public final class LavaplayerAudioStreamSmoke {
|
||||
throw new IllegalStateException("Closing the stream did not destroy its player");
|
||||
}
|
||||
}
|
||||
|
||||
private static void putLittleEndian(byte[] data, int offset, short sample) {
|
||||
data[offset] = (byte) sample;
|
||||
data[offset + 1] = (byte) (sample >>> 8);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -23,7 +23,8 @@ public final class WhiteDiscPlaybackSmoke {
|
||||
public static void main(String[] args) throws Exception {
|
||||
DefaultAudioPlayerManager manager = new DefaultAudioPlayerManager();
|
||||
manager.getConfiguration().setOutputFormat(
|
||||
new Pcm16AudioDataFormat(1, LavaplayerAudioStream.SAMPLE_RATE, 960, false));
|
||||
new Pcm16AudioDataFormat(LavaplayerAudioStream.SOURCE_CHANNELS,
|
||||
LavaplayerAudioStream.SAMPLE_RATE, LavaplayerAudioStream.FRAME_SAMPLES, false));
|
||||
manager.registerSourceManager(new YoutubeAudioSourceManager(false, new Ios()));
|
||||
try {
|
||||
AudioTrack track = load(manager).get(30, TimeUnit.SECONDS);
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ fabric_api_version=0.158.0+26.2
|
||||
# Standby modules stay at zero until their migration really begins.
|
||||
mod_version=0.0.0-alpha.0
|
||||
module_lifecycle=standby
|
||||
ambiance_version=0.0.0-alpha.21
|
||||
ambiance_version=0.0.0-alpha.22
|
||||
ambiance_lifecycle=active
|
||||
redstoner_version=0.0.0-alpha.11
|
||||
redstoner_lifecycle=active
|
||||
|
||||
Reference in New Issue
Block a user