Skip to content

Commit

Permalink
Allow drum note samples to play to completion
Browse files Browse the repository at this point in the history
  • Loading branch information
hedgecrw committed May 31, 2024
1 parent 85c128d commit b1c8a48
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 92 deletions.
26 changes: 15 additions & 11 deletions library/webaudioapi/modules/Track.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,13 @@ export function createTrack(name, audioContext, tempo, keySignature, trackAudioS
* @param {number} startTime - Global API time at which to start playing the note
* @param {number} duration - {@link module:Constants.Duration Duration} for which to continue playing the note
* @param {ModificationDetails[]} modifications - One or more {@link ModificationDetails Modifications} to apply to the note
* @param {boolean} [fromChord=false] - Whether this note is being played from the {@link playChord playChord()} function
* @param {boolean} isDrumNote - Whether this note is a drum note (i.e., not affected by key or duration)
* @param {boolean} [fromChord] - Whether this note is being played from the {@link playChord playChord()} function
* @returns {number} Duration (in seconds) of the note being played
* @memberof Track
* @instance
*/
function playNote(note, startTime, duration, modifications, fromChord=false) {
function playNote(note, startTime, duration, modifications, isDrumNote, fromChord=false) {
if (!instrument)
throw new WebAudioApiErrors.WebAudioTrackError(`The current track (${name}) cannot play a note without first setting up an instrument`);

Expand Down Expand Up @@ -354,10 +355,11 @@ export function createTrack(name, audioContext, tempo, keySignature, trackAudioS
const noteSource = instrument.getNote(note.note);
const noteVolume = new GainNode(audioContext, { gain: note.velocity });
noteSource.connect(noteVolume).connect(audioSink);
noteVolume.gain.setTargetAtTime(0.0, startTime + note.startTimeOffset + durationSeconds, 0.03);
if (!isDrumNote)
noteVolume.gain.setTargetAtTime(0.0, startTime + note.startTimeOffset + durationSeconds, 0.03);
noteSource.onended = sourceEnded.bind(this, noteSource, noteVolume);
audioSources.push(noteSource);
noteSource.start(startTime + note.startTimeOffset, 0, durationSeconds + 0.200);
noteSource.start(startTime + note.startTimeOffset, 0, isDrumNote ? undefined : (durationSeconds + 0.200));
}
if (newTies.includes(note.note))
waitingTies.push(newTies.splice(newTies.indexOf(note.note), 1)[0]);
Expand All @@ -383,17 +385,18 @@ export function createTrack(name, audioContext, tempo, keySignature, trackAudioS
* @param {Array<Array>}} chord - Array of `[note, duration, mods]` corresponding to the chord to be played
* @param {number} startTime - Global API time at which to start playing the chord
* @param {ModificationDetails[]} modifications - One or more {@link ModificationDetails Modifications} to apply to the chord
* @param {boolean} areDrumNotes - Whether this chord contains only drum notes (i.e., not affected by key or duration)
* @returns {number} Duration (in seconds) of the chord being played
* @memberof Track
* @instance
*/
function playChord(chord, startTime, modifications) {
function playChord(chord, startTime, modifications, areDrumNotes) {
chordIndex = (chordIndex + 1) % 2;
let minDuration = Number.POSITIVE_INFINITY;
for (const chordItem of chord) {
const [note, duration, noteMods] = chordItem;
const mods = modifications.concat(noteMods ? (Array.isArray(noteMods) ? noteMods : [noteMods]) : []);
minDuration = Math.min(minDuration, playNote(Number(note), startTime, Number(duration), mods, true));
minDuration = Math.min(minDuration, playNote(Number(note), startTime, Number(duration), mods, areDrumNotes, true));
}
chordDynamicUpdated = false;
return minDuration;
Expand All @@ -416,23 +419,24 @@ export function createTrack(name, audioContext, tempo, keySignature, trackAudioS
* @param {Array<Array|Array<Array>>} sequence - Array of `[note, duration, mods]` and/or chords corresponding to the sequence to be played
* @param {number} startTime - Global API time at which to start playing the sequence
* @param {ModificationDetails[]} modifications - One or more {@link ModificationDetails Modifications} to apply to the sequence
* @param {boolean} areDrumNotes - Whether this sequence contains only drum notes (i.e., not affected by key or duration)
* @returns {number} Duration (in seconds) of the sequence being played
* @memberof Track
* @instance
*/
function playSequence(sequence, startTime, modifications) {
function playSequence(sequence, startTime, modifications, areDrumNotes) {
let noteIndex = 0;
const originalStartTime = startTime;
for (const sequenceItem of sequence) {
++noteIndex;
for (const modification of modifications)
modification.value = inferModificationParametersFromSequence(modification.type, sequence, noteIndex, modification.value);
if (Array.isArray(sequenceItem[0]))
startTime += playChord(sequenceItem, startTime, modifications);
startTime += playChord(sequenceItem, startTime, modifications, areDrumNotes);
else {
const [note, duration, noteMods] = sequenceItem;
const mods = (noteMods ? (Array.isArray(noteMods) ? noteMods : [noteMods]) : []).concat(modifications);
startTime += playNote(Number(note), startTime, Number(duration), mods);
startTime += playNote(Number(note), startTime, Number(duration), mods, areDrumNotes);
}
}
return startTime - originalStartTime;
Expand Down Expand Up @@ -486,13 +490,13 @@ export function createTrack(name, audioContext, tempo, keySignature, trackAudioS
unmatchedNotes[note] = [ Number(noteTime), getMidiVelocity(midiData) ];
else if ((command === MidiCommand.NoteOff) && (note in unmatchedNotes)) {
const noteDuration = ((!duration || (Number(noteTime) <= duration)) ? Number(noteTime) : duration) - unmatchedNotes[note][0];
playNote(note, startTime + unmatchedNotes[note][0], -noteDuration, [{ type: ModificationType.Velocity, value: unmatchedNotes[note][1] }]);
playNote(note, startTime + unmatchedNotes[note][0], -noteDuration, [{ type: ModificationType.Velocity, value: unmatchedNotes[note][1] }], false);
delete unmatchedNotes[note];
}
}
for (const [note, noteData] of Object.entries(unmatchedNotes)) {
const noteDuration = audioClip.getDuration() - noteData[0];
playNote(note, startTime + noteData[0], -noteDuration, [{ type: ModificationType.Velocity, value: noteData[1] }]);
playNote(note, startTime + noteData[0], -noteDuration, [{ type: ModificationType.Velocity, value: noteData[1] }], false);
}
expectedDuration = (duration && (duration < audioClip.getDuration())) ? duration : audioClip.getDuration();
}
Expand Down
15 changes: 9 additions & 6 deletions library/webaudioapi/webAudioAPI.src.js
Original file line number Diff line number Diff line change
Expand Up @@ -996,20 +996,21 @@ export class WebAudioAPI {
* @param {number} startTime - Global API time at which to start playing the note
* @param {number} duration - {@link module:Constants.Duration Duration} for which to continue playing the note
* @param {ModificationDetails|ModificationDetails[]} [modifications] - Optional individual or list of modifications to apply to the note
* @param {boolean} [isDrumNote] - Optional flag indicating whether this note is a drum note (i.e., not affected by key or duration)
* @returns {Promise<number>} Duration (in seconds) of the note being played
* @see {@link module:Constants.Note Note}
* @see {@link module:Constants.Duration Duration}
* @see {@link module:Constants.ModificationType ModificationType}
* @see {@link WebAudioAPI#getModification getModification()}
*/
async playNote(trackName, note, startTime, duration, modifications=[]) {
async playNote(trackName, note, startTime, duration, modifications=[], isDrumNote=false) {
const mods = (modifications ? (Array.isArray(modifications) ? modifications : [modifications]) : []);
if (!(trackName in this.#tracks))
throw new WebAudioApiErrors.WebAudioTargetError(`The target track name (${trackName}) does not exist`);
else
checkModifications(mods, true);
const noteInKey = note ? (Number(note) + this.#key.offsets[Number(note) % 12]) : 0;
return await this.#tracks[trackName].playNote(noteInKey, Number(startTime), Number(duration), mods);
return await this.#tracks[trackName].playNote(noteInKey, Number(startTime), Number(duration), mods, isDrumNote);
}

/**
Expand All @@ -1030,13 +1031,14 @@ export class WebAudioAPI {
* @param {Array<Array>} chord - Array of `[note, duration, mods]` corresponding to the chord to be played
* @param {number} startTime - Global API time at which to start playing the chord
* @param {ModificationDetails[]} [modifications] - Optional individual or list of modifications to apply to the chord
* @param {boolean} [areDrumNotes] - Optional flag indicating whether this chord contains only drum notes (i.e., not affected by key or duration)
* @returns {Promise<number>} Duration (in seconds) of the chord being played
* @see {@link module:Constants.Note Note}
* @see {@link module:Constants.Duration Duration}
* @see {@link module:Constants.ModificationType ModificationType}
* @see {@link WebAudioAPI#getModification getModification()}
*/
async playChord(trackName, chord, startTime, modifications=[]) {
async playChord(trackName, chord, startTime, modifications=[], areDrumNotes=false) {
const mods = (modifications ? (Array.isArray(modifications) ? modifications : [modifications]) : []);
if (!(trackName in this.#tracks))
throw new WebAudioApiErrors.WebAudioTargetError(`The target track name (${trackName}) does not exist`);
Expand All @@ -1046,7 +1048,7 @@ export class WebAudioAPI {
checkModifications(mods, true);
for (const chordItem of chord)
chordItem[0] = chordItem[0] ? (Number(chordItem[0]) + this.#key.offsets[Number(chordItem[0]) % 12]) : 0;
return await this.#tracks[trackName].playChord(chord, Number(startTime), mods);
return await this.#tracks[trackName].playChord(chord, Number(startTime), mods, areDrumNotes);
}

/**
Expand All @@ -1068,13 +1070,14 @@ export class WebAudioAPI {
* @param {Array<Array|Array<Array>>} sequence - Array of `[note, duration, mods]` and/or chords corresponding to the sequence to be played
* @param {number} startTime - Global API time at which to start playing the sequence
* @param {ModificationDetails[]} [modifications] - Optional individual or list of modifications to apply to the sequence
* @param {boolean} [areDrumNotes] - Optional flag indicating whether this sequence contains only drum notes (i.e., not affected by key or duration)
* @returns {Promise<number>} Duration (in seconds) of the sequence being played
* @see {@link module:Constants.Note Note}
* @see {@link module:Constants.Duration Duration}
* @see {@link module:Constants.ModificationType ModificationType}
* @see {@link WebAudioAPI#getModification getModification()}
*/
async playSequence(trackName, sequence, startTime, modifications=[]) {
async playSequence(trackName, sequence, startTime, modifications=[], areDrumNotes=false) {
const mods = (modifications ? (Array.isArray(modifications) ? modifications : [modifications]) : []);
if (!(trackName in this.#tracks))
throw new WebAudioApiErrors.WebAudioTargetError(`The target track name (${trackName}) does not exist`);
Expand All @@ -1090,7 +1093,7 @@ export class WebAudioAPI {
else
sequenceItem[0] = sequenceItem[0] ? (Number(sequenceItem[0]) + this.#key.offsets[Number(sequenceItem[0]) % 12]) : 0;
}
return await this.#tracks[trackName].playSequence(sequence, Number(startTime), mods);
return await this.#tracks[trackName].playSequence(sequence, Number(startTime), mods, areDrumNotes);
}

/**
Expand Down
Loading

0 comments on commit b1c8a48

Please sign in to comment.