Skip to content

Commit

Permalink
Fix MIDI playback clipping issues
Browse files Browse the repository at this point in the history
  • Loading branch information
hedgecrw committed Jul 21, 2023
1 parent 0d0b8d1 commit 1b9337b
Show file tree
Hide file tree
Showing 4 changed files with 20 additions and 18 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,5 @@ Javascript library to generate music using the Web Audio API
- [ ] Import good quality SoundFonts from https://sites.google.com/site/soundfonts4u/
- [ ] For all effects, add parameter to allow effect to slowly take effect (setTargetAtTime)
- [ ] Make library fully General MIDI 2 compliant
- [ ] Choose device to use for audio output (instead of default speakers)
- [ ] Create startAndExport() to mimic start() on AudioContext, except that it also streams to an AudioBuffer until stop() is called
- [ ] Create export() to mimic start(), except that exporting is done as quickly as possible and nothing comes out of speakers (requires knowing when to stop???)
4 changes: 2 additions & 2 deletions library/webaudioapi/effects/Panning.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export class Panning extends EffectBase {
* @returns {Promise<boolean>} Whether the effect update was successfully applied
*/
async update({leftToRightRatio}, updateTime) {
if (!intensity)
throw new WebAudioApiErrors.WebAudioValueError('Cannot update the Volume effect without at least one of the following parameters: "leftToRightRatio"');
if (leftToRightRatio == null)
throw new WebAudioApiErrors.WebAudioValueError('Cannot update the Panning effect without at least one of the following parameters: "leftToRightRatio"');
const panningValue = 2.0 * (leftToRightRatio - 0.5);
this.#panningNode.pan.setValueAtTime(panningValue, updateTime == null ? this.audioContext.currentTime : updateTime);
return true;
Expand Down
2 changes: 1 addition & 1 deletion library/webaudioapi/effects/Volume.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class Volume extends EffectBase {
* @returns {Promise<boolean>} Whether the effect update was successfully applied
*/
async update({intensity}, updateTime) {
if (!intensity)
if (intensity == null)
throw new WebAudioApiErrors.WebAudioValueError('Cannot update the Volume effect without at least one of the following parameters: "intensity"');
this.#volumeNode.gain.setValueAtTime(intensity, updateTime == null ? this.audioContext.currentTime : updateTime);
return true;
Expand Down
31 changes: 17 additions & 14 deletions library/webaudioapi/modules/Track.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,8 @@ export function createTrack(name, audioContext, tempo, trackAudioSink) {
if (!instrument)
throw new WebAudioApiErrors.WebAudioTrackError(`The current track (${name}) cannot play a note without first setting up an instrument`);
const noteSource = instrument.getNote(note); // TODO: Method to getNoteContinuous so it loops
const noteVolume = new GainNode(audioContext);
const noteVolume = new GainNode(audioContext, { gain: velocity });
noteSource.connect(noteVolume).connect(audioSink);
noteVolume.gain.setValueAtTime(velocity, 0.0);
const noteStorage = createAsyncNote(note, noteSource, noteVolume);
noteSource.onended = stopNoteAsync.bind(this, noteStorage); // TODO: Don't need this if continuous instrument
asyncAudioSources.push(noteStorage);
Expand Down Expand Up @@ -233,14 +232,12 @@ export function createTrack(name, audioContext, tempo, trackAudioSink) {
throw new WebAudioApiErrors.WebAudioTrackError(`The current track (${name}) cannot play a note without first setting up an instrument`);
const durationSeconds = (duration < 0) ? -duration : (60.0 / ((duration / tempo.beatBase) * tempo.beatsPerMinute));
const noteSource = instrument.getNote(note);
const noteVolume = new GainNode(audioContext);
const noteVolume = new GainNode(audioContext, { gain: velocity });
noteSource.connect(noteVolume).connect(audioSink);
noteVolume.gain.setValueAtTime(velocity, 0.0);
noteVolume.gain.setTargetAtTime(0.0, startTime + durationSeconds - 0.03, 0.03);
noteVolume.gain.setTargetAtTime(0.0, startTime + durationSeconds, 0.03);
noteSource.onended = sourceEnded.bind(this, noteSource, noteVolume);
audioSources.push(noteSource);
noteSource.start(startTime);
noteSource.stop(startTime + durationSeconds);
noteSource.start(startTime, 0, durationSeconds + 0.200);
return durationSeconds;
}

Expand Down Expand Up @@ -269,9 +266,9 @@ export function createTrack(name, audioContext, tempo, trackAudioSink) {
if (duration) {
const clipVolume = new GainNode(audioContext);
clipSource.connect(clipVolume).connect(audioSink);
clipVolume.gain.setTargetAtTime(0.0, startTime + duration - 0.03, 0.03);
clipVolume.gain.setTargetAtTime(0.0, startTime + duration, 0.03);
clipSource.onended = sourceEnded.bind(this, clipSource, clipVolume);
clipSource.start(startTime, 0, duration);
clipSource.start(startTime, 0, duration + 0.200);
}
else {
clipSource.connect(audioSink);
Expand All @@ -295,6 +292,10 @@ export function createTrack(name, audioContext, tempo, trackAudioSink) {
delete unmatchedNotes[note];
}
}
for (const [note, noteData] of Object.entries(unmatchedNotes)) {
const noteDuration = audioClip.getDuration() - noteData[0];
playNote(note, noteData[1], startTime + noteData[0], -noteDuration);
}
expectedDuration = (duration && (duration < audioClip.getDuration())) ? duration : audioClip.getDuration();
}
return expectedDuration;
Expand Down Expand Up @@ -342,12 +343,10 @@ export function createTrack(name, audioContext, tempo, trackAudioSink) {

function playNoteOffline(offlineContext, note, velocity, startTime, duration) {
const noteSource = instrument.getNoteOffline(offlineContext, note);
const noteVolume = new GainNode(offlineContext);
const noteVolume = new GainNode(offlineContext, { gain: velocity });
noteSource.connect(noteVolume).connect(offlineContext.destination);
noteVolume.gain.setValueAtTime(velocity, 0.0);
noteVolume.gain.setTargetAtTime(0.0, startTime + duration - 0.03, 0.03);
noteSource.start(startTime);
noteSource.stop(startTime + duration);
noteVolume.gain.setTargetAtTime(0.0, startTime + duration, 0.03);
noteSource.start(startTime, 0, duration + 0.200);
noteSources.push(noteSource);
}

Expand Down Expand Up @@ -458,6 +457,10 @@ export function createTrack(name, audioContext, tempo, trackAudioSink) {
delete unmatchedNotes[note];
}
}
for (const [note, noteData] of Object.entries(unmatchedNotes)) {
const noteDuration = recordedDuration - noteData[0];
playNoteOffline(offlineContext, note, noteData[1], noteData[0], noteDuration);
}
const renderedData = await offlineContext.startRendering();
noteSources.splice(0, noteSources.length);
return getEncoderFor(Number(encodingType)).encode(renderedData);
Expand Down

0 comments on commit 1b9337b

Please sign in to comment.