Ship the Android sound packs as Opus
The build encodes TeamSpeak's packs to Ogg Opus at 64 kbps before bundling
them, which takes the sounds from 21.3 MB to 2.0 MB, in the APK and again
once unpacked on the phone. The encoding runs the desktop code's libopus
on the build machine, through a build-only :desktop module compiled from
the same sources as the Maven one.
Both clients play Opus sounds: the sound player (now PackSoundPlayer, as it
no longer plays only wave files) decodes Ogg Opus with the platform's
libopus, and a pack's play("x.wav") finds x.opus, so converted packs keep
their scripts. SoundPackConverter turns any pack folder into Opus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package com.ts3client.sound;
|
||||
|
||||
import com.ts3client.audio.desktop.NativeOpusCodec;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/** A pack converted to Opus plays back as the sound it was, found under its old name. */
|
||||
class OggOpusFileTest {
|
||||
|
||||
@TempDir
|
||||
File dir;
|
||||
|
||||
@Test
|
||||
void convertedPackPlaysTheSameSound() throws IOException {
|
||||
File source = new File(dir, "wav/pack");
|
||||
Files.createDirectories(source.toPath());
|
||||
Files.writeString(new File(source, "settings.ini").toPath(),
|
||||
"[info]\nname = Test\n[soundfiles]\nCONNECTION_CONNECTED = play(\"tone.wav\")\n");
|
||||
int rate = 44_100;
|
||||
float[] tone = new float[rate / 2 + 123];
|
||||
for (int i = 0; i < tone.length; i++) tone[i] = (float) (0.5 * Math.sin(2 * Math.PI * 440 * i / rate));
|
||||
writeWave(new File(source, "tone.wav"), tone, rate);
|
||||
|
||||
File target = new File(dir, "opus");
|
||||
NativeOpusCodec opus = new NativeOpusCodec();
|
||||
SoundPackConverter.convert(new File(dir, "wav"), target, opus);
|
||||
|
||||
SoundPack pack = SoundPack.load(new File(target, "pack"));
|
||||
assertNotNull(pack);
|
||||
File file = pack.file("tone.wav");
|
||||
assertEquals("tone.opus", file.getName());
|
||||
|
||||
float[] expected = WaveFile.readMono(new File(source, "tone.wav")).at48k();
|
||||
float[] decoded = OggOpusFile.readMono(file, opus);
|
||||
assertEquals(expected.length, decoded.length);
|
||||
double dot = 0, a = 0, b = 0;
|
||||
for (int i = 0; i < decoded.length; i++) {
|
||||
dot += expected[i] * decoded[i];
|
||||
a += expected[i] * expected[i];
|
||||
b += decoded[i] * decoded[i];
|
||||
}
|
||||
double correlation = dot / Math.sqrt(a * b);
|
||||
assertTrue(correlation > 0.99, "correlation " + correlation);
|
||||
}
|
||||
|
||||
private static void writeWave(File file, float[] samples, int rate) throws IOException {
|
||||
ByteBuffer b = ByteBuffer.allocate(44 + samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
|
||||
b.put("RIFF".getBytes()).putInt(36 + samples.length * 2).put("WAVE".getBytes());
|
||||
b.put("fmt ".getBytes()).putInt(16).putShort((short) 1).putShort((short) 1)
|
||||
.putInt(rate).putInt(rate * 2).putShort((short) 2).putShort((short) 16);
|
||||
b.put("data".getBytes()).putInt(samples.length * 2);
|
||||
for (float s : samples) b.putShort((short) Math.round(s * 32767));
|
||||
Files.write(file.toPath(), b.array());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user