m8-js
Version:
Library for loading and interacting with Dirtywave M8 instrument/song files.
1,099 lines (881 loc) • 34.2 kB
JavaScript
/* Copyright 2022 Jeremy Whitlock
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use theme file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { bytesForSkippedData, bytesFromBool, bytesFromFloatLE, bytesFromString, readFloatLE, readUInt16LE, toM8HexStr } = require('./lib/helpers')
const { Scale } = require('./lib/types/Scale')
const { Theme } = require('./lib/types/Theme')
const { VERSION_1_4_0, VERSION_2_5_0, InstrumentKinds } = require('./lib/constants')
const FMSynth = require('./lib/types/instruments/FMSynth')
const M8File = require('./lib/types/internal/M8File')
const M8FileReader = require('./lib/types/internal/M8FileReader')
const Macrosynth = require('./lib/types/instruments/Macrosynth')
const MIDIOut = require('./lib/types/instruments/MIDIOut')
const None = require('./lib/types/instruments/None')
const RGB = require('./lib/types/internal/RGB')
const Sampler = require('./lib/types/instruments/Sampler')
const Song = require('./lib/types/Song')
const Table = require('./lib/types/internal/Table')
const Wavsynth = require('./lib/types/instruments/Wavsynth')
// TODO: Add debug support
// TODO: Add error handling (Arguments in constructors, functions, ...)
// TODO: Add support for all constructors to take arguments
// TODO: Consider @typedefs for JavaScript Object representation
// TODO: Consider options objects for constructor arguments ()
/**
* Dumps an M8 Instrument to its bytes representation.
*
* @param {module:m8-js/lib/types/instruments.FMSYNTH|module:m8-js/lib/types/instruments.MACROSYNTH|module:m8-js/lib/types/instruments.MIDIOUT|module:m8-js/lib/types/instruments.NONE|module:m8-js/lib/types/instruments.SAMPLER|module:m8-js/lib/types/instruments.WAVSYNTH} instrument - The M8 Instrument to dump
* @param {Boolean} [skipHeader=false] - Whether or not to include the M8 file header in the returned bytes
*
* @returns {Array<Number>}
*/
const dumpInstrument = (instrument, skipHeader = false, offset = 0) => {
const bytes = []
if (!skipHeader) {
bytes.push(...instrument.headerAsBytes())
}
offset += bytes.length
bytes.push(instrument.kind())
bytes.push(...bytesFromString(instrument.name, 12, instrument.kind() === 0xFF ? 0xFF : 0x00))
bytes.push(bytesFromBool(instrument.transpose))
bytes.push(instrument.tableTick)
// Do not write these values for 'MIDI OUT', they aren't here when reading
if (instrument.kind() !== 0x03) {
bytes.push(instrument.volume)
bytes.push(instrument.pitch)
bytes.push(instrument.fineTune)
}
// Instrument-specific parameters
// Read instrument-specific parameters
switch (instrument.kind()) {
// WAVSYNTH
case 0x00:
bytes.push(instrument.instrParams.shape)
bytes.push(instrument.instrParams.size)
bytes.push(instrument.instrParams.mult)
bytes.push(instrument.instrParams.warp)
bytes.push(instrument.instrParams.mirror)
break
// MACROSYNTH
case 0x01:
bytes.push(instrument.instrParams.shape)
bytes.push(instrument.instrParams.timbre)
bytes.push(instrument.instrParams.color)
bytes.push(instrument.instrParams.degrade)
bytes.push(instrument.instrParams.redux)
break
// SAMPLER
case 0x02:
bytes.push(instrument.instrParams.playMode)
bytes.push(instrument.instrParams.slice)
bytes.push(instrument.instrParams.start)
bytes.push(instrument.instrParams.loopStart)
bytes.push(instrument.instrParams.length)
bytes.push(instrument.instrParams.degrade)
break
// MIDIOUT
case 0x03:
bytes.push(instrument.instrParams.port)
bytes.push(instrument.instrParams.channel)
bytes.push(instrument.instrParams.bankSelect)
bytes.push(instrument.instrParams.programChange)
// Write 3 empty bytes (unused data)
bytes.push(0x00, 0x00, 0x00)
for (let i = 0; i < instrument.instrParams.customCC.length; i++) {
const customCC = instrument.instrParams.customCC[i]
bytes.push(customCC.number)
bytes.push(customCC.defaultValue)
}
break
// FMSYNTH
case 0x04:
bytes.push(instrument.instrParams.algo)
// If supported, write the synth shapes
if (instrument.m8FileVersion.compare(VERSION_1_4_0) >= 0) {
for (let i = 0; i < instrument.instrParams.operators.length; i++) {
bytes.push(instrument.instrParams.operators[i].shape)
}
}
// Read the ratios
for (let i = 0; i < instrument.instrParams.operators.length; i++) {
const operator = instrument.instrParams.operators[i]
bytes.push(operator.ratio)
bytes.push(operator.ratioFine)
}
// Read the feedback/volume of each operator
for (let i = 0; i < instrument.instrParams.operators.length; i++) {
const operator = instrument.instrParams.operators[i]
bytes.push(operator.level)
bytes.push(operator.feedback)
}
// Read first modulator slot controls
for (let i = 0; i < instrument.instrParams.operators.length; i++) {
const operator = instrument.instrParams.operators[i]
bytes.push(operator.modA)
}
// Read second modulator slot controls
for (let i = 0; i < instrument.instrParams.operators.length; i++) {
const operator = instrument.instrParams.operators[i]
bytes.push(operator.modB)
}
// Read modulation sources
bytes.push(instrument.instrParams.mod1)
bytes.push(instrument.instrParams.mod2)
bytes.push(instrument.instrParams.mod3)
bytes.push(instrument.instrParams.mod4)
break
// NONE
case 0xFF:
// Do nothing
break
default:
/* istanbul ignore next */
throw new TypeError(`Unsupported Instrument type: ${toM8HexStr(instrument.kind())}`)
}
// Filter parameters
bytes.push(instrument.filterParams.type)
bytes.push(instrument.filterParams.cutoff)
bytes.push(instrument.filterParams.res)
// Amplifier parameters
bytes.push(instrument.ampParams.amp)
bytes.push(instrument.ampParams.limit)
// Write mixer parameters
bytes.push(instrument.mixerParams.pan)
bytes.push(instrument.mixerParams.dry)
bytes.push(instrument.mixerParams.cho)
bytes.push(instrument.mixerParams.del)
bytes.push(instrument.mixerParams.rev)
// Envelope parameters
for (let i = 0; i < instrument.envelopes.length; i++) {
const env = instrument.envelopes[i]
bytes.push(env.dest)
bytes.push(env.amount)
bytes.push(env.attack)
bytes.push(env.hold)
bytes.push(env.decay)
bytes.push(env.retrigger)
}
// LFO parameters
for (let i = 0; i < instrument.lfos.length; i++) {
const lfo = instrument.lfos[i]
bytes.push(lfo.shape)
bytes.push(lfo.dest)
bytes.push(lfo.triggerMode)
bytes.push(lfo.freq)
bytes.push(lfo.amount)
bytes.push(lfo.retrigger)
}
const writeLength = skipHeader
? (offset + 0x57) - (offset + bytes.length)
: (offset + 0x57) - bytes.length
// Fill in the empty values between instrument parameters and the sample path (when present)
bytes.push(...bytesForSkippedData(instrument.m8FileReader,
offset + bytes.length,
writeLength,
0xFF))
// Sample Path (when present)
bytes.push(...bytesFromString(instrument.kind() === 0x02 ? instrument.instrParams.samplePath : '', 128))
// Write table data whenever writing an Instrument file versus being writing a Song file
if (!skipHeader) {
bytes.push(...dumpTable(instrument.table))
}
return bytes
}
/**
* Dumps an M8 Scale to its bytes representation.
*
* @param {module:m8-js/lib/types.Scale} scale - The M8 Scale to dump
* @param {Boolean} [skipHeader=false] - Whether or not to include the M8 file header in the returned bytes
*
* @returns {Array<Number>}
*/
const dumpScale = (scale, skipHeader = false) => {
const bytes = []
const emptyByte = skipHeader ? 0xFF : 0x00
if (!skipHeader) {
bytes.push(...scale.headerAsBytes())
}
let noteBits = ''
for (let i = 0; i < scale.intervals.length; i++) {
noteBits += scale.intervals[i].enabled === true ? '1' : '0'
}
const rawNoteMap = parseInt(noteBits.split('').reverse().join(''), 2)
bytes.push(rawNoteMap & 0xFF)
bytes.push((rawNoteMap >> 8) & 0xFF)
for (let i = 0; i < scale.intervals.length; i++) {
const interval = scale.intervals[i]
bytes.push(interval.offsetA)
bytes.push(interval.offsetB)
}
bytes.push(...bytesFromString(scale.name, 16, emptyByte))
return bytes
}
/**
* Dumps an M8 Song to its bytes representation.
*
* @param {module:m8-js/lib/types.Song} song - The Song to dump
*
* @returns {Array<Number>}
*/
const dumpSong = (song) => {
const bytes = song.headerAsBytes()
const startLen = bytes.length
bytes.push(...bytesFromString(song.directory, 128))
// Unlike useSkippedBytes, we need to go and backfill the "garbage" after the directory name.
for (let i = startLen + song.directory.length + 1; i < startLen + 128; i++) {
let skippedValue = 0x00
if (song.m8FileReader?.skipped.indexOf(i) > -1) {
skippedValue = song.m8FileReader.bytes[i]
}
bytes[i] = skippedValue
}
bytes.push(song.transpose)
bytes.push(...bytesFromFloatLE(song.tempo))
bytes.push(song.quantize)
bytes.push(...bytesFromString(song.name, 12))
// MIDI Settings
bytes.push(bytesFromBool(song.midiSettings.receiveSync))
bytes.push(song.midiSettings.receiveTransport)
bytes.push(bytesFromBool(song.midiSettings.sendSync))
bytes.push(song.midiSettings.sendTransport)
bytes.push(song.midiSettings.recordNoteChannel)
bytes.push(bytesFromBool(song.midiSettings.recordNoteVelocity))
bytes.push(song.midiSettings.recordNoteDelayKillCommands)
bytes.push(song.midiSettings.controlMapChannel)
bytes.push(song.midiSettings.songRowCueChannel)
for (let i = 0; i < 8; i++) {
bytes.push(song.midiSettings.trackInputChannel[i])
}
for (let i = 0; i < 8; i++) {
bytes.push(song.midiSettings.trackInputInstrument[i])
}
bytes.push(bytesFromBool(song.midiSettings.trackInputProgramChange))
bytes.push(song.midiSettings.trackInputMode)
// Song key and skipped/unknown data
bytes.push(song.key)
bytes.push(...bytesForSkippedData(song.m8FileReader, bytes.length, 18, 0x00))
// Read Mixer Settings
bytes.push(song.mixerSettings.masterVolume)
bytes.push(song.mixerSettings.masterLimit)
for (let i = 0; i < 8; i++) {
bytes.push(song.mixerSettings.trackVolume[i])
}
bytes.push(song.mixerSettings.chorusVolume)
bytes.push(song.mixerSettings.delayVolume)
bytes.push(song.mixerSettings.reverbVolume)
bytes.push(song.mixerSettings.analogInputVolume[0])
bytes.push(song.mixerSettings.analogInputVolume[1])
bytes.push(song.mixerSettings.usbInputVolume)
bytes.push(song.mixerSettings.analogInputChorus[0])
bytes.push(song.mixerSettings.analogInputDelay[0])
bytes.push(song.mixerSettings.analogInputReverb[0])
bytes.push(song.mixerSettings.analogInputChorus[1])
bytes.push(song.mixerSettings.analogInputDelay[1])
bytes.push(song.mixerSettings.analogInputReverb[1])
bytes.push(song.mixerSettings.usbInputChorus)
bytes.push(song.mixerSettings.usbInputDelay)
bytes.push(song.mixerSettings.usbInputReverb)
bytes.push(song.mixerSettings.djFilter)
bytes.push(song.mixerSettings.djFilterPeak)
// Skipped data
bytes.push(...bytesForSkippedData(song.m8FileReader, bytes.length, 5, 0x00))
// Grooves
for (let i = 0; i < song.grooves.length; i++) {
const groove = song.grooves[i]
for (let j = 0; j < groove.steps.length; j++) {
bytes.push(groove.steps[j])
}
}
// Song steps
for (let i = 0; i < song.steps.length; i++) {
const step = song.steps[i]
for (let j = 0; j < step.tracks.length; j++) {
bytes.push(step.tracks[j])
}
}
// Phrases
for (let i = 0; i < song.phrases.length; i++) {
const phrase = song.phrases[i]
for (let j = 0; j < phrase.steps.length; j++) {
const step = phrase.steps[j]
bytes.push(step.note)
bytes.push(step.volume)
bytes.push(step.instrument)
for (let k = 0; k < 3; k++) {
const fx = step.fx[k]
bytes.push(fx.command)
bytes.push(fx.value)
}
}
}
// Chains
for (let i = 0; i < song.chains.length; i++) {
const chain = song.chains[i]
for (let j = 0; j < chain.steps.length; j++) {
const step = chain.steps[j]
bytes.push(step.phrase)
bytes.push(step.transpose)
}
}
// Tables
for (let i = 0; i < song.tables.length; i++) {
// We cannot assume that the tables are synced up for Instruments so use the Instrument table if possible
bytes.push(...dumpTable(song.instruments[i]?.table || song.tables[i]))
}
// Instruments
for (let i = 0; i < song.instruments.length; i++) {
bytes.push(...dumpInstrument(song.instruments[i], true, bytes.length))
}
// Skipped data
bytes.push(...bytesForSkippedData(song.m8FileReader, bytes.length, 3, 0x00))
// Effects
bytes.push(song.effectsSettings.chorusSettings.modDepth)
bytes.push(song.effectsSettings.chorusSettings.modFreq)
bytes.push(song.effectsSettings.chorusSettings.width)
bytes.push(song.effectsSettings.chorusSettings.reverbSend)
// Skipped data
bytes.push(...bytesForSkippedData(song.m8FileReader, bytes.length, 3, 0x00))
bytes.push(song.effectsSettings.delaySettings.filterHP)
bytes.push(song.effectsSettings.delaySettings.filterLP)
bytes.push(song.effectsSettings.delaySettings.timeL)
bytes.push(song.effectsSettings.delaySettings.timeR)
bytes.push(song.effectsSettings.delaySettings.feedback)
bytes.push(song.effectsSettings.delaySettings.width)
bytes.push(song.effectsSettings.delaySettings.reverbSend)
// Skipped data
bytes.push(...bytesForSkippedData(song.m8FileReader, bytes.length, 1, 0x00))
bytes.push(song.effectsSettings.reverbSettings.filterHP)
bytes.push(song.effectsSettings.reverbSettings.filterLP)
bytes.push(song.effectsSettings.reverbSettings.size)
bytes.push(song.effectsSettings.reverbSettings.damping)
bytes.push(song.effectsSettings.reverbSettings.modDepth)
bytes.push(song.effectsSettings.reverbSettings.modFreq)
bytes.push(song.effectsSettings.reverbSettings.width)
// Skipped data between Instruments and MIDI Mappings (when present)
bytes.push(...bytesForSkippedData(song.m8FileReader, bytes.length, 0x1A5FE - bytes.length, 0xFF))
// MIDI Mappings
for (let i = 0; i < song.midiMappings.length; i++) {
const midiMapping = song.midiMappings[i]
bytes.push(midiMapping.channel)
bytes.push(midiMapping.controlNum)
bytes.push(midiMapping.type)
bytes.push(midiMapping.instrIndex)
bytes.push(midiMapping.paramIndex)
bytes.push(midiMapping.minValue)
bytes.push(midiMapping.maxValue)
}
// Scales (when supported)
if (song.m8FileVersion.compare(VERSION_2_5_0) >= 0) {
// Skipped data between MIDI Mappings and Scales (when present)
bytes.push(...bytesForSkippedData(song.m8FileReader, bytes.length, 0x1AA7E - bytes.length, 0xFF))
for (let i = 0; i < song.scales.length; i++) {
bytes.push(...dumpScale(song.scales[i], true))
}
} else {
bytes.push(...bytesForSkippedData(song.m8FileReader, bytes.length, 256, 0x00))
}
return bytes
}
/**
* Dumps an M8 Table to its bytes representation.
*
* @param {module:m8-js/lib/types.Table} table - The Table to dump
*
* @returns {Array<Number>}
*/
const dumpTable = (table) => {
const bytes = []
for (let i = 0; i < table.steps.length; i++) {
const step = table.steps[i]
bytes.push(step.transpose)
bytes.push(step.volume)
for (let j = 0; j < step.fx.length; j++) {
const fx = step.fx[j]
bytes.push(fx.command)
bytes.push(fx.value)
}
}
return bytes
}
/**
* Dumps an M8 Theme to its bytes representation.
*
* @param {module:m8-js/lib/types.Theme} theme - The M8 Theme to dump
*
* @returns {Array<Number>}
*/
const dumpTheme = (theme) => {
const bytes = theme.headerAsBytes()
bytes.push(theme.background.r, theme.background.g, theme.background.b)
bytes.push(theme.textEmpty.r, theme.textEmpty.g, theme.textEmpty.b)
bytes.push(theme.textInfo.r, theme.textInfo.g, theme.textInfo.b)
bytes.push(theme.textDefault.r, theme.textDefault.g, theme.textDefault.b)
bytes.push(theme.textValue.r, theme.textValue.g, theme.textValue.b)
bytes.push(theme.textTitle.r, theme.textTitle.g, theme.textTitle.b)
bytes.push(theme.playMarker.r, theme.playMarker.g, theme.playMarker.b)
bytes.push(theme.cursor.r, theme.cursor.g, theme.cursor.b)
bytes.push(theme.selection.r, theme.selection.g, theme.selection.b)
bytes.push(theme.scopeSlider.r, theme.scopeSlider.g, theme.scopeSlider.b)
bytes.push(theme.meterLow.r, theme.meterLow.g, theme.meterLow.b)
bytes.push(theme.meterMid.r, theme.meterMid.g, theme.meterMid.b)
bytes.push(theme.meterPeak.r, theme.meterPeak.g, theme.meterPeak.b)
return bytes
}
/**
* Loads an M8 Instrument from its bytes.
*
* @param {module:m8-js/lib/types.M8FileReader} fileReader - The M8 file reader
*
* @returns {module:m8-js/lib/types/instruments.FMSYNTH|module:m8-js/lib/types/instruments.MACROSYNTH|module:m8-js/lib/types/instruments.MIDIOUT|module:m8-js/lib/types/instruments.NONE|module:m8-js/lib/types/instruments.SAMPLER|module:m8-js/lib/types/instruments.WAVSYNTH}
*/
const loadInstrument = (fileReader) => {
const startPos = fileReader.cursor
// Read parameters that ALL instruments have
const kind = fileReader.read()
const name = fileReader.readStr(12)
const transpose = Boolean(fileReader.read())
const tableTick = fileReader.read()
let volume = 0x00
let pitch = 0x00
let fineTune = 0x80
// It appears that these are not present for 'MIDI OUT'
if (kind !== 0x03) {
volume = fileReader.read()
pitch = fileReader.read()
fineTune = fileReader.read()
}
let instr
// Read instrument-specific parameters
switch (kind) {
case 0x00:
instr = new Wavsynth(fileReader)
instr.instrParams.shape = fileReader.read()
instr.instrParams.size = fileReader.read()
instr.instrParams.mult = fileReader.read()
instr.instrParams.warp = fileReader.read()
instr.instrParams.mirror = fileReader.read()
break
case 0x01:
instr = new Macrosynth(fileReader)
instr.instrParams.shape = fileReader.read()
instr.instrParams.timbre = fileReader.read()
instr.instrParams.color = fileReader.read()
instr.instrParams.degrade = fileReader.read()
instr.instrParams.redux = fileReader.read()
break
case 0x02:
instr = new Sampler(fileReader)
instr.instrParams.playMode = fileReader.read()
instr.instrParams.slice = fileReader.read()
instr.instrParams.start = fileReader.read()
instr.instrParams.loopStart = fileReader.read()
instr.instrParams.length = fileReader.read()
instr.instrParams.degrade = fileReader.read()
break
case 0x03:
instr = new MIDIOut(fileReader)
instr.instrParams.port = fileReader.read()
instr.instrParams.channel = fileReader.read()
instr.instrParams.bankSelect = fileReader.read()
instr.instrParams.programChange = fileReader.read()
// Discard the next 3 bytes (unused data)
fileReader.read(3)
for (let i = 0; i < instr.instrParams.customCC.length; i++) {
const customCC = instr.instrParams.customCC[i]
customCC.number = fileReader.read()
customCC.defaultValue = fileReader.read()
}
break
case 0x04:
instr = new FMSynth(fileReader)
instr.instrParams.algo = fileReader.read()
// If supported, read the synth shapes
if (fileReader.m8Version.compare(VERSION_1_4_0) >= 0) {
for (let i = 0; i < instr.instrParams.operators.length; i++) {
instr.instrParams.operators[i].shape = fileReader.read()
}
}
// Read the ratios
for (let i = 0; i < instr.instrParams.operators.length; i++) {
const operator = instr.instrParams.operators[i]
operator.ratio = fileReader.read()
operator.ratioFine = fileReader.read()
}
// Read the feedback/volume of each operator
for (let i = 0; i < instr.instrParams.operators.length; i++) {
const operator = instr.instrParams.operators[i]
operator.level = fileReader.read()
operator.feedback = fileReader.read()
}
// Read first modulator slot controls
for (let i = 0; i < instr.instrParams.operators.length; i++) {
const operator = instr.instrParams.operators[i]
operator.modA = fileReader.read()
}
// Read second modulator slot controls
for (let i = 0; i < instr.instrParams.operators.length; i++) {
const operator = instr.instrParams.operators[i]
operator.modB = fileReader.read()
}
// Read modulation sources
instr.instrParams.mod1 = fileReader.read()
instr.instrParams.mod2 = fileReader.read()
instr.instrParams.mod3 = fileReader.read()
instr.instrParams.mod4 = fileReader.read()
break
case 0xFF:
instr = new None(fileReader)
break
default:
/* istanbul ignore next */
throw new TypeError(`Unsupported Instrument type: ${toM8HexStr(kind)}`)
}
// Read parameters that ALL instruments have
// instr.kind is set by the instrument class itself
instr.name = name
instr.transpose = transpose
instr.tableTick = tableTick
// It appears that these are not present for 'MIDI OUT'
if (instr.kind() !== InstrumentKinds.MIDIOUT) {
instr.volume = volume
instr.pitch = pitch
instr.fineTune = fineTune
}
// Read filter parameters
instr.filterParams.type = fileReader.read()
instr.filterParams.cutoff = fileReader.read()
instr.filterParams.res = fileReader.read()
// Read amplifier parameters
instr.ampParams.amp = fileReader.read()
instr.ampParams.limit = fileReader.read()
// Read mixer parameters
instr.mixerParams.pan = fileReader.read()
instr.mixerParams.dry = fileReader.read()
instr.mixerParams.cho = fileReader.read()
instr.mixerParams.del = fileReader.read()
instr.mixerParams.rev = fileReader.read()
// Read envelope parameters
for (let i = 0; i < instr.envelopes.length; i++) {
const env = instr.envelopes[i]
env.dest = fileReader.read()
env.amount = fileReader.read()
env.attack = fileReader.read()
env.hold = fileReader.read()
env.decay = fileReader.read()
env.retrigger = fileReader.read()
}
// Read LFO parametersj
for (let i = 0; i < instr.lfos.length; i++) {
const lfo = instr.lfos[i]
lfo.shape = fileReader.read()
lfo.dest = fileReader.read()
lfo.triggerMode = fileReader.read()
lfo.freq = fileReader.read()
lfo.amount = fileReader.read()
lfo.retrigger = fileReader.read()
}
// Skip to the sample path (when present) and record the unused bytes
fileReader.skipTo(startPos + 0x57) // Jump amount differs based on instrument type
// Read Sample Path (Unfortunate that it happens here)
if (instr.kind() === InstrumentKinds.SAMPLER) {
instr.instrParams.samplePath = fileReader.readStr(127)
// Discard the next byte
fileReader.skip(1)
} else {
// Discard the bytes corresponding to the sample path when it's not present
fileReader.skip(128)
}
// Read table data whenever the Instrument is read from an Instrument file versus being read from a Song file
if (fileReader.fileTypeToStr() === 'Instrument') {
instr.table = loadTable(fileReader)
}
return instr
}
/**
* Loads an M8 Scale from its bytes.
*
* @param {module:m8-js/lib/types.M8FileReader} fileReader - The M8 file reader
*
* @returns {module:m8-js/lib/types.Scale}
*/
const loadScale = (fileReader) => {
const scale = new Scale(fileReader)
const noteMap = readUInt16LE(fileReader.read(2))
// Read interval enablements
for (let i = 0; i < scale.intervals.length; i++) {
const interval = scale.intervals[i]
interval.enabled = Boolean((noteMap >> i) & 0x1)
}
// Read interval offsets
for (let i = 0; i < scale.intervals.length; i++) {
const interval = scale.intervals[i]
interval.offsetA = fileReader.read()
interval.offsetB = fileReader.read()
}
// Read name
scale.name = fileReader.readStr(16)
return scale
}
/**
* Loads an M8 Song from its bytes.
*
* @param {module:m8-js/lib/types.M8FileReader} fileReader - The M8 file reader
*
* @returns {module:m8-js/lib/types.Song}
*/
const loadSong = (fileReader) => {
const song = new Song(fileReader)
song.directory = fileReader.readStr(128)
song.transpose = fileReader.read()
// Tempo is stored in 4 bytes as a 32-bit float
song.tempo = readFloatLE(fileReader.read(4))
song.quantize = fileReader.read()
song.name = fileReader.readStr(12)
// Read MIDI Settings
song.midiSettings.receiveSync = Boolean(fileReader.read())
song.midiSettings.receiveTransport = fileReader.read()
song.midiSettings.sendSync = Boolean(fileReader.read())
song.midiSettings.sendTransport = fileReader.read()
song.midiSettings.recordNoteChannel = fileReader.read()
song.midiSettings.recordNoteVelocity = Boolean(fileReader.read())
song.midiSettings.recordNoteDelayKillCommands = fileReader.read()
song.midiSettings.controlMapChannel = fileReader.read()
song.midiSettings.songRowCueChannel = fileReader.read()
for (let i = 0; i < 8; i++) {
song.midiSettings.trackInputChannel[i] = fileReader.read()
}
for (let i = 0; i < 8; i++) {
song.midiSettings.trackInputInstrument[i] = fileReader.read()
}
song.midiSettings.trackInputProgramChange = Boolean(fileReader.read())
song.midiSettings.trackInputMode = fileReader.read()
song.key = fileReader.read()
// Discard the next 18 bytes (empty data)
fileReader.skip(18)
// Read Mixer Settings
song.mixerSettings.masterVolume = fileReader.read()
song.mixerSettings.masterLimit = fileReader.read()
for (let i = 0; i < 8; i++) {
song.mixerSettings.trackVolume[i] = fileReader.read()
}
song.mixerSettings.chorusVolume = fileReader.read()
song.mixerSettings.delayVolume = fileReader.read()
song.mixerSettings.reverbVolume = fileReader.read()
song.mixerSettings.analogInputVolume = [fileReader.read(), fileReader.read()]
song.mixerSettings.usbInputVolume = fileReader.read()
song.mixerSettings.analogInputChorus[0] = fileReader.read()
song.mixerSettings.analogInputDelay[0] = fileReader.read()
song.mixerSettings.analogInputReverb[0] = fileReader.read()
song.mixerSettings.analogInputChorus[1] = fileReader.read()
song.mixerSettings.analogInputDelay[1] = fileReader.read()
song.mixerSettings.analogInputReverb[1] = fileReader.read()
song.mixerSettings.usbInputChorus = fileReader.read()
song.mixerSettings.usbInputDelay = fileReader.read()
song.mixerSettings.usbInputReverb = fileReader.read()
song.mixerSettings.djFilter = fileReader.read()
song.mixerSettings.djFilterPeak = fileReader.read()
// Discard the next 5 bytes (unknown data)
fileReader.skip(5)
// Read Grooves
for (let i = 0; i < song.grooves.length; i++) {
const groove = song.grooves[i]
for (let j = 0; j < groove.steps.length; j++) {
groove.steps[j] = fileReader.read()
}
}
// Read song steps
for (let i = 0; i < 256; i++) {
const step = song.steps[i]
for (let j = 0; j < 8; j++) {
step.tracks[j] = fileReader.read()
}
}
// Read Phrases
for (let i = 0; i < song.phrases.length; i++) {
const phrase = song.phrases[i]
for (let j = 0; j < phrase.steps.length; j++) {
const step = phrase.steps[j]
step.note = fileReader.read()
step.volume = fileReader.read()
step.instrument = fileReader.read()
for (let k = 0; k < 3; k++) {
const fx = step.fx[k]
fx.command = fileReader.read()
fx.value = fileReader.read()
}
}
}
// Read Chains
for (let i = 0; i < song.chains.length; i++) {
const chain = song.chains[i]
for (let j = 0; j < chain.steps.length; j++) {
const step = chain.steps[j]
step.phrase = fileReader.read()
step.transpose = fileReader.read()
}
}
// Read Tables
for (let i = 0; i < song.tables.length; i++) {
song.tables[i] = loadTable(fileReader)
}
// Read Instruments
for (let i = 0; i < song.instruments.length; i++) {
song.instruments[i] = loadInstrument(fileReader)
// Update the instrument's table data reference
song.instruments[i].table = song.tables[i]
}
// Discard the next 3 bytes (unused data)
fileReader.skip(3)
// Read Effects
song.effectsSettings.chorusSettings.modDepth = fileReader.read()
song.effectsSettings.chorusSettings.modFreq = fileReader.read()
song.effectsSettings.chorusSettings.width = fileReader.read()
song.effectsSettings.chorusSettings.reverbSend = fileReader.read()
// Discard the next 3 bytes (unused data)
fileReader.skip(3)
song.effectsSettings.delaySettings.filterHP = fileReader.read()
song.effectsSettings.delaySettings.filterLP = fileReader.read()
song.effectsSettings.delaySettings.timeL = fileReader.read()
song.effectsSettings.delaySettings.timeR = fileReader.read()
song.effectsSettings.delaySettings.feedback = fileReader.read()
song.effectsSettings.delaySettings.width = fileReader.read()
song.effectsSettings.delaySettings.reverbSend = fileReader.read()
// Discard the next 1 byte (unused data)
fileReader.skip(1)
song.effectsSettings.reverbSettings.filterHP = fileReader.read()
song.effectsSettings.reverbSettings.filterLP = fileReader.read()
song.effectsSettings.reverbSettings.size = fileReader.read()
song.effectsSettings.reverbSettings.damping = fileReader.read()
song.effectsSettings.reverbSettings.modDepth = fileReader.read()
song.effectsSettings.reverbSettings.modFreq = fileReader.read()
song.effectsSettings.reverbSettings.width = fileReader.read()
// Skip ahead to this specific position (unknown data)
// TODO: Look into this
fileReader.skipTo(0x1A5FE)
// Read MIDI Mappings
for (let i = 0; i < song.midiMappings.length; i++) {
const midiMapping = song.midiMappings[i]
midiMapping.channel = fileReader.read()
midiMapping.controlNum = fileReader.read()
midiMapping.type = fileReader.read()
midiMapping.instrIndex = fileReader.read()
midiMapping.paramIndex = fileReader.read()
midiMapping.minValue = fileReader.read()
midiMapping.maxValue = fileReader.read()
}
// Read Scales (when supported)
if (fileReader.m8Version.compare(VERSION_2_5_0) >= 0) {
// Skip ahead to this specific position (unknown data)
// TODO: Look into this
fileReader.skipTo(0x1AA7E)
for (let i = 0; i < song.scales.length; i++) {
song.scales[i] = loadScale(fileReader)
}
}
return song
}
/**
* Loads an M8 Table from its bytes.
*
* @param {module:m8-js/lib/types.M8FileReader} fileReader - The M8 file reader
*
* @returns {module:m8-js/lib/types.Table}
*/
const loadTable = (fileReader) => {
const table = new Table()
for (let i = 0; i < table.steps.length; i++) {
const step = table.steps[i]
step.transpose = fileReader.read()
step.volume = fileReader.read()
for (let j = 0; j < step.fx.length; j++) {
const fx = step.fx[j]
fx.command = fileReader.read()
fx.value = fileReader.read()
}
}
return table
}
/**
* Loads an M8 Theme from its bytes.
*
* @param {module:m8-js/lib/types.M8FileReader} fileReader - The M8 file reader
*
* @returns {module:m8-js/lib/types.Theme}
*/
const loadTheme = (fileReader) => {
const theme = new Theme(fileReader)
Theme.getObjectProperties().forEach((prop) => {
// Skip the file metadata as it's not required
if (prop === 'fileMetadata') {
return
}
theme[prop] = new RGB(...fileReader.read(3))
})
return theme
}
/**
* Dumps an M8 file and returns its bytes.
*
* @param {module:m8-js/lib/types.M8File} m8File - The M8File instance to dump
*
* @returns {Uint8Array}
*/
const dumpM8File = (m8File) => {
const fileTypeStr = M8File.typeToStr(m8File.m8FileType)
let bytes
switch (fileTypeStr) {
case 'Instrument':
bytes = dumpInstrument(m8File)
break
case 'Scale':
bytes = dumpScale(m8File)
break
case 'Song':
bytes = dumpSong(m8File)
break
case 'Theme':
bytes = dumpTheme(m8File)
break
default:
throw new TypeError(`Unsupported file type: ${fileTypeStr}`)
}
return Uint8Array.from(bytes)
}
/**
* Loads an M8 file and returns the appropriate corresdponding object.
*
* @param {Array<Number>} bytes - The raw M8 file bytes
*
* @returns {module:m8-js/lib/types.Scale|module:m8-js/lib/types.Song|module:m8-js/lib/types.Theme|module:m8-js/lib/types/instruments.FMSYNTH|module:m8-js/lib/types/instruments.MACROSYNTH|module:m8-js/lib/types/instruments.MIDIOUT|module:m8-js/lib/types/instruments.NONE|module:m8-js/lib/types/instruments.SAMPLER|module:m8-js/lib/types/instruments.WAVSYNTH}
*/
const loadM8File = (bytes) => {
const m8FileReader = new M8FileReader(bytes)
switch (m8FileReader.fileTypeToStr()) {
case 'Instrument':
return loadInstrument(m8FileReader)
case 'Scale':
return loadScale(m8FileReader)
case 'Song':
return loadSong(m8FileReader)
case 'Theme':
return loadTheme(m8FileReader)
default:
throw new TypeError(`Unsupported file type: ${m8FileReader.fileTypeToStr()}`)
}
}
// Exports
module.exports = {
dumpM8File,
loadM8File
}