Skip to content

Commit

Permalink
renderer rework
Browse files Browse the repository at this point in the history
sequencer is now independent of the renderer
  • Loading branch information
spessasus committed Jun 19, 2024
1 parent a66e1ce commit e217c6e
Show file tree
Hide file tree
Showing 13 changed files with 585 additions and 543 deletions.
192 changes: 24 additions & 168 deletions src/spessasynth_lib/sequencer/sequencer.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import {MIDI} from "../midi_parser/midi_loader.js";
import { DEFAULT_PERCUSSION, Synthetizer } from '../synthetizer/synthetizer.js';
import { messageTypes, MidiMessage } from '../midi_parser/midi_message.js'
import { consoleColors } from '../utils/other.js'
import { Synthetizer } from '../synthetizer/synthetizer.js';
import { messageTypes } from '../midi_parser/midi_message.js'
import { workletMessageType } from '../synthetizer/worklet_system/worklet_utilities/worklet_message.js'
import {
WorkletSequencerMessageType,
WorkletSequencerReturnMessageType,
} from './worklet_sequencer/sequencer_message.js'
import { readBytesAsUintBigEndian } from '../utils/byte_functions.js'
import { ShiftableByteArray } from '../utils/shiftable_array.js'
import { SpessaSynthInfo, SpessaSynthWarn } from '../utils/loggin.js'
import { SpessaSynthWarn } from '../utils/loggin.js'

/**
* sequencer.js
* purpose: plays back the midi file decoded by midi_loader.js, including support for multi-channel midis (adding channels when more than 1 midi port is detected)
*/

const MIN_NOTE_TIME = 0.02;

