UNPKG

@camoto/gamemusic

Version:

Read and write music files used by DOS games

458 lines (420 loc) 15.6 kB
/* * OPL utility functions. * * Copyright (C) 2010-2021 Adam Nielsen <malvineous@shikadi.net> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import Debug from '../debug.js'; const debug = Debug.extend('util:opl:index'); import * as Events from '../../interface/events/index.js'; import * as Patch from '../../interface/patch/index.js'; import Music from '../../interface/music/index.js'; import Rhythm from './rhythm.js'; export { default as generateOPL } from './generate.js'; export { default as parseOPL } from './parse.js'; /** * Utility functions for OPL operations. */ export default class UtilOPL { /** * Is the given value a valid OPL register? * * @param {Number} reg * OPL register to check. * * @return {Boolean} true if register is valid, false if the number is not a * valid OPL register. */ static validRegister(reg) { return !( (reg == 0x06) || (reg == 0x07) || ((reg >= 0x09) && (reg <= 0x1F)) || ((reg >= 0x36) && (reg <= 0x3F)) || ((reg >= 0x56) && (reg <= 0x5F)) || ((reg >= 0x76) && (reg <= 0x7F)) || ((reg >= 0x96) && (reg <= 0x9F)) || ((reg >= 0xA9) && (reg <= 0xAF)) || ((reg >= 0xB9) && (reg <= 0xBC)) || ((reg >= 0xBE) && (reg <= 0xBF)) || ((reg >= 0xC9) && (reg <= 0xDF)) || (reg >= 0xF6) ); } /** * Convert a logarithmic volume into a gamemusicjs linear velocity. * * @param {Number} vol * Logarithmic volume value, with 0 being silent, and `max` being loudest. * * @param {Number} max * Maximum value of `vol`. * * @return Linear value between 0.0 and 1.0 inclusive, with 0 being silent * and 1 being loudest. */ static log_volume_to_lin_velocity(vol, max) { return (1 - Math.log(max + 1 - vol) / Math.log(max + 1)); } /** * Convert a gamemusicjs linear velocity into a logarithmic volume value. * * @param {Number} vel * Linear velocity, with 0.0 being silent, and 1.0 being loudest. * * @param {Number} max * Maximum value of the return value, when `vel` is 1. * * @return {Number} Logarithmic value between 0 and max inclusive. */ static lin_velocity_to_log_volume(vel, max) { return Math.round((max + 1) - Math.pow(max + 1, 1 - vel)); } static fnumToFrequency(fnum, block, conversionFactor = 49716) { if ((block < 0) || (block >= 8)) { throw new Error(`Cannot convert OPL fnum to frequency: block ${block} is out of range (0..7).`); } if ((fnum < 0) || (fnum >= 1024)) { throw new Error(`Cannot convert OPL fnum to frequency: fnum ${fnum} is out of range (0..1023).`); } return (conversionFactor * fnum) * Math.pow(2, block - 20); } static frequencyToFnum(freq, curBlock, conversionFactor = 49716) { const debug = Debug.extend('frequencyToFnum'); // Special case to avoid divide by zero if (freq === 0) { return { block: curBlock, // any block will work fnum: 0, clip: false, }; } // Special case for frequencies too high to produce if (freq > 6208.431) { return { block: 7, fnum: 1023, clip: true, }; } // Original formula const getFnum = (f, b) => Math.round(f * Math.pow(2, 20 - b) / conversionFactor); // Slightly more efficient version //const getFnum = (f, b) => ((f << (20 - b)) / conversionFactor + 0.5) >>> 0; if (curBlock <= 7) { // We've already got a block, see if we can use that const fnum = getFnum(freq, curBlock); if ((fnum > 100) && (fnum < 900)) { // Fits in the middle of the existing block pretty well, so let's keep it return { block: curBlock, fnum: fnum >>> 0, clip: false, }; } } /// This formula will provide a pretty good estimate as to the best block to /// use for a given frequency. It tries to use the lowest possible block /// number that is capable of representing the given frequency. This is /// because as the block number increases, the precision decreases (i.e. there /// are larger steps between adjacent note frequencies.) The 6M constant is /// the largest frequency (in milliHertz) that can be represented by the /// block/fnum system. //int invertedBlock = log2(6208431 / milliHertz); // Very low frequencies will produce very high inverted block numbers, but // as they can all be covered by inverted block 7 (block 0) we can just clip // the value. //if (invertedBlock > 7) invertedBlock = 7; //*block = 7 - invertedBlock; let block; // This is a bit more efficient and doesn't need log2() from math.h /* if (freq > 3104.215) *block = 7; else if (freq > 1552.107) *block = 6; else if (freq > 776.053) *block = 5; else if (freq > 388.026) *block = 4; else if (freq > 194.013) *block = 3; else if (freq > 97.006) *block = 2; else if (freq > 48.503) *block = 1; else *block = 0; */ // Do it based on octave instead if (freq > 2048) block = 7; else if (freq > 1024) block = 6; else if (freq > 512) block = 5; else if (freq > 256) block = 4; else if (freq > 128) block = 3; else if (freq > 64) block = 2; else if (freq > 32) block = 1; else block = 0; let fnum = getFnum(freq, block); let clip = false; if ((block === 7) && (fnum > 1023)) { debug(`Clipped ${freq} Hz due to block=7, fnum=${fnum}`); clip = true; fnum = 1023; } return { block: block, fnum: fnum >>> 0, clip: clip, }; } /** * Supplied with a channel, return the offset from a base OPL register for the * first operator/slot (modulator). * * For example, channel 4's modulator is operator 7 which is at offset 0x09. * Since 0x60 is the attack/decay function, register 0x69 will thus set the * attack/decay for channel 4's modulator (slot 0). * * @param {Number} channel * OPL channel. Channels go from 0 to 8 inclusive for OPL2/3, and 0 to 17 * inclusive for OPL3. * * @param {Number} slot * Which slot (operator) to use. 0 is the modulator, 1 is the carrier, and * 2 and 3 are used when the channel is in four-operator mode. * * @return {Number} Offset into OPL register map for the given channel's * slot/operator. The offset has 0x100 added to it for offsets in the * second chip's register map (or OPL3 upper registers), which will only * be seen for channels >= 9. */ static oplOperatorOffset(channel, slot) { const chipOffset = 0x100 * (channel / 9 >>> 0); const chipChannel = channel % 9; return ( chipOffset + (chipChannel / 3 >>> 0) * 8 + (chipChannel % 3) + slot * 3 + (slot / 2 >>> 0) * 2 ); } /** * Extract the OPL settings into an object. * * @param {Array<Number>} regs * OPL register array, 512 elements (256 registers by two chips). * * @param {Number} channel * OPL channel to extract, 0..8 for OPL2/3 channels 1..9, 9..17 for OPL3 * channels 10..18. * * @param {Array<Number>} slots * Array of 1 to 4 slots to read, with the value controlling which slot * is read into which. `[0, 1, 2, 3]` will read a 4-op OPL3 instrument, * [-1, 0, -1, -1] will read the second operator into the first * instrument's slot. Swapping slots like this is only useful for rhythm * mode percussion instruments. * * @return {Object} Current channel settings. */ static getChannelSettings(regs, channel, slots) { const chipOffset = 0x100 * (channel / 9 >>> 0); const chipChannel = channel % 9; function getOp(chipOperOffset) { return { enableTremolo: (regs[UtilOPL.BASE_CHAR_MULT + chipOperOffset] >> 7) & 1, enableVibrato: (regs[UtilOPL.BASE_CHAR_MULT + chipOperOffset] >> 6) & 1, enableSustain: (regs[UtilOPL.BASE_CHAR_MULT + chipOperOffset] >> 5) & 1, enableKSR: (regs[UtilOPL.BASE_CHAR_MULT + chipOperOffset] >> 4) & 1, freqMult: regs[UtilOPL.BASE_CHAR_MULT + chipOperOffset] & 0x0F, scaleLevel: regs[UtilOPL.BASE_SCAL_LEVL + chipOperOffset] >> 6, outputLevel: regs[UtilOPL.BASE_SCAL_LEVL + chipOperOffset] & 0x3F, attackRate: regs[UtilOPL.BASE_ATCK_DCAY + chipOperOffset] >> 4, decayRate: regs[UtilOPL.BASE_ATCK_DCAY + chipOperOffset] & 0x0F, sustainRate: regs[UtilOPL.BASE_SUST_RLSE + chipOperOffset] >> 4, releaseRate: regs[UtilOPL.BASE_SUST_RLSE + chipOperOffset] & 0x0F, waveSelect: regs[UtilOPL.BASE_WAVE + chipOperOffset] & 0x07, }; } const regOffset = chipOffset + chipChannel; let patch = new Patch.OPL({ slot: [], feedback: (regs[UtilOPL.BASE_FEED_CONN + regOffset] >> 1) & 0x07, connection: regs[UtilOPL.BASE_FEED_CONN + regOffset] & 1, }); // If slot[1] == 2 then get slot 1 and store it in patch.slot[2]. for (let s = 0; s < 4; s++) { if ((slots[s] < 0) || (slots[s] === undefined)) continue; const operatorOffset = this.oplOperatorOffset(channel, s); patch.slot[slots[s]] = getOp(operatorOffset); } return { fnum: ((regs[UtilOPL.BASE_KEYON_FREQ + regOffset] & 0x3) << 8) | regs[UtilOPL.BASE_FNUM_L + regOffset], block: ((regs[UtilOPL.BASE_KEYON_FREQ + regOffset] >> 2) & 0x7), noteOn: !!(regs[UtilOPL.BASE_KEYON_FREQ + regOffset] & 0x20), panLeft: !!(regs[UtilOPL.BASE_FEED_CONN + regOffset] & 0x10), panRight: !!(regs[UtilOPL.BASE_FEED_CONN + regOffset] & 0x20), patch: patch, }; } // slots = [x, y] means load patch.slot[0] into slot[x] and patch.slot[1] into // slot[y]. This allows single-slot patches to be loaded into slot 0 or 1 for // rhythm mode. static setPatch(oplState, channel, slots, patch) { function setOp(chipOperOffset, op) { if (op.freqMult & ~0x0F) { throw new Error(`Slot freqMult is out of range (${op.freqMult} > 15).`); } if (op.scaleLevel & ~0x03) { throw new Error(`Slot scaleLevel is out of range (${op.scaleLevel} > 3).`); } if (op.outputLevel & ~0x3F) { throw new Error(`Slot outputLevel is out of range (${op.outputLevel} > 63).`); } if (op.attackRate & ~0x0F) { throw new Error(`Slot attackRate is out of range (${op.attackRate} > 15).`); } if (op.decayRate & ~0x0F) { throw new Error(`Slot decayRate is out of range (${op.decayRate} > 15).`); } if (op.sustainRate & ~0x0F) { throw new Error(`Slot sustainRate is out of range (${op.sustainRate} > 15).`); } if (op.releaseRate & ~0x0F) { throw new Error(`Slot releaseRate is out of range (${op.releaseRate} > 15).`); } if (op.waveSelect & ~0x07) { throw new Error(`waveSelect is out of range (${op.waveSelect} > 7).`); } oplState[UtilOPL.BASE_CHAR_MULT + chipOperOffset] = (op.enableTremolo ? 0x80 : 0) | (op.enableVibrato ? 0x40 : 0) | (op.enableSustain ? 0x20 : 0) | (op.enableKSR ? 0x10 : 0) | (op.freqMult & 0x0F) ; oplState[UtilOPL.BASE_SCAL_LEVL + chipOperOffset] = (op.scaleLevel & 0x03) << 6 | (op.outputLevel & 0x3F) ; oplState[UtilOPL.BASE_ATCK_DCAY + chipOperOffset] = (op.attackRate & 0x0F) << 4 | (op.decayRate & 0x0F) ; oplState[UtilOPL.BASE_SUST_RLSE + chipOperOffset] = (op.sustainRate & 0x0F) << 4 | (op.releaseRate & 0x0F) ; oplState[UtilOPL.BASE_WAVE + chipOperOffset] = op.waveSelect & 0x07; } for (let s = 0; s < 4; s++) { if (slots[s] >= 0) { if (!patch.slot[slots[s]]) { const hasSlots = Object.keys(patch.slot).join(', '); throw new Error(`Tried to assign patch ("${patch.title}") to ` + `slot/operator ${s} but this patch only has settings for ` + `operators [${hasSlots}].`); } const operatorOffset = this.oplOperatorOffset(channel, s); setOp(operatorOffset, patch.slot[slots[s]]); } } const chipOffset = 0x100 * (channel / 9 >>> 0); const chipChannel = channel % 9; const regOffset = chipOffset + chipChannel; oplState[UtilOPL.BASE_FEED_CONN + regOffset] = ((patch.feedback & 0x07) << 1) | (patch.connection & 0x01) ; } /** * Find the given patch in a list of instruments, appending it if it's new. * * Postconditions: `patches` may have the new `target` patch appended to it. * * @param {Array<Patch>} patches * List of known patches. If the target patch is found in this list the * index into this array is returned, otherwise `target` is appended onto * `patches` and the index of this newly added item is returned. * * @param {Patch} target * The patch being searched for. * * @return {Number} Index into `patches` where the instrument can be found. */ static findAddPatch(patches, target) { const debug = Debug.extend('findAddPatch'); const idx = patches.findIndex(p => target.equalTo(p)); if (idx >= 0) return idx; // Patch not found, add it. debug(`Found new patch: ${target}`); return patches.push(target) - 1; } /** * Callback function for `UtilMusic.splitEvents()` for standard OPL split. * * If OPL data was parsed with `UtilOPL.parseOPL()` then when it is split into * tracks with `UtilMusic.splitEvents()`, this function can be passed as the * callback parameter. * * It will split events up into one OPL channel per track, taking into account * OPL rhythm mode and four-operator channels. */ static standardTrackSplitConfig(ev) { if ( (ev.custom.oplChannelType === undefined) || (ev.custom.oplChannelIndex === undefined) ) { let tc = new Music.TrackConfiguration({ channelType: Music.TrackConfiguration.ChannelType.OPLT, // should be 'any OPL' really channelIndex: 0, }); tc.trackIndex = 0; return tc; } let tc = new Music.TrackConfiguration({ channelType: ev.custom.oplChannelType, channelIndex: ev.custom.oplChannelIndex, }); // Add 1 to leave trackIndex 0 clear for global events. tc.trackIndex = ev.custom.oplChannelIndex + 1; if (ev.custom.oplChannelType === Music.TrackConfiguration.ChannelType.OPLR) { // Bump the rhythm instruments to tracks following the usual 18 melodic // channels. Empty tracks will be removed later. tc.trackIndex += 18; } return tc; } } UtilOPL.Rhythm = Rhythm; UtilOPL.BASE_CHAR_MULT = 0x20; UtilOPL.BASE_SCAL_LEVL = 0x40; UtilOPL.BASE_ATCK_DCAY = 0x60; UtilOPL.BASE_SUST_RLSE = 0x80; UtilOPL.BASE_FNUM_L = 0xA0; UtilOPL.BASE_KEYON_FREQ = 0xB0; UtilOPL.BASE_RHYTHM = 0xBD; UtilOPL.BASE_WAVE = 0xE0; UtilOPL.BASE_FEED_CONN = 0xC0; // Values for MusicHandler.metadata().caps.supportedEvents for songs that use // the standard OPL parse()/generate() functions. UtilOPL.oplSupportedEvents = [ new Events.Configuration({option: Events.Configuration.Option.EmptyEvent}), new Events.Configuration({option: Events.Configuration.Option.EnableOPL3}), new Events.Configuration({option: Events.Configuration.Option.EnableDeepTremolo}), new Events.Configuration({option: Events.Configuration.Option.EnableDeepVibrato}), new Events.Configuration({option: Events.Configuration.Option.EnableRhythm}), new Events.Configuration({option: Events.Configuration.Option.EnableWaveSel}), new Events.Delay(), new Events.Effect({pitchbend: 1, volume: 1}), // both supported new Events.NoteOff(), new Events.NoteOn({frequency: 1, velocity: 1, instrument: 1}), // velocity supported ];