UNPKG

m8-js

Version:

Library for loading and interacting with Dirtywave M8 instrument/song files.

750 lines (615 loc) 27 kB
/* Copyright 2023 Jeremy Whitlock * * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this 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 { DefaultTheme } = require('../types/Theme') const { InstrumentKinds, VERSION_1_4_0 } = require('../constants') const { loadM8File } = require('../..') const { existsSync, readFileSync, writeFileSync } = require('fs') const { getNote, toM8Bool, toM8HexStr, toM8Num } = require('../helpers') const clc = require('cli-color') const commander = require('commander') const M8File = require('../types/internal/M8File') const x256 = require('x256') const m8Text = {} /** * Gets the starting row number based on what was requested. * * @param {Number} startingRow - The starting row * @param {Number} maxValue - The maximum value * * @returns {Number} */ const getStartingRow = (startingRow, maxValue) => { // Starting row must is 1-based but traversal is 0-based, and with the // number of rows being displayed will be 16, we offset the maximum by // 15. const maxStartingRow = maxValue - 15 if (startingRow < 0 || startingRow > maxValue) { throw new commander.InvalidArgumentError("option '-s, --starting-row <number>' must be between " + `00 and ${toM8HexStr(maxValue)}`) } // If the user specifies a starting row within the last page, set the // starting row to the first item of the last page if (startingRow > maxStartingRow) { startingRow = maxStartingRow } return startingRow } /** * Loads the M8 file and verifies its type. * * @param {String} path - The M8 file path * @param {Array<String>|String} type - The type(s) allowed for the M8 file * * @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 loadM8FileAndVerify = (path, type) => { const bytesFromDisk = Uint8Array.from(readFileSync(path)) const m8File = loadM8File(bytesFromDisk) const m8FileType = M8File.typeToStr(m8File.m8FileType) if (typeof type === 'string') { if (m8FileType !== type) { throw new commander.InvalidArgumentError(`m8-file must be a ${type} file`) } } else { if (typeof type !== 'undefined' && type.indexOf(m8FileType) === -1) { throw new commander.InvalidArgumentError(`m8-file must be a ${type.join(' or ')} file`) } } return m8File } /** * Loads an M8 Theme. * * @param {module:m8-js/lib/types.Theme} themeOrPath - The M8 Theme to load */ const loadTheme = (themeOrPath) => { let theme if (typeof themeOrPath === 'undefined') { theme = DefaultTheme } else if (themeOrPath.constructor.name === 'Theme') { theme = themeOrPath } else { theme = loadM8FileAndVerify(themeOrPath) if (theme.constructor.name !== 'Theme') { throw new commander.InvalidOptionArgumentError("option '-T, --theme <path>' must be to an M8 Theme file") } } m8Text.default = clc.xterm(x256(theme.textDefault.r, theme.textDefault.g, theme.textDefault.b)) m8Text.empty = clc.xterm(x256(theme.textEmpty.r, theme.textEmpty.g, theme.textEmpty.b)) m8Text.info = clc.xterm(x256(theme.textInfo.r, theme.textInfo.g, theme.textInfo.b)) m8Text.title = clc.xterm(x256(theme.textTitle.r, theme.textTitle.g, theme.textTitle.b)) m8Text.value = clc.xterm(x256(theme.textValue.r, theme.textValue.g, theme.textValue.b)) } /** * Parses the hex string into a number. * * @param {String} value - The value expected to represent a hex integer * * @returns {Number} */ const parseCLIHexInt = (value) => { const parsedValue = parseInt(value, 16) if (isNaN(parsedValue)) { throw new commander.InvalidOptionArgumentError('not a hex number') } return parsedValue } /** * Parses the decimal string into a number. * * @param {String} value - The value expected to represent a decimal integer * * @returns {Number} */ const parseCLIInt = (value) => { const parsedValue = parseInt(value, 10) if (isNaN(parsedValue)) { throw new commander.InvalidOptionArgumentError('not a number') } return parsedValue } /** * Prints an M8 Instrument. * * @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 print * @param {Number} [instrIndex] - The instrument index */ const printInstrument = (instrument, instrIndex) => { const ampParams = instrument.ampParams const filterParams = instrument.filterParams const instrParams = instrument.instrParams const kind = instrument.kind() const mixerParams = instrument.mixerParams let instrumentData = '' if (kind === InstrumentKinds.MIDIOUT) { instrumentData = ` ${m8Text.default('PORT')} ${m8Text.value(toM8HexStr(instrParams.port))}${m8Text.title(instrParams.portToStr())} ${m8Text.default('CHANNEL')} ${m8Text.value(toM8Num(instrParams.channel))} ${m8Text.default('BANK:PG')} ` if (instrParams.bankSelect === 0xFF) { instrumentData += m8Text.empty(toM8Num('---')) } else { instrumentData += m8Text.value(toM8Num(instrParams.bankSelect, 3)) } instrumentData += m8Text.info(':') if (instrParams.programChange === 0xFF) { instrumentData += m8Text.empty(toM8Num('---')) } else { instrumentData += m8Text.value(toM8Num(instrParams.programChange, 3)) } instrumentData += '\n' for (let i = 0; i < instrParams.customCC.length; i++) { const customCC = instrParams.customCC[i] // 97 is the char code for 'a' instrumentData += `${m8Text.default('CC' + String.fromCharCode(97 + i).toUpperCase() + ' CC:VAL')} ` if (customCC.number === 0xFF) { instrumentData += m8Text.empty(toM8Num('---')) } else { instrumentData += m8Text.value(toM8Num(customCC.number, 3)) } instrumentData += m8Text.info(':') if (customCC.defaultValue === 0xFF) { instrumentData += m8Text.empty('--') } else { instrumentData += m8Text.value(toM8HexStr(customCC.defaultValue)) } if (i < 10) { instrumentData += '\n' } } } else if (kind !== InstrumentKinds.NONE) { const leftColumn = [] const rightColumn = [] let levFbData = '' let modAData = '' let modBData = '' let ratioData = '' let sanitizedSamplePath switch (kind) { case InstrumentKinds.FMSYNTH: instrumentData = ` ${m8Text.default('ALGO')} ${m8Text.value(toM8HexStr(instrParams.algo))}${m8Text.title(instrParams.algoToStr())} ` if (instrument.m8FileVersion.compare(VERSION_1_4_0) >= 0) { instrumentData += ' ' for (let i = 0; i < instrParams.operators.length; i++) { const operator = instrParams.operators[i] const charStr = String.fromCharCode(97 + i).toUpperCase() instrumentData += `${m8Text.default(charStr)} ${m8Text.value(operator.shapeToStr())}` if (i < instrParams.operators.length - 1) { instrumentData += ' ' } } } for (let i = 0; i < instrParams.operators.length; i++) { const operator = instrParams.operators[i] ratioData += m8Text.value(toM8Num(operator.ratio) + '.' + toM8Num(operator.ratioFine)) if (i < instrParams.operators.length - 1) { ratioData += ' ' } } for (let i = 0; i < instrParams.operators.length; i++) { const operator = instrParams.operators[i] levFbData += m8Text.value(toM8HexStr(operator.level)) + m8Text.info('/') + m8Text.value(toM8HexStr(operator.feedback)) if (i < instrParams.operators.length - 1) { levFbData += ' ' } } for (let i = 0; i < instrParams.operators.length; i++) { const operator = instrParams.operators[i] const modAStr = instrParams.modToStr(operator.modA) const modBStr = instrParams.modToStr(operator.modB) if (modAStr === '-----') { modAData += m8Text.empty(modAStr) } else { modAData += m8Text.value(modAStr.substring(0, 2)) + m8Text.title(modAStr.substring(2)) } if (modBStr === '-----') { modBData += m8Text.empty(modBStr) } else { modBData += m8Text.value(modBStr.substring(0, 2)) + m8Text.title(modBStr.substring(2)) } if (i < instrParams.operators.length - 1) { modAData += ' ' modBData += ' ' } } instrumentData += ` ${m8Text.default('RATIO')} ${ratioData} ${m8Text.default('LEV/FB')} ${levFbData} ${m8Text.default('MOD')} ${modAData} ${modBData} ` leftColumn.push(['MOD1', instrParams.mod1]) leftColumn.push(['MOD2', instrParams.mod2]) leftColumn.push(['MOD3', instrParams.mod3]) leftColumn.push(['MOD4', instrParams.mod4]) break case InstrumentKinds.WAVSYNTH: instrumentData = ` ${m8Text.default('SHAPE')} ${m8Text.value(toM8HexStr(instrParams.shape))}${m8Text.title(instrParams.shapeToStr())} ` leftColumn.push(['SIZE', instrParams.size]) leftColumn.push(['MULT', instrParams.mult]) leftColumn.push(['WARP', instrParams.warp]) leftColumn.push(['MIRROR', instrParams.mirror]) break case InstrumentKinds.MACROSYNTH: instrumentData = ` ${m8Text.default('SHAPE')} ${m8Text.value(toM8HexStr(instrParams.shape))}${m8Text.title(instrParams.shapeToStr())} ` leftColumn.push(['TIMBRE', instrParams.timbre]) leftColumn.push(['COLOR', instrParams.color]) leftColumn.push(['DEGRADE', instrParams.degrade]) leftColumn.push(['REDUX', instrParams.redux]) break case InstrumentKinds.SAMPLER: sanitizedSamplePath = instrParams.samplePathToStr() instrumentData = ` ${m8Text.default('SAMPLE')}` if (sanitizedSamplePath.length > 0) { instrumentData += ` ${m8Text.title(sanitizedSamplePath)}` } instrumentData += '\n\n' leftColumn.push([ 'SLICE', instrParams.slice, instrParams.slice === 0x00 ? 'OFF' : toM8Num(instrParams.slice, 3)]) leftColumn.push(['PLAY', instrParams.playMode, instrParams.playModeToStr()]) leftColumn.push(['START', instrParams.start]) leftColumn.push(['LOOP ST', instrParams.loopStart]) leftColumn.push(['LENGTH', instrParams.length]) leftColumn.push(['DETUNE', instrument.fineTune]) leftColumn.push(['DEGRADE', instrParams.degrade]) break } // Filter Parameters leftColumn.push(['FILTER', filterParams.type, instrument.filterParams.typeToStr(instrument.kind(), instrument.m8FileVersion)]) leftColumn.push(['CUTOFF', filterParams.cutoff]) leftColumn.push(['RES', filterParams.res]) // Amplifier Parameters rightColumn.push(['AMP', ampParams.amp]) rightColumn.push(['LIM', ampParams.limit, ampParams.limitToStr()]) // Mixer Parameters rightColumn.push(['PAN', mixerParams.pan]) rightColumn.push(['DRY', mixerParams.dry]) rightColumn.push(['CHO', mixerParams.cho]) rightColumn.push(['DEL', mixerParams.del]) rightColumn.push(['REV', mixerParams.rev]) // As of right now, left column will ALWAYS have more items than right for (let i = 0; i < leftColumn.length; i++) { const leftCol = leftColumn[i] const leftLabel = leftCol[0] const leftValue = leftCol[1] const leftExtra = leftCol[2] const rightCol = rightColumn[i] || [] const rightLabel = rightCol[0] const rightValue = rightCol[1] const rightExtra = rightCol[2] instrumentData += m8Text.default(leftLabel) + ' '.repeat(8 - leftLabel.length) instrumentData += m8Text.value(toM8HexStr(leftValue)) if (typeof leftExtra !== 'undefined') { instrumentData += m8Text.title(leftExtra) } if (typeof rightLabel !== 'undefined') { instrumentData += ' '.repeat(8 - (typeof leftExtra === 'undefined' ? 0 : leftExtra.length)) instrumentData += m8Text.default(rightLabel) instrumentData += ' ' instrumentData += m8Text.value(toM8HexStr(rightValue)) if (typeof rightExtra !== 'undefined') { instrumentData += m8Text.title(rightExtra) } } instrumentData += '\n' } } console.log(`${m8Text.title('INST.' + (typeof instrIndex === 'undefined' ? '' : ' ' + toM8HexStr(instrIndex)))} ${m8Text.default('TYPE')} ${m8Text.value(instrument.kindToStr())} ${m8Text.default('NAME')} ${m8Text.value(instrument.name)}${m8Text.empty('-'.repeat(12 - instrument.name.length))} ${m8Text.default('TRANSP.')} ${m8Text.value(toM8Bool(instrument.transpose))} ${m8Text.default('TABLE TIC')} ${m8Text.value(toM8HexStr(instrument.tableTick))} ${instrumentData}`) } /** * Prints the M8 file version. * * @param {String} m8FilePath - The M8 file path * @param {Array<String>|String} type - THe allowed M8 file type * @param {String} themePath - The M8 Theme path */ const printM8FileVersion = (m8FilePath, type, themePath) => { const m8File = loadM8FileAndVerify(m8FilePath, type) // Load the theme loadTheme(themePath) console.log(`${m8Text.title('M8 VERSION')} ${m8Text.value(m8File.m8FileVersion)} `) } /** * Prints a Phrase. * * @param {module:m8-js/lib/types.Song} song - The M8 Song * @param {Number} phraseNum - The Phrase number * @param {Number} [trackNum] - The track number * @param {Number} [songStepNum] - The SongStep number * @param {Number} [chainStepNum] - The ChainStep number */ const printPhrase = (song, phraseNum, trackNum, songStepNum, chainStepNum) => { let needsPhraseAt = false let phraseData = '' const phrase = song.phrases[phraseNum] let lastInstr for (let i = 0; i < 16; i++) { const step = phrase.steps[i] const noteStr = step.noteToStr(i) const instrNum = phrase.findPhraseStepInstrumentNum(i) let instrument // Try to find the last instrument for the phrase in isolation if (instrNum === 0xFF) { // If it can't be found and we already know the previous instrument, use it if (typeof lastInstr !== 'undefined') { instrument = lastInstr } } else { // If we found a new instrument, use it instrument = song.instruments[instrNum] } // If we still have no instrument and we're using 'phrase-at', attempt to find it if (typeof instrument === 'undefined' && typeof trackNum !== 'undefined') { instrument = song.findPhraseStepInstrument(trackNum, songStepNum, chainStepNum, i) } if (i % 4 === 0) { phraseData += m8Text.default(toM8HexStr(i, 0)) } else { phraseData += m8Text.info(toM8HexStr(i, 0)) } ;[step.note, step.volume, step.instrument].forEach((val, j) => { if (val === 0xFF) { phraseData += ` ${m8Text.empty(j === 0 ? noteStr : '--')}` } else { phraseData += ` ${m8Text.value(j === 0 ? noteStr : toM8HexStr(val))}` } }) step.fx.forEach((fx) => { const fxVal = toM8HexStr(fx.value) let fxCmd = fx.commandToStr(instrument) if (fxCmd.endsWith('?')) { if (typeof trackNum === 'undefined') { needsPhraseAt = true } fxCmd = fxCmd.substring(0, fxCmd.length - 1) + (typeof trackNum === 'undefined' ? '*' : '?') } if (fxCmd === '---') { phraseData += ` ${m8Text.empty(fxCmd)}${m8Text.info(fxVal)}` } else { phraseData += ` ${m8Text.value(fxCmd)}${m8Text.value(fxVal)}` } }) if (i < 15) { phraseData += '\n' } } if (needsPhraseAt) { phraseData += '\n\n' phraseData += '* The command affects an instrument outside of this phrase and cannot be\n' phraseData += " identified. Please use the 'phrase-at' command for its full representation." } console.log(`${m8Text.title('PHRASE ' + toM8HexStr(phraseNum) + (song.isPhraseUsageUnique(phraseNum) ? '*' : ''))} ${m8Text.default(' N V I FX1 FX2 FX3')} ${phraseData} `) } /** * Prints a Scale. * * @param {module:m8-js/lib/types.Scale} scale - The Scale to print * @param {Number} [key=0x00] - The Song key * @param {Number} [index] - The Scale index */ const printScale = (scale, key = 0x00, index) => { let scaleData = '' for (let i = 0; i < 12; i++) { const interval = scale.intervals[i] let noteIndex = key + i if (noteIndex > 11) { noteIndex = noteIndex - 12 } const note = getNote(noteIndex) if (note.indexOf('#') === -1) { scaleData += `${m8Text.default(note)} ` } else { scaleData += `${m8Text.info(note)} ` } if (interval.enabled) { const offsetStr = interval.offsetToStr() scaleData += `${m8Text.value('ON')}` if (offsetStr[0] !== '-') { scaleData += ' ' } scaleData += m8Text.value(offsetStr) } else { scaleData += `${m8Text.empty('--')} ${m8Text.empty('--')} ${m8Text.empty('--')}` } if (i < 11) { scaleData += '\n' } } console.log(`${m8Text.title('SCALE' + (typeof index === 'undefined' ? '' : ' ' + toM8HexStr(index)))} ${m8Text.default('KEY')} ${m8Text.value(getNote(key))} ${m8Text.default('I EN OFFSET')} ${scaleData} ${m8Text.default('NAME')} ${m8Text.value(scale.name)}${m8Text.empty('-'.repeat(16 - scale.name.length))} `) } /** * Prints an M8 Song. * * @param {module:m8-js/lib/types.Song} song - The M8 Song to print * @param {Number} [startingRow=0x00] - The starting row */ const printSong = (song, startingRow = 0x00) => { let songData = '' for (let i = startingRow; i < startingRow + 16; i++) { const step = song.steps[i] songData += m8Text.default(toM8HexStr(i)) for (let j = 0; j < 8; j++) { const chain = step.tracks[j] if (chain === 0xFF) { songData += ` ${m8Text.empty('--')}` } else { if (song.isChainEmpty(chain)) { songData += ` ${m8Text.default(toM8HexStr(chain))}` } else { songData += ` ${m8Text.value(toM8HexStr(chain))}` } } } if (i < startingRow + 15) { songData += '\n' } } console.log(`${m8Text.title('SONG')} ${m8Text.default(' 1 2 3 4 5 6 7 8')} ${songData} `) } /** * Prints a Table. * * @param {module:m8-js/lib/types/internal.Table} table - The Table to print * @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 Instrument the Table is associated with * @param {Number} index - The Table index */ const printTable = (table, instrument, index) => { let tableData = '' for (let i = 0; i < table.steps.length; i++) { const step = table.steps[i] if (i % 4 === 0) { tableData += m8Text.default(toM8HexStr(i, 0)) } else { tableData += m8Text.info(toM8HexStr(i, 0)) } tableData += ` ${m8Text.value(toM8HexStr(step.transpose))}` if (step.volume === 0xFF) { tableData += ` ${m8Text.empty('--')}` } else { tableData += ` ${m8Text.value(toM8HexStr(step.volume))}` } step.fx.forEach((fx) => { const fxCmd = fx.commandToStr(instrument) const fxVal = toM8HexStr(fx.value) if (fxCmd === '---') { tableData += ` ${m8Text.empty(fxCmd)}${m8Text.info(fxVal)}` } else { tableData += ` ${m8Text.value(fxCmd)}${m8Text.value(fxVal)}` } }) if (i < 15) { tableData += '\n' } } console.log(`${m8Text.title('TABLE' + (typeof index === 'undefined' ? '' : ' ' + toM8HexStr(index)))} ${m8Text.default(' N V FX1 FX2 FX3')} ${tableData} `) } /** * Prints an M8 Theme. * * @param {module:m8-js/lib/types.theme} theme - The M8 Theme to print */ const printTheme = (theme) => { console.log(`${m8Text.title('THEME SETTINGS')} ${m8Text.default('BACKGROUND')} ${m8Text.value(toM8HexStr(theme.background.r))} ${m8Text.value(toM8HexStr(theme.background.g))} ${m8Text.value(toM8HexStr(theme.background.b))} ${clc.xterm(x256(theme.background.r, theme.background.g, theme.background.b))('\u25A0\u25A0\u25A0')} ${m8Text.default('TEXT:EMPTY')} ${m8Text.value(toM8HexStr(theme.textEmpty.r))} ${m8Text.value(toM8HexStr(theme.textEmpty.g))} ${m8Text.value(toM8HexStr(theme.textEmpty.b))} ${clc.xterm(x256(theme.textEmpty.r, theme.textEmpty.g, theme.textEmpty.b))('\u25A0\u25A0\u25A0')} ${m8Text.default('TEXT:INFO')} ${m8Text.value(toM8HexStr(theme.textInfo.r))} ${m8Text.value(toM8HexStr(theme.textInfo.g))} ${m8Text.value(toM8HexStr(theme.textInfo.b))} ${clc.xterm(x256(theme.textInfo.r, theme.textInfo.g, theme.textInfo.b))('\u25A0\u25A0\u25A0')} ${m8Text.default('TEXT:DEFAULT')} ${m8Text.value(toM8HexStr(theme.textDefault.r))} ${m8Text.value(toM8HexStr(theme.textDefault.g))} ${m8Text.value(toM8HexStr(theme.textDefault.b))} ${clc.xterm(x256(theme.textDefault.r, theme.textDefault.g, theme.textDefault.b))('\u25A0\u25A0\u25A0')} ${m8Text.default('TEXT:VALUE')} ${m8Text.value(toM8HexStr(theme.textValue.r))} ${m8Text.value(toM8HexStr(theme.textValue.g))} ${m8Text.value(toM8HexStr(theme.textValue.b))} ${clc.xterm(x256(theme.textValue.r, theme.textValue.g, theme.textValue.b))('\u25A0\u25A0\u25A0')} ${m8Text.default('TEXT:TITLES')} ${m8Text.value(toM8HexStr(theme.textTitle.r))} ${m8Text.value(toM8HexStr(theme.textTitle.g))} ${m8Text.value(toM8HexStr(theme.textTitle.b))} ${clc.xterm(x256(theme.textTitle.r, theme.textTitle.g, theme.textTitle.b))('\u25A0\u25A0\u25A0')} ${m8Text.default('PLAY MARKERS')} ${m8Text.value(toM8HexStr(theme.playMarker.r))} ${m8Text.value(toM8HexStr(theme.playMarker.g))} ${m8Text.value(toM8HexStr(theme.playMarker.b))} ${clc.xterm(x256(theme.playMarker.r, theme.playMarker.g, theme.playMarker.b))('\u25A0\u25A0\u25A0')} ${m8Text.default('CURSOR')} ${m8Text.value(toM8HexStr(theme.cursor.r))} ${m8Text.value(toM8HexStr(theme.cursor.g))} ${m8Text.value(toM8HexStr(theme.cursor.b))} ${clc.xterm(x256(theme.cursor.r, theme.cursor.g, theme.cursor.b))('\u25A0\u25A0\u25A0')} ${m8Text.default('SELECTION')} ${m8Text.value(toM8HexStr(theme.selection.r))} ${m8Text.value(toM8HexStr(theme.selection.g))} ${m8Text.value(toM8HexStr(theme.selection.b))} ${clc.xterm(x256(theme.selection.r, theme.selection.g, theme.selection.b))('\u25A0\u25A0\u25A0')} ${m8Text.default('SCOPE/SLIDER')} ${m8Text.value(toM8HexStr(theme.scopeSlider.r))} ${m8Text.value(toM8HexStr(theme.scopeSlider.g))} ${m8Text.value(toM8HexStr(theme.scopeSlider.b))} ${clc.xterm(x256(theme.scopeSlider.r, theme.scopeSlider.g, theme.scopeSlider.b))('\u25A0\u25A0\u25A0')} ${m8Text.default('METER LOW')} ${m8Text.value(toM8HexStr(theme.meterLow.r))} ${m8Text.value(toM8HexStr(theme.meterLow.g))} ${m8Text.value(toM8HexStr(theme.meterLow.b))} ${clc.xterm(x256(theme.meterLow.r, theme.meterLow.g, theme.meterLow.b))('\u25A0\u25A0\u25A0')} ${m8Text.default('METER MID')} ${m8Text.value(toM8HexStr(theme.meterMid.r))} ${m8Text.value(toM8HexStr(theme.meterMid.g))} ${m8Text.value(toM8HexStr(theme.meterMid.b))} ${clc.xterm(x256(theme.meterMid.r, theme.meterMid.g, theme.meterMid.b))('\u25A0\u25A0\u25A0')} ${m8Text.default('METER PEAK')} ${m8Text.value(toM8HexStr(theme.meterPeak.r))} ${m8Text.value(toM8HexStr(theme.meterPeak.g))} ${m8Text.value(toM8HexStr(theme.meterPeak.b))} ${clc.xterm(x256(theme.meterPeak.r, theme.meterPeak.g, theme.meterPeak.b))('\u25A0\u25A0\u25A0')} `) } /** * Validates the Instrument option. * * @param {module:m8-js/lib/types.Song|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} instrOrSong - The Song or Instrument * @param {Number} instrIndex - The Instrument index */ const validateInstrumentOption = (instrOrSong, instrIndex) => { if (instrOrSong.constructor.name === 'Song') { if (typeof instrIndex === 'undefined') { throw new commander.InvalidOptionArgumentError("error: required option (for Song files) '-i, --instrument " + "<number>' not specified") } validateOptionLimits(instrIndex, '-i, --instrument <number>', 0, 127) } else { if (typeof instrIndex !== 'undefined') { throw new commander.InvalidOptionArgumentError("error: option '--instrument' cannot be used with Instrument " + 'files') } } } /** * Validate option limits. * * @param {Number} val - The value being validated * @param {String} option - The option name * @param {Number} min - The minimum value * @param {Number} max - The maximum value */ const validateOptionLimits = (val, option, min, max) => { if (typeof val === 'undefined' || val < 0 || val > max) { throw new commander.InvalidArgumentError(`option '${option}' must be between ${toM8HexStr(min)} and ` + `${toM8HexStr(max)}`) } } /** * Writes a file to disk. * * @param {String} m8FilePath - The path to the M8 file to write * @param {Array<Number>|String} data - The data to write */ const writeFileToDisk = (m8FilePath, data) => { // Do not overwrite file (We could revisit this at a later date but for safety, let's not for now.) if (existsSync(m8FilePath)) { throw new commander.CommanderError(1, 'm8.export.FileExists', `Cannot write to file at ${m8FilePath}: File exists`) } writeFileSync(m8FilePath, data) } module.exports = { getStartingRow, loadM8FileAndVerify, loadTheme, m8Text, parseCLIHexInt, parseCLIInt, printInstrument, printM8FileVersion, printPhrase, printScale, printSong, printTable, printTheme, validateInstrumentOption, validateOptionLimits, writeFileToDisk }