|
1 | 1 | import Flac from 'libflacjs/dist/libflac.js'
|
2 | 2 | import { getPersistentValue } from './config.ts'
|
3 | 3 | import { AudioContext, IAudioBuffer, IAudioContext, IAudioBufferSourceNode, IGainNode } from 'standardized-audio-context'
|
| 4 | +import { OpusDecoder as WasmOpusDecoder } from "opus-decoder"; |
4 | 5 |
|
5 | 6 |
|
6 | 7 | declare global {
|
@@ -619,33 +620,7 @@ class Decoder {
|
619 | 620 | return new SampleFormat();
|
620 | 621 | }
|
621 | 622 |
|
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> { |
649 | 624 | return null;
|
650 | 625 | }
|
651 | 626 | }
|
@@ -765,6 +740,81 @@ class FlacDecoder extends Decoder {
|
765 | 740 | cacheInfo: { isCachedChunk: boolean, cachedBlocks: number } = { isCachedChunk: false, cachedBlocks: 0 };
|
766 | 741 | }
|
767 | 742 |
|
| 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 | + |
768 | 818 | class PlayBuffer {
|
769 | 819 | constructor(buffer: IAudioBuffer, playTime: number, source: IAudioBufferSourceNode<IAudioContext>, destination: IGainNode<IAudioContext>) {
|
770 | 820 | this.buffer = buffer;
|
@@ -887,7 +937,6 @@ class SnapStream {
|
887 | 937 | this.decoder = new PcmDecoder();
|
888 | 938 | } else if (codec.codec === "opus") {
|
889 | 939 | this.decoder = new OpusDecoder();
|
890 |
| - alert("Codec not supported: " + codec.codec); |
891 | 940 | } else {
|
892 | 941 | alert("Codec not supported: " + codec.codec);
|
893 | 942 | }
|
@@ -925,10 +974,14 @@ class SnapStream {
|
925 | 974 | } else if (type === 2) {
|
926 | 975 | const pcmChunk = new PcmChunkMessage(msg.data, this.sampleFormat as SampleFormat);
|
927 | 976 | 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 | + }); |
932 | 985 | }
|
933 | 986 | } else if (type === 3) {
|
934 | 987 | this.serverSettings = new ServerSettingsMessage(msg.data);
|
|
0 commit comments