|
|
import { MIDISequenceData } from "./midi_sequence.js"; |
|
|
import { getStringBytes, readBytesAsString } from "../utils/byte_functions/string.js"; |
|
|
import { messageTypes, MIDIMessage } from "./midi_message.js"; |
|
|
import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js"; |
|
|
import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js"; |
|
|
import { consoleColors, formatTitle, sanitizeKarLyrics } from "../utils/other.js"; |
|
|
import { writeMIDI } from "./midi_writer.js"; |
|
|
import { applySnapshotToMIDI, modifyMIDI } from "./midi_editor.js"; |
|
|
import { writeRMIDI } from "./rmidi_writer.js"; |
|
|
import { getUsedProgramsAndKeys } from "./used_keys_loaded.js"; |
|
|
import { IndexedByteArray } from "../utils/indexed_array.js"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BasicMIDI extends MIDISequenceData |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
embeddedSoundFont = undefined; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tracks = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
isDLSRMIDI = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static copyFrom(mid) |
|
|
{ |
|
|
const m = new BasicMIDI(); |
|
|
m._copyFromSequence(mid); |
|
|
|
|
|
m.isDLSRMIDI = mid.isDLSRMIDI; |
|
|
m.embeddedSoundFont = mid.embeddedSoundFont ? mid.embeddedSoundFont.slice(0) : undefined; |
|
|
m.tracks = mid.tracks.map(track => [...track]); |
|
|
|
|
|
return m; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_parseInternal() |
|
|
{ |
|
|
SpessaSynthGroup( |
|
|
"%cInterpreting MIDI events...", |
|
|
consoleColors.info |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let karaokeHasTitle = false; |
|
|
|
|
|
this.keyRange = { max: 0, min: 127 }; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let copyrightComponents = []; |
|
|
let copyrightDetected = false; |
|
|
if (typeof this.RMIDInfo["ICOP"] !== "undefined") |
|
|
{ |
|
|
|
|
|
copyrightDetected = true; |
|
|
} |
|
|
|
|
|
|
|
|
let nameDetected = false; |
|
|
if (typeof this.RMIDInfo["INAM"] !== "undefined") |
|
|
{ |
|
|
|
|
|
nameDetected = true; |
|
|
} |
|
|
|
|
|
|
|
|
let loopStart = null; |
|
|
let loopEnd = null; |
|
|
|
|
|
for (let i = 0; i < this.tracks.length; i++) |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
const track = this.tracks[i]; |
|
|
const usedChannels = new Set(); |
|
|
let trackHasVoiceMessages = false; |
|
|
|
|
|
for (const e of track) |
|
|
{ |
|
|
|
|
|
if (e.messageStatusByte >= 0x80 && e.messageStatusByte < 0xF0) |
|
|
{ |
|
|
trackHasVoiceMessages = true; |
|
|
|
|
|
for (let j = 0; j < e.messageData.length; j++) |
|
|
{ |
|
|
e.messageData[j] = Math.min(127, e.messageData[j]); |
|
|
} |
|
|
|
|
|
if (e.ticks > this.lastVoiceEventTick) |
|
|
{ |
|
|
this.lastVoiceEventTick = e.ticks; |
|
|
} |
|
|
|
|
|
|
|
|
switch (e.messageStatusByte & 0xF0) |
|
|
{ |
|
|
|
|
|
case messageTypes.controllerChange: |
|
|
switch (e.messageData[0]) |
|
|
{ |
|
|
case 2: |
|
|
case 116: |
|
|
loopStart = e.ticks; |
|
|
break; |
|
|
|
|
|
case 4: |
|
|
case 117: |
|
|
if (loopEnd === null) |
|
|
{ |
|
|
loopEnd = e.ticks; |
|
|
} |
|
|
else |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
loopEnd = 0; |
|
|
} |
|
|
break; |
|
|
|
|
|
case 0: |
|
|
|
|
|
if (this.isDLSRMIDI && e.messageData[1] !== 0 && e.messageData[1] !== 127) |
|
|
{ |
|
|
SpessaSynthInfo( |
|
|
"%cDLS RMIDI with offset 1 detected!", |
|
|
consoleColors.recognized |
|
|
); |
|
|
this.bankOffset = 1; |
|
|
} |
|
|
} |
|
|
break; |
|
|
|
|
|
|
|
|
case messageTypes.noteOn: |
|
|
usedChannels.add(e.messageStatusByte & 0x0F); |
|
|
const note = e.messageData[0]; |
|
|
this.keyRange.min = Math.min(this.keyRange.min, note); |
|
|
this.keyRange.max = Math.max(this.keyRange.max, note); |
|
|
break; |
|
|
} |
|
|
} |
|
|
e.messageData.currentIndex = 0; |
|
|
const eventText = readBytesAsString(e.messageData, e.messageData.length); |
|
|
e.messageData.currentIndex = 0; |
|
|
|
|
|
switch (e.messageStatusByte) |
|
|
{ |
|
|
case messageTypes.setTempo: |
|
|
|
|
|
e.messageData.currentIndex = 0; |
|
|
this.tempoChanges.push({ |
|
|
ticks: e.ticks, |
|
|
tempo: 60000000 / readBytesAsUintBigEndian(e.messageData, 3) |
|
|
}); |
|
|
e.messageData.currentIndex = 0; |
|
|
break; |
|
|
|
|
|
case messageTypes.marker: |
|
|
|
|
|
const text = eventText.trim().toLowerCase(); |
|
|
switch (text) |
|
|
{ |
|
|
default: |
|
|
break; |
|
|
|
|
|
case "start": |
|
|
case "loopstart": |
|
|
loopStart = e.ticks; |
|
|
break; |
|
|
|
|
|
case "loopend": |
|
|
loopEnd = e.ticks; |
|
|
} |
|
|
e.messageData.currentIndex = 0; |
|
|
break; |
|
|
|
|
|
case messageTypes.copyright: |
|
|
if (!copyrightDetected) |
|
|
{ |
|
|
e.messageData.currentIndex = 0; |
|
|
copyrightComponents.push(readBytesAsString( |
|
|
e.messageData, |
|
|
e.messageData.length, |
|
|
undefined, |
|
|
false |
|
|
)); |
|
|
e.messageData.currentIndex = 0; |
|
|
} |
|
|
break; |
|
|
|
|
|
case messageTypes.lyric: |
|
|
|
|
|
|
|
|
|
|
|
if (eventText.trim().startsWith("@KMIDI KARAOKE FILE")) |
|
|
{ |
|
|
this.isKaraokeFile = true; |
|
|
SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized); |
|
|
} |
|
|
|
|
|
if (this.isKaraokeFile) |
|
|
{ |
|
|
|
|
|
e.messageStatusByte = messageTypes.text; |
|
|
} |
|
|
else |
|
|
{ |
|
|
|
|
|
this.lyrics.push(e.messageData); |
|
|
this.lyricsTicks.push(e.ticks); |
|
|
break; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
case messageTypes.text: |
|
|
|
|
|
|
|
|
|
|
|
const checkedText = eventText.trim(); |
|
|
if (checkedText.startsWith("@KMIDI KARAOKE FILE")) |
|
|
{ |
|
|
this.isKaraokeFile = true; |
|
|
|
|
|
SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized); |
|
|
} |
|
|
else if (this.isKaraokeFile) |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
if (checkedText.startsWith("@T") || checkedText.startsWith("@A")) |
|
|
{ |
|
|
if (!karaokeHasTitle) |
|
|
{ |
|
|
this.midiName = checkedText.substring(2).trim(); |
|
|
karaokeHasTitle = true; |
|
|
nameDetected = true; |
|
|
|
|
|
this.rawMidiName = getStringBytes(this.midiName); |
|
|
} |
|
|
else |
|
|
{ |
|
|
|
|
|
copyrightComponents.push(checkedText.substring(2).trim()); |
|
|
} |
|
|
} |
|
|
else if (checkedText[0] !== "@") |
|
|
{ |
|
|
|
|
|
this.lyrics.push(sanitizeKarLyrics(e.messageData)); |
|
|
this.lyricsTicks.push(e.ticks); |
|
|
} |
|
|
} |
|
|
break; |
|
|
|
|
|
case messageTypes.trackName: |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
this.usedChannelsOnTrack.push(usedChannels); |
|
|
|
|
|
|
|
|
this.trackNames[i] = ""; |
|
|
const trackName = track.find(e => e.messageStatusByte === messageTypes.trackName); |
|
|
if (trackName) |
|
|
{ |
|
|
trackName.messageData.currentIndex = 0; |
|
|
const name = readBytesAsString(trackName.messageData, trackName.messageData.length); |
|
|
this.trackNames[i] = name; |
|
|
|
|
|
|
|
|
|
|
|
if (!trackHasVoiceMessages) |
|
|
{ |
|
|
copyrightComponents.push(name); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
this.tempoChanges.reverse(); |
|
|
|
|
|
SpessaSynthInfo( |
|
|
`%cCorrecting loops, ports and detecting notes...`, |
|
|
consoleColors.info |
|
|
); |
|
|
|
|
|
const firstNoteOns = []; |
|
|
for (const t of this.tracks) |
|
|
{ |
|
|
const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn); |
|
|
if (firstNoteOn) |
|
|
{ |
|
|
firstNoteOns.push(firstNoteOn.ticks); |
|
|
} |
|
|
} |
|
|
this.firstNoteOn = Math.min(...firstNoteOns); |
|
|
|
|
|
SpessaSynthInfo( |
|
|
`%cFirst note-on detected at: %c${this.firstNoteOn}%c ticks!`, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized, |
|
|
consoleColors.info |
|
|
); |
|
|
|
|
|
|
|
|
if (loopStart !== null && loopEnd === null) |
|
|
{ |
|
|
|
|
|
loopStart = this.firstNoteOn; |
|
|
loopEnd = this.lastVoiceEventTick; |
|
|
} |
|
|
else |
|
|
{ |
|
|
if (loopStart === null) |
|
|
{ |
|
|
loopStart = this.firstNoteOn; |
|
|
} |
|
|
|
|
|
if (loopEnd === null || loopEnd === 0) |
|
|
{ |
|
|
loopEnd = this.lastVoiceEventTick; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.loop = { start: loopStart, end: loopEnd }; |
|
|
|
|
|
SpessaSynthInfo( |
|
|
`%cLoop points: start: %c${this.loop.start}%c end: %c${this.loop.end}`, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized |
|
|
); |
|
|
|
|
|
|
|
|
let portOffset = 0; |
|
|
this.midiPorts = []; |
|
|
this.midiPortChannelOffsets = []; |
|
|
for (let trackNum = 0; trackNum < this.tracks.length; trackNum++) |
|
|
{ |
|
|
this.midiPorts.push(-1); |
|
|
if (this.usedChannelsOnTrack[trackNum].size === 0) |
|
|
{ |
|
|
continue; |
|
|
} |
|
|
for (const e of this.tracks[trackNum]) |
|
|
{ |
|
|
if (e.messageStatusByte !== messageTypes.midiPort) |
|
|
{ |
|
|
continue; |
|
|
} |
|
|
const port = e.messageData[0]; |
|
|
this.midiPorts[trackNum] = port; |
|
|
if (this.midiPortChannelOffsets[port] === undefined) |
|
|
{ |
|
|
this.midiPortChannelOffsets[port] = portOffset; |
|
|
portOffset += 16; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let defaultPort = Infinity; |
|
|
for (let port of this.midiPorts) |
|
|
{ |
|
|
if (port !== -1) |
|
|
{ |
|
|
if (defaultPort > port) |
|
|
{ |
|
|
defaultPort = port; |
|
|
} |
|
|
} |
|
|
} |
|
|
if (defaultPort === Infinity) |
|
|
{ |
|
|
defaultPort = 0; |
|
|
} |
|
|
this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultPort : port); |
|
|
|
|
|
if (this.midiPortChannelOffsets.length === 0) |
|
|
{ |
|
|
this.midiPortChannelOffsets = [0]; |
|
|
} |
|
|
if (this.midiPortChannelOffsets.length < 2) |
|
|
{ |
|
|
SpessaSynthInfo(`%cNo additional MIDI Ports detected.`, consoleColors.info); |
|
|
} |
|
|
else |
|
|
{ |
|
|
this.isMultiPort = true; |
|
|
SpessaSynthInfo(`%cMIDI Ports detected!`, consoleColors.recognized); |
|
|
} |
|
|
|
|
|
|
|
|
if (!nameDetected) |
|
|
{ |
|
|
if (this.tracks.length > 1) |
|
|
{ |
|
|
|
|
|
|
|
|
if ( |
|
|
this.tracks[0].find( |
|
|
message => message.messageStatusByte >= messageTypes.noteOn |
|
|
&& |
|
|
message.messageStatusByte < messageTypes.polyPressure |
|
|
) === undefined |
|
|
) |
|
|
{ |
|
|
|
|
|
let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName); |
|
|
if (name) |
|
|
{ |
|
|
this.rawMidiName = name.messageData; |
|
|
name.messageData.currentIndex = 0; |
|
|
this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false); |
|
|
} |
|
|
} |
|
|
} |
|
|
else |
|
|
{ |
|
|
|
|
|
let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName); |
|
|
if (name) |
|
|
{ |
|
|
this.rawMidiName = name.messageData; |
|
|
name.messageData.currentIndex = 0; |
|
|
this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (!copyrightDetected) |
|
|
{ |
|
|
this.copyright = copyrightComponents |
|
|
|
|
|
.map(c => c.trim().replace(/(\r?\n)+/g, "\n")) |
|
|
|
|
|
.filter(c => c.length > 0) |
|
|
|
|
|
.join("\n") || ""; |
|
|
} |
|
|
|
|
|
this.midiName = this.midiName.trim(); |
|
|
this.midiNameUsesFileName = false; |
|
|
|
|
|
if (this.midiName.length === 0) |
|
|
{ |
|
|
SpessaSynthInfo( |
|
|
`%cNo name detected. Using the alt name!`, |
|
|
consoleColors.info |
|
|
); |
|
|
this.midiName = formatTitle(this.fileName); |
|
|
this.midiNameUsesFileName = true; |
|
|
|
|
|
this.rawMidiName = new Uint8Array(this.midiName.length); |
|
|
for (let i = 0; i < this.midiName.length; i++) |
|
|
{ |
|
|
this.rawMidiName[i] = this.midiName.charCodeAt(i); |
|
|
} |
|
|
} |
|
|
else |
|
|
{ |
|
|
SpessaSynthInfo( |
|
|
`%cMIDI Name detected! %c"${this.midiName}"`, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!this.tracks.some(t => t[0].ticks === 0)) |
|
|
{ |
|
|
const track = this.tracks[0]; |
|
|
|
|
|
track.unshift(new MIDIMessage( |
|
|
0, |
|
|
messageTypes.trackName, |
|
|
new IndexedByteArray(this.rawMidiName.buffer) |
|
|
)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.duration = this.MIDIticksToSeconds(this.lastVoiceEventTick); |
|
|
|
|
|
SpessaSynthInfo("%cSuccess!", consoleColors.recognized); |
|
|
SpessaSynthGroupEnd(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
flush() |
|
|
{ |
|
|
|
|
|
for (const t of this.tracks) |
|
|
{ |
|
|
|
|
|
t.sort((e1, e2) => e1.ticks - e2.ticks); |
|
|
} |
|
|
this._parseInternal(); |
|
|
} |
|
|
} |
|
|
|
|
|
BasicMIDI.prototype.writeMIDI = writeMIDI; |
|
|
BasicMIDI.prototype.modifyMIDI = modifyMIDI; |
|
|
BasicMIDI.prototype.applySnapshotToMIDI = applySnapshotToMIDI; |
|
|
BasicMIDI.prototype.writeRMIDI = writeRMIDI; |
|
|
BasicMIDI.prototype.getUsedProgramsAndKeys = getUsedProgramsAndKeys; |
|
|
|
|
|
export { BasicMIDI }; |