From 966d6245ce6bbdac15dffb5897b344e7d6da110d Mon Sep 17 00:00:00 2001 From: Garrett Grimm Date: Sat, 21 Mar 2026 23:07:26 -0700 Subject: [PATCH 1/2] Fix events at final tick not firing during playback The endOfFile() check in playLoop() was evaluated before track events were processed, causing any events at the final tick to be skipped. Move the check to after event emission so all events fire before the player stops or loops. Closes #98 Co-Authored-By: Claude Opus 4.6 --- src/player.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/player.js b/src/player.js index 78132b9..f233dcf 100644 --- a/src/player.js +++ b/src/player.js @@ -201,24 +201,6 @@ class Player { this.inLoop = true; this.tick = this.getCurrentTick(); - if (!dryRun && this.endOfFile()) { - if (this.loop) { - this.resetTracks(); - this.setTempo(this.defaultTempo); - this.startTick = 0; - this.startTime = Date.now(); - this.scheduledTime = Date.now(); - this.tick = 0; - this.triggerPlayerEvent('endOfFile'); - } else { - this.stop(); - this.triggerPlayerEvent('endOfFile'); - } - - this.inLoop = false; - return; - } - this.tracks.forEach(function(track, index) { let result = track.handleEvent(this.tick, dryRun); @@ -249,6 +231,24 @@ class Player { }, this); + if (!dryRun && this.endOfFile()) { + if (this.loop) { + this.resetTracks(); + this.setTempo(this.defaultTempo); + this.startTick = 0; + this.startTime = Date.now(); + this.scheduledTime = Date.now(); + this.tick = 0; + this.triggerPlayerEvent('endOfFile'); + } else { + this.stop(); + this.triggerPlayerEvent('endOfFile'); + } + + this.inLoop = false; + return; + } + if (!dryRun && this.isPlaying()) this.triggerPlayerEvent('playing', {tick: this.tick}); this.inLoop = false; } From f45e2c42319c5d9bea7cb656d357658e35c68458 Mon Sep 17 00:00:00 2001 From: Garrett Grimm Date: Sun, 22 Mar 2026 07:43:04 -0700 Subject: [PATCH 2/2] Add test for events at the final tick being emitted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verifies that Note off events at the last tick of a MIDI file are emitted as midiEvents before endOfFile fires — the exact scenario reported in #98. Co-Authored-By: Claude Opus 4.6 --- test/test.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/test.js b/test/test.js index aec2483..2f14098 100644 --- a/test/test.js +++ b/test/test.js @@ -577,6 +577,39 @@ describe('MidiPlayerJS', function() { }); }); + describe('#final tick events', function () { + beforeEach(function() { + this.clock = sinon.useFakeTimers(); + this.clock.tick(5000); // set start time + }); + afterEach(function() { + sinon.restore(); + }); + + it('should emit events at the final tick before endOfFile fires', function () { + // Note On C4 at tick 0, Note Off C4 at tick 96 (= totalTicks), then End of Track + var midi = buildMidi([ + 0x00, 0x90, 0x3C, 0x7F, // Note On C4 vel 127 at tick 0 + 0x60, 0x80, 0x3C, 0x00, // Note Off C4 at tick 96 + ].concat(EOT)); + var events = []; + var endOfFileCount = 0; + var Player = new MidiPlayer.Player(); + Player.on('midiEvent', function(event) { events.push(event); }); + Player.on('endOfFile', function() { endOfFileCount++; }); + Player.loadArrayBuffer(midi.buffer); + Player.play(); + + // Advance well past the song length + this.clock.tick(2000); + + var noteOffEvents = events.filter(function(e) { return e.name === 'Note off'; }); + assert.ok(noteOffEvents.length > 0, 'Note off at final tick should have been emitted'); + assert.equal(noteOffEvents[0].tick, 96, 'Note off should be at the final tick'); + assert.equal(endOfFileCount, 1, 'endOfFile should have fired'); + }); + }); + describe('#loop', function () { beforeEach(function() { this.clock = sinon.useFakeTimers();