Looping a drum pattern #176
-
|
I need some ideas to correctly loop a MIDI drum track, to avoid missing the last beat(s). Here's a basic drum file. If you load that into SpessaSynth, the last beat will end abruptly, right after the last voice has played, but before the actual end of the bar. I understand that MIDI does not have the concept of bars, but my question is: How to convince a player (SpessaSynth specifically, but preferably any MIDI player) to "wait" until the end of the last bar before looping? In my own MIDI player (that I'm hoping to deprecate in favour of SpessaSynth), I artificially ensured that the MIDI End Of Track meta message would occur at the end of the last bar. I then instructed the MIDI player to keep playing until it encounters that message. |
Beta Was this translation helpful? Give feedback.
Replies: 6 comments 11 replies
-
|
By default, spessasynth sets the loop to first note on and end on last voice event. End of track is ignored. In the file you provided, the end of track doesn't seem to be offset like you mentioned:
You can override loop end with the last event tick. Here's a complete example using the 4.0 API: import Speaker from "speaker";
import fs from "fs/promises";
import {
BasicMIDI,
SoundBankLoader,
SpessaSynthProcessor,
SpessaSynthSequencer
} from "../src";
import { Readable } from "node:stream";
const midiFile = await fs.readFile("./basic-baião-2-4.mid");
const sfFile = await fs.readFile(
"/home/spessasus/htdocs/SpessaSynth/soundfonts/gm.dls"
);
const synth = new SpessaSynthProcessor(48000);
synth.soundBankManager.addSoundBank(
SoundBankLoader.fromArrayBuffer(sfFile.buffer as ArrayBuffer),
"main"
);
const seq = new SpessaSynthSequencer(synth);
// Enable loop
seq.loopCount = Infinity;
const midi = BasicMIDI.fromArrayBuffer(midiFile.buffer as ArrayBuffer);
// Find last event tick
midi.loop.end = Math.max(
...midi.tracks.map((t) => t.events[t.events.length - 1].ticks)
);
seq.loadNewSongList([midi]);
seq.play();
// Play the midi via speaker
const bufSize = 128;
const audioStream = new Readable({
read() {
const left = new Float32Array(bufSize);
const right = new Float32Array(bufSize);
const arr = [left, right];
seq.processTick();
synth.renderAudio(arr, [], []);
const interleaved = new Float32Array(left.length * 2);
for (let i = 0; i < left.length; i++) {
interleaved[i * 2] = left[i];
interleaved[i * 2 + 1] = right[i];
}
const buffer = Buffer.alloc(interleaved.length * 4);
for (let i = 0; i < interleaved.length; i++) {
buffer.writeFloatLE(interleaved[i], i * 4);
}
this.push(buffer);
}
});
const speaker = new Speaker({
sampleRate: 44100,
channels: 2,
bitDepth: 32,
// @ts-expect-error badly typed package
float: true
});
audioStream.pipe(speaker);Note that this doesn't work with the provided file as its EndOfTrack doesn't occur at the end of the bar, but if you adjust the file then it should work. Another alternative is replacing endOfTrack with something like controller change that does nothing. Then it will work straight away. PS: MIDI Files can have bars :-)
|
Beta Was this translation helpful? Give feedback.
-
Yes, sorry, I should have clarified that I move the End Of Track message in real time when I read the file.
Thanks! Just to make sure I understand: The actual place to set the loop time is
Thanks! I will try that as it sounds much more seamless.
Please, say more :-) |
Beta Was this translation helpful? Give feedback.
-
I implemented this (on v3.x) and it's working great. Thanks again! Here's the code gist: const midi = new MIDI(incoming_midi_buffer);
const duration = official_duration_as_calculated_outside_of_midi;
midi.tracks[0].push({
ticks: Math.round(duration / (60000 / midi.tempoChanges[0].tempo / midi.timeDivision)),
messageStatusByte: 185,
messageData: new Uint16Array([50, 0]),
});
midi.flush(); |
Beta Was this translation helpful? Give feedback.
-
|
With the release of v4.0.0, it's no longer possible to push an event to On the other hand, Is there a non-hacky way to add an event to an existing |
Beta Was this translation helpful? Give feedback.
-
|
Answering my own question: The code snippet above becomes: const midi = BasicMIDI.fromArrayBuffer(incoming_midi_buffer);
const duration = official_duration_as_calculated_outside_of_midi;
midi.tracks[0].addEvent({
ticks: Math.round(duration / (60000 / midi.tempoChanges[0].tempo / midi.timeDivision)),
statusByte: midiMessageTypes.controllerChange,
data: new Uint8Array([50, 0]),
}, -1);
midi.flush(); |
Beta Was this translation helpful? Give feedback.
-
|
Actually, the above snippet broke the modified MIDI. A warning message said:
but the code seems to remove the event before EOT, resulting in MIDI data loss. The snippet that's working for me now is: const midi = BasicMIDI.fromArrayBuffer(incoming_midi_buffer);
const duration = official_duration_as_calculated_outside_of_midi;
const ticks = Math.round(duration / (60000 / midi.tempoChanges[0].tempo / midi.timeDivision));
midi.tracks[0].deleteEvent(-1);
midi.tracks[0].pushEvent({
ticks,
statusByte: midiMessageTypes.controllerChange,
data: new Uint8Array([50, 0]),
});
midi.tracks[0].pushEvent({
ticks,
statusByte: midiMessageTypes.endOfTrack,
data: new Uint8Array(),
});
midi.flush();where I first remove the existing EOT, then append the dummy CC followed by a new EOT at the same tick. |
Beta Was this translation helpful? Give feedback.


By default, spessasynth sets the loop to first note on and end on last voice event. End of track is ignored.
In the file you provided, the end of track doesn't seem to be offset like you mentioned:
You can override loop end with the last event tick. Here's a complete example using the 4.0 API: