UNPKG

@camoto/gamemusic

Version:

Read and write music files used by DOS games

361 lines (320 loc) 12.1 kB
/* * Parse OPL register/value pairs and convert to Event instances. * * 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 g_debug = Debug.extend('util:opl:parse'); import * as Events from '../../interface/events/index.js'; import { ChannelType } from '../../interface/music/track-configuration.js'; import UtilOPL from './index.js'; /** * Examine the changed OPL data and produce events describing it. * * Postconditions: `oplPrevState` is updated with the values that have been * actioned. Some values won't be copied across, such as instrument * settings if they are applied to a channel that is not being played. * When the note is eventually played on the channel, that's when the * instrument settings will be examined, actioned, and copied into * `oplPrevState`. * * @private */ function appendOPLEvents(patches, events, oplState, oplStatePrev, hasKeyOn) { const debug = g_debug.extend('appendOPLEvents'); let oplDiff = []; for (let i = 0; i < oplState.length; i++) { // By XOR'ing the values we'll end up with only the bits within each // byte that have changed. If the value is 0, it means the register // hasn't been changed. oplDiff[i] = (oplStatePrev[i] || 0) ^ (oplState[i] || 0); } if (oplDiff[0x01]) { if (oplDiff[0x01] & 0x20) { // Wavesel mode changed events.push(new Events.Configuration({ option: Events.Configuration.Option.EnableWaveSel, value: !!(oplState[0x01] & 0x20), })); } // Mark register as processed. oplStatePrev[0x01] = oplState[0x01]; } if (oplDiff[0x105]) { if (oplDiff[0x105] & 0x01) { // OPL3 mode changed events.push(new Events.Configuration({ option: Events.Configuration.Option.EnableOPL3, value: !!(oplState[0x105] & 0x01), })); } // Mark register as processed. oplStatePrev[0x105] = oplState[0x105]; } if (oplDiff[0xBD]) { // We are assuming these values are set globally for the whole chip, even on // an OPL3 where the register only exists for the lower registers. // @todo Confirm this applies to all 18 channels and not just the lower 9. if (oplDiff[0xBD] & 0x80) { // tremolo mode changed events.push(new Events.Configuration({ option: Events.Configuration.Option.EnableDeepTremolo, value: !!(oplState[0xBD] & 0x80), })); } if (oplDiff[0xBD] & 0x40) { // vibrato mode changed events.push(new Events.Configuration({ option: Events.Configuration.Option.EnableDeepVibrato, value: !!(oplState[0xBD] & 0x40), })); } if (oplDiff[0xBD] & 0x20) { // rhythm mode changed events.push(new Events.Configuration({ option: Events.Configuration.Option.EnableRhythm, value: !!(oplState[0xBD] & 0x20), })); } // Mark just these bits as processed, so the other bits can be handled // later in the percussive note-on handler. const mask = 0x80 + 0x40 + 0x20; oplStatePrev[0xBD] &= mask; oplStatePrev[0xBD] |= oplState[0xBD] & mask; } function calcCustom(channel, slots, rhythm) { if (rhythm > 0) { return { oplChannelType: ChannelType.OPLR, oplChannelIndex: rhythm, }; } if (slots[3] >= 0) { // 4op return { oplChannelType: ChannelType.OPLF, oplChannelIndex: channel, }; } return { oplChannelType: ChannelType.OPLT, oplChannelIndex: channel, }; } // Handle new note on events (not notes currently playing) const checkForNote = (channel, slots, rhythm) => { // If the channel is >= 8, set chipOffset to 0x100, otherwise use 0. const chipOffset = 0x100 * (channel / 9 >>> 0); const chipChannel = channel % 9; const rhythmBit = 1 << (rhythm - 1); // If this is a rhythm instrument, use its keyon bit, otherwise use the // normal channel keyon bit. const keyOnChange = !!(rhythm ? oplDiff[0xBD] & rhythmBit // 0xBD is in lower register set only : oplDiff[chipChannel + 0xB0 + chipOffset] & 0x20); const keyOn = !!(rhythm ? oplState[0xBD] & rhythmBit : oplState[chipOffset + 0xB0 + chipChannel] & 0x20); // True if a keyoff was followed by a keyon without any delay in between. const thisHasKeyOn = (rhythm === 0) ? hasKeyOn.melodic[channel] : hasKeyOn.rhythm[rhythm]; const keyOnImmediate = !!(keyOn && thisHasKeyOn && !keyOnChange); // Ignore this channel if the note hasn't changed (was already playing or // already off). if (!keyOnChange && !keyOnImmediate) return; // Mark register as processed. const setPrevState = () => { if (!rhythm) { for (const i of [0xB0]) { const offset = chipOffset + i + chipChannel; oplStatePrev[offset] = oplState[offset]; } } else { // Mark rhythm-mode keyon bit as processed oplStatePrev[0xBD] &= ~rhythmBit; oplStatePrev[0xBD] |= oplState[0xBD] & rhythmBit; } }; if (!keyOn || keyOnImmediate) { // Note was just switched off let ev = new Events.NoteOff(); ev.custom = calcCustom(channel, slots, rhythm); events.push(ev); if (!keyOn) { // Note is off but wasn't switched back on again setPrevState(); // mark registers as processed return; } } // Compare active patch to known ones, add if not. const channelSettings = UtilOPL.getChannelSettings(oplState, channel, slots); // Mark frequency registers as processed. oplStatePrev[chipOffset + 0xB0 + chipChannel] &= ~0x1F; oplStatePrev[chipOffset + 0xB0 + chipChannel] |= oplState[chipOffset + 0xB0 + chipChannel] & 0x1F; let patch = channelSettings.patch; const idxInstrument = UtilOPL.findAddPatch(patches, patch); const freq = UtilOPL.fnumToFrequency(channelSettings.fnum, channelSettings.block, 49716); const outputSlot = channelSettings.patch.slot[1] || channelSettings.patch.slot[0]; const outputLevel = outputSlot.outputLevel || 0; let ev = new Events.NoteOn({ frequency: freq, velocity: UtilOPL.log_volume_to_lin_velocity(63 - outputLevel, 63), instrument: idxInstrument, }); ev.custom = calcCustom(channel, slots, rhythm); events.push(ev); setPrevState(); // mark registers as processed }; const rhythmOn = !!(oplState[0xBD] & 0x20); // Check if any channels are in four-operator mode. let op4 = []; op4[0] = !!(oplState[0x104] & 0x01); op4[1] = !!(oplState[0x104] & 0x02); op4[2] = !!(oplState[0x104] & 0x04); op4[9] = !!(oplState[0x104] & 0x08); op4[10] = !!(oplState[0x104] & 0x10); op4[11] = !!(oplState[0x104] & 0x20); for (let c = 0; c < 18; c++) { if (op4[c]) { checkForNote(c, [ 0, 1, 2, 3], UtilOPL.Rhythm.NO); // 4op } else if ((c === 6) && rhythmOn) { checkForNote(c, [ 0, 1, -1, -1], UtilOPL.Rhythm.BD); // BD: ch6 slot0+1 = op12+15 } else if ((c === 7) && rhythmOn) { checkForNote(c, [ 0, -1, -1, -1], UtilOPL.Rhythm.HH); // HH: ch7 slot0 = op13 checkForNote(c, [-1, 0, -1, -1], UtilOPL.Rhythm.SD); // SD: ch7 slot1 = op16 } else if ((c === 8) && rhythmOn) { checkForNote(c, [ 0, -1, -1, -1], UtilOPL.Rhythm.TT); // TT: ch8 slot0 = op14 checkForNote(c, [-1, 0, -1, -1], UtilOPL.Rhythm.CY); // CY: ch8 slot1 = op17 } else { checkForNote(c, [ 0, 1, -1, -1], UtilOPL.Rhythm.NO); // 2op } } } /** * Convert an array of OPL register/value pairs into events. * * This works by storing all the register writes until we reach an audible * state (i.e. a delay event with notes playing) whereupon the current OPL * state is converted into Event instances, depending on what has changed * since the previous Events. * * @param {Array} oplData * Array of objects, each of which is one of: * - { delay: 123 } // number of ticks to wait * - { reg: 123, val: 123 } * - { tempo: TempoEvent} // TempoEvent instance * Do not specify the delay in the same object as the reg/val items, as this * is ambiguous and doesn't indicate whether the delay should happen before * or after the reg/val pair. * * @return {Object} `{events: [], patches: []}` where Events is a list of * `Event` instances and `patches` is a list of instruments as `Patch` * instances. * * @alias UtilOPL.parseOPL */ export default function parseOPL(oplData) { const debug = g_debug.extend('parseOPL'); let events = [], patches = []; // * 2 for two chips (OPL3) let oplState = new Array(256 * 2).fill(0); let oplStatePrev = new Array(256 * 2).fill(0); let hasKeyOn = { melodic: [], // 0..17 rhythm: [], // index is UtilOPL.Rhythm.* }; for (const evOPL of oplData) { // If there's a register value then there's no delay, so just accumulate // all the register values for later. This may overwrite some earlier // events, which is fine because with no delays those events wouldn't be // audible anyway. if (evOPL.reg !== undefined) { if (evOPL.delay) { throw new Error('Cannot specify both reg/val and delay in same event.'); } if (evOPL.tempo) { throw new Error('Cannot specify both reg/val and tempo in same event.'); } if ((evOPL.reg & 0xB0) === 0xB0) { // also 1B0 // This could be a keyon/off if (evOPL.reg == 0xBD) { // rhythm for (let r = 1; r < 6; r++) { const rhythmBit = 1 << (r - 1); const prevKeyOn = !!(oplState[0xBD] & rhythmBit); const keyOn = !!(evOPL.val & rhythmBit); if (!prevKeyOn && keyOn) { // We've had a keyoff followed now by a keyon, so flag it. hasKeyOn.rhythm[r] = true; } } } else { // melodic const prevKeyOn = !!(oplState[evOPL.reg] & 0x20); const keyOn = !!(evOPL.val & 0x20); if (!prevKeyOn && keyOn) { // We've had a keyoff followed now by a keyon, so flag it. const chip = evOPL.reg >> 8; const channel = (chip * 9) + evOPL.reg & 0x0F; hasKeyOn.melodic[channel] = true; } } } oplState[evOPL.reg] = evOPL.val; continue; } if (evOPL.tempo) { if (evOPL.delay) { throw new Error('Cannot specify both tempo and delay in same event.'); } if (!(evOPL.tempo instanceof Events.Tempo)) { throw new Error('Must pass TempoEvent instance when setting tempo.'); } if (events[events.length - 1].type === Events.Tempo) { // The previous event was a tempo change, replace it. events.pop(); } if (evOPL.tempo.type !== Events.Tempo) { throw new Error('`tempo` property must be a TempoEvent.'); } events.push(evOPL.tempo); continue; } if (evOPL.delay === undefined) { debug('Got empty OPL event:', evOPL); throw new Error('OPL event has no property of: register, delay, tempo.'); } if (evOPL.delay === 0) { // Skip empty delays. continue; } // If we're here, this is a delay event, so figure out what registers // have changed and write out those events, followed by the delay. appendOPLEvents(patches, events, oplState, oplStatePrev, hasKeyOn); hasKeyOn = { // reset back to empty melodic: [], rhythm: [], }; let lastEvent = events[events.length - 1] || {}; if (lastEvent.type === Events.Delay) { // Previous event was a delay, so nothing has changed since then. Add // our delay onto that to avoid multiple DelayEvents in a row. lastEvent.ticks += evOPL.delay; } else { // Previous event wasn't a delay, so add a new delay. events.push(new Events.Delay({ticks: evOPL.delay})); } } // Append any final event if there was no trailing delay. appendOPLEvents(patches, events, oplState, oplStatePrev, hasKeyOn); return { patches: patches, events: events, }; }