UNPKG

@camoto/gamemusic

Version:

Read and write music files used by DOS games

299 lines (259 loc) 9.25 kB
/* * Utility functions for working with Music 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 debug = Debug.extend('util:music'); import * as Events from '../interface/events/index.js'; import Music from '../interface/music/index.js'; /** * Utility functions for working with Music instances. */ export default class UtilMusic { /** * Split a single list of events into a single pattern of multiple tracks. * * @param {Array<Event>} events * * @param {Function} fnGetTrackConfig * Function called with each event, and returns a TrackConfiguration * instance for the track that event should appear in. * For OPL data produced by {@link UtilOPL.parseOPL UtilOPL.parseOPL()}, the function * `UtilOPL.standardTrackSplitConfig` can be passed here to save writing * your own function for OPL data. * * @return {Music.Pattern} Track list suitable for appending to * {@link Music.patterns}. */ static splitEvents(events, fnGetTrackConfig) { const debug = Debug.extend('splitEvents'); let pattern = new Music.Pattern(); let trackConfig = []; // Split all the events up into separate channels. let absTime = 0; for (const ev of events) { if (ev.type === Events.Delay) { absTime += ev.ticks; continue; } const tc = fnGetTrackConfig(ev); if (!tc) { throw new Error('fnGetTrackConfig failed to return a track index.'); } if (!pattern.tracks[tc.trackIndex]) { pattern.tracks[tc.trackIndex] = new Music.Track({ custom: { absTimeLastEvent: 0, }, }); trackConfig[tc.trackIndex] = tc; } let track = pattern.tracks[tc.trackIndex]; const eventPreDelay = absTime - track.custom.absTimeLastEvent; if (eventPreDelay) { track.events.push(new Events.Delay({ticks: eventPreDelay})); } //let cpEvent = ev.clone(); track.events.push(ev); track.custom.absTimeLastEvent = absTime; } // Tidy up. let cleanTracks = [], cleanTrackConfig = []; for (let idxTrack in pattern.tracks) { if (!pattern.tracks[idxTrack]) continue; delete pattern.tracks[idxTrack].custom.absTimeLastEvent; cleanTracks.push(pattern.tracks[idxTrack]); cleanTrackConfig.push(trackConfig[idxTrack]); } pattern.tracks = cleanTracks; trackConfig = cleanTrackConfig; debug('Split events into channels:', trackConfig.map(tc => `${Music.TrackConfiguration.ChannelType.toString(tc.channelType)}-${tc.channelIndex}` )); debug(`Split events into ${pattern.tracks.length} tracks.`); return { trackConfig: trackConfig, pattern: pattern, }; } static mergeTracks(events, tracks) { const absEvents = []; for (let idxTrack = 0; idxTrack < tracks.length; idxTrack++) { const track = tracks[idxTrack]; let tTrack = 0; for (const ev of track.events) { ev.custom.absTime = tTrack; ev.custom.idxTrack = idxTrack; if (ev.type === Events.Delay) { tTrack += ev.ticks; } else { absEvents.push(ev); } } } // Now convert all the absTime values into DelayEvents. let absLastTime = 0; absEvents.sort((a, b) => a.custom.absTime - b.custom.absTime); for (let ev of absEvents) { if (ev.custom.absTime > absLastTime) { events.push(new Events.Delay({ticks: ev.custom.absTime - absLastTime})); } absLastTime = ev.custom.absTime; delete ev.custom.absTime; events.push(ev); } } /** * Merge only the patterns together, returning one long multi-track pattern. */ static mergePatterns(patterns) { if (patterns.length === 0) return new Music.Pattern(); let finalPattern = patterns[0].clone(); for (let idxPattern = 1; idxPattern < patterns.length; idxPattern++) { const pattern = patterns[idxPattern]; for (let idxTrack = 0; idxTrack < pattern.tracks.length; idxTrack++) { const track = pattern.tracks[idxTrack]; const finalTrack = finalPattern.tracks[idxTrack]; for (const ev of track.events) { const evc = ev.clone(); finalTrack.push(evc); } } } return finalPattern; } static mergePatternsAndTracks(patterns) { let events = []; for (let idxPattern = 0; idxPattern < patterns.length; idxPattern++) { const pattern = patterns[idxPattern]; if ((!pattern.tracks) || (pattern.tracks.length === undefined)) { throw new Error(`Music.patterns[${idxPattern}].tracks must be an array.`); } this.mergeTracks(events, pattern.tracks); } // Now all the DelayEvents have been removed but `absTime` exists on // everything, so run through and convert `absTime` back into DelayEvents, // but this time everything will be in the same track. let finalEvents = []; let lastTick = 0; for (const ev of events) { // There's a delay before the next event, so add a DelayEvent for it. if (ev.custom.absTime > lastTick) { const delta = ev.absTime - lastTick; finalEvents.push(new Events.Delay({ ticks: delta, })); } // Add the event itself. delete ev.custom.absTime; finalEvents.push(ev); } return finalEvents; } /** * Remove all tempo events and run events at fixed timing. * * This is used for formats like IMF that run at a fixed speed. It will copy * all the events into a new array and adjust any `DelayEvent` instances such * that the song will play at the correct speed when played at a rate where * each delay tick represents `usPerTick` microseconds. * * The returned array will have no `TempoEvent` instances, and every other * event will have been cloned. * * @param {Array<Event>} Events to adjust. These are copied and this * parameter is not modified upon return. * * @param {TempoEvent} initialTempo * Song's default tempo unless overridden by additional TempoEvents later. * * @param {TempoEvent} targetTempo * All the DelayEvents will be adjusted so that the song plays at the * correct speed, when played at `targetTempo`. */ static fixedTempo(events, initialTempo, targetTempo) { const debug = Debug.extend('fixedTempo'); debug(`initialTempo=${Math.round(initialTempo.usPerTick)}us targetTempo=${Math.round(targetTempo.usPerTick)}us`); if (!initialTempo || (initialTempo.type !== Events.Tempo)) { throw new Error('fixedTempo(): Bad initialTempo parameter'); } if (!targetTempo || (targetTempo.type !== Events.Tempo)) { throw new Error('fixedTempo(): Bad targetTempo parameter'); } let output = []; const newFactor = newTempo => newTempo.usPerTick / targetTempo.usPerTick; let factor = newFactor(initialTempo); for (const evSrc of events) { if (evSrc.type === Events.Tempo) { factor = newFactor(evSrc); // Don't copy tempo events across. continue; } let evDst = evSrc.clone(); if (evDst.type === Events.Delay) { evDst.ticks *= factor; } output.push(evDst); } return output; } /** * Iterate through all the initial events in a song. * * This finds the first pattern that will be played, then goes through each * track looking for any events that happen before any delay. These are the * events that trigger immediately upon playback with no delay at all. Each * event is passed to the callback until it returns true or the available * events are exhausted. * * @param {Music} music * The song to examine. * * @param {function} cb * The callback of type `function cb(ev) {}` where `ev` is the Event * instance found to occur at the very beginning of the song. This function * should return `false` to continue with the next event, `true` to finish * and not process any more events, or `null` to delete the current event * and then finish. * * @return `true` if the callback returned `true` or `null`, `false` if the * events were all processed but the callback never returned `true` or * `null` for any of them. */ static initialEvents(music, cb) { // TODO: Take into account pattern order const firstPattern = music.patterns[0]; for (const track of firstPattern.tracks) { for (let i = 0; i < track.events.length; i++) { const ev = track.events[i]; // As soon as we hit a delay, any following event will no longer be an // initial one, so we can skip to the next track. if ((ev.type === Events.Delay) && (ev.ticks > 0)) break; // Call the callback, and finish if it returns true. const r = cb(ev); if (r === true) return true; if (r === null) { // Remove this event from the event list. track.events.splice(i, 1); return true; } } } return false; } }