Bin
2025-12-17 611bfe34c3c96199eaaf6cf9e41a75892e44e879
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// eslint-disable-next-line
// @ts-ignore
import { info } from "../Common/Utils";
import { BaseAudioDecoder } from "./BaseAudioDecoder";
 
export class WebAudioDecoder extends BaseAudioDecoder {
  private arraybuffer?: ArrayBuffer;
  private context?: OfflineAudioContext;
 
  /**
   * Initialize the decoder if it has not already been initialized.
   */
  async init(arraybuffer: ArrayBuffer) {
    this.arraybuffer = arraybuffer;
 
    info("decode:worker:ready", this.src);
  }
 
  /**
   * Decode the audio file using the WebAudio API.
   */
  async decode(options?: { multiChannel?: boolean; captureAudioBuffer?: boolean }): Promise<void | AudioBuffer> {
    // If the worker has cached data we can skip the decode step
    if (this.sourceDecoded) {
      info("decode:cached", this.src);
      return;
    }
    if (this.sourceDecodeCancelled) {
      throw new Error("WebAudioDecoder decode cancelled and contains no data, did you call decoder.renew()?");
    }
    // The decoding process is already in progress, so wait for it to finish
    if (this.decodingPromise) {
      info("decode:inprogress", this.src);
      return this.decodingPromise;
    }
    if (!this.arraybuffer) throw new Error("WebAudioDecoder not initialized, did you call decoder.init()?");
 
    info("decode:start", this.src);
 
    // Generate a unique id for this decode operation
    this.decodeId = Date.now();
    // This is a shared promise which will be observed by all instances of the same source
    this.decodingPromise = new Promise((resolve) => (this.decodingResolve = resolve as any));
 
    try {
      const buffer = (await new Promise((resolve, reject) => {
        if (!this.context) {
          this.context = this.createOfflineAudioContext();
        }
        if (!this.context || !this.arraybuffer)
          return reject(new Error("WebAudioDecoder not initialized, did you call decoder.init()?"));
        // Safari doesn't support promise based decodeAudioData by default
        if ("webkitAudioContext" in window) {
          this.context?.decodeAudioData(
            this.arraybuffer,
            (data) => resolve(data),
            (err) => reject(err),
          );
        } else {
          this.context?.decodeAudioData(this.arraybuffer).then(resolve).catch(reject);
        }
      })) as AudioBuffer;
 
      this._channelCount = options?.multiChannel ? buffer.numberOfChannels : 1;
      this._sampleRate = buffer.sampleRate;
      this._duration = buffer.duration;
 
      const chunks = Array.from({ length: this._channelCount }).map(() => Array.from({ length: 1 }) as Float32Array[]);
 
      chunks.forEach((_, index) => {
        chunks[index] = [buffer.getChannelData(index)];
      });
 
      this.chunks = chunks;
 
      info("decode:complete", this.src);
 
      if (options?.captureAudioBuffer) {
        this.buffer = buffer;
      }
 
      return buffer;
    } finally {
      this.dispose();
    }
  }
 
  /**
   * Dispose and free up resources.
   */
  protected dispose() {
    delete this.arraybuffer;
    delete this.context;
 
    this.cleanupResolvers();
  }
 
  private createOfflineAudioContext(sampleRate?: number) {
    if (!(window as any).WebAudioOfflineAudioContext) {
      (window as any).WebAudioOfflineAudioContext = new (
        window.OfflineAudioContext || (window as any).webkitOfflineAudioContext
      )(1, 2, sampleRate ?? this.sampleRate);
    }
    return (window as any).WebAudioOfflineAudioContext;
  }
}