Skip to content

Commit

Permalink
Add looping support in midi-player; Add baiao demo
Browse files Browse the repository at this point in the history
  • Loading branch information
infojunkie committed Aug 11, 2024
1 parent cc88fed commit 5e4350c
Show file tree
Hide file tree
Showing 12 changed files with 1,556 additions and 367 deletions.
Binary file added demo/data/baiao-miranda.mid
Binary file not shown.
1,133 changes: 1,133 additions & 0 deletions demo/data/baiao-miranda.musicxml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions demo/data/baiao-miranda.timemap.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"measure":0,"timestamp":0,"duration":1224},{"measure":1,"timestamp":1224.488,"duration":1224},{"measure":2,"timestamp":2448.976,"duration":1224},{"measure":3,"timestamp":3673.464,"duration":1224}]
4 changes: 3 additions & 1 deletion demo/demo.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,9 @@ function handleAudioDelayChange(e) {

function handleVelocityChange(e) {
g_state.params.set('velocity', e.target.value);
g_state.player?.timingObject.update({ velocity: Number(e.target.value) });
if (g_state.player) {
g_state.player.velocity = Number(e.target.value);
}
savePlayerOptions();
}

Expand Down
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ <h1>MusicXML Player Demo</h1>
<option value="data/salma-ya-salama.mxl">Salma ya Salama (Compressed MusicXML)</option>
<option value="data/asa-branca.musicxml">Asa Branca (Uncompressed MusicXML)</option>
<option value="data/rast-tetrachord.musicxml">Rast Tetrachords (Microtonal MusicXML)</option>
<option value="data/baiao-miranda.musicxml">Baião Groove (Rhythm MusicXML)</option>
<option value="data/jazz.txt">Playlist: Jazz 1400 (iReal Pro)</option>
<option value="data/brazilian.txt">Playlist: Brazilian 150 (iReal Pro)</option>
<option value="data/stevie-wonder.txt">Playlist: Stevie Wonder 30 (iReal Pro)</option>
Expand Down
123 changes: 84 additions & 39 deletions dist/musicxml-player.esm.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*!
* musicxml-player v0.17.2
* musicxml-player v0.18.0
* (c) Karim Ratib <[email protected]> (https://github.com/infojunkie)
* Released under the GPL-3.0-only License.
*/
Expand Down Expand Up @@ -696,6 +696,7 @@ class MidiPlayer {
this._startScheduler = startScheduler;
this._state = null;
this._velocity = 1;
this._repeat = 1;
this._latest = MidiPlayer._getMaxTimestamp(json);
}
get position() {
Expand Down Expand Up @@ -799,24 +800,30 @@ class MidiPlayer {
this._clear();
this._pause(this._state);
}
play(velocity) {
play(velocity, repeat) {
if (this.state !== PlayerState.Stopped) {
throw new Error('The player is not currently stopped.');
}
// Here, we set the internal variable because we're already stopped and no further state adjustment is needed.
if (typeof velocity !== 'undefined') {
this._velocity = velocity;
}
if (typeof repeat !== 'undefined') {
this._repeat = repeat;
}
return this._promise();
}
resume(velocity) {
resume(velocity, repeat) {
if (this.state !== PlayerState.Paused) {
throw new Error('The player is not currently paused.');
}
// Here, we set the public variable to adjust internal state.
if (typeof velocity !== 'undefined') {
this.velocity = velocity;
}
if (typeof repeat !== 'undefined') {
this._repeat = repeat;
}
return this._promise();
}
stop() {
Expand Down Expand Up @@ -844,11 +851,25 @@ class MidiPlayer {
return new Promise((resolve) => {
const { stop: stopScheduler, reset: resetScheduler, now: nowScheduler } = this._startScheduler(({ end, start }) => {
if (this._state === null) {
this._state = { offset: start, resolve, stopScheduler: null, resetScheduler: null, nowScheduler: null, paused: null };
this._state = {
next: null,
nowScheduler: null,
offset: start,
paused: null,
repeat: this._repeat,
resetScheduler: null,
resolve,
stopScheduler: null,
};
}
if (this._state.paused !== null) {
this._state.offset = start - this._state.paused;
this._state.paused = null;
this._state.resolve = resolve;
}
if (this._state.next !== null) {
this._state.offset = this._state.next;
this._state.next = null;
}
this._schedule(start, end, this._state);
});
Expand All @@ -867,8 +888,28 @@ class MidiPlayer {
events
.filter(({ event }) => this._filterMidiMessage(event))
.forEach(({ event, time }) => this._midiOutput.send(this._encodeMidiMessage(event), start + time / this._velocity));
if ((start - state.offset) * this._velocity >= this._latest) {
this._stop(state);
// Check if we're at the end of the file.
const wrapping = (end - state.offset) * this._velocity - this._latest;
if (state.repeat === 0) {
// If we're no longer looping, stop the player.
if (state.nowScheduler !== null && (state.nowScheduler() - state.offset) * this._velocity >= this._latest) {
this._stop(state);
}
}
else if (wrapping >= 0) {
// Decrement the loop counter.
if (state.repeat > 0) {
state.repeat -= 1;
}
// If we're still looping, schedule the starting events from the next cycle.
// Otherwise, wait until next cycle to stop the player.
if (state.repeat !== 0) {
const events2 = this._midiFileSlicer.slice(0, wrapping);
events2
.filter(({ event }) => this._filterMidiMessage(event))
.forEach(({ event, time }) => this._midiOutput.send(this._encodeMidiMessage(event), end + (time - wrapping) / this._velocity));
state.next = state.offset + this._latest / this._velocity;
}
}
}
_stop(state) {
Expand Down Expand Up @@ -933,7 +974,7 @@ const createMidiPlayerFactory = (createMidiFileSlicer, startScheduler) => {
const INTERVAL = 500;
const createStartScheduler = (clearInterval, performance, setInterval) => (next) => {
const start = performance.now();
let nextTick = start + INTERVAL;
let nextTick = start;
let end = nextTick + INTERVAL;
const intervalId = setInterval(() => {
if (performance.now() >= nextTick) {
Expand Down Expand Up @@ -12547,7 +12588,7 @@ const timingObjectConstructor = createTimingObjectConstructor(createCalculateTim
// @todo Expose an isSupported flag which checks for performance.now() support.

var name = "musicxml-player";
var version = "0.17.2";
var version = "0.18.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 @@ -12695,7 +12736,6 @@ class Player {
}
});
}
//private _timingObjectUpdating: boolean;
constructor(_options, _sheet, _parseResult, _musicXml) {
var _a, _b, _c, _d, _e, _f;
this._options = _options;
Expand Down Expand Up @@ -12751,8 +12791,12 @@ class Player {
},
});
// Initialize the playback options.
this._repeat = this._repeatCounter = (_d = this._options.repeat) !== null && _d !== void 0 ? _d : 1;
this._repeat = (_d = this._options.repeat) !== null && _d !== void 0 ? _d : 1;
this._mute = (_e = this._options.mute) !== null && _e !== void 0 ? _e : false;
this._velocity = (_f = this._options.velocity) !== null && _f !== void 0 ? _f : 1;
this._duration =
this._options.converter.timemap.last().timestamp +
this._options.converter.timemap.last().duration;
// Set up resize handling.
// Throttle the resize event https://stackoverflow.com/a/5490021/209184
let timeout = undefined;
Expand All @@ -12764,17 +12808,14 @@ class Player {
});
this._observer.observe(this._sheet);
// Create the TimingObject.
this._timingObject = new timingObjectConstructor({ velocity: (_f = this._options.velocity) !== null && _f !== void 0 ? _f : 1, position: 0 }, 0, this._options.converter.timemap.last().timestamp +
this._options.converter.timemap.last().duration);
this._timingObject = new timingObjectConstructor({ velocity: this._velocity, position: 0 }, 0, this.duration);
this._timingObjectListener = (event) => this._handleTimingObjectChange(event);
//this._timingObjectUpdating = false;
this._timingObject.addEventListener('change', this._timingObjectListener);
}
/**
* Destroy the instance by freeing all resources and disconnecting observers.
*/
destroy() {
this._repeatCounter = 0;
this._timingObject.removeEventListener('change', this._timingObjectListener);
this._sheet.remove();
this._observer.disconnect();
Expand All @@ -12796,7 +12837,7 @@ class Player {
}
// Set the playback position.
// Find the closest instance of the measure based on current playback position.
const position = this._midiPlayer.position - measureOffset;
const position = this.position - measureOffset;
const entry = this._options.converter.timemap
.filter((e) => e.measure == measureIndex)
.sort((a, b) => {
Expand Down Expand Up @@ -12824,7 +12865,6 @@ class Player {
if (this._output instanceof WebAudioFontOutput) {
yield this._output.initialize();
}
this._repeatCounter = this._repeat;
yield this._play();
});
}
Expand All @@ -12835,16 +12875,15 @@ class Player {
if (this._midiPlayer.state !== PlayerState.Playing)
return;
this._midiPlayer.pause();
this._timingObjectUpdate({ velocity: 0 });
this._timingObject.update({ velocity: 0 });
}
/**
* Stop playback and rewind to start.
*/
rewind() {
this._repeatCounter = 0;
this._midiPlayer.stop();
this._options.renderer.moveTo(0, 0, 0);
this._timingObjectUpdate({ velocity: 0, position: 0 });
this._timingObject.update({ velocity: 0, position: 0 });
}
/**
* The version numbers of the player components.
Expand Down Expand Up @@ -12886,10 +12925,17 @@ class Player {
}
/**
* The duration of the score/MIDI file (ms).
* Precomputed in the constructor.
*/
get duration() {
return (this._options.converter.timemap.last().timestamp +
this._options.converter.timemap.last().duration);
return this._duration;
}
/**
* Current position of the player (ms).
*/
get position() {
var _a;
return Math.max(0, Math.min((_a = this._midiPlayer.position) !== null && _a !== void 0 ? _a : 0, this._duration - 1));
}
/**
* The TimingObject attached to the player.
Expand All @@ -12901,7 +12947,7 @@ class Player {
* Repeat count. A value of -1 means loop forever.
*/
set repeat(value) {
this._repeat = value < 0 ? -1 : Math.max(1, Math.trunc(value));
this._repeat = value;
}
/**
* A flag to mute the player's MIDI output.
Expand All @@ -12912,6 +12958,17 @@ class Player {
this.clear();
}
}
/**
* Playback speed. A value of 1 means normal speed.
*/
set velocity(value) {
this._velocity = value;
if (this._midiPlayer.state === PlayerState.Playing) {
this._midiPlayer.pause();
this._timingObject.update({ velocity: this._velocity });
this._play();
}
}
/**
* Implementation of IMidiOutput.send().
*
Expand Down Expand Up @@ -12943,7 +13000,8 @@ class Player {
if (this._midiPlayer.state !== PlayerState.Playing)
return;
// Lookup the current measure number by binary-searching the timemap.
const timestamp = this._midiPlayer.position;
// TODO Optimize search by starting at current measure.
const timestamp = this.position;
const index = binarySearch(this._options.converter.timemap, {
measure: 0,
timestamp,
Expand All @@ -12957,32 +13015,19 @@ class Player {
// Update the cursors and listeners.
const entry = this._options.converter.timemap[index >= 0 ? index : Math.max(0, -index - 2)];
this._options.renderer.moveTo(entry.measure, entry.timestamp, Math.max(0, timestamp - entry.timestamp), entry.duration);
this._timingObjectUpdate({ position: timestamp });
this._timingObject.update({ position: timestamp });
// Schedule next cursor movement.
requestAnimationFrame(synchronizeMidi);
};
// Schedule first cursor movement.
requestAnimationFrame(synchronizeMidi);
// Activate the MIDI player.
const { velocity } = this.timingObject.query();
if (this._midiPlayer.state === PlayerState.Paused) {
yield this._midiPlayer.resume(velocity);
yield this._midiPlayer.resume(this._velocity, this._repeat);
}
else {
yield this._midiPlayer.play(velocity);
yield this._midiPlayer.play(this._velocity, this._repeat);
}
// Repeat if needed.
if (this._midiPlayer.state === PlayerState.Stopped) {
if (this._repeatCounter < 0 || Math.max(0, --this._repeatCounter) > 0) {
this._play();
}
}
});
}
_timingObjectUpdate(newVector) {
return __awaiter(this, void 0, void 0, function* () {
//this._timingObjectUpdating = true;
this._timingObject.update(newVector);
});
}
_handleTimingObjectChange(_event) {
Expand Down
2 changes: 1 addition & 1 deletion dist/musicxml-player.esm.js.map

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions dist/types/Player.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export interface PlayerOptions {
repeat?: number;
/**
* (Optional) Playback speed. A value of 1 means normal speed.
* Can also be changed dynamically via Player.timingObject.update({ velocity }).
* Can also be changed dynamically via Player.velocity attribute.
*/
velocity?: number;
}
Expand All @@ -66,9 +66,10 @@ export declare class Player implements IMidiOutput {
private _midiPlayer;
private _observer;
private _midiFile;
private _duration;
private _mute;
private _repeat;
private _repeatCounter;
private _velocity;
private _timingObject;
private _timingObjectListener;
private constructor();
Expand Down Expand Up @@ -122,8 +123,13 @@ export declare class Player implements IMidiOutput {
get title(): string;
/**
* The duration of the score/MIDI file (ms).
* Precomputed in the constructor.
*/
get duration(): number;
/**
* Current position of the player (ms).
*/
get position(): number;
/**
* The TimingObject attached to the player.
*/
Expand All @@ -136,6 +142,10 @@ export declare class Player implements IMidiOutput {
* A flag to mute the player's MIDI output.
*/
set mute(value: boolean);
/**
* Playback speed. A value of 1 means normal speed.
*/
set velocity(value: number);
/**
* Implementation of IMidiOutput.send().
*
Expand All @@ -151,7 +161,6 @@ export declare class Player implements IMidiOutput {
*/
clear(): void;
private _play;
private _timingObjectUpdate;
private _handleTimingObjectChange;
private static _unroll;
}
Expand Down
2 changes: 1 addition & 1 deletion dist/types/Player.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 5e4350c

Please sign in to comment.