Description of the issue
My simple set up to let the user change the playback speed in Wavesurfer works like a charm on my computer (Chrome, Monterey, MacBook Pro), but it breaks on my mobile, when accessing my website, a Django project hosted on Heroku. The sound is fine as long as I keep a 1.00 x speed, but changing it wreaks havoc: the sound becomes choppy, glitchy, jerky.
The issue seems to lie in the audioContext manipulation that changes the volume of the different channels: if, in the code quoted below, I don’t call changeChannelVolume(), changing the playback rate doesn’t cause any problem. The speed example, where no audioContext, splitter or merger are created, works well with an audioRate different than 1. Again, all of this refers to my iPhone, I don’t experience any issue on my desktop browser.
- Whether I pass “audioRate: 2” as an option to the Wavesurfer creator, or set “audio.playbackRate = 2” doesn’t make a difference.
- Nor specifying “backend: WebAudio”
If you could help me fix this, I would be very grateful!
Reproducing the issue
On my phone, going to https://wavesurfer.xyz/examples/?webaudio.js and adding to the settings “audioRate: 2” causes the same behaviour as in my project, while everything works fine on my computer. Along the same lines, I tried audio.playbackRate = 2. That works on my computer, but causes the same issue on my cellphone.
Audio file format used
WAV, MP3, FLAC
System
Chrome, Safari, Firefox, iOS 17.6.1, iPhone 12 mini
Relevant code snippets
If more is needed I’ll be happy to share other parts of the code, but because the problem happens on one of the official examples, I don’t think this is necessary):
const audio = new Audio()
audio.controls = true
// Assuming only one audio file
{% for audio_file in track.get_audio_files %}
audio.src = '{{ audio_file.url }}'
{% endfor %}
const audioContext = new AudioContext()
const waveforms = [
{% for audio_file in track.get_audio_files %}
WaveSurfer.create({
container: '#waveform-{{ forloop.counter }}',
splitChannels: [
{
waveColor: "rgba(255, 255, 255, 0.7)"
},
{
waveColor: "rgba(255, 255, 255, 0.7)"
},
{
waveColor: "rgba(255, 255, 255, 0.7)"
}
],
barWidth: 5,
// Optionally, specify the spacing between bars
barGap: 5,
// And the bar radius
barRadius: 2,
plugins: [regions, hover]
hideScrollbar: false,
media: audio,
audioRate: 1,
normalize: true
})
{% endfor %}
];
function changeChannelVolume(audio) {
const mediaNode = audioContext.createMediaElementSource(audio)
// Create a splitter node that can split into 3 channels
const splitter = audioContext.createChannelSplitter(3);
// Create gain nodes for each channel
const channel1Gain = audioContext.createGain();
const channel2Gain = audioContext.createGain();
const channel3Gain = audioContext.createGain();
// Create a merger node to combine the 3 channels back into the audio stream
const merger = audioContext.createChannelMerger(3);
// Connect the splitter to the gain nodes
splitter.connect(channel1Gain, 0); // First channel
splitter.connect(channel2Gain, 1); // Second channel
splitter.connect(channel3Gain, 2); // Third channel
// Connect the gain nodes to the merger
channel1Gain.connect(merger, 0, 0); // Channel 1 to input 1 of merger
channel2Gain.connect(merger, 0, 1); // Channel 2 to input 2 of merger
// Blend third channel into both left (channel 1) and right (channel 2)
channel3Gain.connect(merger, 0, 0); // Mix channel 3 into the left channel
channel3Gain.connect(merger, 0, 1); // Mix channel 3 into the right channel
// Connect the merger back to the Wavesurfer's output
merger.connect(audioContext.destination);
// Finally, connect the wavesurfer source to the splitter
console.log("Connecting splitter to audio")
mediaNode.connect(splitter)
// Function to update gain based on slider input
function updateGain(gainNode, sliderId) {
const slider = document.getElementById(sliderId);
slider.addEventListener('change', function () {
gainNode.gain.value = parseFloat(this.value);
});
}
// Bind sliders to their respective gain nodes
updateGain(channel1Gain, 'volume-1');
updateGain(channel2Gain, 'volume-2');
updateGain(channel3Gain, 'volume-3');
}
<button type="button" class="border-0 btn btn-outline-light btn-sm" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Set the playback rate." id="playbackRateButton">
<div class="d-flex flex-nowrap">
<label for="playbackRate" class="form-label" style="margin-right: 0.3em; margin-bottom:0em"><i class="fa-solid fa-person-running"></i></label>
<input type="range" class="align-middle form-range" id="playbackRate" min="0" max="4" step="1" value="1" />
<span id="playbackRate" style="margin-left: 0.3em">1x</span>
</div>
</button>
function playbackListener() {
const speeds = [0.25, 0.5, 1, 2, 4]
// Add event listener for the playback rate slider
document.querySelector('input[id="playbackRate"]').addEventListener('input', (e) => {
// Use the slider value as an index for the speeds array
const speed = speeds[e.target.valueAsNumber]
console.log("speed", speed)
document.querySelector('span[id="playbackRate"]').textContent = speed + 'x';
primaryWaveform.setPlaybackRate(speed, true)
primaryWaveform.play();
});
};
playbackListener();