Skip to content

Commit

Permalink
Support pitch bends #30
Browse files Browse the repository at this point in the history
  • Loading branch information
infojunkie committed May 29, 2023
1 parent 1d2c9b5 commit 369a65f
Show file tree
Hide file tree
Showing 9 changed files with 430 additions and 349 deletions.
89 changes: 60 additions & 29 deletions dist/musicxml-player.esm.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*!
* musicxml-player v0.8.0
* musicxml-player v0.9.0
* (c) Karim Ratib <[email protected]> (https://github.com/infojunkie)
* Released under the GPL-3.0-only License.
*/
Expand Down Expand Up @@ -12473,11 +12473,12 @@ const MIDI_PROGRAM_DEFAULT = 1;
const SCHEDULER_NOTE_LENGTH = 10;
class WebAudioFontOutput {
constructor(midiJson) {
this.audioContext = new audioContextConstructor();
this.player = new WebAudioFontPlayer_1.WebAudioFontPlayer();
this.notes = [];
this._audioContext = new audioContextConstructor();
this._player = new WebAudioFontPlayer_1.WebAudioFontPlayer();
this._notes = [];
this._pitchBends = [];
// Scan the MIDI for "program change" events, and load the corresponding instrument sample for each.
this.channels = midiJson.tracks.reduce((channels, track) => {
this._instruments = midiJson.tracks.reduce((channels, track) => {
const pc = track.find((e) => 'programChange' in e) ||
track.reduce((pc, e) => {
if ('noteOn' in e) {
Expand All @@ -12492,10 +12493,10 @@ class WebAudioFontOutput {
}, null);
if (pc) {
if (pc.channel !== MIDI_CHANNEL_DRUMS) {
const instrumentNumber = this.player.loader.findInstrument(pc.programChange.programNumber);
const instrumentInfo = this.player.loader.instrumentInfo(instrumentNumber);
const instrumentNumber = this._player.loader.findInstrument(pc.programChange.programNumber);
const instrumentInfo = this._player.loader.instrumentInfo(instrumentNumber);
channels[pc.channel] = { instrumentInfo };
this.player.loader.startLoad(this.audioContext, instrumentInfo.url, instrumentInfo.variable);
this._player.loader.startLoad(this._audioContext, instrumentInfo.url, instrumentInfo.variable);
}
else {
channels[MIDI_CHANNEL_DRUMS] = { beats: [] };
Expand All @@ -12504,10 +12505,10 @@ class WebAudioFontOutput {
.filter((e) => 'noteOn' in e)
.map((e) => e.noteOn.noteNumber)),
].forEach((beat) => {
const drumNumber = this.player.loader.findDrum(beat);
const drumInfo = this.player.loader.drumInfo(drumNumber);
const drumNumber = this._player.loader.findDrum(beat);
const drumInfo = this._player.loader.drumInfo(drumNumber);
channels[MIDI_CHANNEL_DRUMS].beats[beat] = { drumInfo };
this.player.loader.startLoad(this.audioContext, drumInfo.url, drumInfo.variable);
this._player.loader.startLoad(this._audioContext, drumInfo.url, drumInfo.variable);
});
}
}
Expand All @@ -12518,7 +12519,7 @@ class WebAudioFontOutput {
// Then cleanup the array to keep the remaining notes.
const scheduleNotes = () => {
const now = performance.now();
this.notes
this._notes
.filter((note) => note.off !== null && note.off <= now)
.forEach((note) => {
// It can happen that the envelope expires before we get here,
Expand All @@ -12531,30 +12532,49 @@ class WebAudioFontOutput {
}
note.envelope = null;
});
this.notes = this.notes.filter((note) => !!note.envelope);
this._notes = this._notes.filter((note) => !!note.envelope);
setTimeout$1(scheduleNotes, SCHEDULER_TIMEOUT);
};
setTimeout$1(scheduleNotes, SCHEDULER_TIMEOUT);
}
send(data, timestamp) {
const event = parseMidiEvent(data);
if ('noteOn' in event) {
this.noteOn(event, timestamp);
this._noteOn(event, timestamp);
}
else if ('noteOff' in event) {
this.noteOff(event, timestamp);
this._noteOff(event, timestamp);
}
else if ('pitchBend' in event) {
this._pitchBend(event, timestamp);
}
}
noteOn(event, timestamp) {
_noteOn(event, timestamp) {
// Schedule the incoming notes to start at the incoming timestamp,
// and add them to the current notes array waiting for their future "off" event.
const instrument = event.channel === MIDI_CHANNEL_DRUMS
? this.channels[event.channel].beats[event.noteOn.noteNumber].drumInfo
.variable
: this.channels[event.channel].instrumentInfo.variable;
const when = this.audioContext.currentTime + (timestamp - performance.now()) / 1000;
const envelope = this.player.queueWaveTable(this.audioContext, this.audioContext.destination, window[instrument], when, event.noteOn.noteNumber, SCHEDULER_NOTE_LENGTH, event.noteOn.velocity / 127);
this.notes.push({
? this._instruments[event.channel].beats[event.noteOn.noteNumber]
.drumInfo.variable
: this._instruments[event.channel].instrumentInfo.variable;
const when = this._timestampToAudioContext(timestamp);
// Find the latest pitch bend that applies here.
const pb = this._pitchBends
.filter((pb) => {
return event.channel === pb.channel && when >= pb.when;
})
.reduce((max, pb) => {
return !max || pb.when > max.when ? pb : max;
}, null);
// Schedule the note.
const envelope = this._player.queueWaveTable(this._audioContext, this._audioContext.destination, window[instrument], when, event.noteOn.noteNumber, SCHEDULER_NOTE_LENGTH, event.noteOn.velocity / 127, pb
? [
{
delta: (pb.pitchBend - 8192) / 4096,
when: 0,
},
]
: []);
this._notes.push({
channel: event.channel,
pitch: event.noteOn.noteNumber,
velocity: event.noteOn.velocity,
Expand All @@ -12563,26 +12583,37 @@ class WebAudioFontOutput {
envelope,
});
}
noteOff(event, timestamp) {
_noteOff(event, timestamp) {
// WebAudioFont cannot schedule a future note cancellation,
// so we identify the target note and set its cancellation timestamp.
// Our own scheduleNotes() scheduler will take care of cancelling the note
// when its timestamp occurs.
const note = this.notes.find((note) => note.pitch === event.noteOff.noteNumber &&
const note = this._notes.find((note) => note.pitch === event.noteOff.noteNumber &&
note.channel === event.channel &&
note.off === null);
if (note) {
note.off = timestamp;
}
}
_pitchBend(event, timestamp) {
// Save the current pitch bend value. It will be used at the next noteOn event.
this._pitchBends.push({
channel: event.channel,
pitchBend: event.pitchBend,
when: this._timestampToAudioContext(timestamp),
});
}
_timestampToAudioContext(timestamp) {
return (this._audioContext.currentTime + (timestamp - performance.now()) / 1000);
}
clear() {
this.player.cancelQueue(this.audioContext);
this.notes = [];
this._player.cancelQueue(this._audioContext);
this._notes = [];
}
}

var name = "musicxml-player";
var version = "0.8.0";
var version = "0.9.0";
var description = "A simple JavaScript component that loads and plays MusicXML files in the browser using Web Audio and Web MIDI.";
var main = "dist/musicxml-player.esm.js";
var type = "module";
Expand Down Expand Up @@ -15388,11 +15419,11 @@ class VerovioRenderer {
note.setAttribute('fill', '#c00');
note.setAttribute('stroke', '#c00');
if (this._options.breaks === 'none') {
note.scrollIntoView({ 'behavior': 'auto', 'inline': 'center' });
note.scrollIntoView({ behavior: 'auto', inline: 'center' });
}
else {
const system = note.closest('.system');
system === null || system === void 0 ? void 0 : system.scrollIntoView({ 'behavior': 'auto', 'block': 'center' });
system === null || system === void 0 ? void 0 : system.scrollIntoView({ behavior: 'auto', block: 'center' });
}
});
}
Expand Down
2 changes: 1 addition & 1 deletion dist/musicxml-player.esm.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/types/VerovioRenderer.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 9 additions & 6 deletions dist/types/WebAudioFontOutput.d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import type { IMidiFile } from 'midi-json-parser-worker';
import type { IMidiOutput } from 'midi-player';
export declare class WebAudioFontOutput implements IMidiOutput {
private audioContext;
private player;
private notes;
private channels;
private _audioContext;
private _player;
private _notes;
private _instruments;
private _pitchBends;
constructor(midiJson: IMidiFile);
send(data: number[] | Uint8Array, timestamp: number): void;
private noteOn;
private noteOff;
private _noteOn;
private _noteOff;
private _pitchBend;
private _timestampToAudioContext;
clear(): void;
}
//# sourceMappingURL=WebAudioFontOutput.d.ts.map
2 changes: 1 addition & 1 deletion dist/types/WebAudioFontOutput.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 369a65f

Please sign in to comment.