Tonejs / Tone.js

A Web Audio framework for making interactive music in the browser.
https://tonejs.github.io
MIT License
13.52k stars 983 forks source link

How to use Tone.js to preserve audio's pitch when changing its playbackRate using AudioBufferSourceNode #951

Closed Jun711 closed 3 years ago

Jun711 commented 3 years ago

I used sample code on MDN github to change an audio's playbackRate but the original audio pitch is not preserved.

audioCtx.decodeAudioData(audioData, function(buffer) {
      myBuffer = buffer;
      source.buffer = myBuffer;
      source.connect(offlineCtx.destination);
      source.playbackRate = 1.5; // <<<<<<<<< added this to change playbackRate
      source.start();
      //source.loop = true;
      offlineCtx.startRendering().then(function(renderedBuffer) {
        console.log('Rendering completed successfully');

        let song = audioCtx.createBufferSource();
        song.buffer = renderedBuffer;

        song.connect(audioCtx.destination);

        play.onclick = function() {
          song.start();
        }
      }).catch(function(err) {
          console.log('Rendering failed: ' + err);
          // Note: The promise should reject when startRendering is called a second time on an OfflineAudioContext
      });
    });

It is the same question as this Stackoverflow question

Jacob suggested in this thread that we can use Tone.js to change the pitch. I am not really sure how to make it make it work.

Screen Shot 2021-09-21 at 6 33 51 PM

I am not sure how I can achieve this and I am not familiar with semitone / cents etc. Can you someone point me to the right direction? Thank you

tambien commented 3 years ago

I would use GrainPlayer which would give you that control. Tone.PitchShift might also work, but would likely introduce more artifacts.

A semitone is a single step in a chromatic scale like between C to C#.

Jun711 commented 3 years ago

Thank you @tambien, I tried GrainPlayer but I am not sure how to make it render audioBuffer that has the playbackRate changed.

This is what I used to try

let offlineCtx = new OfflineAudioContext({
      numberOfChannels: 2,
      length: 44100 * audioBuffer.duration,
      sampleRate: 44100,
});
source = new Tone.GrainPlayer({url: audioBuffer, playbackRate: 2})
source.connect(offlineCtx.destination);
source.start(0);
offlineCtx.startRendering()

This is the code without GrainPlayer

let offlineCtx = new OfflineAudioContext({
      numberOfChannels: 2,
      length: 44100 * audioBuffer.duration,
      sampleRate: 44100,
});
source = offlineCtx.createBufferSource();
source.buffer = audioBuffer;
source.connect(offlineCtx.destination);
// change audio property here
source.start(0);
offlineCtx.startRendering() // this gives me the rendered audioBuffer with playbackRate or other properties changed
Jun711 commented 3 years ago

@tambien Thank you for your help. I have 2 questions if you don't mind.

1. I made a CodePen used GrainPlayer to play audioBook short clip at higher playbackRate and compared it with HTML audio element playing audio at a higher playbackRate. The audio playback by GrainPlayer seems to have some echo. Is that expected?

source = new Tone.GrainPlayer({url: audioBuffer, playbackRate: 2})
source.toDestination()
source.start(0);

https://codepen.io/juny711/pen/abwjaJE You can download the audio I used from my github repo

2) It is the same question as the above. How can I get the renderedAudioBuffer out from GrainPlayer so that I can pass it to OfflineAudioContext or how do I set the destination as OfflineAudioContext?