Skip to content

Commit 860002d

Browse files
Implement opusdecoder with wasm-opus-decoder (#129)
1 parent 0948f3d commit 860002d

File tree

2 files changed

+87
-33
lines changed

2 files changed

+87
-33
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@
1616
"@mui/icons-material": "^6.4.6",
1717
"@mui/material": "^6.4.6",
1818
"libflacjs": "^5.4.0",
19+
"opus-decoder": "^0.7.7",
1920
"react": "^19.0.0",
2021
"react-dom": "^19.0.0",
2122
"standardized-audio-context": "^25.3.77"
2223
},
2324
"devDependencies": {
2425
"@eslint/js": "^9.21.0",
26+
"@types/node": "^22.13.8",
2527
"@types/react": "^19.0.10",
2628
"@types/react-dom": "^19.0.4",
27-
"@types/node": "^22.13.8",
2829
"@vite-pwa/assets-generator": "^0.2.6",
2930
"@vitejs/plugin-react-swc": "^3.8.0",
3031
"eslint": "^9.21.0",

src/snapstream.ts

Lines changed: 85 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Flac from 'libflacjs/dist/libflac.js'
22
import { getPersistentValue } from './config.ts'
33
import { AudioContext, IAudioBuffer, IAudioContext, IAudioBufferSourceNode, IGainNode } from 'standardized-audio-context'
4+
import { OpusDecoder as WasmOpusDecoder } from "opus-decoder";
45

56

67
declare global {
@@ -619,33 +620,7 @@ class Decoder {
619620
return new SampleFormat();
620621
}
621622

622-
decode(_chunk: PcmChunkMessage): PcmChunkMessage | null {
623-
return null;
624-
}
625-
}
626-
627-
628-
class OpusDecoder extends Decoder {
629-
setHeader(buffer: ArrayBuffer): SampleFormat | null {
630-
const view = new DataView(buffer);
631-
const ID_OPUS = 0x4F505553;
632-
if (buffer.byteLength < 12) {
633-
console.error("Opus header too small: " + buffer.byteLength);
634-
return null;
635-
} else if (view.getUint32(0, true) !== ID_OPUS) {
636-
console.error("Opus header too small: " + buffer.byteLength);
637-
return null;
638-
}
639-
640-
const format = new SampleFormat();
641-
format.rate = view.getUint32(4, true);
642-
format.bits = view.getUint16(8, true);
643-
format.channels = view.getUint16(10, true);
644-
console.log("Opus samplerate: " + format.toString());
645-
return format;
646-
}
647-
648-
decode(_chunk: PcmChunkMessage): PcmChunkMessage | null {
623+
decode(_chunk: PcmChunkMessage): PcmChunkMessage | null | Promise<PcmChunkMessage | null> {
649624
return null;
650625
}
651626
}
@@ -765,6 +740,81 @@ class FlacDecoder extends Decoder {
765740
cacheInfo: { isCachedChunk: boolean, cachedBlocks: number } = { isCachedChunk: false, cachedBlocks: 0 };
766741
}
767742

743+
class OpusDecoder extends Decoder {
744+
745+
constructor() {
746+
super();
747+
this.sampleFormat = new SampleFormat();
748+
this.decoder = null;
749+
}
750+
751+
async initDecoder() {
752+
if (!this.decoder) {
753+
this.decoder = new WasmOpusDecoder();
754+
await this.decoder.ready;
755+
await this.decoder.reset();
756+
}
757+
}
758+
759+
setHeader(buffer: ArrayBuffer): SampleFormat | null {
760+
const view = new DataView(buffer);
761+
const ID_OPUS = 0x4F505553;
762+
if (buffer.byteLength < 12) {
763+
console.error("Opus header too small:", buffer.byteLength);
764+
return null;
765+
} else if (view.getUint32(0, true) !== ID_OPUS) {
766+
console.error("Invalid Opus header magic");
767+
return null;
768+
}
769+
770+
this.sampleFormat.rate = view.getUint32(4, true);
771+
this.sampleFormat.bits = view.getUint16(8, true);
772+
this.sampleFormat.channels = view.getUint16(10, true);
773+
774+
this.initDecoder()
775+
.catch(err => console.error("Failed to initialize Opus decoder:", err));
776+
777+
console.log("Opus sampleformat:", this.sampleFormat.toString());
778+
return this.sampleFormat;
779+
}
780+
781+
async decode(chunk: PcmChunkMessage): Promise<PcmChunkMessage | null> {
782+
if (!this.decoder) {
783+
console.error("Opus decoder not initialized");
784+
return null;
785+
}
786+
787+
try {
788+
const decoded = await this.decoder.decodeFrame(new Uint8Array(chunk.payload));
789+
790+
const bytesPerSample = this.sampleFormat.sampleSize();
791+
const buffer = new ArrayBuffer(decoded.channelData[0].length * bytesPerSample * this.sampleFormat.channels);
792+
const view = new DataView(buffer);
793+
794+
for (let i = 0; i < decoded.channelData[0].length; i++) {
795+
for (let channel = 0; channel < this.sampleFormat.channels; channel++) {
796+
const sample = Math.max(-1, Math.min(1, decoded.channelData[channel][i])) * ((1 << (this.sampleFormat.bits - 1)) - 1);
797+
if (bytesPerSample === 4) {
798+
view.setInt32((i * this.sampleFormat.channels + channel) * 4, sample, true);
799+
} else {
800+
view.setInt16((i * this.sampleFormat.channels + channel) * 2, sample, true);
801+
}
802+
}
803+
}
804+
805+
chunk.clearPayload();
806+
chunk.addPayload(buffer);
807+
return chunk;
808+
} catch (err) {
809+
console.error("Failed to decode Opus frame:", err);
810+
return null;
811+
}
812+
}
813+
814+
private decoder: WasmOpusDecoder | null;
815+
private sampleFormat: SampleFormat;
816+
}
817+
768818
class PlayBuffer {
769819
constructor(buffer: IAudioBuffer, playTime: number, source: IAudioBufferSourceNode<IAudioContext>, destination: IGainNode<IAudioContext>) {
770820
this.buffer = buffer;
@@ -887,7 +937,6 @@ class SnapStream {
887937
this.decoder = new PcmDecoder();
888938
} else if (codec.codec === "opus") {
889939
this.decoder = new OpusDecoder();
890-
alert("Codec not supported: " + codec.codec);
891940
} else {
892941
alert("Codec not supported: " + codec.codec);
893942
}
@@ -925,10 +974,14 @@ class SnapStream {
925974
} else if (type === 2) {
926975
const pcmChunk = new PcmChunkMessage(msg.data, this.sampleFormat as SampleFormat);
927976
if (this.decoder) {
928-
const decoded = this.decoder.decode(pcmChunk);
929-
if (decoded) {
930-
this.stream!.addChunk(decoded);
931-
}
977+
const decodedPromise = this.decoder.decode(pcmChunk);
978+
Promise.resolve(decodedPromise).then(decoded => {
979+
if (decoded) {
980+
this.stream!.addChunk(decoded);
981+
}
982+
}).catch(err => {
983+
console.error("Error decoding chunk:", err);
984+
});
932985
}
933986
} else if (type === 3) {
934987
this.serverSettings = new ServerSettingsMessage(msg.data);

0 commit comments

Comments
 (0)