From 502952ece49d33d667e140a5467846bab5ef706c Mon Sep 17 00:00:00 2001 From: Garrett Grimm Date: Sat, 21 Mar 2026 14:07:33 -0700 Subject: [PATCH 1/3] Add getLyrics() method to fetch lyric events Adds a new Player#getLyrics(trackNumber?) method that returns all parsed Lyric meta events from the loaded MIDI file, optionally filtered to a specific 1-based track number. Closes #84 Co-Authored-By: Claude Sonnet 4.6 --- src/player.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/player.js b/src/player.js index 173adbd..9bc4249 100644 --- a/src/player.js +++ b/src/player.js @@ -454,6 +454,17 @@ class Player { return this.tracks.map(track => track.events); } + /** + * Gets all Lyric events, optionally filtered to a specific track. + * @param {number} [trackNumber] - Optional 1-based track number to filter by. + * @return {array} + */ + getLyrics(trackNumber) { + return this.events + .flat() + .filter(e => e.name === 'Lyric' && (trackNumber == null || e.track === trackNumber)); + } + /** * Gets total number of ticks in the loaded MIDI file. * @return {number} From ba9fa3e58ddd430e9bfb88e77dce9fc29f19114f Mon Sep 17 00:00:00 2001 From: Garrett Grimm Date: Sat, 21 Mar 2026 22:06:56 -0700 Subject: [PATCH 2/3] Add tests for getLyrics() Co-Authored-By: Claude Sonnet 4.6 --- test/test.js | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/test/test.js b/test/test.js index a146c22..54b2527 100644 --- a/test/test.js +++ b/test/test.js @@ -546,6 +546,62 @@ describe('MidiPlayerJS', function() { assert.equal(Player.tempoMap[1].tempo, 200); }); + describe('#getLyrics()', function () { + it('should return all lyric events across all tracks', function () { + // Two Lyric events: "Hel-" at tick 0, "lo" at tick 96 + // Lyric: FF 05 + var midi = buildMidi([ + 0x00, 0xFF, 0x05, 0x04, 0x48, 0x65, 0x6C, 0x2D, // "Hel-" at tick 0 + 0x60, 0xFF, 0x05, 0x02, 0x6C, 0x6F, // "lo" at tick 96 + ].concat(EOT)); + var Player = new MidiPlayer.Player(); + Player.loadArrayBuffer(midi.buffer); + var lyrics = Player.getLyrics(); + assert.equal(lyrics.length, 2); + assert.equal(lyrics[0].name, 'Lyric'); + assert.equal(lyrics[0].string, 'Hel-'); + assert.equal(lyrics[0].tick, 0); + assert.equal(lyrics[1].string, 'lo'); + assert.equal(lyrics[1].tick, 96); + }); + + it('should return empty array when no lyric events exist', function () { + var midi = buildMidi([0x00, 0x90, 0x3C, 0x7F].concat(EOT)); + var Player = new MidiPlayer.Player(); + Player.loadArrayBuffer(midi.buffer); + assert.deepEqual(Player.getLyrics(), []); + }); + + it('should filter lyrics by track number', function () { + // Format 1 MIDI with two tracks, each with a Lyric event + var midi = new Uint8Array([ + 0x4D, 0x54, 0x68, 0x64, + 0x00, 0x00, 0x00, 0x06, + 0x00, 0x01, // Format 1 + 0x00, 0x02, // 2 tracks + 0x00, 0x60, // Division = 96 + // Track 1: Lyric "A" (5 bytes) + EOT (4 bytes) = 9 bytes + 0x4D, 0x54, 0x72, 0x6B, + 0x00, 0x00, 0x00, 0x09, + 0x00, 0xFF, 0x05, 0x01, 0x41, // "A" at tick 0 + 0x00, 0xFF, 0x2F, 0x00, + // Track 2: Lyric "B" (5 bytes) + EOT (4 bytes) = 9 bytes + 0x4D, 0x54, 0x72, 0x6B, + 0x00, 0x00, 0x00, 0x09, + 0x00, 0xFF, 0x05, 0x01, 0x42, // "B" at tick 0 + 0x00, 0xFF, 0x2F, 0x00, + ]); + var Player = new MidiPlayer.Player(); + Player.loadArrayBuffer(midi.buffer); + var track1Lyrics = Player.getLyrics(1); + assert.equal(track1Lyrics.length, 1); + assert.equal(track1Lyrics[0].string, 'A'); + var track2Lyrics = Player.getLyrics(2); + assert.equal(track2Lyrics.length, 1); + assert.equal(track2Lyrics[0].string, 'B'); + }); + }); + it('should use default 120 BPM at tick 0 when no Set Tempo at tick 0 exists', function () { // Format 1 MIDI with Set Tempo only at tick 480 (200 BPM) var midi = new Uint8Array([ From fb7e35c132a7a23efd8eab85b2915f25f61a7420 Mon Sep 17 00:00:00 2001 From: Garrett Grimm Date: Sat, 21 Mar 2026 22:12:01 -0700 Subject: [PATCH 3/3] Fix test nesting, add track assertion, clarify JSDoc for getLyrics - Move describe('#getLyrics()') to be a proper sibling under MidiPlayerJS - Add assert.equal(lyrics[0].track, 1) to verify track assignment - Note in JSDoc that only Lyric (FF 05) events are returned, not Text (FF 01) Co-Authored-By: Claude Opus 4.6 --- src/player.js | 3 ++- test/test.js | 63 ++++++++++++++++++++++++++------------------------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/player.js b/src/player.js index 9bc4249..57caec6 100644 --- a/src/player.js +++ b/src/player.js @@ -455,7 +455,8 @@ class Player { } /** - * Gets all Lyric events, optionally filtered to a specific track. + * Gets all Lyric (FF 05) meta events, optionally filtered to a specific track. + * Note: Some MIDI files store lyrics as Text (FF 01) events instead; those are not included here. * @param {number} [trackNumber] - Optional 1-based track number to filter by. * @return {array} */ diff --git a/test/test.js b/test/test.js index 54b2527..8516aad 100644 --- a/test/test.js +++ b/test/test.js @@ -546,6 +546,37 @@ describe('MidiPlayerJS', function() { assert.equal(Player.tempoMap[1].tempo, 200); }); + it('should use default 120 BPM at tick 0 when no Set Tempo at tick 0 exists', function () { + // Format 1 MIDI with Set Tempo only at tick 480 (200 BPM) + var midi = new Uint8Array([ + // MThd + 0x4D, 0x54, 0x68, 0x64, + 0x00, 0x00, 0x00, 0x06, + 0x00, 0x01, // Format 1 + 0x00, 0x02, // 2 tracks + 0x01, 0xE0, // Division = 480 + // Track 1 (tempo track) + 0x4D, 0x54, 0x72, 0x6B, + 0x00, 0x00, 0x00, 0x0C, // 12 bytes + 0x83, 0x60, 0xFF, 0x51, 0x03, 0x04, 0x93, 0xE0, // Set Tempo 200 BPM at tick 480 + 0x00, 0xFF, 0x2F, 0x00, + // Track 2 (empty) + 0x4D, 0x54, 0x72, 0x6B, + 0x00, 0x00, 0x00, 0x04, + 0x00, 0xFF, 0x2F, 0x00, + ]); + var Player = new MidiPlayer.Player(); + Player.loadArrayBuffer(midi.buffer); + + // Should have default 120 at tick 0, then 200 at tick 480 + assert.equal(Player.tempoMap.length, 2); + assert.equal(Player.tempoMap[0].tick, 0); + assert.equal(Player.tempoMap[0].tempo, 120); + assert.equal(Player.tempoMap[1].tick, 480); + assert.equal(Player.tempoMap[1].tempo, 200); + }); + }); + describe('#getLyrics()', function () { it('should return all lyric events across all tracks', function () { // Two Lyric events: "Hel-" at tick 0, "lo" at tick 96 @@ -559,6 +590,7 @@ describe('MidiPlayerJS', function() { var lyrics = Player.getLyrics(); assert.equal(lyrics.length, 2); assert.equal(lyrics[0].name, 'Lyric'); + assert.equal(lyrics[0].track, 1); assert.equal(lyrics[0].string, 'Hel-'); assert.equal(lyrics[0].tick, 0); assert.equal(lyrics[1].string, 'lo'); @@ -601,35 +633,4 @@ describe('MidiPlayerJS', function() { assert.equal(track2Lyrics[0].string, 'B'); }); }); - - it('should use default 120 BPM at tick 0 when no Set Tempo at tick 0 exists', function () { - // Format 1 MIDI with Set Tempo only at tick 480 (200 BPM) - var midi = new Uint8Array([ - // MThd - 0x4D, 0x54, 0x68, 0x64, - 0x00, 0x00, 0x00, 0x06, - 0x00, 0x01, // Format 1 - 0x00, 0x02, // 2 tracks - 0x01, 0xE0, // Division = 480 - // Track 1 (tempo track) - 0x4D, 0x54, 0x72, 0x6B, - 0x00, 0x00, 0x00, 0x0C, // 12 bytes - 0x83, 0x60, 0xFF, 0x51, 0x03, 0x04, 0x93, 0xE0, // Set Tempo 200 BPM at tick 480 - 0x00, 0xFF, 0x2F, 0x00, - // Track 2 (empty) - 0x4D, 0x54, 0x72, 0x6B, - 0x00, 0x00, 0x00, 0x04, - 0x00, 0xFF, 0x2F, 0x00, - ]); - var Player = new MidiPlayer.Player(); - Player.loadArrayBuffer(midi.buffer); - - // Should have default 120 at tick 0, then 200 at tick 480 - assert.equal(Player.tempoMap.length, 2); - assert.equal(Player.tempoMap[0].tick, 0); - assert.equal(Player.tempoMap[0].tempo, 120); - assert.equal(Player.tempoMap[1].tick, 480); - assert.equal(Player.tempoMap[1].tempo, 200); - }); - }); }); \ No newline at end of file