Skip to content

Commit

Permalink
SysEx and MIDI patch
Browse files Browse the repository at this point in the history
fixed playing to external MIDI
added more sysex support
added programName
  • Loading branch information
spessasus committed Sep 26, 2024
1 parent 132df04 commit 7720efd
Show file tree
Hide file tree
Showing 14 changed files with 191 additions and 68 deletions.
3 changes: 2 additions & 1 deletion examples/visualizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ fetch("../soundfonts/GeneralUserGS.sf3").then(async response => {
* create the AnalyserNodes for the channels
*/
const analysers = [];
for (let i = 0; i < 16; i++) {
for (let i = 0; i < 16; i++)
{
analysers.push(context.createAnalyser()); // create analyser
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "SpessaSynth",
"version": "3.20.28",
"version": "3.20.29",
"type": "module",
"scripts": {
"start": "node src/website/server/server.js"
Expand Down
6 changes: 4 additions & 2 deletions src/spessasynth_lib/midi_parser/midi_loader.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { dataBytesAmount, getChannel, messageTypes, MidiMessage } from './midi_message.js'
import { IndexedByteArray } from '../utils/indexed_array.js'
import { arrayToHexString, consoleColors, formatTitle } from '../utils/other.js'
import { consoleColors, formatTitle } from '../utils/other.js'
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from '../utils/loggin.js'
import { readRIFFChunk } from '../soundfont/basic_soundfont/riff_chunk.js'
import { readVariableLengthQuantity } from '../utils/byte_functions/variable_length_quantity.js'
Expand All @@ -10,6 +10,8 @@ import { readLittleEndian } from '../utils/byte_functions/little_endian.js'
import { RMIDINFOChunks } from './rmidi_writer.js'
import { BasicMIDI, MIDIticksToSeconds } from './basic_midi.js'


const GS_TEXT_HEADER = new Uint8Array([0x41, 0x10, 0x45, 0x12, 0x10, 0x00, 0x00]);
/**
* midi_loader.js
* purpose: parses a midi file for the seqyencer, including things like marker or CC 2/4 loop detection, copyright detection etc.
Expand Down Expand Up @@ -366,7 +368,7 @@ class MIDI extends BasicMIDI
// since this is a sysex message
// check for embedded copyright (roland SC display sysex) http://www.bandtrax.com.au/sysex.htm
// header goes like this: 41 10 45 12 10 00 00
if(arrayToHexString(eventData.slice(0, 7)).trim() === "41 10 45 12 10 00 00")
if(eventData.slice(0, 7).every((n, i) => GS_TEXT_HEADER[i] === n))
{
/**
* @type {IndexedByteArray}
Expand Down
1 change: 1 addition & 0 deletions src/spessasynth_lib/midi_parser/midi_message.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export const messageTypes = {
lyric: 0x05,
marker: 0x06,
cuePoint: 0x07,
programName: 0x08,
midiChannelPrefix: 0x20,
midiPort: 0x21,
endOfTrack: 0x2F,
Expand Down
67 changes: 67 additions & 0 deletions src/spessasynth_lib/sequencer/worklet_sequencer/events.js
Original file line number Diff line number Diff line change
@@ -1,5 +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'

/**
* @param messageType {WorkletSequencerMessageType}
Expand Down Expand Up @@ -94,4 +96,69 @@ export function post(messageType, messageData = undefined)
export function sendMIDIMessage(message)
{
this.post(WorkletSequencerReturnMessageType.midiEvent, message);
}

/**
* @this {WorkletSequencer}
* @param channel {number}
* @param type {number}
* @param value {number}
*/
export function sendMIDICC(channel, type, value)
{
channel %= 16;
if(!this.sendMIDIMessages)
{
return;
}
this.sendMIDIMessage([messageTypes.controllerChange | channel, type, value])
}

/**
* @this {WorkletSequencer}
* @param channel {number}
* @param program {number}
*/
export function sendMIDIProgramChange(channel, program)
{
channel %= 16;
if(!this.sendMIDIMessages)
{
return;
}
this.sendMIDIMessage([messageTypes.programChange | channel, program]);
}

/**
* Sets the pitch of the given channel
* @this {WorkletSequencer}
* @param channel {number} usually 0-15: the channel to change pitch
* @param MSB {number} SECOND byte of the MIDI pitchWheel message
* @param LSB {number} FIRST byte of the MIDI pitchWheel message
*/
export function sendMIDIPitchWheel(channel, MSB, LSB)
{
channel %= 16;
if(!this.sendMIDIMessages)
{
return;
}
this.sendMIDIMessage([messageTypes.pitchBend | channel, LSB, MSB]);
}

/**
* @this {WorkletSequencer}
*/
export function sendMIDIReset()
{
if(!this.sendMIDIMessages)
{
return;
}
this.sendMIDIMessage([messageTypes.reset]);
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]);
}
}
62 changes: 29 additions & 33 deletions src/spessasynth_lib/sequencer/worklet_sequencer/play.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { getEvent, messageTypes, midiControllers } from '../../midi_parser/midi_message.js'
import { WorkletSequencerReturnMessageType } from './sequencer_message.js'
import { MIDI_CHANNEL_COUNT } from '../../synthetizer/synthetizer.js'
import { MIDIticksToSeconds } from '../../midi_parser/basic_midi.js'


Expand All @@ -27,15 +26,8 @@ export function _playTo(time, ticks = undefined)
this.oneTickToSeconds = 60 / (120 * this.midiData.timeDivision);
// reset
this.synth.resetAllControllers();
if(this.sendMIDIMessages)
{
this.sendMIDIMessage([messageTypes.reset]);
for(let ch = 0; ch < MIDI_CHANNEL_COUNT; ch++)
{
this.sendMIDIMessage([messageTypes.controllerChange | ch, midiControllers.resetAllControllers, 0]);
}
}
this._resetTimers()
this.sendMIDIReset();
this._resetTimers();

const channelsToSave = this.synth.workletProcessorChannels.length;
/**
Expand Down Expand Up @@ -131,19 +123,19 @@ export function _playTo(time, ticks = undefined)
const controllerNumber = event.messageData[0];
if(isCCNonSkippable(controllerNumber))
{
let ccV = event.messageData[1];
if(controllerNumber === midiControllers.bankSelect)
{
// add the bank to saved
programs[channel].bank = ccV;
break;
}
if(this.sendMIDIMessages)
{
this.sendMIDIMessage([messageTypes.controllerChange | (channel % 16), controllerNumber, event.messageData[1]])
this.sendMIDICC(channel, controllerNumber, ccV);
}
else
{
let ccV = event.messageData[1];
if(controllerNumber === midiControllers.bankSelect)
{
// add the bank to saved
programs[channel].bank = ccV;
break;
}
this.synth.controllerChange(channel, controllerNumber, ccV);
}
}
Expand Down Expand Up @@ -177,25 +169,29 @@ export function _playTo(time, ticks = undefined)
// restoring saved controllers
if(this.sendMIDIMessages)
{
// for all 16 channels
for (let channelNumber = 0; channelNumber < channelsToSave; channelNumber++) {
// send saved pitch bend
this.sendMIDIMessage([messageTypes.pitchBend | (channelNumber % 16), pitchBends[channelNumber] & 0x7F, pitchBends[channelNumber] >> 7]);

// every controller that has changed
savedControllers[channelNumber].forEach((value, index) => {
if(value !== defaultControllerArray[index] && !isCCNonSkippable(index))
{
this.sendMIDIMessage([messageTypes.controllerChange | (channelNumber % 16), index, value])
}
});

for (let channelNumber = 0; channelNumber < channelsToSave; channelNumber++)
{
// restore pitch bends
if(pitchBends[channelNumber] !== undefined)
{
this.sendMIDIPitchWheel(channelNumber, pitchBends[channelNumber] >> 7, pitchBends[channelNumber] & 0x7F);
}
if(savedControllers[channelNumber] !== undefined)
{
// every controller that has changed
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)
{
const bank = programs[channelNumber].actualBank;
this.sendMIDIMessage([messageTypes.controllerChange | (channelNumber % 16), midiControllers.bankSelect, bank]);
this.sendMIDIMessage([messageTypes.programChange | (channelNumber % 16), programs[channelNumber].program]);
this.sendMIDICC(channelNumber, midiControllers.bankSelect, bank);
this.sendMIDIProgramChange(channelNumber, programs[channelNumber].program);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export function _processEvent(event, trackIndex)
case messageTypes.songPosition:
case messageTypes.activeSensing:
case messageTypes.keySignature:
case messageTypes.sequenceNumber:
break;

case messageTypes.text:
Expand All @@ -110,6 +111,7 @@ export function _processEvent(event, trackIndex)
case messageTypes.marker:
case messageTypes.cuePoint:
case messageTypes.instrumentName:
case messageTypes.programName:
this.post(WorkletSequencerReturnMessageType.textEvent, [event.messageData, statusByteData.status])
break;

Expand Down Expand Up @@ -139,7 +141,8 @@ export function _processEvent(event, trackIndex)
*/
export function _addNewMidiPort()
{
for (let i = 0; i < 16; i++) {
for (let i = 0; i < 16; i++)
{
this.synth.createWorkletChannel(true);
if(i === DEFAULT_PERCUSSION)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ 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, sendMIDIMessage } from './events.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'

Expand Down Expand Up @@ -178,16 +186,21 @@ class WorkletSequencer
{
this.clearProcessHandler()
// disable sustain
for (let i = 0; i < 16; i++) {
for (let i = 0; i < 16; i++)
{
this.synth.controllerChange(i, midiControllers.sustainPedal, 0);
}
this.synth.stopAllChannels();
if(this.sendMIDIMessages)
{
for(let note of this.playingNotes)
{
this.sendMIDIMessage([messageTypes.noteOff | (note.channel % 16), note.midiNote]);
}
for (let c = 0; c < MIDI_CHANNEL_COUNT; c++)
{
this.sendMIDIMessage([messageTypes.controllerChange | c, 120, 0]); // all notes off
this.sendMIDIMessage([messageTypes.controllerChange | c, 123, 0]); // all sound off
this.sendMIDICC(c, midiControllers.allNotesOff, 0);
this.sendMIDICC(c, midiControllers.allSoundOff, 0);
}
}
}
Expand Down Expand Up @@ -218,9 +231,15 @@ class WorkletSequencer
}
}

WorkletSequencer.prototype.post = post;
// Web MIDI sending
WorkletSequencer.prototype.sendMIDIMessage = sendMIDIMessage;
WorkletSequencer.prototype.sendMIDIReset = sendMIDIReset;
WorkletSequencer.prototype.sendMIDICC = sendMIDICC;
WorkletSequencer.prototype.sendMIDIProgramChange = sendMIDIProgramChange;
WorkletSequencer.prototype.sendMIDIPitchWheel = sendMIDIPitchWheel;
WorkletSequencer.prototype.assignMIDIPort = assignMIDIPort;

WorkletSequencer.prototype.post = post;
WorkletSequencer.prototype.processMessage = processMessage;

WorkletSequencer.prototype._processEvent = _processEvent;
Expand Down
16 changes: 8 additions & 8 deletions src/spessasynth_lib/synthetizer/worklet_processor.min.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import { SpessaSynthInfo } from '../../../utils/loggin.js'

/**
* @this {SpessaSynthProcessor}
* @param log {boolean}
*/
export function resetAllControllers()
export function resetAllControllers(log= true)
{
SpessaSynthInfo("%cResetting all controllers!", consoleColors.info);
if (log) SpessaSynthInfo("%cResetting all controllers!", consoleColors.info);
this.callEvent("allcontrollerreset", undefined);
for (let channelNumber = 0; channelNumber < this.workletProcessorChannels.length; channelNumber++)
{
Expand Down
Loading

0 comments on commit 7720efd

Please sign in to comment.