-
Notifications
You must be signed in to change notification settings - Fork 168
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ability to change the playback rate of an AudioBufferSourceNode without affecting the pitch #2487
Comments
At the risk of seeming pedantic, I always found the A common use case for changing playback rate without changing pitch is to slow down or speed up voice recordings. This use case has WCAG implications, so the type of algorithm deployed can be pretty important for businesses. There are particular time stretching algorithms that address this need, by ensuring greater vocal intelligibility at the expense of lower dynamic & frequency ranges. For music and sound effects use cases, the granularity of control is crucial since the algorithm deployed has a very noticeable impact upon the way the resulting audio will sound. This means the chosen algorithm has direct aesthetic/artistic consequences.
The playback rate of an audio buffer is conventionally understood to be a multiplier applied to the source's reported sampling frequency, which results in a change in the output pitch when no other DSP is applied. Given this, the current behavior of the My team and I have built an audio player that uses the media element's Our approach is to create an Here's some code -- would love others' thoughts on this approach and happy to answer any questions folks might have: /**
* Timestretch Worklet
* ES6 class abstraction for a phaseVocoder worklet via the AudioWorkletNode. This
* worklet is capable of shifting pitch without affecting the playback speed. Using
* this in combination with adjusting playback speed, it can be used for a
* timestretch effect in which audio playback speed changes without affecting pitch. An
* example implementation of both can be found in www.js
* @public
* @class
*/
class TimestretchWorklet {
/**
* Create new iteration of the TimestretchWorklet class along with a new AudioWorkletNode
* which is available on the class's .workletNode property.
* @param {AudioContext} ctx - Web audio context to be used
* @param {AudioBufferSourceNode} bufferSource - (optional) bufferSource to automatically connect the new AudioWorkletNode to
* @param {string} modulePath - Path to the module (for use when custom paths to local assets are needed, ie: vue.js)
* @param {opts} opts - (optional) Options to pass directly to the AudioWorkletNode
* @param {float} pitch - (optional) Initial pitch shift for the AudioWorkletNode
* @returns {TimestretchWorklet}
*/
static async createWorklet({
ctx,
bufferSource,
modulePath,
opts={},
pitch,
}) {
const worklet = new TimestretchWorklet(ctx)
try {
await ctx.audioWorklet.addModule(modulePath || 'phaseVocoder.js')
} catch (err) {
throw new Error(`Error adding module: ${err}`)
}
try {
worklet.workletNode = new AudioWorkletNode(
ctx,
'phase-vocoder-processor',
opts
)
if (pitch) {
worklet.updatePitch(pitch)
}
if (bufferSource) {
worklet.workletNode.parameters.get('playbackRate').value = bufferSource.playbackRate.value
bufferSource.connect(worklet.workletNode);
worklet.bufferSource = bufferSource;
}
// update playbackRate via message to ensure they stay in sync
worklet.workletNode.port.onmessage = (e) => {
const { data } = e
if (data.type === 'updatePlaybackRate') {
worklet.bufferSource.playbackRate.value = data.rate
}
}
} catch (err) {
throw new Error(`Error creating worklet node: ${err}`)
}
return worklet
}
/**
* Meant for interior use only via the static method createWorklet()
* @param {AudioContext} ctx - Web audio context to be used
*/
constructor(ctx) {
this.bufferSource = null;
this.ctx = ctx;
this.pitch = 1.0;
this.playbackRate = 1.0;
this.workletNode = null;
}
/**
* Connects an audio bufferSource (AudioBufferSourceNode) to the existing AudioWorkletNode
* @param {AudioBufferSourceNode} bufferSource - bufferSource connect the AudioWorkletNode
*/
connectBufferSource(bufferSource) {
if (!this.workleNode) {
throw new Error('No worklet created. Call createWorklet() first')
}
this.workletNode.parameters.get('playbackRate').value = bufferSource.playbackRate.value
bufferSource.connect(this.workletNode)
this.bufferSource = bufferSource;
}
/**
* Updates the pitch of the worklet via an {AudioParam} of the AudioWorkletNode's processor
* @param {float} pitch - Value of the pitch to set (0.1 to 2.0)
*/
updatePitch(pitch) {
this.workletNode.parameters.get('pitchFactor').value = parseFloat(pitch)
}
/**
* Updates the playback rate of the AudioWorkletNode parameter. The processor
* keep adjust the pitch to keep it the same despite the speed change.
* @param {float} pitch - Value of the pitch to set (0.1 to 2.0)
*/
updateSpeed(rate) {
let parsedRate = parseFloat(rate)
this.workletNode.parameters.get('playbackRate').value = parsedRate
}
}
module.exports = TimestretchWorklet |
WG agreed that adding |
The code example provided by @agrathwohl above made me think that it could be enough to add a separate Let's say a separate ┌──────┐ ┌────────────┐ ┌──────┐
│ ABSN │-│playbackRate│ ━━ │ CSN │
└──────┘ └────────────┘ └──────┘
┃ ┃
┌──────┐ ┌────────────┐ ┃
│ PSN │-│playbackRate│ ━━━━━━┛
└──────┘ └────────────┘
┃
┌──────┐
│ DST │
└──────┘ The audio signal would be routed from the While this approach is a little more complex than adding a simple But all of this could already be implemented using an |
After typing all of the above I realized that this is almost the same as the summary of last year's meeting. #2443 (comment) 🤦 |
We think of two paths:
That leads to: partial interface AudioBufferSourceNode {
boolean preservePitch = false;
} |
Will review on next call after grouping all related issues/requests. |
Please add this, in 99% of cases you don't want to change the pitch. And HTML Audio elements already have As a workaround I have been using this AudioWorklet to correct the pitch, but it doesn't really sound that great and you can notice a lot of artifacts. Hopefully a native solution would sound better. https://github.com/olvb/phaze |
Describe the feature
Basically the title - it would be nice to have an easy way to change the playback rate of a source node without affecting the pitch.
Is there a prototype?
Alas, I have no clue how to implement something like at this point.
Describe the feature in more detail
In my particular case, I'm working on a web app that needs synthesized audio alignment and preview. By itself it's very simple - there's no audio know-how involved at all. But users have requested an ability to change the playback rate so they could align and preview the audio data much quicker. Initially I tried simply changing
sourceNode.playbackRate.value
- after all, changing the value of an attribute with the same name on<audio>
and<video>
works just fine. But as you can guess, it didn't produce the desired result.There are also quite a few other instances when people ask for something like this:
One of those is from my own experience, the others are all from just the first page of a Google search. I'm sure I'd be able to find quite a few more if I spent more time on it.
Given that there already is such a functionality when it comes to
playbackRate
of<audio>
and<video>
, I'd think that it shouldn't be that hard from the implementation perspective to add it toAudioBufferSourceNode
as well.The text was updated successfully, but these errors were encountered: