bbc / peaks.js

JavaScript UI component for interacting with audio waveforms
https://waveform.prototyping.bbc.co.uk
GNU Lesser General Public License v3.0
3.2k stars 279 forks source link

Tone.js AudioContext is not recognizes in Peaks.init() #383

Closed noaLeibman closed 3 years ago

noaLeibman commented 3 years ago

Hi! I'm working on a react app with typescript, and I've created a player that uses Tone.Player as an external player, like this:

class Player {
  externalPlayer: Tone.Player;

  constructor(url: string) {
    this.externalPlayer = new Tone.Player(url).toDestination();
    this.externalPlayer.sync();
    this.externalPlayer.start();
  }

  init() {}

  destroy() {
    Tone.context.dispose();
  }

  play() {
    Tone.Transport.start();
    return new Promise<void>((resolve) => {});
  }

  pause() {
    Tone.Transport.pause();
  }

  isPlaying() {
    return Tone.Transport.state === "started";
  }

  seek(time: number) {
    Tone.Transport.seconds = time;
  }

  isSeeking() {
    return false;
  }

  getCurrentTime() {
    return Tone.Time(Tone.Transport.position).toSeconds();
  }

  getDuration() {
    return this.externalPlayer.buffer.duration;
  }

  getBuffer() {
    return this.externalPlayer.buffer.get();
  }
};

In another class, I'm creating a Peaks instance using Tone.context as webAudio: {audioContext: Tone.context}, however I get this error in the Peaks.init callback: Peaks.init(): The webAudio.audioContext option must be a valid AudioContext

I've looked in the debugger and I can see the audioContext does exist and it is an instance of AudioContext. I have also tried using Tone.context.rawContext to get the original WebAudio type but still i get the same error.

Here is the second class I mentioned (Peaks.init is called in load() function):

class PeaksPlayer {
  zoomRef: any;
  overviewRef: any;
  peaks: PeaksInstance | undefined;
  player: Player | undefined; 

  constructor(props: PeaksPlayerProps) {
    this.zoomRef = props.zoomRef;
    this.overviewRef = props.overviewRef;
    this.peaks = undefined;
    this.player = undefined;
  }

  load(url: string) {
    if (!this.player) {
      this.player = new Player(url);
    } else {
      this.player.externalPlayer.load(url);
    }
    if (this.peaks) {
      this.peaks.destroy();
    }
    const options = {
      containers: {
        overview: this.overviewRef.current,
        zoomview: this.zoomRef.current
      },
      keyboard: true,
      webAudio: {
        audioContext: Tone.context.rawContext
      },
      player: this.player,
      zoomLevels: [128, 256, 512, 1024, 2048, 4096]
    };
    Peaks.init(options, (err, peaks) => {
      if (err) {
        console.log(err.message);
      } else {
        this.peaks = peaks;
        console.log('peaks initialized');
      }
    });
  }

  connect(node: any) {
    this.player?.externalPlayer.connect(node);
  }
}

If you any advice on what to do (also on creating this in react, I'm not sure I did the transition from the vanilla JS external-player.html example..) I would really appreciate it. Thanks!

chrisn commented 3 years ago

Have you looked at the example demo page that uses Tone.js? Tone.context should be OK, from what I can tell.

noaLeibman commented 3 years ago

Yes I did, I followed that example when writing this, but I still get that error (Peaks.init(): The webAudio.audioContext option must be a valid AudioContext).

Any idea why this might be happening?

chrisn commented 3 years ago

Looking at Tone.js 14.7.77, I see that Tone.context and Tone.context.rawContext both return Tone.js objects. To get the underlying Web Audio AudioContext, you need Tone.context.rawContext._nativeContext. Why this is so is really a question for Tone.js....

But, why do you need the AudioContext? The Peaks.js demo page uses an AudioBuffer - which is passed to the Tone.js player and to Peaks.js to produce the waveform view - and doesn't pass the AudioContext to Peaks.init(). If you pass an AudioContext then Peaks.js will assume you have an HTMLMediaElement not an external player, so isn't going to work.

noaLeibman commented 3 years ago

Thank you for your answer:)

I tried with just the audioBuffer like that- webAudio: { audioBuffer: this.player.getBuffer() } (which returns this.externalPlayer.buffer.get()). And something strange happens: In debug mode with a breakpoint before the Peaks.init() call, tha callback returns with no error and I can see the waveform. However, without that breakpoint I still get the same error as before.. Not sure what to do from here...

chrisn commented 3 years ago

I don't know what to suggest. As a test, I modified the Peaks.js external demo page to use player.externalPlayer.buffer.get() and it works OK for me.

noaLeibman commented 3 years ago

Turns out I had to await the creation of the Tone.Player (since I passed a URL in the constructor.. silly me).

Thank you so much for your help and this awesome package! :)

chrisn commented 3 years ago

I'm happy to hear you solved the problem!