Skip to content

Commit

Permalink
Fix #15 support playback speed aka velocity
Browse files Browse the repository at this point in the history
  • Loading branch information
infojunkie committed May 19, 2024
1 parent c092cbc commit 07970ef
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 51 deletions.
13 changes: 9 additions & 4 deletions demo/demo.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ function handlePlayPauseKey(e) {
g_state.timingObject?.update({ velocity: 0 });
}
else {
g_state.timingObject?.update({ velocity: 1 });
g_state.timingObject?.update({ velocity: Number(document.getElementById('velocity').value) });
}
}
}
Expand Down Expand Up @@ -388,14 +388,18 @@ function handleAudioLoaded(e) {
);
}

function handleAudioDelaychange(e) {
function handleAudioDelayChange(e) {
setTimingsrc(
document.getElementById('audio-track'),
g_state.timingObject,
({ position, ...vector }) => ({ ...vector, position: position + Number(document.getElementById('audio-offset').value) / 1000 })
);
}

function handleVelocityChange(e) {
g_state.timingObject?.update({ velocity: Number(e.target.value) });
}

document.addEventListener('DOMContentLoaded', async () => {
// Load the parameters from local storage and/or the URL.
const params = new URLSearchParams(document.location.search);
Expand Down Expand Up @@ -424,7 +428,7 @@ document.addEventListener('DOMContentLoaded', async () => {
}
});
document.getElementById('play').addEventListener('click', async () => {
g_state.timingObject?.update({ velocity: 1 });
g_state.timingObject?.update({ velocity: Number(document.getElementById('velocity').value) });
});
document.getElementById('pause').addEventListener('click', async () => {
g_state.timingObject?.update({ velocity: 0 });
Expand All @@ -440,7 +444,8 @@ document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('ireal').addEventListener('change', handleIRealChange);
document.getElementById('audio-file').addEventListener('change', handleAudioChange);
document.getElementById('audio-track').addEventListener('loadeddata', handleAudioLoaded);
document.getElementById('audio-offset').addEventListener('change', handleAudioDelaychange);
document.getElementById('audio-offset').addEventListener('change', handleAudioDelayChange);
document.getElementById('velocity').addEventListener('change', handleVelocityChange);
document.querySelectorAll('.player-option').forEach(element => {
if (!!g_state.options[element.id.replace('option-', '')]) {
element.setAttribute('checked', 'checked');
Expand Down
5 changes: 5 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ <h1>MusicXML Player Demo</h1>
</div>
</div>

<div>
<label for="velocity">Velocity</label>
<input type="number" id="velocity" name="velocity" value="1" min="0.25" max="2" step="0.25"/>
</div>

<div id="player">
<button class="player" id="rewind"></button>
<button class="player" id="pause"></button>
Expand Down
111 changes: 88 additions & 23 deletions dist/musicxml-player.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -699,29 +699,42 @@ class MidiPlayer {
this._velocity = 1;
}
get position() {
// STOPPED: Position is undefined.
if (this.state === PlayerState.Stopped) {
return null;
return undefined;
}
const state = this._state;
// PAUSED: Position of pause offset in real-time space.
if (state.paused !== null) {
return state.paused;
return state.paused * this._velocity;
}
// PLAYING: Currrent offset in real-time space.
const nowScheduler = state.nowScheduler;
return nowScheduler() - state.offset;
return (nowScheduler() - state.offset) * this._velocity;
}
set position(position) {
var _a;
// STOPPED: Exception.
if (this.state === PlayerState.Stopped) {
throw new Error('The player is currently stopped.');
}
// No change, do nothing.
if (Math.abs(position - this.position) < Number.EPSILON) {
return;
}
// Whatever comes next, stop current notes.
this._clear();
const state = this._state;
// PAUSED: Reposition pause offset in velocity space.
// Decrement by small value to ensure current events are not missed.
if (this.state === PlayerState.Paused) {
state.paused = position - 1;
state.paused = position / this._velocity - 1;
}
// PLAYING: Reposition playing offset in velocity space.
// Reset the scheduler instantaneously.
else if (this.state === PlayerState.Playing) {
const nowScheduler = state.nowScheduler;
state.offset = nowScheduler() - position;
state.offset = nowScheduler() - position / this._velocity;
(_a = state.resetScheduler) === null || _a === void 0 ? void 0 : _a.call(state);
}
}
Expand All @@ -735,11 +748,49 @@ class MidiPlayer {
return PlayerState.Playing;
}
get velocity() {
// STOPPED: Velocity is undefined.
if (this.state === PlayerState.Stopped) {
return undefined;
}
// PAUSED: Velocity is 0.
if (this.state === PlayerState.Paused) {
return 0;
}
return this._velocity;
}
set velocity(velocity) {
// TODO: Handle zero velocity.
this._velocity = velocity;
var _a;
// STOPPED: Exception.
if (this.state === PlayerState.Stopped) {
throw new Error('The player is currently stopped.');
}
// No change, do nothing.
if (Math.abs(velocity - this._velocity) < Number.EPSILON) {
return;
}
// Whatever comes next, stop current notes.
this._clear();
const state = this._state;
// PAUSED: If v > 0, reposition paused offset in new velocity space.
if (this.state === PlayerState.Paused) {
if (Math.abs(velocity) > Number.EPSILON) {
state.paused = this.position / velocity;
this._velocity = velocity;
}
}
// PLAYING: If v > 0, reposition playing offset in new velocity space.
// If v == 0, pause (without saving v to remember current velocity).
else if (this.state === PlayerState.Playing) {
if (Math.abs(velocity) > Number.EPSILON) {
const nowScheduler = state.nowScheduler;
state.offset = nowScheduler() - this.position / velocity;
this._velocity = velocity;
(_a = state.resetScheduler) === null || _a === void 0 ? void 0 : _a.call(state);
}
else {
this._pause(this._state);
}
}
}
pause() {
if (this.state !== PlayerState.Playing) {
Expand All @@ -748,16 +799,20 @@ class MidiPlayer {
this._clear();
this._pause(this._state);
}
play() {
play(velocity = 1) {
if (this.state !== PlayerState.Stopped) {
throw new Error('The player is not currently stopped.');
}
// Set internal variable only because we are currently stopped.
this._velocity = velocity;
return this._promise();
}
resume() {
resume(velocity = 1) {
if (this.state !== PlayerState.Paused) {
throw new Error('The player is not currently paused.');
}
// Set public variable to adjust internal state.
this.velocity = velocity;
return this._promise();
}
stop() {
Expand Down Expand Up @@ -804,12 +859,13 @@ class MidiPlayer {
});
}
_schedule(start, end, state) {
const events = this._midiFileSlicer.slice(start - state.offset, end - state.offset);
const events = this._midiFileSlicer.slice((start - state.offset) * this._velocity, (end - state.offset) * this._velocity);
events
.filter(({ event }) => this._filterMidiMessage(event))
.forEach(({ event, time }) => {
this._midiOutput.send(this._encodeMidiMessage(event), start + time);
state.latest = Math.max(state.latest, start + time);
const timestamp = start + time / this._velocity;
this._midiOutput.send(this._encodeMidiMessage(event), timestamp);
state.latest = Math.max(state.latest, timestamp);
});
const endedTracks = events.filter(({ event }) => MidiPlayer._isEndOfTrack(event)).length;
state.endedTracks += endedTracks;
Expand Down Expand Up @@ -12314,18 +12370,19 @@ class Player {
}
moveTo(measureIndex, measureStart, measureOffset) {
// Set the playback position.
this._midiPlayer.position = this._timemap[measureIndex].start + measureOffset;
this._midiPlayer.position =
this._timemap[measureIndex].start + measureOffset;
// Set the cursor position.
this._renderer.moveTo(measureIndex, measureStart, measureOffset);
}
play() {
return __awaiter(this, void 0, void 0, function* () {
return __awaiter(this, arguments, void 0, function* (velocity = 1) {
if (this._midiPlayer.state === PlayerState.Playing)
return;
if (this._output instanceof WebAudioFontOutput) {
yield this._output.init();
}
yield this._play();
yield this._play(velocity);
});
}
pause() {
Expand Down Expand Up @@ -12389,7 +12446,7 @@ class Player {
var _a, _b;
(_b = (_a = this._output).clear) === null || _b === void 0 ? void 0 : _b.call(_a);
}
_play() {
_play(velocity) {
return __awaiter(this, void 0, void 0, function* () {
const synchronizeMidi = () => {
if (this._midiPlayer.state !== PlayerState.Playing)
Expand All @@ -12414,26 +12471,34 @@ class Player {
requestAnimationFrame(synchronizeMidi);
// Activate the MIDI player.
if (this._midiPlayer.state === PlayerState.Paused) {
yield this._midiPlayer.resume();
yield this._midiPlayer.resume(velocity);
}
else {
yield this._midiPlayer.play();
yield this._midiPlayer.play(velocity);
}
});
}
_handleTimingsrcChange(_event) {
var _a;
const vector = (_a = this._timingsrc) === null || _a === void 0 ? void 0 : _a.query();
if ((vector === null || vector === void 0 ? void 0 : vector.velocity) === 0) {
if ((vector === null || vector === void 0 ? void 0 : vector.position) === 0) {
const timingsrc = this._timingsrc;
const vector = timingsrc.query();
if (Math.abs(vector.velocity) < Number.EPSILON) {
if (Math.abs(vector.position) < Number.EPSILON) {
this.rewind();
}
else {
this.pause();
}
}
else {
this.play();
switch (this._midiPlayer.state) {
case PlayerState.Playing:
this._midiPlayer.velocity = vector.velocity;
break;
case PlayerState.Paused:
case PlayerState.Stopped:
this.play(vector.velocity);
break;
}
}
}
_midiPlayerStop() {
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/Player.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export declare class Player implements IMidiOutput {
private constructor();
destroy(): void;
moveTo(measureIndex: MeasureIndex, measureStart: MillisecsTimestamp, measureOffset: MillisecsTimestamp): void;
play(): Promise<void>;
play(velocity?: number): Promise<void>;
pause(): Promise<void>;
rewind(): Promise<void>;
get musicXml(): string;
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.

20 changes: 10 additions & 10 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "musicxml-player",
"version": "0.15.5",
"version": "0.16.0",
"description": "A simple JavaScript component that loads and plays MusicXML files in the browser using Web Audio and Web MIDI.",
"main": "dist/musicxml-player.esm.js",
"type": "module",
Expand Down
Loading

0 comments on commit 07970ef

Please sign in to comment.