export class Sequencer {
/**
* Creates a new Midi sequencer for playing back MIDI files
Expand Down Expand Up @@ -55,11 +52,6 @@ export class Sequencer {
*/
this.duration = 0;

/**
* @type {Object<string, function(MIDI)>}
*/
this.onSongChange = {};

this.synth.sequencerCallbackFunction = this._handleMessage.bind(this);

this.loadNewSongList(parsedMidis);
Expand Down Expand Up @@ -138,7 +130,6 @@ export class Sequencer {
this.songIndex = messageData[1];
this.midiData = songChangeData;
this.duration = this.midiData.duration;
this.calculateNoteTimes(songChangeData.tracks);
Object.entries(this.onSongChange).forEach((callback) => callback[1](songChangeData));
this.unpause()
break;
Expand All @@ -158,18 +149,11 @@ export class Sequencer {
* @type {number}
*/
const time = messageData;
if(this.onTimeChange)
{
this.onTimeChange(time);
}
Object.entries(this.onTimeChange).forEach((callback) => callback[1](time));
this.unpause()
this._recalculateStartTime(time);
break;

case WorkletSequencerReturnMessageType.resetRendererIndexes:
this.resetRendererIndexes();
break;

case WorkletSequencerReturnMessageType.pause:
this.pausedTime = this.currentTime;
this.isFinished = messageData;
Expand Down Expand Up @@ -223,6 +207,16 @@ export class Sequencer {
callback(this.midiData);
}

/**
* Adds a new event that gets called when the time changes
* @param callback {function(number)} the new time, in seconds
* @param id {string} must be unique
*/
addOnTimeChangeEvent(callback, id)
{
this.onTimeChange[id] = callback;
}

/**
* @param parsedMidis {MIDI[]}
*/
Expand Down Expand Up @@ -291,151 +285,6 @@ export class Sequencer {
this._sendMessage(WorkletSequencerMessageType.setTime, time);
}

resetRendererIndexes()
{
if(!this.renderer)
{
return;
}
this.renderer.noteStartTime = this.absoluteStartTime;
this.renderer.noteTimes.forEach(n => n.renderStartIndex = 0);
}

/**
* Connects a midi renderer
* @param renderer {Renderer}
*/
connectRenderer(renderer)
{
this.renderer = renderer;
this.calculateNoteTimes(this.midiData.tracks);
}

/**
* @param trackData {MidiMessage[][]}
*/
calculateNoteTimes(trackData)
{
if(this.midiData === undefined)
{
return;
}

/**
* gets tempo from the midi message
* @param event {MidiMessage}
* @return {number} the tempo in bpm
*/
function getTempo(event)
{
// simulate shiftableByteArray
event.messageData = new ShiftableByteArray(event.messageData.buffer);
event.messageData.currentIndex = 0;
return 60000000 / readBytesAsUintBigEndian(event.messageData, 3);
}

/**
* an array of 16 arrays (channels) and the notes are stored there
* @typedef {{
* midiNote: number,
* start: number,
* length: number,
* velocity: number,
* }} NoteTime
*
* @typedef {{
* notes: NoteTime[],
* renderStartIndex: number
* }[]} NoteTimes
*/

/**
* @type {NoteTimes}
*/


const noteTimes = [];
let events = trackData.flat();
events.sort((e1, e2) => e1.ticks - e2.ticks);
for (let i = 0; i < 16; i++)
{
noteTimes.push({renderStartIndex: 0, notes: []});
}
let elapsedTime = 0;
let oneTickToSeconds = 60 / (120 * this.midiData.timeDivision);
let eventIndex = 0;
let unfinished = 0;
while(eventIndex < events.length)
{
const event = events[eventIndex];

const status = event.messageStatusByte >> 4;
const channel = event.messageStatusByte & 0x0F;

// note off
if(status === 0x8)
{
const note = noteTimes[channel].notes.findLast(n => n.midiNote === event.messageData[0] && n.length === -1)
if(note) {
const time = elapsedTime - note.start;
note.length = (time < MIN_NOTE_TIME && channel === DEFAULT_PERCUSSION ? MIN_NOTE_TIME : time);
}
unfinished--;
}
// note on
else if(status === 0x9)
{
if(event.messageData[1] === 0)
{
// nevermind, its note off
const note = noteTimes[channel].notes.findLast(n => n.midiNote === event.messageData[0] && n.length === -1)
if(note) {
const time = elapsedTime - note.start;
note.length = (time < MIN_NOTE_TIME && channel === DEFAULT_PERCUSSION ? MIN_NOTE_TIME : time);
}
unfinished--;
}
else {
noteTimes[event.messageStatusByte & 0x0F].notes.push({
midiNote: event.messageData[0],
start: elapsedTime,
length: -1,
velocity: event.messageData[1] / 127
});
unfinished++;
}
}
// set tempo
else if(event.messageStatusByte === 0x51)
{
oneTickToSeconds = 60 / (getTempo(event) * this.midiData.timeDivision);
}

if(++eventIndex >= events.length) break;

elapsedTime += oneTickToSeconds * (events[eventIndex].ticks - event.ticks);
}

// finish the unfinished notes
if(unfinished > 0)
{
// for every channel, for every note that is unfinished (has -1 length)
noteTimes.forEach((channel, channelNumber) =>
channel.notes.filter(n => n.length === -1).forEach(note =>
{
const time = elapsedTime - note.start;
note.length = (time < MIN_NOTE_TIME && channelNumber === DEFAULT_PERCUSSION ? MIN_NOTE_TIME : time);
})
)
}

SpessaSynthInfo(`%cFinished loading note times and ready to render the sequence!`, consoleColors.info);
if(this.renderer)
{
this.renderer.connectSequencer(noteTimes, this);
}
}

/**
* @param output {MIDIOutput}
*/
Expand Down Expand Up @@ -505,6 +354,12 @@ export class Sequencer {
this._sendMessage(WorkletSequencerMessageType.stop);
}

/**
* @type {Object<string, function(MIDI)>}
* @private
*/
onSongChange = {};

/**
* Fires on text event
* @param data {Uint8Array} the data text
Expand All @@ -514,7 +369,8 @@ export class Sequencer {

/**
* Fires when CurrentTime changes
* @param time {number} the time that was changed to
* @type {Object<string, function(number)>} the time that was changed to
* @private
*/
onTimeChange;
onTimeChange = {};
}
1 change: 0 additions & 1 deletion src/spessasynth_lib/sequencer/worklet_sequencer/play.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,6 @@ export function setTimeTicks(ticks)
}
this.play();
this.post(WorkletSequencerReturnMessageType.timeChange, this.currentTime);
this.post(WorkletSequencerReturnMessageType.resetRendererIndexes);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,5 @@ export const WorkletSequencerReturnMessageType = {
songChange: 1, // [midiData<MIDI>, songIndex<number>]
textEvent: 2, // [messageData<number[]>, statusByte<number]
timeChange: 3, // newTime<number>
resetRendererIndexes: 4, // no data
pause: 5, // no data
pause: 4, // no data
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ class WorkletSequencer
return;
}
this.play();
this.post(WorkletSequencerReturnMessageType.resetRendererIndexes);
}

/**
Expand Down
6 changes: 5 additions & 1 deletion src/website/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,13 @@ const RENDER_AUDIO_TIME_INTERVAL = 500;
this.playerUI.connectSequencer(this.seq);

// connect to the renderer;
this.seq.connectRenderer(this.renderer);
this.renderer.connectSequencer(this.seq);

// play the midi
this.seq.play(true);

// this.renderer.onRender = () => {
// console.log("frame")
// }
}
}
Loading

0 comments on commit e217c6e

Please sign in to comment.