Skip to content

Commit

Permalink
The Grand Code Reformat Update
Browse files Browse the repository at this point in the history
I finally figured out how to do it!
spessasus committed Oct 6, 2024
1 parent 7a0278a commit 8035d9d
Showing 227 changed files with 7,354 additions and 6,025 deletions.
12 changes: 7 additions & 5 deletions .idea/codeStyles/Project.xml

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

9 changes: 9 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

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

90 changes: 64 additions & 26 deletions src/spessasynth_lib/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
# spessasynth_lib

**A powerful soundfont/MIDI JavaScript library for the browsers.**

```shell
npm install --save spessasynth_lib
```

### [Project site (consider giving it a star!)](https://github.com/spessasus/SpessaSynth)

### [Demo](https://spessasus.github.io/SpessaSynth)

### [Complete documentation](https://github.com/spessasus/SpessaSynth/wiki/Usage-As-Library)

#### Basic example: play a single note

```js
import {Synthetizer} from "spessasynth_lib"
import { Synthetizer } from "spessasynth_lib"

const sfont = await (await fetch("soundfont.sf3")).arrayBuffer();
const ctx = new AudioContext();
// make sure you copied the worklet processor!
await ctx.audioWorklet.addModule("./worklet_processor.min.js");
const synth = new Synthetizer(ctx.destination, sfont);
document.getElementById("button").onclick = async () => {
document.getElementById("button").onclick = async () =>
{
await ctx.resume();
synth.programChange(0, 48); // strings ensemble
synth.noteOn(0, 52, 127);
@@ -28,37 +33,55 @@ document.getElementById("button").onclick = async () => {
## Current Features

### Easy Integration

- **Modular design:** Easy integration into other projects (load what you need)
- **[Detailed documentation:](https://github.com/spessasus/SpessaSynth/wiki/Home)** With [examples!](https://github.com/spessasus/SpessaSynth/wiki/Usage-As-Library#examples)
- **Easy to Use:** basic setup is just [two lines of code!](https://github.com/spessasus/SpessaSynth/wiki/Usage-As-Library#minimal-setup)
- **[Detailed documentation:](https://github.com/spessasus/SpessaSynth/wiki/Home)**
With [examples!](https://github.com/spessasus/SpessaSynth/wiki/Usage-As-Library#examples)
- **Easy to Use:** basic setup is
just [two lines of code!](https://github.com/spessasus/SpessaSynth/wiki/Usage-As-Library#minimal-setup)
- **No dependencies:** _batteries included!_

### Powerful SoundFont Synthesizer

- Suitable for both **real-time** and **offline** synthesis
- **Excellent SoundFont support:**
- **Generator Support**
- **Modulator Support:** *First (to my knowledge) JavaScript SoundFont synth with that feature!*
- **SoundFont3 Support:** Play compressed SoundFonts!
- **Experimental SF2Pack Support:** Play soundfonts compressed with BASSMIDI! (*Note: only works with vorbis compression*)
- **Can load very large SoundFonts:** up to 4GB! *Note: Only Firefox handles this well; Chromium has a hard-coded memory limit*
- **Generator Support**
- **Modulator Support:** *First (to my knowledge) JavaScript SoundFont synth with that feature!*
- **SoundFont3 Support:** Play compressed SoundFonts!
- **Experimental SF2Pack Support:** Play soundfonts compressed with BASSMIDI! (*Note: only works with vorbis
compression*)
- **Can load very large SoundFonts:** up to 4GB! *Note: Only Firefox handles this well; Chromium has a hard-coded
memory limit*
- **Soundfont manager:** Stack multiple soundfonts!
- **DLS Level 1 and 2 Support:** *internally converted to sf2*
- **Reverb and chorus support:** [customizable!](https://github.com/spessasus/SpessaSynth/wiki/Synthetizer-Class#effects-configuration-object)
- **Export audio files** using [OfflineAudioContext](https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext)
- **[Custom modulators for additional controllers](https://github.com/spessasus/SpessaSynth/wiki/Modulator-Class#default-modulators):** Why not?
- **Reverb and chorus support:
** [customizable!](https://github.com/spessasus/SpessaSynth/wiki/Synthetizer-Class#effects-configuration-object)
- **Export audio files**
using [OfflineAudioContext](https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext)
-
*

*[Custom modulators for additional controllers](https://github.com/spessasus/SpessaSynth/wiki/Modulator-Class#default-modulators):
** Why not?

- **Written using AudioWorklets:**
- Runs in a **separate thread** for maximum performance
- Supported by all modern browsers
- Runs in a **separate thread** for maximum performance
- Supported by all modern browsers
- **Unlimited channel count:** Your CPU is the limit!
- **Excellent MIDI Standards Support:**
- **MIDI Controller Support:** Default supported controllers [here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-controllers)
- **MIDI Tuning Standard Support:** [more info here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#midi-tuning-standard)
- [Full **RPN** and limited **NRPN** support](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-registered-parameters)
- Supports some [**Roland GS** and **Yamaha XG** system exclusives](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-system-exclusives)
- **MIDI Controller Support:** Default supported
controllers [here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-controllers)
- **MIDI Tuning Standard Support:
** [more info here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#midi-tuning-standard)
- [Full **RPN** and limited **NRPN
** support](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-registered-parameters)
- Supports some [**Roland GS** and **Yamaha XG
** system exclusives](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-system-exclusives)

- **High-performance mode:** Play Rush E! _note: may kill your browser ;)_

### Built-in Powerful and Fast Sequencer

- **Supports MIDI formats 0, 1, and 2:** _note: format 2 support is experimental as it's very, very rare_
- **[Multi-Port MIDI](https://github.com/spessasus/SpessaSynth/wiki/About-Multi-Port) support:** More than 16 channels!
- **Smart preloading:** Only preloads the samples used in the MIDI file for smooth playback (down to key and velocity!)
@@ -68,54 +91,69 @@ document.getElementById("button").onclick = async () => {
- **Loop points support:** Ensures seamless loops

### Read and Write SoundFont and MIDI Files with Ease

#### Read and write MIDI files

- **Smart name detection:** Handles incorrectly formatted and non-standard track names
- **Raw name available:** Decode in any encoding! *(Kanji? No problem!)*
- **Port detection during load time:** Manage ports and channels easily!
- **Used channels on track:** Quickly determine which channels are used
- **Key range detection:** Detect the key range of the MIDI
- **Easy MIDI editing:** Use [helper functions](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#modifymidi) to modify the song to your needs!
- **Easy MIDI editing:**
Use [helper functions](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#modifymidi) to modify the song
to your needs!
- **Loop detection:** Automatically detects loops in MIDIs (e.g., from _Touhou Project_)
- **First note detection:** Skip unnecessary silence at the start by jumping to the first note!
- **[Write MIDI files from scratch](https://github.com/spessasus/SpessaSynth/wiki/Creating-MIDI-Files)**
- **Easy saving:** Save with just [one function!](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#writemidifile)
- **Easy saving:** Save with
just [one function!](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#writemidifile)

#### Read and write [RMID files with embedded SF2 soundfonts](https://github.com/spessasus/sf2-rmidi-specification#readme)

- **[Level 4](https://github.com/spessasus/sf2-rmidi-specification#level-4) compliance:** Reads and writes *everything!*
- **Compression and trimming support:** Reduce a MIDI file with a 1GB soundfont to **as small as 5MB**!
- **DLS Version support:** The original legacy format with bank offset detection!
- **Automatic bank shifting and validation:** Every soundfont *just works!*
- **Metadata support:** Add title, artist, album name and cover and more! And of course read them too! *(In any encoding!)*
- **Metadata support:** Add title, artist, album name and cover and more! And of course read them too! *(In any
encoding!)*
- **Compatible with [Falcosoft Midi Player 6!](https://falcosoft.hu/softwares.html#midiplayer)**
- **Easy saving:** [As simple as saving a MIDI file!](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#writermidi)
- **Easy saving:
** [As simple as saving a MIDI file!](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#writermidi)

#### Read and write SoundFont2 files
- **Easy info access:** Just an [object of strings!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#soundfontinfo)

- **Easy info access:** Just
an [object of strings!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#soundfontinfo)
- **Smart trimming:** Trim the SoundFont to only include samples used in the MIDI *(down to key and velocity!)*
- **sf3 conversion:** Compress SoundFont2 files to SoundFont3 with variable quality!
- **Easy saving:** Also just [one function!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#write)

#### Read and write SoundFont3 files

- Same features as SoundFont2 but with now with **Ogg Vorbis compression!**
- **Variable compression quality:** You choose between file size and quality!
- **Compression preserving:** Avoid decompressing and recompressing uncompressed samples for minimal quality loss!

#### Read and play DLS Level 1 or 2 files

- Read DLS (DownLoadable Sounds) files as SF2 files!
- **Works like a normal soundfont:** *Saving it as sf2 is still [just one function!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#write)*
- **Works like a normal soundfont:** *Saving it as sf2 is
still [just one function!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#write)*
- Converts articulators to both **modulators** and **generators**!
- Works with both unsigned 8-bit samples and signed 16-bit samples!
- **Covers special generator cases:** *such as modLfoToPitch*!
- **Correct volume:** *looking at you, Viena and gm.sf2!*
- Support built right into the synthesizer!

### Export MIDI as WAV

- Save the MIDI file as WAV audio!
- **Metadata support:** *Embed metadata such as title, artist, album and more!*
- **Cue points:** *Write MIDI loop points as cue points!*
- **Loop multiple times:** *Render two (or more) loops into the file for seamless transitions!*
- *That's right, saving as WAV is also [just one function!](https://github.com/spessasus/SpessaSynth/wiki/Writing-Wave-Files#audiobuffertowav)*

- *That's right, saving as WAV is
also [just one function!](https://github.com/spessasus/SpessaSynth/wiki/Writing-Wave-Files#audiobuffertowav)*

# License

MIT License, except for the stbvorbis_sync.js in the `externals` folder which is licensed under the Apache-2.0 license.
1 change: 1 addition & 0 deletions src/spessasynth_lib/external_midi/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
## This is the MIDI handling folder.

The code here is respnsible for dealing with MIDI Inputs and outputs
and also for the WebMidiLink functionality.
63 changes: 37 additions & 26 deletions src/spessasynth_lib/external_midi/midi_handler.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Synthetizer } from '../synthetizer/synthetizer.js'
import { consoleColors } from '../utils/other.js';
import { SpessaSynthInfo, SpessaSynthWarn } from '../utils/loggin.js'
import { Synthetizer } from "../synthetizer/synthetizer.js";
import { consoleColors } from "../utils/other.js";
import { SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js";

/**
* midi_handler.js
@@ -12,8 +12,9 @@ const NO_INPUT = null;
export class MIDIDeviceHandler
{
constructor()
{}

{
}

/**
* @returns {Promise<boolean>} if succeded
*/
@@ -27,7 +28,8 @@ export class MIDIDeviceHandler
* @type {MIDIOutput}
*/
this.selectedOutput = NO_INPUT;
if(navigator.requestMIDIAccess) {
if (navigator.requestMIDIAccess)
{
// prepare the midi access
try
{
@@ -36,23 +38,23 @@ export class MIDIDeviceHandler
this.outputs = response.outputs;
SpessaSynthInfo("%cMIDI handler created!", consoleColors.recognized);
return true;
}
catch (e) {
} catch (e)
{
SpessaSynthWarn(`Could not get MIDI Devices:`, e);
this.inputs = [];
this.outputs = [];
return false
return false;
}
}
else
{
SpessaSynthWarn("Web MIDI Api not supported!", consoleColors.unrecognized);
this.inputs = [];
this.outputs = [];
return false
return false;
}
}

/**
* Connects the sequencer to a given MIDI output port
* @param output {MIDIOutput}
@@ -62,11 +64,13 @@ export class MIDIDeviceHandler
{
this.selectedOutput = output;
seq.connectMidiOutput(output);
SpessaSynthInfo(`%cPlaying MIDI to %c${output.name}`,
SpessaSynthInfo(
`%cPlaying MIDI to %c${output.name}`,
consoleColors.info,
consoleColors.recognized);
consoleColors.recognized
);
}

/**
* Disconnects a midi output port from the sequencer
* @param seq {Sequencer}
@@ -75,10 +79,12 @@ export class MIDIDeviceHandler
{
this.selectedOutput = NO_INPUT;
seq.connectMidiOutput(undefined);
SpessaSynthInfo("%cDisconnected from MIDI out.",
consoleColors.info);
SpessaSynthInfo(
"%cDisconnected from MIDI out.",
consoleColors.info
);
}

/**
* Connects a MIDI input to the synthesizer
* @param input {MIDIInput}
@@ -87,30 +93,35 @@ export class MIDIDeviceHandler
connectDeviceToSynth(input, synth)
{
this.selectedInput = input;
input.onmidimessage = event => {
input.onmidimessage = event =>
{
synth.sendMessage(event.data);
}
SpessaSynthInfo(`%cListening for messages on %c${input.name}`,
};
SpessaSynthInfo(
`%cListening for messages on %c${input.name}`,
consoleColors.info,
consoleColors.recognized);
consoleColors.recognized
);
}

/**
* @param input {MIDIInput}
*/
disconnectDeviceFromSynth(input)
{
this.selectedInput = NO_INPUT;
input.onmidimessage = undefined;
SpessaSynthInfo(`%cDisconnected from %c${input.name}`,
SpessaSynthInfo(
`%cDisconnected from %c${input.name}`,
consoleColors.info,
consoleColors.recognized);
consoleColors.recognized
);
}

disconnectAllDevicesFromSynth()
{
this.selectedInput = NO_INPUT;
for(const i of this.inputs)
for (const i of this.inputs)
{
i[1].onmidimessage = undefined;
}
25 changes: 13 additions & 12 deletions src/spessasynth_lib/external_midi/web_midi_link.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Synthetizer } from '../synthetizer/synthetizer.js'
import { consoleColors } from '../utils/other.js'
import { SpessaSynthInfo } from '../utils/loggin.js'
import { Synthetizer } from "../synthetizer/synthetizer.js";
import { consoleColors } from "../utils/other.js";
import { SpessaSynthInfo } from "../utils/loggin.js";

/**
* web_midi_link.js
@@ -15,28 +15,29 @@ export class WebMidiLinkHandler
*/
constructor(synth)
{

window.addEventListener("message", msg => {
if(typeof msg.data !== "string")

window.addEventListener("message", msg =>
{
if (typeof msg.data !== "string")
{
return
return;
}
/**
* @type {string[]}
*/
const data = msg.data.split(",");
if(data[0] !== "midi")
if (data[0] !== "midi")
{
return;
}

data.shift(); // remove MIDI

const midiData = data.map(byte => parseInt(byte, 16));

synth.sendMessage(midiData);
});

SpessaSynthInfo("%cWeb MIDI Link handler created!", consoleColors.recognized);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
declare type DecodedData =
{
data: Float32Array[],
error: string|null,
sampleRate: number,
eof: boolean
}
{
data: Float32Array[],
error: string | null,
sampleRate: number,
eof: boolean
}

declare const stbvorbis: {
decode(buffer: ArrayBuffer): DecodedData
1 change: 1 addition & 0 deletions src/spessasynth_lib/midi_parser/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
## This is the MIDI file parsing folder.

The code here is responsible for parsing the MIDI files and interpreting the messsages.
All the events are defined in the `midi_message.js` file.
43 changes: 22 additions & 21 deletions src/spessasynth_lib/midi_parser/basic_midi.js
Original file line number Diff line number Diff line change
@@ -16,97 +16,97 @@ export class BasicMIDI
* The tempo changes in the sequence, ordered from last to first
* @type {{ticks: number, tempo: number}[]}
*/
this.tempoChanges = [{ticks: 0, tempo: 120}];
this.tempoChanges = [{ ticks: 0, tempo: 120 }];
/**
* Contains the copyright strings
* @type {string}
*/
this.copyright = "";

/**
* The amount of tracks in the sequence
* @type {number}
*/
this.tracksAmount = 0;

/**
* The lyrics of the sequence as binary chunks
* @type {Uint8Array[]}
*/
this.lyrics = [];

/**
* First note on of the MIDI file
* @type {number}
*/
this.firstNoteOn = 0;

/**
* The MIDI's key range
* @type {{min: number, max: number}}
*/
this.keyRange = { min: 0, max: 127 };

/**
* The last voice (note on, off, cc change etc.) event tick
* @type {number}
*/
this.lastVoiceEventTick = 0;

/**
* Midi port numbers for each track
* @type {number[]}
*/
this.midiPorts = [0];

/**
* Channel offsets for each port, using the SpessaSynth method
* @type {number[]}
*/
this.midiPortChannelOffsets = [0];

/**
* All channels that each track uses
* @type {Set<number>[]}
*/
this.usedChannelsOnTrack = [];

/**
* The loop points (in ticks) of the sequence
* @type {{start: number, end: number}}
*/
this.loop = { start: 0, end: 0 };

/**
* The sequence's name
* @type {string}
*/
this.midiName = "";

/**
* The file name of the sequence, if provided in the MIDI class
* @type {string}
*/
this.fileName = "";

/**
* The raw, encoded MIDI name.
* @type {Uint8Array}
*/
this.rawMidiName = undefined;

/**
* The MIDI's embedded soundfont
* @type {ArrayBuffer|undefined}
*/
this.embeddedSoundFont = undefined;

/**
* The MIDI file's format
* @type {number}
*/
this.format = 0;

/**
* The RMID Info data if RMID, otherwise undefined
* @type {Object<string, IndexedByteArray>}
@@ -117,7 +117,7 @@ export class BasicMIDI
* @type {number}
*/
this.bankOffset = 0;

/**
* The actual track data of the MIDI file
* @type {MidiMessage[][]}
@@ -132,22 +132,23 @@ export class BasicMIDI
* @param mid {BasicMIDI} the MIDI
* @returns {number} time in seconds
*/
export function MIDIticksToSeconds(ticks, mid) {
export function MIDIticksToSeconds(ticks, mid)
{
let totalSeconds = 0;

while (ticks > 0)
{
// tempo changes are reversed so the first element is the last tempo change
// and the last element is the first tempo change
// (always at tick 0 and tempo 120)
// find the last tempo change that has occurred
let tempo = mid.tempoChanges.find(v => v.ticks < ticks);

// calculate the difference and tempo time
let timeSinceLastTempo = ticks - tempo.ticks;
totalSeconds += (timeSinceLastTempo * 60) / (tempo.tempo * mid.timeDivision);
ticks -= timeSinceLastTempo;
}

return totalSeconds;
}
95 changes: 49 additions & 46 deletions src/spessasynth_lib/midi_parser/midi_builder.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { BasicMIDI, MIDIticksToSeconds } from './basic_midi.js'
import { messageTypes, MidiMessage } from './midi_message.js'
import { IndexedByteArray } from '../utils/indexed_array.js'
import { readBytesAsUintBigEndian } from '../utils/byte_functions/big_endian.js'
import { SpessaSynthWarn } from '../utils/loggin.js'
import { BasicMIDI, MIDIticksToSeconds } from "./basic_midi.js";
import { messageTypes, MidiMessage } from "./midi_message.js";
import { IndexedByteArray } from "../utils/indexed_array.js";
import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";
import { SpessaSynthWarn } from "../utils/loggin.js";

export class MIDIBuilder extends BasicMIDI
{
@@ -18,113 +18,116 @@ export class MIDIBuilder extends BasicMIDI
this.midiName = name;
this.encoder = new TextEncoder();
this.rawMidiName = this.encoder.encode(name);

// create the first track with the file name
this.addNewTrack(name);
this.addSetTempo(0, initialTempo);
}

/**
* Updates all internal values
*/
flush()
{

// find first note on
const firstNoteOns = [];
for(const t of this.tracks)
for (const t of this.tracks)
{
// sost the track by ticks
t.sort((e1, e2) => e1.ticks - e2.ticks);
const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn);
if(firstNoteOn)
if (firstNoteOn)
{
firstNoteOns.push(firstNoteOn.ticks);
}
}
this.firstNoteOn = Math.min(...firstNoteOns);

// find tempo changes
// and used channels on tracks
// and midi ports
// and last voice event tick
// and loop
this.lastVoiceEventTick = 0
this.tempoChanges = [{ticks: 0, tempo: 120}];
this.lastVoiceEventTick = 0;
this.tempoChanges = [{ ticks: 0, tempo: 120 }];
this.midiPorts = [];
this.midiPortChannelOffsets = [];
let portOffset = 0;
/**
* @type {Set<number>[]}
*/
this.usedChannelsOnTrack = this.tracks.map(() => new Set());
this.tracks.forEach((t, trackNum) => {
this.tracks.forEach((t, trackNum) =>
{
this.midiPorts.push(-1);
t.forEach(e => {
t.forEach(e =>
{
// last voice event tick
if(e.messageStatusByte >= 0x80 && e.messageStatusByte < 0xF0)
if (e.messageStatusByte >= 0x80 && e.messageStatusByte < 0xF0)
{
if(e.ticks > this.lastVoiceEventTick)
if (e.ticks > this.lastVoiceEventTick)
{
this.lastVoiceEventTick = e.ticks;
}
}

// tempo, used channels, port
if(e.messageStatusByte === messageTypes.setTempo)
if (e.messageStatusByte === messageTypes.setTempo)
{
this.tempoChanges.push({
ticks: e.ticks,
tempo: 60000000 / readBytesAsUintBigEndian(e.messageData, 3)
tempo: 60000000 / readBytesAsUintBigEndian(
e.messageData,
3
)
});
}
else
if((e.messageStatusByte & 0xF0) === messageTypes.noteOn)
else if ((e.messageStatusByte & 0xF0) === messageTypes.noteOn)
{
this.usedChannelsOnTrack[trackNum].add(e.messageData[0]);
}
else
if(e.messageStatusByte === messageTypes.midiPort)
else if (e.messageStatusByte === messageTypes.midiPort)
{
const port = e.messageData[0];
this.midiPorts[trackNum] = port;
if(this.midiPortChannelOffsets[port] === undefined)
if (this.midiPortChannelOffsets[port] === undefined)
{
this.midiPortChannelOffsets[port] = portOffset;
portOffset += 16;
}
}
})
});
});

this.loop = {start: this.firstNoteOn, end: this.lastVoiceEventTick};

this.loop = { start: this.firstNoteOn, end: this.lastVoiceEventTick };
// reverse tempo and compute duration
this.tempoChanges.reverse();
this.duration = MIDIticksToSeconds(this.lastVoiceEventTick, this);

// fix midi ports:
// midi tracks without ports will have a value of -1
// if all ports have a value of -1, set it to 0, otherwise take the first midi port and replace all -1 with it
// why do this? some midis (for some reason) specify all channels to port 1 or else, but leave the conductor track with no port pref.
// this spessasynth to reserve the first 16 channels for the conductor track (which doesn't play anything) and use additional 16 for the actual ports.
let defaultP = 0;
for(let port of this.midiPorts)
for (let port of this.midiPorts)
{
if(port !== -1)
if (port !== -1)
{
defaultP = port;
break;
}
}
this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultP : port);
// add dummy port if empty
if(this.midiPortChannelOffsets.length === 0)
if (this.midiPortChannelOffsets.length === 0)
{
this.midiPortChannelOffsets = [0];
}
}

/**
* Adds a new "set tempo" message
* @param ticks {number} the tick number of the event
@@ -133,17 +136,17 @@ export class MIDIBuilder extends BasicMIDI
addSetTempo(ticks, tempo)
{
const array = new IndexedByteArray(3);

tempo = 60000000 / tempo;

// Extract each byte in big-endian order
array[0] = (tempo >> 16) & 0xFF;
array[1] = (tempo >> 8) & 0xFF;
array[2] = tempo & 0xFF;

this.addEvent(ticks, 0, messageTypes.setTempo, array);
}

/**
* Adds a new MIDI track
* @param name {string} the new track's name
@@ -152,7 +155,7 @@ export class MIDIBuilder extends BasicMIDI
addNewTrack(name, port = 0)
{
this.tracksAmount++;
if(this.tracksAmount > 1)
if (this.tracksAmount > 1)
{
this.format = 1;
}
@@ -163,7 +166,7 @@ export class MIDIBuilder extends BasicMIDI
this.addEvent(0, this.tracksAmount - 1, messageTypes.trackName, this.encoder.encode(name));
this.addEvent(0, this.tracksAmount - 1, messageTypes.midiPort, [port]);
}

/**
* Adds a new MIDI Event
* @param ticks {number} the tick time of the event
@@ -173,11 +176,11 @@ export class MIDIBuilder extends BasicMIDI
*/
addEvent(ticks, track, event, eventData)
{
if(!this.tracks[track])
if (!this.tracks[track])
{
throw new Error(`Track ${track} does not exist. Add it via addTrack method.`);
}
if(event === messageTypes.endOfTrack)
if (event === messageTypes.endOfTrack)
{
SpessaSynthWarn("The EndOfTrack is added automatically. Ignoring!");
return;
@@ -196,7 +199,7 @@ export class MIDIBuilder extends BasicMIDI
new IndexedByteArray(0)
));
}

/**
* Adds a new Note On event
* @param ticks {number} the tick time of the event
@@ -217,7 +220,7 @@ export class MIDIBuilder extends BasicMIDI
[midiNote, velocity]
);
}

/**
* Adds a new Note Off event
* @param ticks {number} the tick time of the event
@@ -236,7 +239,7 @@ export class MIDIBuilder extends BasicMIDI
[midiNote, 64]
);
}

/**
* Adds a new Controller Change event
* @param ticks {number} the tick time of the event
@@ -257,7 +260,7 @@ export class MIDIBuilder extends BasicMIDI
[controllerNumber, controllerValue]
);
}

/**
* Adds a new Pitch Wheel event
* @param ticks {number} the tick time of the event
34 changes: 17 additions & 17 deletions src/spessasynth_lib/midi_parser/midi_data.js
Original file line number Diff line number Diff line change
@@ -29,81 +29,81 @@ export class MidiData
* @type {string}
*/
this.copyright = midi.copyright;

/**
* The amount of tracks in the sequence
* @type {number}
*/
this.tracksAmount = midi.tracksAmount;

/**
* The lyrics of the sequence as binary chunks
* @type {Uint8Array[]}
*/
this.lyrics = midi.lyrics;

this.firstNoteOn = midi.firstNoteOn;

/**
* The MIDI's key range
* @type {{min: number, max: number}}
*/
this.keyRange = midi.keyRange;

/**
* The last voice (note on, off, cc change etc.) event tick
* @type {number}
*/
this.lastVoiceEventTick = midi.lastVoiceEventTick;

/**
* Midi port numbers for each track
* @type {number[]}
*/
this.midiPorts = midi.midiPorts;

/**
* Channel offsets for each port, using the SpessaSynth method
* @type {number[]}
*/
this.midiPortChannelOffsets = midi.midiPortChannelOffsets;

/**
* All channels that each track uses
* @type {Set<number>[]}
*/
this.usedChannelsOnTrack = midi.usedChannelsOnTrack;

/**
* The loop points (in ticks) of the sequence
* @type {{start: number, end: number}}
*/
this.loop = midi.loop;

/**
* The sequence's name
* @type {string}
*/
this.midiName = midi.midiName;

/**
* The file name of the sequence, if provided in the MIDI class
* @type {string}
*/
this.fileName = midi.fileName;

/**
* The raw, encoded MIDI name.
* @type {Uint8Array}
*/
this.rawMidiName = midi.rawMidiName;

/**
* Indicates if the midi has an embedded soundfont
* @type {boolean}
*/
this.isEmbedded = midi.embeddedSoundFont !== undefined;

/**
* The RMID Info data if RMID, otherwise undefined
* @type {Object<string, IndexedByteArray>}
@@ -128,20 +128,20 @@ export const DUMMY_MIDI_DATA = {
start: 0,
end: 123456
},

lastVoiceEventTick: 123456,
lyrics: [],
copyright: "",
midiPorts: [],
midiPortChannelOffsets: [],
tracksAmount: 0,
tempoChanges: [{ticks: 0, tempo: 120}],
tempoChanges: [{ ticks: 0, tempo: 120 }],
fileName: "NOT_LOADED.mid",
midiName: "Loading...",
rawMidiName: new Uint8Array([76, 111, 97, 100, 105, 110, 103, 46, 46, 46]), // "Loading..."
usedChannelsOnTrack: [],
timeDivision: 0,
keyRange: {min: 0, max: 127},
keyRange: { min: 0, max: 127 },
isEmbedded: false,
RMIDInfo: undefined,
bankOffset: 0
275 changes: 170 additions & 105 deletions src/spessasynth_lib/midi_parser/midi_editor.js

Large diffs are not rendered by default.

346 changes: 193 additions & 153 deletions src/spessasynth_lib/midi_parser/midi_loader.js

Large diffs are not rendered by default.

40 changes: 23 additions & 17 deletions src/spessasynth_lib/midi_parser/midi_message.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {IndexedByteArray} from "../utils/indexed_array.js";
import { IndexedByteArray } from "../utils/indexed_array.js";

/**
* midi_message.js
@@ -12,7 +12,8 @@ export class MidiMessage
* @param byte {number} the message status byte
* @param data {IndexedByteArray}
*/
constructor(ticks, byte, data) {
constructor(ticks, byte, data)
{
// absolute ticks from the start
this.ticks = ticks;
// message status byte (for meta it's the second byte)
@@ -29,13 +30,15 @@ export class MidiMessage
* @param statusByte
* @returns {number} channel is -1 for system messages -2 for meta and -3 for sysex
*/
export function getChannel(statusByte) {
export function getChannel(statusByte)
{
const eventType = statusByte & 0xF0;
const channel = statusByte & 0x0F;

let resultChannel = channel;

switch (eventType) {

switch (eventType)
{
// midi (and meta and sysex headers)
case 0x80:
case 0x90:
@@ -45,13 +48,14 @@ export function getChannel(statusByte) {
case 0xD0:
case 0xE0:
break;

case 0xF0:
switch (channel) {
switch (channel)
{
case 0x0:
resultChannel = -3;
break;

case 0x1:
case 0x2:
case 0x3:
@@ -68,17 +72,17 @@ export function getChannel(statusByte) {
case 0xE:
resultChannel = -1;
break;

case 0xF:
resultChannel = -2;
break;
}
break;

default:
resultChannel = -1;
}

return resultChannel;
}

@@ -127,18 +131,20 @@ export const messageTypes = {
* @param statusByte {number} the status byte
* @returns {{channel: number, status: number}} channel will be -1 for sysex and meta
*/
export function getEvent(statusByte) {
export function getEvent(statusByte)
{
const status = statusByte & 0xF0;
const channel = statusByte & 0x0F;

let eventChannel = -1;
let eventStatus = statusByte;

if (status >= 0x80 && status <= 0xE0) {

if (status >= 0x80 && status <= 0xE0)
{
eventChannel = channel;
eventStatus = status;
}

return {
status: eventStatus,
channel: eventChannel
26 changes: 13 additions & 13 deletions src/spessasynth_lib/midi_parser/midi_writer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { messageTypes } from './midi_message.js'
import { writeVariableLengthQuantity } from '../utils/byte_functions/variable_length_quantity.js'
import { writeBytesAsUintBigEndian } from '../utils/byte_functions/big_endian.js'
import { messageTypes } from "./midi_message.js";
import { writeVariableLengthQuantity } from "../utils/byte_functions/variable_length_quantity.js";
import { writeBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";

/**
* Exports the midi as a .mid file
@@ -13,12 +13,12 @@ export function writeMIDIFile(midi)
* @type {Uint8Array[]}
*/
const binaryTrackData = [];
for(const track of midi.tracks)
for (const track of midi.tracks)
{
const binaryTrack = [];
let currentTick = 0;
let runningByte = undefined;
for(const event of track)
for (const event of track)
{
// ticks stored in MIDI are absolute, but .mid wants relative. Convert them here.
const deltaTicks = event.ticks - currentTick;
@@ -27,13 +27,13 @@ export function writeMIDIFile(midi)
*/
let messageData;
// determine the message
if(event.messageStatusByte <= messageTypes.keySignature || event.messageStatusByte === messageTypes.sequenceSpecific)
if (event.messageStatusByte <= messageTypes.keySignature || event.messageStatusByte === messageTypes.sequenceSpecific)
{
// this is a meta message
// syntax is FF<type><length><data>
messageData = [0xff, event.messageStatusByte, ...writeVariableLengthQuantity(event.messageData.length), ...event.messageData];
}
else if(event.messageStatusByte === messageTypes.systemExclusive)
else if (event.messageStatusByte === messageTypes.systemExclusive)
{
// this is a system exclusive message
// syntax is F0<length><data>
@@ -43,7 +43,7 @@ export function writeMIDIFile(midi)
{
// this is a midi message
messageData = [];
if(runningByte !== event.messageStatusByte)
if (runningByte !== event.messageStatusByte)
{
// running byte was not the byte we want. Add the byte here.
runningByte = event.messageStatusByte;
@@ -61,19 +61,19 @@ export function writeMIDIFile(midi)
}
binaryTrackData.push(new Uint8Array(binaryTrack));
}

/**
* @param text {string}
* @param arr {number[]}
*/
function writeText(text, arr)
{
for(let i = 0; i < text.length; i++)
for (let i = 0; i < text.length; i++)
{
arr.push(text.charCodeAt(i));
}
}

// write the file
const binaryData = [];
// write header
@@ -82,9 +82,9 @@ export function writeMIDIFile(midi)
binaryData.push(0, midi.format); // format
binaryData.push(...writeBytesAsUintBigEndian(midi.tracksAmount, 2)); // num tracks
binaryData.push(...writeBytesAsUintBigEndian(midi.timeDivision, 2)); // time division

// write tracks
for(const track of binaryTrackData)
for (const track of binaryTrackData)
{
// write track header
writeText("MTrk", binaryData); // MTrk
229 changes: 140 additions & 89 deletions src/spessasynth_lib/midi_parser/rmidi_writer.js

Large diffs are not rendered by default.

91 changes: 51 additions & 40 deletions src/spessasynth_lib/midi_parser/used_keys_loaded.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from '../utils/loggin.js'
import { consoleColors } from '../utils/other.js'
import { DEFAULT_PERCUSSION } from '../synthetizer/synthetizer.js'
import { messageTypes, midiControllers } from './midi_message.js'
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js";
import { consoleColors } from "../utils/other.js";
import { DEFAULT_PERCUSSION } from "../synthetizer/synthetizer.js";
import { messageTypes, midiControllers } from "./midi_message.js";

/**
* @param mid {BasicMIDI}
@@ -10,91 +10,100 @@ import { messageTypes, midiControllers } from './midi_message.js'
*/
export function getUsedProgramsAndKeys(mid, soundfont)
{
SpessaSynthGroupCollapsed("%cSearching for all used programs and keys...",
consoleColors.info);
SpessaSynthGroupCollapsed(
"%cSearching for all used programs and keys...",
consoleColors.info
);
// find every bank:program combo and every key:velocity for each. Make sure to care about ports and drums
const channelsAmount = 16 + mid.midiPortChannelOffsets.reduce((max, cur) => cur > max ? cur: max);
const channelsAmount = 16 + mid.midiPortChannelOffsets.reduce((max, cur) => cur > max ? cur : max);
/**
* @type {{program: number, bank: number, drums: boolean, string: string}[]}
*/
const channelPresets = [];
for (let i = 0; i < channelsAmount; i++) {
for (let i = 0; i < channelsAmount; i++)
{
const bank = i % 16 === DEFAULT_PERCUSSION ? 128 : 0;
channelPresets.push({
program: 0,
bank: bank,
drums: i % 16 === DEFAULT_PERCUSSION, // drums appear on 9 every 16 channels,
string: `${bank}:0`,
string: `${bank}:0`
});
}

function updateString(ch)
{
// check if this exists in the soundfont
let exists = soundfont.getPreset(ch.bank, ch.program);
ch.bank = exists.bank;
ch.program = exists.program;
ch.string = ch.bank + ":" + ch.program;
if(!usedProgramsAndKeys[ch.string])
if (!usedProgramsAndKeys[ch.string])
{
SpessaSynthInfo(`%cDetected a new preset: %c${ch.string}`,
SpessaSynthInfo(
`%cDetected a new preset: %c${ch.string}`,
consoleColors.info,
consoleColors.recognized);
consoleColors.recognized
);
usedProgramsAndKeys[ch.string] = new Set();
}
}

/**
* find all programs used and key-velocity combos in them
* bank:program each has a set of midiNote-velocity
* @type {Object<string, Set<string>>}
*/
const usedProgramsAndKeys = {};

/**
* indexes for tracks
* @type {number[]}
*/
const eventIndexes = Array(mid.tracks.length).fill(0);
let remainingTracks = mid.tracks.length;

function findFirstEventIndex()
{
let index = 0;
let ticks = Infinity;
mid.tracks.forEach((track, i) => {
if(eventIndexes[i] >= track.length)
mid.tracks.forEach((track, i) =>
{
if (eventIndexes[i] >= track.length)
{
return;
}
if(track[eventIndexes[i]].ticks < ticks)
if (track[eventIndexes[i]].ticks < ticks)
{
index = i;
ticks = track[eventIndexes[i]].ticks;
}
});
return index;
}

const ports = mid.midiPorts.slice();
// check for xg
let system = "gs";
while(remainingTracks > 0)
while (remainingTracks > 0)
{
let trackNum = findFirstEventIndex();
const track = mid.tracks[trackNum];
if(eventIndexes[trackNum] >= track.length)
if (eventIndexes[trackNum] >= track.length)
{
remainingTracks--;
continue;
}
const event = track[eventIndexes[trackNum]];
eventIndexes[trackNum]++;

if(event.messageStatusByte === messageTypes.midiPort)
if (event.messageStatusByte === messageTypes.midiPort)
{
ports[trackNum] = event.messageData[0];
continue;
}
const status = event.messageStatusByte & 0xF0;
if(
if (
status !== messageTypes.noteOn &&
status !== messageTypes.controllerChange &&
status !== messageTypes.programChange &&
@@ -105,31 +114,31 @@ export function getUsedProgramsAndKeys(mid, soundfont)
}
const channel = (event.messageStatusByte & 0xF) + mid.midiPortChannelOffsets[ports[trackNum]] || 0;
let ch = channelPresets[channel];
switch(status)
switch (status)
{
case messageTypes.programChange:
ch.program = event.messageData[0];
updateString(ch);
break;

case messageTypes.controllerChange:
if(event.messageData[0] !== midiControllers.bankSelect)
if (event.messageData[0] !== midiControllers.bankSelect)
{
// we only care about bank select
continue;
}
if(system === "gs" && ch.drums)
if (system === "gs" && ch.drums)
{
// gs drums get changed via sysex, ignore here
continue;
}
const bank = event.messageData[1];
const realBank = Math.max(0, bank - mid.bankOffset);
if(system === "xg")
if (system === "xg")
{
// check for xg drums
const drumsBool = bank === 120 || bank === 126 || bank === 127;
if(drumsBool !== ch.drums)
if (drumsBool !== ch.drums)
{
// drum change is a program change
ch.drums = drumsBool;
@@ -145,31 +154,31 @@ export function getUsedProgramsAndKeys(mid, soundfont)
channelPresets[channel].bank = realBank;
// do not update the data, bank change doesnt change the preset
break;

case messageTypes.noteOn:
if(event.messageData[1] === 0)
if (event.messageData[1] === 0)
{
// that's a note off
continue;
}
updateString(ch);
usedProgramsAndKeys[ch.string].add(`${event.messageData[0]}-${event.messageData[1]}`);
break;

case messageTypes.systemExclusive:
// check for drum sysex
if(
if (
event.messageData[0] !== 0x41 || // roland
event.messageData[2] !== 0x42 || // GS
event.messageData[3] !== 0x12 || // GS
event.messageData[4] !== 0x40 || // system parameter
(event.messageData[5] & 0x10 ) === 0 || // part parameter
(event.messageData[5] & 0x10) === 0 || // part parameter
event.messageData[6] !== 0x15 // drum pars

)
{
// check for XG
if(
if (
event.messageData[0] === 0x43 && // yamaha
event.messageData[2] === 0x4C && // sXG ON
event.messageData[5] === 0x7E &&
@@ -187,16 +196,18 @@ export function getUsedProgramsAndKeys(mid, soundfont)
ch.bank = isDrum ? 128 : 0;
updateString(ch);
break;

}
}
for(const key of Object.keys(usedProgramsAndKeys))
for (const key of Object.keys(usedProgramsAndKeys))
{
if(usedProgramsAndKeys[key].size === 0)
if (usedProgramsAndKeys[key].size === 0)
{
SpessaSynthInfo(`%cDetected change but no keys for %c${key}`,
SpessaSynthInfo(
`%cDetected change but no keys for %c${key}`,
consoleColors.info,
consoleColors.value)
consoleColors.value
);
delete usedProgramsAndKeys[key];
}
}
13 changes: 10 additions & 3 deletions src/spessasynth_lib/sequencer/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
## This is the sequencer's folder.

The code here is responsible for playing back the parsed MIDI sequence with the synthesizer.

### Message protocol:

#### Message structure

```js
const message = {
messageType: number, // WorkletSequencerMessageType
@@ -11,13 +14,17 @@ const message = {
```

#### To worklet
Sequencer uses `Synthetizer`'s `post` method to post a message with `messageData` set to `workletMessageType.sequencerSpecific`.

Sequencer uses `Synthetizer`'s `post` method to post a message with `messageData` set to
`workletMessageType.sequencerSpecific`.
The `messageData` is set to the sequencer's message.

#### From worklet
`WorkletSequencer` uses `SpessaSynthProcessor`'s post to send a message with `messageData` set to `returnMessageType.sequencerSpecific`.
The `messageData` is set to the sequencer's return message.

`WorkletSequencer` uses `SpessaSynthProcessor`'s post to send a message with `messageData` set to
`returnMessageType.sequencerSpecific`.
The `messageData` is set to the sequencer's return message.

### Process tick

`processTick` is called every time the `process` method is called via `SpessaSynthProcessor.processTickCallback`.
460 changes: 230 additions & 230 deletions src/spessasynth_lib/sequencer/sequencer.js

Large diffs are not rendered by default.

46 changes: 23 additions & 23 deletions src/spessasynth_lib/sequencer/worklet_sequencer/events.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { returnMessageType } from '../../synthetizer/worklet_system/message_protocol/worklet_message.js'
import { WorkletSequencerMessageType, WorkletSequencerReturnMessageType } from './sequencer_message.js'
import { messageTypes, midiControllers } from '../../midi_parser/midi_message.js'
import { MIDI_CHANNEL_COUNT } from '../../synthetizer/synthetizer.js'
import { returnMessageType } from "../../synthetizer/worklet_system/message_protocol/worklet_message.js";
import { WorkletSequencerMessageType, WorkletSequencerReturnMessageType } from "./sequencer_message.js";
import { messageTypes, midiControllers } from "../../midi_parser/midi_message.js";
import { MIDI_CHANNEL_COUNT } from "../../synthetizer/synthetizer.js";

/**
* @param messageType {WorkletSequencerMessageType}
@@ -14,39 +14,39 @@ export function processMessage(messageType, messageData)
{
default:
break;

case WorkletSequencerMessageType.loadNewSongList:
this.loadNewSongList(messageData);
break;

case WorkletSequencerMessageType.pause:
this.pause();
break;

case WorkletSequencerMessageType.play:
this.play(messageData);
break;

case WorkletSequencerMessageType.stop:
this.stop();
break;

case WorkletSequencerMessageType.setTime:
this.currentTime = messageData;
break;

case WorkletSequencerMessageType.changeMIDIMessageSending:
this.sendMIDIMessages = messageData;
break;

case WorkletSequencerMessageType.setPlaybackRate:
this.playbackRate = messageData;
break;

case WorkletSequencerMessageType.setLoop:
this.loop = messageData;
break;

case WorkletSequencerMessageType.changeSong:
if (messageData)
{
@@ -57,11 +57,11 @@ export function processMessage(messageType, messageData)
this.previousSong();
}
break;

case WorkletSequencerMessageType.getMIDI:
this.post(WorkletSequencerReturnMessageType.getMIDI, this.midiData);
break;

case WorkletSequencerMessageType.setSkipToFirstNote:
this._skipToFirstNoteOn = messageData;
break;
@@ -76,7 +76,7 @@ export function processMessage(messageType, messageData)
*/
export function post(messageType, messageData = undefined)
{
if(!this.synth.enableEventSystem)
if (!this.synth.enableEventSystem)
{
return;
}
@@ -86,7 +86,7 @@ export function post(messageType, messageData = undefined)
messageType: messageType,
messageData: messageData
}
})
});
}

/**
@@ -107,11 +107,11 @@ export function sendMIDIMessage(message)
export function sendMIDICC(channel, type, value)
{
channel %= 16;
if(!this.sendMIDIMessages)
if (!this.sendMIDIMessages)
{
return;
}
this.sendMIDIMessage([messageTypes.controllerChange | channel, type, value])
this.sendMIDIMessage([messageTypes.controllerChange | channel, type, value]);
}

/**
@@ -122,7 +122,7 @@ export function sendMIDICC(channel, type, value)
export function sendMIDIProgramChange(channel, program)
{
channel %= 16;
if(!this.sendMIDIMessages)
if (!this.sendMIDIMessages)
{
return;
}
@@ -139,7 +139,7 @@ export function sendMIDIProgramChange(channel, program)
export function sendMIDIPitchWheel(channel, MSB, LSB)
{
channel %= 16;
if(!this.sendMIDIMessages)
if (!this.sendMIDIMessages)
{
return;
}
@@ -151,12 +151,12 @@ export function sendMIDIPitchWheel(channel, MSB, LSB)
*/
export function sendMIDIReset()
{
if(!this.sendMIDIMessages)
if (!this.sendMIDIMessages)
{
return;
}
this.sendMIDIMessage([messageTypes.reset]);
for(let ch = 0; ch < MIDI_CHANNEL_COUNT; ch++)
for (let ch = 0; ch < MIDI_CHANNEL_COUNT; ch++)
{
this.sendMIDIMessage([messageTypes.controllerChange | ch, midiControllers.allSoundOff, 0]);
this.sendMIDIMessage([messageTypes.controllerChange | ch, midiControllers.resetAllControllers, 0]);
137 changes: 75 additions & 62 deletions src/spessasynth_lib/sequencer/worklet_sequencer/play.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getEvent, messageTypes, midiControllers } from '../../midi_parser/midi_message.js'
import { WorkletSequencerReturnMessageType } from './sequencer_message.js'
import { MIDIticksToSeconds } from '../../midi_parser/basic_midi.js'
import { getEvent, messageTypes, midiControllers } from "../../midi_parser/midi_message.js";
import { WorkletSequencerReturnMessageType } from "./sequencer_message.js";
import { MIDIticksToSeconds } from "../../midi_parser/basic_midi.js";


// an array with preset default values
@@ -28,14 +28,14 @@ export function _playTo(time, ticks = undefined)
this.synth.resetAllControllers();
this.sendMIDIReset();
this._resetTimers();

const channelsToSave = this.synth.workletProcessorChannels.length;
/**
* save pitch bends here and send them only after
* @type {number[]}
*/
const pitchBends = Array(channelsToSave).fill(8192);

/**
* Save programs here and send them only after
* @type {{program: number, bank: number, actualBank: number}[]}
@@ -46,25 +46,25 @@ export function _playTo(time, ticks = undefined)
programs.push({
program: -1,
bank: 0,
actualBank: 0,
actualBank: 0
});
}

const isCCNonSkippable = controllerNumber => (
controllerNumber === midiControllers.dataDecrement ||
controllerNumber === midiControllers.dataIncrement ||
controllerNumber === midiControllers.dataEntryMsb ||
controllerNumber === midiControllers.dataDecrement ||
controllerNumber === midiControllers.dataDecrement ||
controllerNumber === midiControllers.dataIncrement ||
controllerNumber === midiControllers.dataEntryMsb ||
controllerNumber === midiControllers.dataDecrement ||
controllerNumber === midiControllers.lsbForControl6DataEntry ||
controllerNumber === midiControllers.RPNLsb ||
controllerNumber === midiControllers.RPNMsb ||
controllerNumber === midiControllers.NRPNLsb ||
controllerNumber === midiControllers.NRPNMsb ||
controllerNumber === midiControllers.bankSelect ||
controllerNumber === midiControllers.lsbForControl0BankSelect||
controllerNumber === midiControllers.RPNLsb ||
controllerNumber === midiControllers.RPNMsb ||
controllerNumber === midiControllers.NRPNLsb ||
controllerNumber === midiControllers.NRPNMsb ||
controllerNumber === midiControllers.bankSelect ||
controllerNumber === midiControllers.lsbForControl0BankSelect ||
controllerNumber === midiControllers.resetAllControllers
);

/**
* Save controllers here and send them only after
* @type {number[][]}
@@ -74,63 +74,63 @@ export function _playTo(time, ticks = undefined)
{
savedControllers.push(Array.from(defaultControllerArray));
}

while(true)
while (true)
{
// find next event
let trackIndex = this._findFirstEventIndex();
let event = this.tracks[trackIndex][this.eventIndex[trackIndex]];
if(ticks !== undefined)
if (ticks !== undefined)
{
if(event.ticks >= ticks)
if (event.ticks >= ticks)
{
break;
}
}
else
{
if(this.playedTime >= time)
if (this.playedTime >= time)
{
break;
}
}

// skip note ons
const info = getEvent(event.messageStatusByte);
// Keep in mind midi ports to determine channel!!
const channel = info.channel + (this.midiPortChannelOffsets[this.midiPorts[trackIndex]] || 0);
switch(info.status)
switch (info.status)
{
// skip note messages
case messageTypes.noteOn:
case messageTypes.noteOff:
case messageTypes.keySignature:
break;

// skip pitch bend
case messageTypes.pitchBend:
pitchBends[channel] = event.messageData[1] << 7 | event.messageData[0];
break;

case messageTypes.programChange:
const p = programs[channel];
p.program = event.messageData[0];
p.actualBank = p.bank;
break;

case messageTypes.controllerChange:
// do not skip data entries
const controllerNumber = event.messageData[0];
if(isCCNonSkippable(controllerNumber))
if (isCCNonSkippable(controllerNumber))
{
let ccV = event.messageData[1];
if(controllerNumber === midiControllers.bankSelect)
if (controllerNumber === midiControllers.bankSelect)
{
// add the bank to saved
programs[channel].bank = ccV;
break;
}
if(this.sendMIDIMessages)
if (this.sendMIDIMessages)
{
this.sendMIDICC(channel, controllerNumber, ccV);
}
@@ -141,53 +141,59 @@ export function _playTo(time, ticks = undefined)
}
else
{
if(savedControllers[channel] === undefined)
if (savedControllers[channel] === undefined)
{
savedControllers[channel] = Array.from(defaultControllerArray);
}
savedControllers[channel][controllerNumber] = event.messageData[1];
}
break;

default:
this._processEvent(event, trackIndex);
break;
}

this.eventIndex[trackIndex]++;
// find next event
trackIndex = this._findFirstEventIndex();
let nextEvent = this.tracks[trackIndex][this.eventIndex[trackIndex]];
if(nextEvent === undefined)
if (nextEvent === undefined)
{
this.stop();
return false;
}
this.playedTime += this.oneTickToSeconds * (nextEvent.ticks - event.ticks);
}

// restoring saved controllers
if(this.sendMIDIMessages)
if (this.sendMIDIMessages)
{
for (let channelNumber = 0; channelNumber < channelsToSave; channelNumber++)
{
// restore pitch bends
if(pitchBends[channelNumber] !== undefined)
if (pitchBends[channelNumber] !== undefined)
{
this.sendMIDIPitchWheel(channelNumber, pitchBends[channelNumber] >> 7, pitchBends[channelNumber] & 0x7F);
this.sendMIDIPitchWheel(
channelNumber,
pitchBends[channelNumber] >> 7,
pitchBends[channelNumber] & 0x7F
);
}
if(savedControllers[channelNumber] !== undefined)
if (savedControllers[channelNumber] !== undefined)
{
// every controller that has changed
savedControllers[channelNumber].forEach((value, index) => {
if(value !== defaultControllerArray[index] && !isCCNonSkippable(index))
savedControllers[channelNumber].forEach((value, index) =>
{
if (value !== defaultControllerArray[index] && !isCCNonSkippable(
index))
{
this.sendMIDICC(channelNumber, index, value);
}
})
});
}
// restore programs
if(programs[channelNumber].program >= 0 && programs[channelNumber].actualBank >= 0)
if (programs[channelNumber].program >= 0 && programs[channelNumber].actualBank >= 0)
{
const bank = programs[channelNumber].actualBank;
this.sendMIDICC(channelNumber, midiControllers.bankSelect, bank);
@@ -201,22 +207,28 @@ export function _playTo(time, ticks = undefined)
for (let channelNumber = 0; channelNumber < channelsToSave; channelNumber++)
{
// restore pitch bends
if(pitchBends[channelNumber] !== undefined)
if (pitchBends[channelNumber] !== undefined)
{
this.synth.pitchWheel(channelNumber, pitchBends[channelNumber] >> 7, pitchBends[channelNumber] & 0x7F);
}
if(savedControllers[channelNumber] !== undefined)
if (savedControllers[channelNumber] !== undefined)
{
// every controller that has changed
savedControllers[channelNumber].forEach((value, index) => {
if(value !== defaultControllerArray[index] && !isCCNonSkippable(index))
savedControllers[channelNumber].forEach((value, index) =>
{
if (value !== defaultControllerArray[index] && !isCCNonSkippable(
index))
{
this.synth.controllerChange(channelNumber, index, value);
this.synth.controllerChange(
channelNumber,
index,
value
);
}
})
});
}
// restore programs
if(programs[channelNumber].program >= 0 && programs[channelNumber].actualBank >= 0)
if (programs[channelNumber].program >= 0 && programs[channelNumber].actualBank >= 0)
{
const bank = programs[channelNumber].actualBank;
this.synth.controllerChange(channelNumber, midiControllers.bankSelect, bank);
@@ -234,34 +246,35 @@ export function _playTo(time, ticks = undefined)
*/
export function play(resetTime = false)
{
if(this.midiData === undefined)
if (this.midiData === undefined)
{
return;
}

// reset the time if necesarry
if(resetTime)
if (resetTime)
{
this.currentTime = 0;
return;
}

if(this.currentTime >= this.duration)
if (this.currentTime >= this.duration)
{
this.currentTime = 0;
return;
}

// unpause if paused
if(this.paused)
if (this.paused)
{
// adjust the start time
this._recalculateStartTime(this.pausedTime)
this._recalculateStartTime(this.pausedTime);
this.pausedTime = undefined;
}
if(!this.sendMIDIMessages)
if (!this.sendMIDIMessages)
{
this.playingNotes.forEach(n => {
this.playingNotes.forEach(n =>
{
this.synth.noteOn(n.channel, n.midiNote, n.velocity, false, true);
});
}
@@ -283,7 +296,7 @@ export function setTimeTicks(ticks)
);
const isNotFinished = this._playTo(0, ticks);
this._recalculateStartTime(this.playedTime);
if(!isNotFinished)
if (!isNotFinished)
{
return;
}
71 changes: 39 additions & 32 deletions src/spessasynth_lib/sequencer/worklet_sequencer/process_event.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { getEvent, messageTypes } from '../../midi_parser/midi_message.js'
import { WorkletSequencerReturnMessageType } from './sequencer_message.js'
import { consoleColors } from '../../utils/other.js'
import { SpessaSynthWarn } from '../../utils/loggin.js'
import { readBytesAsUintBigEndian } from '../../utils/byte_functions/big_endian.js'
import { DEFAULT_PERCUSSION } from '../../synthetizer/synthetizer.js'
import { getEvent, messageTypes } from "../../midi_parser/midi_message.js";
import { WorkletSequencerReturnMessageType } from "./sequencer_message.js";
import { consoleColors } from "../../utils/other.js";
import { SpessaSynthWarn } from "../../utils/loggin.js";
import { readBytesAsUintBigEndian } from "../../utils/byte_functions/big_endian.js";
import { DEFAULT_PERCUSSION } from "../../synthetizer/synthetizer.js";

/**
* Processes a single event
@@ -14,23 +14,27 @@ import { DEFAULT_PERCUSSION } from '../../synthetizer/synthetizer.js'
*/
export function _processEvent(event, trackIndex)
{
if(this.ignoreEvents) return;
if(this.sendMIDIMessages)
if (this.ignoreEvents)
{
if(event.messageStatusByte >= 0x80)
return;
}
if (this.sendMIDIMessages)
{
if (event.messageStatusByte >= 0x80)
{
this.sendMIDIMessage([event.messageStatusByte, ...event.messageData]);
return;
}
}
const statusByteData = getEvent(event.messageStatusByte);
const offset = this.midiPortChannelOffsets[this.midiPorts[trackIndex]] || 0
const offset = this.midiPortChannelOffsets[this.midiPorts[trackIndex]] || 0;
statusByteData.channel += offset;
// process the event
switch (statusByteData.status) {
switch (statusByteData.status)
{
case messageTypes.noteOn:
const velocity = event.messageData[1];
if(velocity > 0)
if (velocity > 0)
{
this.synth.noteOn(statusByteData.channel, event.messageData[0], velocity);
this.playingNotes.push({
@@ -44,56 +48,56 @@ export function _processEvent(event, trackIndex)
this.synth.noteOff(statusByteData.channel, event.messageData[0]);
const toDelete = this.playingNotes.findIndex(n =>
n.midiNote === event.messageData[0] && n.channel === statusByteData.channel);
if(toDelete !== -1)
if (toDelete !== -1)
{
this.playingNotes.splice(toDelete, 1);
}
}
break;

case messageTypes.noteOff:
this.synth.noteOff(statusByteData.channel, event.messageData[0]);
const toDelete = this.playingNotes.findIndex(n =>
n.midiNote === event.messageData[0] && n.channel === statusByteData.channel);
if(toDelete !== -1)
if (toDelete !== -1)
{
this.playingNotes.splice(toDelete, 1);
}
break;

case messageTypes.pitchBend:
this.synth.pitchWheel(statusByteData.channel, event.messageData[1], event.messageData[0]);
break;

case messageTypes.controllerChange:
this.synth.controllerChange(statusByteData.channel, event.messageData[0], event.messageData[1]);
break;

case messageTypes.programChange:
this.synth.programChange(statusByteData.channel, event.messageData[0]);
break;

case messageTypes.polyPressure:
this.synth.polyPressure(statusByteData.channel, event.messageData[0], event.messageData[1]);
break;

case messageTypes.channelPressure:
this.synth.channelPressure(statusByteData.channel, event.messageData[0]);
break;

case messageTypes.systemExclusive:
this.synth.systemExclusive(event.messageData, offset);
break;

case messageTypes.setTempo:
this.oneTickToSeconds = 60 / (getTempo(event) * this.midiData.timeDivision);
if(this.oneTickToSeconds === 0)
if (this.oneTickToSeconds === 0)
{
this.oneTickToSeconds = 60 / (120 * this.midiData.timeDivision);
SpessaSynthWarn("invalid tempo! falling back to 120 BPM");
}
break;

// recongized but ignored
case messageTypes.timeSignature:
case messageTypes.endOfTrack:
@@ -104,7 +108,7 @@ export function _processEvent(event, trackIndex)
case messageTypes.sequenceNumber:
case messageTypes.sequenceSpecific:
break;

case messageTypes.text:
case messageTypes.lyric:
case messageTypes.copyright:
@@ -113,24 +117,27 @@ export function _processEvent(event, trackIndex)
case messageTypes.cuePoint:
case messageTypes.instrumentName:
case messageTypes.programName:
this.post(WorkletSequencerReturnMessageType.textEvent, [event.messageData, statusByteData.status])
this.post(WorkletSequencerReturnMessageType.textEvent, [event.messageData, statusByteData.status]);
break;

case messageTypes.midiPort:
this.assignMIDIPort(trackIndex, event.messageData[0]);
break;

case messageTypes.reset:
this.synth.stopAllChannels();
this.synth.resetAllControllers();
break;

default:
SpessaSynthWarn(`%cUnrecognized Event: %c${event.messageStatusByte}%c status byte: %c${Object.keys(messageTypes).find(k => messageTypes[k] === statusByteData.status)}`,
SpessaSynthWarn(
`%cUnrecognized Event: %c${event.messageStatusByte}%c status byte: %c${Object.keys(
messageTypes).find(k => messageTypes[k] === statusByteData.status)}`,
consoleColors.warn,
consoleColors.unrecognized,
consoleColors.warn,
consoleColors.value);
consoleColors.value
);
break;
}
}
@@ -145,7 +152,7 @@ export function _addNewMidiPort()
for (let i = 0; i < 16; i++)
{
this.synth.createWorkletChannel(true);
if(i === DEFAULT_PERCUSSION)
if (i === DEFAULT_PERCUSSION)
{
this.synth.setDrums(this.synth.workletProcessorChannels.length - 1, true);
}
29 changes: 15 additions & 14 deletions src/spessasynth_lib/sequencer/worklet_sequencer/process_tick.js
Original file line number Diff line number Diff line change
@@ -6,55 +6,55 @@
export function _processTick()
{
let current = this.currentTime;
while(this.playedTime < current)
while (this.playedTime < current)
{
// find next event
let trackIndex = this._findFirstEventIndex();
let event = this.tracks[trackIndex][this.eventIndex[trackIndex]];
this._processEvent(event, trackIndex);

this.eventIndex[trackIndex]++;

// find next event
trackIndex = this._findFirstEventIndex();
if(this.tracks[trackIndex].length <= this.eventIndex[trackIndex])
if (this.tracks[trackIndex].length <= this.eventIndex[trackIndex])
{
// song has ended
if(this.loop)
if (this.loop)
{
this.setTimeTicks(this.midiData.loop.start);
return;
}
this.eventIndex[trackIndex]--;
this.pause(true);
if(this.songs.length > 1)
if (this.songs.length > 1)
{
this.nextSong();
}
return;
}
let eventNext = this.tracks[trackIndex][this.eventIndex[trackIndex]];
this.playedTime += this.oneTickToSeconds * (eventNext.ticks - event.ticks);

// loop
if((this.midiData.loop.end <= event.ticks) && this.loop && this.currentLoopCount > 0)
if ((this.midiData.loop.end <= event.ticks) && this.loop && this.currentLoopCount > 0)
{
this.currentLoopCount--;
this.setTimeTicks(this.midiData.loop.start);
return;
}
// if song has ended
else if(current >= this.duration)
else if (current >= this.duration)
{
if(this.loop && this.currentLoopCount > 0)
if (this.loop && this.currentLoopCount > 0)
{
this.currentLoopCount--;
this.setTimeTicks(this.midiData.loop.start);
return;
}
this.eventIndex[trackIndex]--;
this.pause(true);
if(this.songs.length > 1)
if (this.songs.length > 1)
{
this.nextSong();
}
@@ -72,12 +72,13 @@ export function _findFirstEventIndex()
{
let index = 0;
let ticks = Infinity;
this.tracks.forEach((track, i) => {
if(this.eventIndex[i] >= track.length)
this.tracks.forEach((track, i) =>
{
if (this.eventIndex[i] >= track.length)
{
return;
}
if(track[this.eventIndex[i]].ticks < ticks)
if (track[this.eventIndex[i]].ticks < ticks)
{
index = i;
ticks = track[this.eventIndex[i]].ticks;
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ export const WorkletSequencerMessageType = {
changeSong: 8,
getMIDI: 9,
setSkipToFirstNote: 10
}
};

/**
*
@@ -37,5 +37,5 @@ export const WorkletSequencerReturnMessageType = {
timeChange: 3, // newAbsoluteTime<number>
pause: 4, // no data
getMIDI: 5, // midiData<MIDI>
midiError: 6, // errorMSG<string>
}
midiError: 6 // errorMSG<string>
};
93 changes: 52 additions & 41 deletions src/spessasynth_lib/sequencer/worklet_sequencer/song_control.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { WorkletSequencerReturnMessageType } from './sequencer_message.js'
import { consoleColors, formatTime } from '../../utils/other.js'
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from '../../utils/loggin.js'
import { MidiData } from '../../midi_parser/midi_data.js'
import { MIDI } from '../../midi_parser/midi_loader.js'
import { getUsedProgramsAndKeys } from '../../midi_parser/used_keys_loaded.js'
import { MIDIticksToSeconds } from '../../midi_parser/basic_midi.js'
import { WorkletSequencerReturnMessageType } from "./sequencer_message.js";
import { consoleColors, formatTime } from "../../utils/other.js";
import {
SpessaSynthGroupCollapsed,
SpessaSynthGroupEnd,
SpessaSynthInfo,
SpessaSynthWarn
} from "../../utils/loggin.js";
import { MidiData } from "../../midi_parser/midi_data.js";
import { MIDI } from "../../midi_parser/midi_loader.js";
import { getUsedProgramsAndKeys } from "../../midi_parser/used_keys_loaded.js";
import { MIDIticksToSeconds } from "../../midi_parser/basic_midi.js";

/**
* @param trackNum {number}
@@ -14,22 +19,22 @@ import { MIDIticksToSeconds } from '../../midi_parser/basic_midi.js'
export function assignMIDIPort(trackNum, port)
{
// assign new 16 channels if the port is not occupied yet
if(this.midiPortChannelOffset === 0)
if (this.midiPortChannelOffset === 0)
{
this.midiPortChannelOffset += 16;
this.midiPortChannelOffsets[port] = 0;
}

if(this.midiPortChannelOffsets[port] === undefined)
if (this.midiPortChannelOffsets[port] === undefined)
{
if(this.synth.workletProcessorChannels.length < this.midiPortChannelOffset + 15)
if (this.synth.workletProcessorChannels.length < this.midiPortChannelOffset + 15)
{
this._addNewMidiPort();
}
this.midiPortChannelOffsets[port] = this.midiPortChannelOffset;
this.midiPortChannelOffset += 16;
}

this.midiPorts[trackNum] = port;
}

@@ -45,25 +50,25 @@ export function loadNewSequence(parsedMidi)
{
throw "No tracks supplied!";
}

this.oneTickToSeconds = 60 / (120 * parsedMidi.timeDivision)

this.oneTickToSeconds = 60 / (120 * parsedMidi.timeDivision);
/**
* @type {BasicMIDI}
*/
this.midiData = parsedMidi;

this.currentLoopCount = this.loopCount;

// check for embedded soundfont
if(this.midiData.embeddedSoundFont !== undefined)
if (this.midiData.embeddedSoundFont !== undefined)
{
SpessaSynthInfo("%cEmbedded soundfont detected! Using it.", consoleColors.recognized);
this.synth.setEmbeddedSoundFont(this.midiData.embeddedSoundFont, this.midiData.bankOffset);
}
else
{
if(this.synth.overrideSoundfont)
if (this.synth.overrideSoundfont)
{
// clean up the embedded soundfont
this.synth.clearSoundFont(true, true);
@@ -76,50 +81,56 @@ export function loadNewSequence(parsedMidi)
const bank = parseInt(programBank.split(":")[0]);
const program = parseInt(programBank.split(":")[1]);
const preset = this.synth.getPreset(bank, program);
SpessaSynthInfo(`%cPreloading used samples on %c${preset.presetName}%c...`,
SpessaSynthInfo(
`%cPreloading used samples on %c${preset.presetName}%c...`,
consoleColors.info,
consoleColors.recognized,
consoleColors.info)
for (const combo of combos) {
consoleColors.info
);
for (const combo of combos)
{
const split = combo.split("-");
preset.preloadSpecific(parseInt(split[0]), parseInt(split[1]));
}
}
SpessaSynthGroupEnd();
}

/**
* the midi track data
* @type {MidiMessage[][]}
*/
this.tracks = this.midiData.tracks;

// copy over the port data
this.midiPorts = this.midiData.midiPorts;

// clear last port data
this.midiPortChannelOffset = 0;
this.midiPortChannelOffsets = {};
// assign port offsets
this.midiData.midiPorts.forEach((port, trackIndex) => {
this.midiData.midiPorts.forEach((port, trackIndex) =>
{
this.assignMIDIPort(trackIndex, port);
});

/**
* Same as Audio.duration (seconds)
* @type {number}
*/
this.duration = this.midiData.duration;
this.firstNoteTime = MIDIticksToSeconds(this.midiData.firstNoteOn, this.midiData);
SpessaSynthInfo(`%cTotal song time: ${formatTime(Math.ceil(this.duration)).time}`, consoleColors.recognized);

this.post(WorkletSequencerReturnMessageType.songChange, [new MidiData(this.midiData), this.songIndex]);

this.synth.resetAllControllers();
if(this.duration <= 1)
if (this.duration <= 1)
{
SpessaSynthWarn(`%cVery short song: (${formatTime(Math.round(this.duration)).time}). Disabling loop!`,
consoleColors.warn);
SpessaSynthWarn(
`%cVery short song: (${formatTime(Math.round(this.duration)).time}). Disabling loop!`,
consoleColors.warn
);
this.loop = false;
}
this.play(true);
@@ -135,29 +146,29 @@ export function loadNewSongList(midiBuffers)
* parse the MIDIs (only the array buffers, MIDI is unchanged)
* @type {BasicMIDI[]}
*/
this.songs = midiBuffers.reduce((mids, b) => {
if(b.duration)
this.songs = midiBuffers.reduce((mids, b) =>
{
if (b.duration)
{
mids.push(b);
return mids;
}
try
{
mids.push(new MIDI(b.binary, b.altName || ""));
}
catch (e)
} catch (e)
{
this.post(WorkletSequencerReturnMessageType.midiError, e.message);
return mids;
}
return mids;
}, []);
if(this.songs.length < 1)
if (this.songs.length < 1)
{
return;
}
this.songIndex = 0;
if(this.songs.length > 1)
if (this.songs.length > 1)
{
this.loop = false;
}
@@ -169,7 +180,7 @@ export function loadNewSongList(midiBuffers)
*/
export function nextSong()
{
if(this.songs.length === 1)
if (this.songs.length === 1)
{
this.currentTime = 0;
return;
@@ -184,13 +195,13 @@ export function nextSong()
*/
export function previousSong()
{
if(this.songs.length === 1)
if (this.songs.length === 1)
{
this.currentTime = 0;
return;
}
this.songIndex--;
if(this.songIndex < 0)
if (this.songIndex < 0)
{
this.songIndex = this.songs.length - 1;
}
108 changes: 54 additions & 54 deletions src/spessasynth_lib/sequencer/worklet_sequencer/worklet_sequencer.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { WorkletSequencerReturnMessageType } from './sequencer_message.js'
import { _addNewMidiPort, _processEvent } from './process_event.js'
import { _findFirstEventIndex, _processTick } from './process_tick.js'
import { assignMIDIPort, loadNewSequence, loadNewSongList, nextSong, previousSong } from './song_control.js'
import { _playTo, _recalculateStartTime, play, setTimeTicks } from './play.js'
import { messageTypes, midiControllers } from '../../midi_parser/midi_message.js'
import { WorkletSequencerReturnMessageType } from "./sequencer_message.js";
import { _addNewMidiPort, _processEvent } from "./process_event.js";
import { _findFirstEventIndex, _processTick } from "./process_tick.js";
import { assignMIDIPort, loadNewSequence, loadNewSongList, nextSong, previousSong } from "./song_control.js";
import { _playTo, _recalculateStartTime, play, setTimeTicks } from "./play.js";
import { messageTypes, midiControllers } from "../../midi_parser/midi_message.js";
import {
post,
processMessage,
sendMIDICC,
sendMIDIMessage,
sendMIDIPitchWheel,
sendMIDIProgramChange,
sendMIDIReset,
} from './events.js'
import { SpessaSynthWarn } from '../../utils/loggin.js'
import { MIDI_CHANNEL_COUNT } from '../../synthetizer/synthetizer.js'
sendMIDIReset
} from "./events.js";
import { SpessaSynthWarn } from "../../utils/loggin.js";
import { MIDI_CHANNEL_COUNT } from "../../synthetizer/synthetizer.js";

class WorkletSequencer
{
@@ -25,47 +25,47 @@ class WorkletSequencer
{
this.synth = spessasynthProcessor;
this.ignoreEvents = false;

/**
* If the event should instead be sent back to the main thread instead of synth
* @type {boolean}
*/
this.sendMIDIMessages = false;

this.loopCount = Infinity;
this.currentLoopCount = this.loopCount;

// event's number in this.events
/**
* @type {number[]}
*/
this.eventIndex = [];
this.songIndex = 0;

// tracks the time that we have already played
/**
* @type {number}
*/
this.playedTime = 0;

/**
* The (relative) time when the sequencer was paused. If it's not paused then it's undefined.
* @type {number}
*/
this.pausedTime = undefined;

/**
* Absolute playback startTime, bases on the synth's time
* @type {number}
*/
this.absoluteStartTime = currentTime;

/**
* Controls the playback's rate
* @type {number}
*/
this._playbackRate = 1;

/**
* Currently playing notes (for pausing and resuming)
* @type {{
@@ -75,37 +75,37 @@ class WorkletSequencer
* }[]}
*/
this.playingNotes = [];

// controls if the sequencer loops (defaults to true)
this.loop = true;

/**
* the current track data
* @type {BasicMIDI}
*/
this.midiData = undefined;

/**
* midi port number for the corresponding track
* @type {number[]}
*/
this.midiPorts = [];

this.midiPortChannelOffset = 0;

/**
* midi port: channel offset
* @type {Object<number, number>}
*/
this.midiPortChannelOffsets = {};

/**
* @type {boolean}
* @private
*/
this._skipToFirstNoteOn = true;
}

/**
* @param value {number}
*/
@@ -115,24 +115,24 @@ class WorkletSequencer
this._playbackRate = value;
this.currentTime = time;
}

get currentTime()
{
// return the paused time if it's set to something other than undefined
if(this.pausedTime)
if (this.pausedTime)
{
return this.pausedTime;
}

return (currentTime - this.absoluteStartTime) * this._playbackRate;
}

set currentTime(time)
{
if(time > this.duration || time < 0)
if (time > this.duration || time < 0)
{
// time is 0
if(this._skipToFirstNoteOn)
if (this._skipToFirstNoteOn)
{
this.setTimeTicks(this.midiData.firstNoteOn - 1);
}
@@ -142,9 +142,9 @@ class WorkletSequencer
}
return;
}
if(this._skipToFirstNoteOn)
if (this._skipToFirstNoteOn)
{
if(time < this.firstNoteTime)
if (time < this.firstNoteTime)
{
this.setTimeTicks(this.midiData.firstNoteOn - 1);
return;
@@ -156,20 +156,29 @@ class WorkletSequencer
this.post(WorkletSequencerReturnMessageType.timeChange, currentTime - time);
const isNotFinished = this._playTo(time);
this._recalculateStartTime(time);
if(!isNotFinished)
if (!isNotFinished)
{
return;
}
this.play();
}


/**
* true if paused, false if playing or stopped
* @returns {boolean}
*/
get paused()
{
return this.pausedTime !== undefined;
}

/**
* Pauses the playback
* @param isFinished {boolean}
*/
pause(isFinished = false)
{
if(this.paused)
if (this.paused)
{
SpessaSynthWarn("Already paused");
return;
@@ -178,22 +187,22 @@ class WorkletSequencer
this.stop();
this.post(WorkletSequencerReturnMessageType.pause, isFinished);
}

/**
* Stops the playback
*/
stop()
{
this.clearProcessHandler()
this.clearProcessHandler();
// disable sustain
for (let i = 0; i < 16; i++)
{
this.synth.controllerChange(i, midiControllers.sustainPedal, 0);
}
this.synth.stopAllChannels();
if(this.sendMIDIMessages)
if (this.sendMIDIMessages)
{
for(let note of this.playingNotes)
for (let note of this.playingNotes)
{
this.sendMIDIMessage([messageTypes.noteOff | (note.channel % 16), note.midiNote]);
}
@@ -204,27 +213,18 @@ class WorkletSequencer
}
}
}

_resetTimers()
{
this.playedTime = 0
this.playedTime = 0;
this.eventIndex = Array(this.tracks.length).fill(0);
}

/**
* true if paused, false if playing or stopped
* @returns {boolean}
*/
get paused()
{
return this.pausedTime !== undefined;
}


setProcessHandler()
{
this.synth.processTickCallback = this._processTick.bind(this);
}

clearProcessHandler()
{
this.synth.processTickCallback = undefined;
@@ -257,4 +257,4 @@ WorkletSequencer.prototype._playTo = _playTo;
WorkletSequencer.prototype.setTimeTicks = setTimeTicks;
WorkletSequencer.prototype._recalculateStartTime = _recalculateStartTime;

export { WorkletSequencer }
export { WorkletSequencer };
3 changes: 2 additions & 1 deletion src/spessasynth_lib/soundfont/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## This is the SoundFont2 parsing library.

The code here is responsible for parsing the SoundFont2 file and
providing an easy way to get the data out.
providing an easy way to get the data out.
Default modulators are also stored here (in `modulators.js`)

`basic_soundfont` folder contains the classes that represent the soundfont file.
32 changes: 16 additions & 16 deletions src/spessasynth_lib/soundfont/basic_soundfont/basic_instrument.js
Original file line number Diff line number Diff line change
@@ -14,54 +14,54 @@ export class BasicInstrument
this.instrumentZones = [];
this._useCount = 0;
}


/**
* @returns {number}
*/
get useCount()
{
return this._useCount;
}

addUseCount()
{
this._useCount++;
this.instrumentZones.forEach(z => z.useCount++);
}

removeUseCount()
{
this._useCount--;
for(let i = 0; i < this.instrumentZones.length; i++)
for (let i = 0; i < this.instrumentZones.length; i++)
{
if(this.safeDeleteZone(i))
if (this.safeDeleteZone(i))
{
i--;
}
}
}

/**
* @returns {number}
*/
get useCount()
{
return this._useCount;
}


deleteInstrument()
{
this.instrumentZones.forEach(z => z.deleteZone());
this.instrumentZones.length = 0;
}

/**
* @param index {number}
* @returns {boolean} is the zone has been deleted
*/
safeDeleteZone(index)
{
this.instrumentZones[index].useCount--;
if(this.instrumentZones[index].useCount < 1)
if (this.instrumentZones[index].useCount < 1)
{
this.deleteZone(index);
return true;
}
return false;
}

/**
* @param index {number}
*/
144 changes: 88 additions & 56 deletions src/spessasynth_lib/soundfont/basic_soundfont/basic_preset.js
Original file line number Diff line number Diff line change
@@ -32,29 +32,29 @@ export class BasicPreset
* @type {number}
*/
this.bank = 0;

/**
* The preset's zones
* @type {BasicPresetZone[]}
*/
this.presetZones = [];

/**
* SampleID offset for this preset
* @type {number}
*/
this.sampleIDOffset = 0;

/**
* Stores already found getSamplesAndGenerators for reuse
* @type {SampleAndGenerators[][][]}
*/
this.foundSamplesAndGenerators = [];
for(let i = 0; i < 128; i++)
for (let i = 0; i < 128; i++)
{
this.foundSamplesAndGenerators[i] = [];
}

/**
* unused metadata
* @type {number}
@@ -70,20 +70,20 @@ export class BasicPreset
* @type {number}
*/
this.morphology = 0;

/**
* Default modulators
* @type {Modulator[]}
*/
this.defaultModulators = modulators;
}

deletePreset()
{
this.presetZones.forEach(z => z.deleteZone());
this.presetZones.length = 0;
}

/**
* @param index {number}
*/
@@ -92,7 +92,7 @@ export class BasicPreset
this.presetZones[index].deleteZone();
this.presetZones.splice(index, 1);
}

/**
* Preloads all samples (async)
*/
@@ -102,31 +102,33 @@ export class BasicPreset
{
for (let velocity = 0; velocity < 128; velocity++)
{
this.getSamplesAndGenerators(key, velocity).forEach(samandgen => {
if(!samandgen.sample.isSampleLoaded)
this.getSamplesAndGenerators(key, velocity).forEach(samandgen =>
{
if (!samandgen.sample.isSampleLoaded)
{
samandgen.sample.getAudioData();
}
})
});
}
}
}

/**
* Preloads a specific key/velocity combo
* @param key {number}
* @param velocity {number}
*/
preloadSpecific(key, velocity)
{
this.getSamplesAndGenerators(key, velocity).forEach(samandgen => {
if(!samandgen.sample.isSampleLoaded)
this.getSamplesAndGenerators(key, velocity).forEach(samandgen =>
{
if (!samandgen.sample.isSampleLoaded)
{
samandgen.sample.getAudioData();
}
})
});
}

/**
* Returns generatorTranslator and generators for given note
* @param midiNote {number}
@@ -136,21 +138,21 @@ export class BasicPreset
getSamplesAndGenerators(midiNote, velocity)
{
const memorized = this.foundSamplesAndGenerators[midiNote][velocity];
if(memorized)
if (memorized)
{
return memorized;
}

if(this.presetZones.length < 1)
if (this.presetZones.length < 1)
{
return [];
}

function isInRange(min, max, number)
{
return number >= min && number <= max;
}

/**
* @param main {Generator[]}
* @param adder {Generator[]}
@@ -159,7 +161,7 @@ export class BasicPreset
{
main.push(...adder.filter(g => !main.find(mg => mg.generatorType === g.generatorType)));
}

/**
* @param main {Modulator[]}
* @param adder {Modulator[]}
@@ -168,31 +170,39 @@ export class BasicPreset
{
main.push(...adder.filter(m => !main.find(mm => Modulator.isIdentical(m, mm))));
}

/**
* @type {SampleAndGenerators[]}
*/
let parsedGeneratorsAndSamples = [];

/**
* global zone is always first, so it or nothing
* @type {Generator[]}
*/
let globalPresetGenerators = this.presetZones[0].isGlobal ? [...this.presetZones[0].generators] : [];

let globalPresetModulators = this.presetZones[0].isGlobal ? [...this.presetZones[0].modulators] : [];

// find the preset zones in range
let presetZonesInRange = this.presetZones.filter(currentZone =>
(
isInRange(currentZone.keyRange.min, currentZone.keyRange.max, midiNote)
isInRange(
currentZone.keyRange.min,
currentZone.keyRange.max,
midiNote
)
&&
isInRange(currentZone.velRange.min, currentZone.velRange.max, velocity)
isInRange(
currentZone.velRange.min,
currentZone.velRange.max,
velocity
)
) && !currentZone.isGlobal);

presetZonesInRange.forEach(zone =>
{
if(zone.instrument.instrumentZones.length < 1)
if (zone.instrument.instrumentZones.length < 1)
{
return;
}
@@ -204,70 +214,92 @@ export class BasicPreset
*/
let globalInstrumentGenerators = zone.instrument.instrumentZones[0].isGlobal ? [...zone.instrument.instrumentZones[0].generators] : [];
let globalInstrumentModulators = zone.instrument.instrumentZones[0].isGlobal ? [...zone.instrument.instrumentZones[0].modulators] : [];

let instrumentZonesInRange = zone.instrument.instrumentZones
.filter(currentZone =>
(
isInRange(currentZone.keyRange.min,
isInRange(
currentZone.keyRange.min,
currentZone.keyRange.max,
midiNote)
midiNote
)
&&
isInRange(currentZone.velRange.min,
isInRange(
currentZone.velRange.min,
currentZone.velRange.max,
velocity)
velocity
)
) && !currentZone.isGlobal
);

instrumentZonesInRange.forEach(instrumentZone =>
{
let instrumentGenerators = [...instrumentZone.generators];
let instrumentModulators = [...instrumentZone.modulators];

addUnique(presetGenerators, globalPresetGenerators);

addUnique(
presetGenerators,
globalPresetGenerators
);
// add the unique global preset generators (local replace global(


// add the unique global instrument generators (local replace global)
addUnique(instrumentGenerators, globalInstrumentGenerators);

addUniqueMods(presetModulators, globalPresetModulators);
addUniqueMods(instrumentModulators, globalInstrumentModulators);

addUnique(
instrumentGenerators,
globalInstrumentGenerators
);

addUniqueMods(
presetModulators,
globalPresetModulators
);
addUniqueMods(
instrumentModulators,
globalInstrumentModulators
);

// default mods
addUniqueMods(instrumentModulators, this.defaultModulators);

addUniqueMods(
instrumentModulators,
this.defaultModulators
);

/**
* sum preset modulators to instruments (amount) sf spec page 54
* @type {Modulator[]}
*/
const finalModulatorList = [...instrumentModulators];
for(let i = 0; i < presetModulators.length; i++)
for (let i = 0; i < presetModulators.length; i++)
{
let mod = presetModulators[i];
const identicalInstrumentModulator = finalModulatorList.findIndex(m => Modulator.isIdentical(mod, m));
if(identicalInstrumentModulator !== -1)
const identicalInstrumentModulator = finalModulatorList.findIndex(
m => Modulator.isIdentical(mod, m));
if (identicalInstrumentModulator !== -1)
{
// sum the amounts (this makes a new modulator because otherwise it would overwrite the one in the soundfont!!!
finalModulatorList[identicalInstrumentModulator] = finalModulatorList[identicalInstrumentModulator].sumTransform(mod);
finalModulatorList[identicalInstrumentModulator] = finalModulatorList[identicalInstrumentModulator].sumTransform(
mod);
}
else
{
finalModulatorList.push(mod);
}
}


// combine both generators and add to the final result
parsedGeneratorsAndSamples.push({
instrumentGenerators: instrumentGenerators,
presetGenerators: presetGenerators,
modulators: finalModulatorList,
sample: instrumentZone.sample,
sampleID: instrumentZone.generators.find(g => g.generatorType === generatorTypes.sampleID).generatorValue
sampleID: instrumentZone.generators.find(
g => g.generatorType === generatorTypes.sampleID).generatorValue
});
});
});

// save and return
this.foundSamplesAndGenerators[midiNote][velocity] = parsedGeneratorsAndSamples;
return parsedGeneratorsAndSamples;
83 changes: 45 additions & 38 deletions src/spessasynth_lib/soundfont/basic_soundfont/basic_sample.js
Original file line number Diff line number Diff line change
@@ -3,9 +3,10 @@
* purpose: parses soundfont samples, resamples if needed.
* loads sample data, handles async loading of sf3 compressed samples
*/
import { SpessaSynthWarn } from '../../utils/loggin.js'
import { SpessaSynthWarn } from "../../utils/loggin.js";

export class BasicSample {
export class BasicSample
{
/**
* The basic representation of a soundfont sample
* @param sampleName {string}
@@ -25,108 +26,114 @@ export class BasicSample {
sampleLink,
sampleType,
loopStart,
loopEnd,
) {
loopEnd
)
{
/**
* Sample's name
* @type {string}
*/
this.sampleName = sampleName
this.sampleName = sampleName;
/**
* Sample rate in Hz
* @type {number}
*/
this.sampleRate = sampleRate
this.sampleRate = sampleRate;
/**
* Original pitch of the sample as a MIDI note number
* @type {number}
*/
this.samplePitch = samplePitch
this.samplePitch = samplePitch;
/**
* Pitch correction, in cents. Can be negative
* @type {number}
*/
this.samplePitchCorrection = samplePitchCorrection
this.samplePitchCorrection = samplePitchCorrection;
/**
* Sample link, currently unused.
* @type {number}
*/
this.sampleLink = sampleLink
this.sampleLink = sampleLink;
/**
* Type of the sample, an enum
* @type {number}
*/
this.sampleType = sampleType
this.sampleType = sampleType;
/**
* Relative to start of the sample in sample points
* @type {number}
*/
this.sampleLoopStartIndex = loopStart
this.sampleLoopStartIndex = loopStart;
/**
* Relative to start of the sample in sample points
* @type {number}
*/
this.sampleLoopEndIndex = loopEnd;

/**
* Indicates if the sample is compressed
* @type {boolean}
*/
this.isCompressed = (sampleType & 0x10) > 0

this.isCompressed = (sampleType & 0x10) > 0;
/**
* The compressed sample data if it was compressed by spessasynth
* @type {Uint8Array}
*/
this.compressedData = undefined;

/**
* The sample's use count
* @type {number}
*/
this.useCount = 0;
}

/**
* @returns {Uint8Array|IndexedByteArray}
*/
getRawData()
{
const e = new Error('Not implemented')
e.name = 'NotImplementedError'
throw e
const e = new Error("Not implemented");
e.name = "NotImplementedError";
throw e;
}

/**
* @param quality {number}
* @param encodeVorbis {EncodeVorbisFunction}
*/
compressSample(quality, encodeVorbis) {
compressSample(quality, encodeVorbis)
{
// no need to compress
if (this.isCompressed) {
return
if (this.isCompressed)
{
return;
}
// compress, always mono!
try {
this.compressedData = encodeVorbis([this.getAudioData()], 1, this.sampleRate, quality)
try
{
this.compressedData = encodeVorbis([this.getAudioData()], 1, this.sampleRate, quality);
// flag as compressed
this.sampleType |= 0x10
this.isCompressed = true
} catch (e) {
SpessaSynthWarn(`Failed to compress ${this.sampleName}. Leaving as uncompressed!`)
this.isCompressed = false
this.compressedData = undefined
this.sampleType &= -17
this.sampleType |= 0x10;
this.isCompressed = true;
} catch (e)
{
SpessaSynthWarn(`Failed to compress ${this.sampleName}. Leaving as uncompressed!`);
this.isCompressed = false;
this.compressedData = undefined;
this.sampleType &= -17;
}

}

/**
* @returns {Float32Array}
*/
getAudioData() {
const e = new Error('Not implemented')
e.name = 'NotImplementedError'
throw e
getAudioData()
{
const e = new Error("Not implemented");
e.name = "NotImplementedError";
throw e;
}
}
Loading

0 comments on commit 8035d9d

Please sign in to comment.