audio-source-composer
Version:
Audio Source Composer
1,120 lines (919 loc) • 42.2 kB
JavaScript
import ConfigListener from "./config/ConfigListener";
import Instruction from "./instruction/Instruction";
import InstructionIterator from "./instruction/iterator/InstructionIterator";
import ProgramList from "../programs";
import TrackIterator from "./track/TrackIterator";
import TrackPlayback from "./track/TrackPlayback";
import MIDIFileSupport from "./file/MIDIFileSupport";
import JSONFileSupport from "./file/JSONFileSupport";
import ProgramLoader from "./program/ProgramLoader";
import Values from "./values/Values";
// TODO: can be handled cleaner
ProgramList.addAllPrograms();
// const DEFAULT_PROGRAM_CLASS = 'PolyphonyProgram';
class Song {
constructor(songData={}) {
this.eventListeners = [];
this.programLoader = new ProgramLoader(this);
this.volume = null;
this.lastVolumeGain = null;
this.playback = null;
this.lastPracticeProgram = null; // TODO: ugly?
this.playbackPosition = 0;
this.data = Object.assign({
title: Song.generateTitle(),
uuid: new Values().generateUUID(),
version: '0.0.1',
dateCreated: new Date().getTime(),
datePublished: null,
comment: null,
url: null,
artistURL: null,
timeDivision: 96 * 4,
beatsPerMinute: 120,
beatsPerMeasure: 4,
startTrack: 'root',
programs: [ // Also called 'programs' or 'patches'
['oscillator', {
type: 'pulse'
}],
],
tracks: {
root: [
[0, '@track*'],
// [96*12, '@track0', 96*4, 96*4, 'D4'],
// [96*12, '@track0', 96*4, 96*4*2, 'E4'],
],
track0: [
// ['!d', 'Effect'],
[0, '!p', 0],
// [0, 'C4', 96],
// [96, 'D4', 96],
// [96, 'E4', 96],
// [96, 'F4', 96],
// [96, 'G4', 96],
// [96, 'A4', 96 * 8],
],
// track1: [
// ['!p', 1],
// [64, 'A3', 64],
// // [64, 'Aq3', 64],
// // [64, 'A#3', 64],
// [64, 'A#q3', 64],
// // [64, 'B3', 64],
// // [64, 'Bq3', 64],
// [64, 'C4', 64],
// // [64, 'Cq4', 64],
// // [64, 'C#4', 64],
// [64, 'C#q4', 64],
// // [64, 'D4', 64],
// // [64, 'Dq4', 64],
// [64, 'D#4', 64],
// // [64, 'D#q4', 64],
// // [64, 'E4', 64],
// [64, 'Eq4', 64],
// // [64, 'E#4', 64],
// // [64, 'E#q4', 64],
// [64, 'F#4', 64],
// // [64, 'F#q4', 64],
// // [64, 'G4', 64],
// [64, 'Gq4', 64],
// // [64, 'G#4', 64],
// // [64, 'G#q4', 64],
// [64, 'A4', 64],
// // [64, 'Aq4', 64],
// // [64, 'A#4', 64],
// [64, 'A#q4', 64],
// // [64, 'B4', 64],
// // [64, 'Bq4', 64],
// [64, 'C5', 64],
// // [64, 'Cq4', 64],
// // [64, 'C#4', 64],
// [64, 'C#q5', 64],
// // [64, 'D4', 64],
// // [64, 'Dq4', 64],
// [64, 'D#5', 64],
// // [64, 'D#q4', 64],
// // [64, 'E4', 64],
// [64, 'Eq5', 64],
// // [64, 'E#4', 64],
// // [64, 'E#q4', 64],
// [64, 'F#5', 64],
// // [64, 'F#q4', 64],
// // [64, 'G4', 64],
// [64, 'Gq5', 64],
// // [64, 'G#4', 64],
// // [64, 'G#q4', 64],
// [64, 'A5', 64],
// ]
}
}, songData);
this.dataProxy = new Proxy(this.data, new ConfigListener(this, []));
this.history = [];
// this.values = new SongValues(this);
// this.loadSongData(songData);
// this.programLoadAll();
// this.dispatchEventCallback = e => this.dispatchEvent(e);
// this.data = songData;
// Process all instructions
if(!this.data.tracks)
throw new Error("No tracks found in song data");
Instruction.processInstructionTracks(this.data.tracks);
// let loadingPrograms = 0;
// console.log("Song data loaded: ", songData);
}
getProxiedData() { return this.dataProxy; }
getTimeDivision() { return this.data.timeDivision; }
/** @deprecated? **/
connect(destination) {
this.destination = destination;
}
/** Events and Listeners **/
dispatchEvent(e) {
for (const [eventName, listenerCallback] of this.eventListeners) {
if(e.name === eventName || eventName === '*') {
listenerCallback(e);
}
}
}
addEventListener(eventName, listenerCallback) {
this.eventListeners.push([eventName, listenerCallback]);
}
removeEventListener(eventName, listenerCallback) {
for (let i = 0; i < this.eventListeners.length; i++) {
const [eventName2, listenerCallback2] = this.eventListeners[i];
if(eventName === eventName2 && listenerCallback === listenerCallback2) {
this.eventListeners.splice(i, 1);
break;
}
}
}
getProgramDispatchEvent(programID) {
return e => {
e.programID = programID;
this.dispatchEvent(e);
return e;
}
}
unloadAll() {
ProgramLoader.unloadAllPrograms();
this.eventListeners = [];
}
/** Song Data **/
loadSongHistory(songHistory) {
this.history = songHistory;
}
/** Instruction Tracks **/
getStartTrackName() {
return typeof this.data.startTrack === "undefined"
? this.data.startTrack
: Object.keys(this.data.tracks)[0];
}
hasTrack(trackName) {
return typeof this.data.tracks[trackName] !== "undefined";
}
trackEach(callback) {
const tracks = this.data.tracks;
return Object.keys(tracks).map(function(trackName) {
return callback(trackName, tracks[trackName]);
});
}
generateInstructionTrackName(trackName = 'track') {
const tracks = this.data.tracks;
for (let i = 0; i <= 999; i++) {
const currentTrackName = trackName + i;
if (!tracks.hasOwnProperty(currentTrackName))
return currentTrackName;
}
throw new Error("Failed to generate group name");
}
/** Instructions **/
instructionIndexOf(trackName, instructionData) {
// if (instruction instanceof Instruction)
// instruction = instruction.data;
if(!this.data.tracks[trackName])
throw new Error("Invalid instruction track: " + trackName);
let instructionList = this.data.tracks[trackName];
instructionData = ConfigListener.resolveProxiedObject(instructionData);
// instructionList = ConfigListener.resolveProxiedObject(instructionList);
const p = instructionList.indexOf(instructionData);
if (p === -1)
throw new Error("Instruction not found in instruction list");
return p;
}
instructionGetList(trackName) {
if(!this.data.tracks[trackName])
throw new Error("Invalid instruction track: " + trackName);
return this.data.tracks[trackName];
}
instructionDataGetByIndex(trackName, index) {
if(!this.data.tracks[trackName])
throw new Error("Invalid instruction track: " + trackName);
let instructionList = this.instructionGetList(trackName);
if(index < 0 || index > instructionList.length)
throw new Error("Index is out or range: " + index);
if(!instructionList[index])
throw new Error("Invalid instruction index: " + index);
return instructionList[index];
}
/** Modify Instructions **/
/** TODO: fix insertion bugs **/
instructionInsertAtPosition(trackName, insertPositionInTicks, insertInstructionData) {
if (typeof insertPositionInTicks === 'string')
insertPositionInTicks = Values.instance.parseDurationAsTicks(insertPositionInTicks, this.data.timeDivision);
if (!Number.isInteger(insertPositionInTicks))
throw new Error("Invalid integer: " + typeof insertPositionInTicks);
if (!insertInstructionData)
throw new Error("Invalid insert instruction");
if(typeof insertInstructionData[1] !== "string")
throw new Error("Invalid instruction command string: " + typeof insertInstructionData[1])
insertInstructionData = Instruction.parseInstructionData(insertInstructionData);
let instructionList = this.data.tracks[trackName];
const iterator = InstructionIterator.getIteratorFromSong(this, trackName); // this.instructionGetIterator(trackName);
let instructionData = iterator.nextInstructionData();
while (instructionData) {
// if(instruction.deltaDuration > 0) {
const currentPositionInTicks = iterator.getPositionInTicks();
if (currentPositionInTicks > insertPositionInTicks) {
// Delta note appears after note to be inserted
const splitDuration = [
insertPositionInTicks - (currentPositionInTicks - instructionData[0]),
currentPositionInTicks - insertPositionInTicks
];
const modifyIndex = iterator.getIndex();
// Make following delta note smaller
this.instructionReplaceDeltaDuration(trackName, modifyIndex, splitDuration[1]);
// Insert new note before delta note.
insertInstructionData[0] = splitDuration[0]; // Make new note equal the rest of the duration
this.instructionInsertAtIndex(trackName, modifyIndex, insertInstructionData);
return modifyIndex; // this.splitPauseInstruction(trackName, i,insertPosition - groupPosition , insertInstruction);
} else if (currentPositionInTicks === insertPositionInTicks) {
// Delta note plays at the same time as new note, append after
let lastInsertIndex;
// Search for last insert position
for (lastInsertIndex = iterator.getIndex() + 1; lastInsertIndex < instructionList.length; lastInsertIndex++)
if (instructionList[lastInsertIndex][0] > 0)
break;
insertInstructionData[0] = 0; // TODO: is this correct?
this.instructionInsertAtIndex(trackName, lastInsertIndex, insertInstructionData);
return lastInsertIndex;
}
// groupPosition += instruction.deltaDuration;
// lastDeltaInstructionIndex = i;
// }
// if (!instruction)
// break;
instructionData = iterator.nextInstructionData();
}
if (iterator.getPositionInTicks() >= insertPositionInTicks)
throw new Error("Something went wrong");
// Insert a new pause at the end of the song, lasting until the new note?
let lastPauseIndex = instructionList.length;
// this.instructionInsertAtIndex(trackName, lastPauseIndex, {
// command: '!pause',
// duration: insertPosition - groupPosition
// });
// Insert new note
insertInstructionData[0] = insertPositionInTicks - iterator.getPositionInTicks();
this.instructionInsertAtIndex(trackName, lastPauseIndex, insertInstructionData);
return lastPauseIndex;
}
instructionInsertAtIndex(trackName, insertIndex, insertInstructionData) {
if (!insertInstructionData)
throw new Error("Invalid insert instruction");
insertInstructionData = Instruction.parseInstructionData(insertInstructionData);
if(!this.data.tracks[trackName])
throw new Error("Invalid instruction track: " + trackName);
const track = this.dataProxy.tracks[trackName];
track.splice(insertIndex, 0, insertInstructionData);
return insertIndex;
}
instructionDeleteAtIndex(trackName, deleteIndex) {
if(!this.data.tracks[trackName])
throw new Error("Invalid instruction track: " + trackName);
const track = this.dataProxy.tracks[trackName];
if(!track[deleteIndex])
throw new Error(`Invalid delete index (${trackName}): ${deleteIndex}`);
const deleteInstructionData = track[deleteIndex];
if (deleteInstructionData[0] > 0) {
// let instructionList = this.instructionGetList(trackName);
if (track.length > deleteIndex + 1) {
const nextInstruction = track[deleteIndex + 1];
// const nextInstruction = this.instructionDataGetByIndex(trackName, deleteIndex + 1, false);
// this.getInstruction(trackName, deleteIndex+1).deltaDuration =
// nextInstruction.deltaDuration + deleteInstruction.deltaDuration;
this.instructionReplaceDeltaDuration(trackName, deleteIndex + 1, nextInstruction[0] + deleteInstructionData[0])
}
}
track.splice(deleteIndex, 1);
// return this.spliceDataByPath(['instructions', trackName, deleteIndex], 1);
}
instructionReplaceDeltaDuration(trackName, replaceIndex, newDelta) {
this.instructionReplaceArg(trackName, replaceIndex, 0, newDelta);
}
instructionReplaceArg(trackName, replaceIndex, argIndex, newArgValue) {
// console.log('instructionReplaceArg', trackName, replaceIndex, argIndex, newArgValue);
const track = this.dataProxy.tracks[trackName];
if(!track[replaceIndex])
throw new Error(`Invalid replace index (${trackName}): ${replaceIndex}`);
track[replaceIndex][argIndex] = newArgValue;
}
instructionReplaceArgByType(trackName, replaceIndex, argType, newArgValue) {
const track = this.dataProxy.tracks[trackName];
if(!track[replaceIndex])
throw new Error(`Invalid replace index (${trackName}): ${replaceIndex}`);
const instructionData = track[replaceIndex]; // this.instructionDataGetByIndex(trackName, replaceIndex);
const processor = new Instruction(instructionData);
processor.updateArg(argType, newArgValue)
}
/** @deprecated Use custom arg renderer **/
instructionReplaceCommand(trackName, replaceIndex, newCommand) {
//: TODO: check for recursive group
const track = this.dataProxy.tracks[trackName];
if(!track[replaceIndex])
throw new Error(`Invalid replace index (${trackName}): ${replaceIndex}`);
const instructionData = track[replaceIndex]; // this.instructionDataGetByIndex(trackName, replaceIndex);
instructionData[1] = newCommand;
}
// instructionReplaceProgram(trackName, replaceIndex, programID) {
// this.instructionGetByIndex(trackName, replaceIndex).program = programID;
// }
// /** @deprecated Use custom arg renderer **/
// instructionReplaceDuration(trackName, replaceIndex, newDuration) {
// if (typeof newDuration === 'string')
// newDuration = Values.instance.parseDurationAsTicks(newDuration, this.data.timeDivision);
// const instruction = this.instructionDataGetByIndex(trackName, replaceIndex);
// instruction.durationTicks = newDuration;
// }
//
// /** @deprecated Use custom arg renderer **/
// instructionReplaceVelocity(trackName, replaceIndex, newVelocity) {
// if (!Number.isInteger(newVelocity))
// throw new Error("Velocity must be an integer: " + newVelocity);
// if (newVelocity < 0)
// throw new Error("Velocity must be a positive integer: " + newVelocity);
// const instruction = this.instructionDataGetByIndex(trackName, replaceIndex);
// instruction.velocity = newVelocity;
// console.log('instruction', instruction);
// }
/** Song Tracks **/
trackAdd(newTrackName, instructionList) {
if (this.data.tracks.hasOwnProperty(newTrackName))
throw new Error("New group already exists: " + newTrackName);
const tracks = this.dataProxy.tracks;
tracks[newTrackName] = instructionList || [];
}
trackRemove(removeTrackName) {
if (removeTrackName === 'root')
throw new Error("Cannot remove root instruction track, n00b");
if (!this.data.tracks.hasOwnProperty(removeTrackName))
throw new Error("Existing group not found: " + removeTrackName);
console.log("TODO: remove track commands");
const tracks = this.dataProxy.tracks;
delete tracks[removeTrackName];
}
trackRename(oldTrackName, newTrackName) {
if (oldTrackName === 'root')
throw new Error("Cannot rename root instruction track, n00b");
if (!this.data.tracks.hasOwnProperty(oldTrackName))
throw new Error("Existing group not found: " + oldTrackName);
if (this.data.tracks.hasOwnProperty(newTrackName))
throw new Error("New group already exists: " + newTrackName);
const removedGroupData = this.data.tracks[oldTrackName];
const tracks = this.dataProxy.tracks;
delete tracks[oldTrackName];
tracks[newTrackName] = removedGroupData;
}
// trackGetIterator(destination, onEvent=null) {
// return new TrackIterator(destination, this, this.getStartTrackName(), onEvent);
// }
/** Playback Timing **/
getSongLengthInSeconds() {
const iterator = new TrackIterator(this, this.getStartTrackName());
iterator.seekToEnd();
// console.log('getSongLengthInSeconds()', iterator.getEndPositionInSeconds())
return iterator.getEndPositionInSeconds();
}
/** @deprecated **/
getSongPositionFromTicks(songPositionInTicks) {
return this.getGroupPositionFromTicks(this.getStartTrackName(), songPositionInTicks);
}
// Refactor
/** @deprecated **/
getGroupPositionFromTicks(trackName, groupPositionInTicks) {
const iterator = InstructionIterator.getIteratorFromSong(this, trackName); // this.instructionGetIterator(trackName);
while (true) {
if (iterator.getPositionInTicks() >= groupPositionInTicks || !iterator.nextInstructionData())
break;
}
let currentPosition = iterator.getPositionInSeconds();
if (groupPositionInTicks > iterator.getPositionInTicks()) {
const elapsedTicks = groupPositionInTicks - iterator.getPositionInTicks();
currentPosition += Song.ticksToSeconds(elapsedTicks, iterator.getBeatsPerMinute(), iterator.getTimeDivision());
} else if (groupPositionInTicks < iterator.getPositionInTicks()) {
const elapsedTicks = iterator.getPositionInTicks() - groupPositionInTicks;
currentPosition -= Song.ticksToSeconds(elapsedTicks, iterator.getBeatsPerMinute(), iterator.getTimeDivision());
}
// console.info("getGroupPositionFromTicks", groupPositionInTicks, currentPosition);
return currentPosition;
}
// getSongPositionInTicks(positionInSeconds = null) {
// if (positionInSeconds === null)
// positionInSeconds = this.getSongPlaybackPosition();
// return this.getGroupPositionInTicks(this.getStartTrackName(), positionInSeconds);
// }
// getGroupPositionInTicks(trackName, positionInSeconds) {
// const iterator = InstructionIterator.getIteratorFromSong(this, trackName); // this.instructionGetIterator(trackName);
// while (true) {
// if (iterator.getPositionInSeconds() >= positionInSeconds || !iterator.nextInstructionData())
// break;
// }
//
// let currentPositionInTicks = iterator.getPositionInTicks();
// if (positionInSeconds > iterator.getPositionInSeconds()) {
// const elapsedTime = positionInSeconds - iterator.getPositionInSeconds();
// currentPositionInTicks += Song.secondsToTicks(elapsedTime, iterator.getBeatsPerMinute());
//
// } else if (positionInSeconds < iterator.getPositionInSeconds()) {
// const elapsedTime = iterator.getPositionInSeconds() - positionInSeconds;
// currentPositionInTicks -= Song.secondsToTicks(elapsedTime, iterator.getBeatsPerMinute());
// }
//
// // console.info("getSongPositionInTicks", positionInSeconds, currentPositionInTicks);
// return currentPositionInTicks;
// }
static ticksToSeconds(elapsedTicks, beatsPerMinute, timeDivision) {
return (elapsedTicks / timeDivision) * (60 / beatsPerMinute);
}
static secondsToTicks(elapsedTime, beatsPerMinute, timeDivision) {
return Math.round((elapsedTime * timeDivision) / (60 / beatsPerMinute));
}
/** Programs **/
// All programs are sent a 0 frequency play in order to pre-load samples.
hasProgram(programID) {
return !!this.data.programs[programID];
}
// /** @deprecated Use custom arg processor **/
// playProgram(destination, program, noteFrequency, noteStartTime, noteDuration=null, noteVelocity=null, onstart=null, onended=null) {
// if(onstart !== null) {
// let currentTime = destination.context.currentTime;
// setTimeout(onstart, (noteStartTime - currentTime) * 1000);
// }
// return program.playFrequency(destination, noteFrequency, noteStartTime, noteDuration, noteVelocity, onended);
// }
programGetData(programID, proxiedData=true) { return this.programLoader.getData(programID, proxiedData); }
programGetClassName(programID) { return this.programLoader.getClassName(programID); }
programGetClass(programID) { return this.programLoader.getClass(programID); }
programGetConfig(programID, proxiedData=true) { return this.programLoader.getConfig(programID, proxiedData); }
// programGetList() {
// return this.data.programs;
// }
programEach(callback) {
// const data = this.dataProxy;
return this.data.programs.map(function(entry, programID) {
const [className, config] = entry || [null, {}];
return callback(programID, className, config);
});
}
/** Asset Loading **/
async programLoadAll() {
const programList = this.data.programs;
// console.log('programList', programList);
const promises = [];
for (let programID = 0; programID < programList.length; programID++) {
if (programList[programID]) {
const instance = this.programLoadInstanceFromID(programID);
// console.log('instance', instance, programID);
if(instance.waitForAssetLoad)
promises.push(instance.waitForAssetLoad());
}
}
for(let i=0; i < promises.length; i++)
await promises[i];
this.dispatchEvent({
type: 'song:loaded',
song: this
});
}
programLoadInstanceFromID(programID) {
return this.programLoader.loadInstanceFromID(programID);
// this.dispatchEvent({
// type: 'programs:instance',
// program,
// programID,
// song: this
// });
}
programLoadRenderer(programID, props={}) {
return this.programLoader.programLoadRenderer(programID, props);
}
programAdd(programClassName, programConfig={}) {
if (typeof programConfig !== 'object')
throw new Error("Invalid programs config object");
if (!programClassName)
throw new Error("Invalid Program Class");
const programList = this.dataProxy.programs;
const programID = programList.length;
this.data.programs[programID] = [programClassName, programConfig];
this.programLoadInstanceFromID(programID);
return programID;
}
programReplace(programID, programClassName, programConfig={}) {
// Preserve old programs name
// if (oldConfig && oldConfig.title && !programConfig.title)
// programConfig.title = oldConfig.title;
this.stopPlayback();
const programList = this.dataProxy.programs;
const oldConfig = this.data.programs[programID];
programList[programID] = [programClassName, programConfig];
const instance = this.programLoadInstanceFromID(programID);
console.log('programReplace', programID, programClassName, programConfig, instance);
return oldConfig;
}
programRename(programID, newTitle) {
const config = this.programGetConfig(programID);
config.title = newTitle;
}
programRemove(programID) {
const programList = this.dataProxy.programs;
if (typeof programList[programID] === "undefined")
return console.error("Invalid program ID: " + programID);
const isLastProgram = programID === programList.length - 1;
const oldConfig = programList[programID];
if(isLastProgram) {
programList.splice(programID, 1);
} else {
programList[programID] = null;
}
// this.programUnload(programID);
// console.log('programRemove', programID, this.dataProxy.programs)
return oldConfig;
}
/** Playback **/
setVolume(newVolume) {
this.volume = newVolume;
if(this.lastVolumeGain)
this.lastVolumeGain.gain.value = newVolume;
}
getSongPlaybackPosition() {
if (this.playback)
return this.playback.getPlaybackPosition();
return this.playbackPosition;
}
setPlaybackPositionInTicks(songPositionInTicks) {
if (!Number.isInteger(songPositionInTicks))
throw new Error("Invalid start position in ticks");
// TODO: is start position beyond song's ending?
const playbackPosition = this.getSongPositionFromTicks(songPositionInTicks);
return this.setPlaybackPosition(playbackPosition);
}
setPlaybackPosition(songPosition) {// TODO: duplicate values? Does the song need to store position?
songPosition = parseFloat(songPosition);
if (Number.isNaN(songPosition))
throw new Error("Invalid start position");
// this.playback.setPlaybackPosition(this.getAudioContext().currentTime - this.playbackPosition);
// let isPlaying = !!this.playback;
if (this.playback) {
this.stopPlayback();
}
this.playbackPosition = songPosition;
this.dispatchEvent({
type: 'song:seek',
position: this.playbackPosition,
// positionInTicks: this.getSongPositionInTicks(this.playbackPosition), // TODO: use iterator
song: this
});
// console.log('setPlaybackPosition', songPosition);
// if (isPlaying) {
// const oldDestination = this.playback.destination;
// this.playback = new InstructionPlayback(oldDestination, this, this.getStartTrackName(), oldDestination.context.currentTime - this.playbackPosition);
// // this.playback.awaitPlaybackReachedEnd()
// // .then((reachedEnding) => reachedEnding ? this.stopPlayback(true) : null);
// }
// const positionInTicks = this.getSongPositionInTicks(this.playbackPosition);
// console.log("Seek position: ", this.playbackPosition, positionInTicks);
}
play(destination, startPosition=null, onended=null) {
// destination = this.getVolumeGain(destination);
// const audioContext = destination.context;
if (this.playback) {
this.stopPlayback();
// this.setPlaybackPosition(0);
// throw new Error("Song is already playing");
}
// await this.init(audioContext);
if(startPosition === null)
startPosition = this.playbackPosition;
if(startPosition > 0) {
if(startPosition >= this.getSongLengthInSeconds())
startPosition = 0;
}
// console.log("Start playback:", destination, startPosition, onended);
const playback = new TrackPlayback(destination, this, this.getStartTrackName()); // , this.dispatchEventCallback);
this.playback = playback;
playback.play(startPosition)
this.dispatchEvent({
type: 'song:play',
playback: this.playback,
// positionInTicks: this.getSongPositionInTicks(this.playbackPosition), // TODO: use iterator
song: this
});
playback.awaitPlaybackReachedEnd()
.then(() => {
if(onended)
onended();
this.dispatchEvent({
type: 'song:end',
playback: this.playback,
// positionInTicks: this.getSongPositionInTicks(this.playbackPosition), // TODO: use iterator
song: this
});
// this.setPlaybackPosition(0);
// if(this.playback)
// this.stopPlayback();
});
return playback;
// const reachedEnding = await this.playback.awaitPlaybackReachedEnd();
// if(reachedEnding)
// this.stopPlayback(true);
}
// stopProgramPlayback(programID) {
// this.programLoader.stopProgramPlayback(programID);
// // let programClass = this.programLoader.programGetClass(programID);
// // if(typeof programClass.stopPlayback !== "function")
// // return console.error(programClass.name + ".stopPlayback is not a function");
// // programClass.stopPlayback();
// }
stopPlayback() {
if (!this.playback)
return console.warn("Playback is already stopped");
const playback = this.playback;
this.playback = null;
this.playbackPosition = playback.getPositionInSeconds();
playback.stopPlayback(false);
// ProgramLoader.stopAllPlayback(); // TODO: redundant? necessary if no playback
// TODO: move to playback class
// for (let i = 0; i < this.playbackEndCallbacks.length; i++)
// this.playbackEndCallbacks[i]();
// this.playbackEndCallbacks = [];
// for (let i = 0; i < this.waitCancels.length; i++)
// this.waitCancels[i]();
// this.waitCancels = [];
// console.log("End playback:", this.playbackPosition);
this.dispatchEvent({
type: 'song:end',
playback: this.playback,
// positionInTicks: this.getSongPositionInTicks(this.playbackPosition), // TODO: use iterator
song: this
});
}
pause() {
if (this.isPaused)
throw new Error("Song is already paused");
this.isPaused = true;
}
resume() {
if (!this.isPaused)
throw new Error("Song is not paused");
this.isPaused = false;
}
isPlaying() {
return this.playback && this.playback.isActive();
}
playSelectedInstructions(destination, trackName, selectedIndices) {
// destination = this.getVolumeGain(destination);
// TrackIterator find playback position of first index start point
if(this.playback)
this.stopPlayback();
if(selectedIndices.length > 0) {
const playback = new TrackPlayback(destination, this, trackName, function (commandString, trackStats) {
if (trackStats.trackName !== trackName)
return true;
const index = trackStats.currentIndex;
return selectedIndices.indexOf(index) !== -1;
})
this.playback = playback;
// TrackPlayback with selective callback
playback.playAtStartingTrackIndex(selectedIndices[0])
// playback.play(destination);
}
// for(let i=0; i<selectedIndices.length; i++) {
// const selectedIndex = selectedIndices[i];
// const instruction = song.instructionGetByIndex(trackName, selectedIndex);
// song.playInstruction(destination, instruction, trackState.programID);
// }
}
getPracticeProgram(programID) {
let programInstance;
const [lastPracticeProgram, lastPracticeProgramConfig] = this.lastPracticeProgram || [null, null, null];
const [className, programConfig] = this.programGetData(programID, false);
if(lastPracticeProgramConfig !== programConfig) {
ProgramLoader.stopAllPlayback();
programInstance = ProgramLoader.loadInstance(className, programConfig, this.getProgramDispatchEvent(programID));
this.lastPracticeProgram = [programInstance, programConfig];
// console.log("Loading program for MIDI playback: ", programID, className, programConfig);
} else {
programInstance = lastPracticeProgram;
}
return programInstance;
}
playMIDIEvent(destination, programID, eventData) {
let program = this.getPracticeProgram(programID);
if(program.playMIDIEvent)
program.playMIDIEvent(destination, eventData);
else
console.warn("Program " + program.constructor.name + " has no method 'playMIDIEvent'");
}
playInstrumentFrequency(destination, programID, frequencyString, startTime=null, duration=null, velocity=null) {
let program = this.getPracticeProgram(programID);
const frequency = Values.instance.parseFrequencyString(frequencyString);
// console.log('playInstrumentFrequency', {program, frequencyString, frequency, startTime, duration, velocity})
// const playEvent = {
// type: 'program:play',
// programID,
// frequency,
// velocity
// }
// this.dispatchEvent(playEvent);
return program.playFrequency(destination, frequency, startTime, duration, velocity);
}
// playInstructionAtIndex(destination, trackName, instructionIndex, noteStartTime = null) {
// const instruction = this.instructionGetByIndex(trackName, instructionIndex, false);
// if (instruction)
// this.playInstruction(instruction, noteStartTime);
// else
// console.warn("No instruction at index");
// }
// playInstruction(destination, instruction, program, noteStartTime = null, onstart=null, onended=null) {
// // destination = this.getVolumeGain(destination);
//
// const audioContext = destination.context;
// if (!instruction instanceof Instruction)
// throw new Error("Invalid instruction");
//
// // if(this.playback)
// // this.stopPlayback();
//
// // if (instruction instanceof ASCTrackInstruction) { // Handled in TrackPlayback
// // return new TrackPlayback(destination, this, instruction.getTrackName(), noteStartTime);
// // }
//
//
// // const noteDuration = (instruction.duration || 1) * (60 / beatsPerMinute);
//
// let noteDuration = null;
// if(typeof instruction.durationTicks !== "undefined") {
// let beatsPerMinute = this.data.beatsPerMinute; // getStartingBeatsPerMinute();
// let timeDivision = this.data.timeDivision;
// const noteDurationTicks = instruction.durationTicks; // (timeDivision);
// noteDuration = (noteDurationTicks / timeDivision) / (beatsPerMinute / 60);
// }
//
// let currentTime = audioContext.currentTime;
//
// if (!noteStartTime && noteStartTime !== 0)
// noteStartTime = currentTime;
//
//
// this.playProgram(destination, program, instruction.command, noteStartTime, noteDuration, instruction.velocity, onstart, onended);
// // Wait for note to start
// // if (noteStartTime > currentTime) {
// // await this.wait(noteStartTime - currentTime);
// // // await new Promise((resolve, reject) => setTimeout(resolve, (noteStartTime - currentTime) * 1000));
// // }
//
// // Dispatch note start event
// // this.dispatchEvent({
// // type: 'note:start',
// // trackName,
// // instruction,
// // song: this
// // });
//
// // currentTime = audioContext.currentTime;
// // if (noteStartTime + noteDuration > currentTime) {
// // await this.wait(noteStartTime + noteDuration - currentTime);
// // // await new Promise((resolve, reject) => setTimeout(resolve, (noteStartTime + noteDuration - currentTime) * 1000));
// // }
// // // TODO: check for song stop
// // // Dispatch note end event
// // this.dispatchEvent({
// // type: 'note:end',
// // trackName,
// // instruction,
// // song: this
// // });
// }
/** Song Modification History **/
queueHistoryAction(action, pathList, data = null, oldData = null) {
if(Array.isArray(pathList))
pathList = pathList.join('.');
const historyAction = [
action, pathList,
];
if (data !== null || oldData !== null)
historyAction.push(data);
if (oldData !== null)
historyAction.push(oldData);
// this.history.push(historyAction);
// setTimeout(() => {
this.dispatchEvent({
type: 'song:modified',
historyAction,
song: this
});
return historyAction;
}
/** History **/
applyHistoryActions(songHistory) {
for (let i = 0; i < songHistory.length; i++) {
const historyAction = songHistory[i];
this.applyHistoryAction(historyAction);
}
}
applyHistoryAction(...args) {
const historyAction = args.shift();
const path = args.shift().split('.');
const lastPath = path.pop();
const songData = this.data;
let target = songData;
for(let i=0; i<path.length; i++) {
target = target[path[i]];
}
switch (historyAction) {
// case 'reset':
// Object.assign(this.data, historyAction.data);
// break;
case 'set':
const newValue = args.shift();
target[lastPath] = newValue;
break;
case 'delete':
delete target[lastPath];
break;
case 'replace':
const replaceValue = args.shift();
const oldValue = args.shift();
if(oldValue !== target[lastPath]) {
console.warn(`Replace value mismatch: ${oldValue} !== ${songData[lastPath]}`)
}
target[lastPath] = replaceValue;
break;
default:
throw new Error("Unknown history action: " + historyAction);
}
}
/** Static Fle Support Module **/
static getFileSupportModule(filePath) {
// const AudioSourceLoader = customElements.get('audio-source-loader');
// const requireAsync = AudioSourceLoader.getRequireAsync(thisModule);
const fileExt = filePath.split('.').pop().toLowerCase();
let library;
switch (fileExt) {
case 'mid':
case 'midi':
return new MIDIFileSupport();
// const {MIDISupport} = require('../file/MIDIFile.js');
// return new MIDISupport;
//
case 'json':
library = new JSONFileSupport();
// await library.init();
return library;
//
case 'nsf':
case 'nsfe':
case 'spc':
case 'gym':
case 'vgm':
case 'vgz':
case 'ay':
case 'sgc':
case 'kss':
throw new Error("Unsupported");
// library = new GMESongFile();
// library.init();
// return library;
//
// case 'mp3':
// const {MP3Support} = require('../file/MP3File.js');
// return new MP3Support;
default:
throw new Error("Unknown file module for file type: " + fileExt);
}
};
/** Generate Song Data **/
static generateTitle() {
return `Untitled (${new Date().toJSON().slice(0, 10).replace(/-/g, '/')})`;
}
}
Song.DEFAULT_VOLUME = 0.7;
// Song.loadSongFromMIDIFile = async function (file, defaultProgramURL = null) {
// defaultProgramURL = defaultProgramURL || this.getDefaultProgramURL();
// const midiSupport = new MIDIImport();
// const songData = await midiSupport.loadSongFromMidiFile(file, defaultProgramURL);
// const song = new Song();
// await song.loadSongData(songData);
// return song;
// };
export default Song;