diff --git a/plugins/default/sound/typescriptAPI/Sup.Audio.Conductor.d.ts.txt b/plugins/default/sound/typescriptAPI/Sup.Audio.Conductor.d.ts.txt new file mode 100644 index 000000000..5c6e914cb --- /dev/null +++ b/plugins/default/sound/typescriptAPI/Sup.Audio.Conductor.d.ts.txt @@ -0,0 +1,97 @@ +/* + * A music conductor that schedules audio events, in the form of MultiSoundPlayers, + * based on beats per phrase according to an internal metronome/interval that + * uses the Web Audio API's clock for timing. + * + */ + +declare namespace Sup { + namespace Audio { + class Conductor { + + // Construct a Conductor. + // + // - bpm -> beats per minute + // - timesig -> time signature (how many beats in a cycle, + // i.e. num of beats * num of measures) + // - players -> an object with player names as keys and MultiSoundPlayers as values, + // e.g. {"violin": MultiSoundPlayer, "piano": MultiSoundPlayer} + constructor(bpm: number, timesig: number, players: any); + + // Initialize Conductor's members via a params object + initializeParams(params: {bpm: number, timesig: number, players: any}); + + // Start the Conductor's interval and player scheduling + start(); + // Stop the Conductor and reset params for next start() + stop(); + + // Have all players play init sample if !initPlayed, otherwise loop sample + playAllInitOrLoop(); + // Have all players play tail samples for Conductor's current beatNum + playAllTails(); + + // Return a player + getPlayer(playerName: string): MultiSoundPlayer; + // Call reset() on all players + resetAllPlayers(); + + // Set the params that Conductor will be reinitialized with for next section + setNextParams(params: {bpm: number, timesig: number, players: any}); + // Set whether Conductor should transition upon next available beat + setTransition(transition: boolean); + // Set whether Conductor should go to next section at the next available transition + // (if false, Conductor will call stop() upon transitioning) + setToNext(toNext: boolean); + // Get whether Conductor is transitioning + isTransitioning(): boolean; + + // Activate or deactivate a player + activatePlayer(playerName: string); + deactivatePlayer(playerName: string); + + // Activate or deactivate multiple players + activatePlayers(playerNames: Array); + deactivatePlayers(playerNames: Array); + + // Activate or deactivate all players + activateAllPlayers(); + deactivateAllPlayers(); + + // Fade a single player, multiple players, or all players + fadePlayer(playerName: string, targetVolume: number, fadeLength: number /* in ms */); + fadePlayers(playerNames: Array, targetVolume: number, fadeLength: number); + fadeAllPlayers(targetVolume: number, fadeLength: number); + + // Get the current beat number + getBeatNum(): number; + // Update the next beat's time based on the latest beat time and BPM + updateNextBeatTime(); + // Return time, in seconds, until the next downbeat + // ("downbeat" refers to when beatNum==0) + getSecondsLeftUntilNextDownbeat(bpmIn?: number): number; + // Return time, in milliseconds, until the next downbeat + getMillisecondsLeftUntilNextDownbeat(bpm?: number): number; + // Return time, in milliseconds, until the next available transition beat + getMillisecondsLeftUntilNextTransitionBeat(): number; + + // Return the Conductor's BPM + getBpm(): number; + // Return the Conductor's AudioContext + getContext(): any; + + // Schedule an event to occur a certain number of milliseconds in the future + scheduleEvent(msFromNow: number, event: any); + + // Set console logging on/off + setLogOutput(logOutput: boolean); + // Log to console if logOutput is true + log(message?: any); + } + + namespace Conductor { + // Return the next beat time (in seconds) + function calculateNextBeatTime(currentBeatTime: number, bpm: number): number; + } + } +} \ No newline at end of file diff --git a/plugins/default/sound/typescriptAPI/Sup.Audio.Conductor.ts.txt b/plugins/default/sound/typescriptAPI/Sup.Audio.Conductor.ts.txt new file mode 100644 index 000000000..966c1e1ca --- /dev/null +++ b/plugins/default/sound/typescriptAPI/Sup.Audio.Conductor.ts.txt @@ -0,0 +1,352 @@ +/* + * A music conductor that schedules audio events, in the form of MultiSoundPlayers, + * based on beats per phrase according to an internal metronome/interval that + * uses the Web Audio API's clock for timing. + * + */ + +namespace Sup { + export namespace Audio { + export class Conductor { + bpm: number; + timesig: number; // beats per phrase + beatNum: number; + transitionBeats: Array; + transition: boolean; + players: any; // dictionary of players + ctx: any; // audio context + nextBeatTime: number; + lookaheadTime: number; + active: boolean; + interval: any; // the internal metronome + intervalMs: number; + logOutput: boolean; + + toNext: boolean; + nextParams: any; // bpm, timesig, players + + /* + Constructs a Conductor. + + - bpm -> beats per minute + - timesig -> time signature (how many beats in a cycle) + - players -> a dictionary of MultiSoundPlayers + */ + constructor(bpm: number, timesig: number, players: any) { + // initialize parameters + this.initializeParams({bpm, timesig, players}); + + // create audio context and setup beat time + this.ctx = player.gameInstance.audio.getContext(); + this.nextBeatTime = this.ctx.currentTime; + // this.updateNextBeatTime(); + } + + initializeParams(params: {bpm: number, timesig: number, players: any}) { + // set default params + this.beatNum = 0; + this.lookaheadTime = 0; + this.active = false; + this.transition = false; + this.toNext = false; + this.nextParams = {}; + this.logOutput = false; + this.intervalMs = 1; + + // set params passed in + this.bpm = params.bpm; + this.timesig = params.timesig; + this.transitionBeats = []; + this.players = params.players; + + // get transition beats from players' tails + for (var pk in this.players) { + let player = this.players[pk]; + for (var beat in player.tails) { + this.transitionBeats.push(parseInt(beat)); + } + } + } + + getPlayer(playerName): MultiSoundPlayer { + return this.players[playerName]; + } + + /* + Begin the Conductor's scheduling. + */ + start() { + let conductor = this; + + this.log("started"); + // this.nextBeatTime = this.ctx.currentTime; + this.active = true; + + // set the interval and handle beats + players + this.interval = Sup.setInterval(this.intervalMs, function() { + if (conductor.nextBeatTime < conductor.ctx.currentTime + conductor.lookaheadTime) { + conductor.updateNextBeatTime(); + conductor.log(conductor.beatNum); + + if (conductor.transition) { + let beatIndex = conductor.transitionBeats.indexOf(conductor.beatNum); + if (beatIndex >= 0) { // transition beat + conductor.playAllTails(); + // conductor.active = false; + + // either do nothing OR begin next section + // depending on active settings + if (conductor.toNext) { + conductor.resetAllPlayers(); // reset current players + conductor.initializeParams(conductor.nextParams); // then load next ones + // conductor.resetAllPlayers(); + } + else { // stop + conductor.stop(); + } + + conductor.log("playing tails for beat " + conductor.beatNum); + conductor.beatNum = -1; // hacky + } + else { + // conductor.log("sorry, no tails found for beat " + conductor.beatNum); + } + } else { + if (conductor.beatNum <= 0) { // downbeat (allow -1 for toNexts) + conductor.playAllInitOrLoop(); + + conductor.log("playing downbeat!"); + conductor.log(conductor.players); + } + } + + // increment the beat number + conductor.beatNum = (conductor.beatNum + 1) % conductor.timesig; + } + }); + } + + /* + Stop the Conductor and reset params for next start(). + */ + stop() { + this.active = false; + Sup.clearInterval(this.interval); + this.beatNum = 0; + this.transition = false; + this.log("stopped " + this.interval); + this.resetAllPlayers(); + } + + /* + Update the next beat time based on the latest beat time and BPM. + */ + updateNextBeatTime() { + let nextBeatTime = Sup.Audio.Conductor.calculateNextBeatTime(this.nextBeatTime, this.bpm); + // this.log(this.bpm); + // this.log(this.nextBeatTime + " -> " + nextBeatTime); + this.nextBeatTime = nextBeatTime; + } + + playAllInitOrLoop() { + for (var pk in this.players) { + let player = this.players[pk]; + player.playInitOrLoop(this.beatNum); + } + } + + playAllTails() { + for (var pk in this.players) { + let player = this.players[pk]; + player.playTail(this.beatNum); + + // player.init.stop(); + // player.loop.stop(); + } + } + + resetAllPlayers() { + for (var pk in this.players) { + let player = this.players[pk]; + player.reset(); + } + } + + setTransition(transition: boolean) { + this.transition = transition; + } + + setNextParams(params: {bpm: number, timesig: number, players: any}) { + this.nextParams = params; + } + + setToNext(toNext: boolean) { + this.toNext = toNext; + } + + isTransitioning(): boolean { + return this.transition; + } + + activatePlayer(playerName: string) { + if (this.players[playerName]) { + this.players[playerName].activate(); + } + else { + this.log("activatePlayer: no player named " + playerName + " found!"); + } + } + + deactivatePlayer(playerName: string) { + if (this.players[playerName]) { + this.players[playerName].deactivate(); + this.players[playerName].reset(); + } + else { + this.log("deactivatePlayer: no player named " + playerName + " found!"); + } + } + + activatePlayers(playerNames: Array) { + for (var playerName of playerNames) { + this.activatePlayer(playerName); + } + } + + deactivatePlayers(playerNames: Array) { + for (var playerName of playerNames) { + this.deactivatePlayer(playerName); + } + } + + activateAllPlayers() { + for (var pk in this.players) { + this.activatePlayer(pk); + } + } + + deactivateAllPlayers() { + for (var pk in this.players) { + this.deactivatePlayer(pk); + } + } + + fadePlayer(playerName: string, targetVolume: number, fadeLength: number /* in ms */) { + if (this.players[playerName]) { + this.players[playerName].fade(targetVolume, fadeLength); + } + else { + this.log("fadePlayer: no player named " + playerName + " found!"); + } + } + + fadePlayers(playerNames: Array, targetVolume: number, fadeLength: number) { + for (var playerName of playerNames) { + this.fadePlayer(playerName, targetVolume, fadeLength); + } + } + + fadeAllPlayers(targetVolume: number, fadeLength: number) { + for (var pk in this.players) { + this.fadePlayer(pk, targetVolume, fadeLength); + } + } + + getContext(): any { + return this.ctx; + } + + getBeatNum(): number { + return this.beatNum; + } + + getBpm(): number { + return this.bpm; + } + + getSecondsLeftUntilNextDownbeat(bpmIn?: number): number { + let bpm = this.bpm; + if (bpmIn) { + bpm = bpmIn; + } + // this.log("bpm is " + bpm); + let beatsLeft = this.timesig - this.beatNum; + let secondsLeft = Conductor.calculateNextBeatTime(0, bpm) * beatsLeft; + // return secondsLeft * 1000; + return secondsLeft; + } + + getMillisecondsLeftUntilNextDownbeat(bpmIn?: number): number { + return this.getSecondsLeftUntilNextDownbeat(bpmIn) * 1000; + } + + getMillisecondsLeftUntilNextTransitionBeat(): number { + if (this.transitionBeats.length < 1) { + this.log("no transition beats!"); + return -1; + } + + let numericComparison = function(a, b) { + return a - b; + }; + let tbSorted = this.transitionBeats.slice().sort(numericComparison); + tbSorted.push(tbSorted[0] + this.timesig); // add first transition beat of next cycle + // let nextTransitionBeat = tbSorted[tbSorted.length-1]; + tbSorted.reverse(); + + let nextTransitionBeat = tbSorted[0]; + for (var i = 0; i < tbSorted.length; i++) { + if (tbSorted[i] > this.beatNum) { + nextTransitionBeat = tbSorted[i]; + } + } + this.log("current beat: " + this.beatNum + ", nextTransitionBeat: " + nextTransitionBeat); + + let beatsLeft = nextTransitionBeat - this.beatNum; + if (beatsLeft < 0) { + beatsLeft *= -1; + } + let secondsLeft = Conductor.calculateNextBeatTime(0, this.bpm) * beatsLeft; + return secondsLeft * 1000; + } + + scheduleEvent(msFromNow: number, event: any) { + let ctx = this.ctx; + let eventTime = this.ctx.currentTime + (msFromNow / 1000); + let eventInterval = Sup.setInterval(this.intervalMs, function() { + // Sup.log(eventTime + ", " + ctx.currentTime); + if (eventTime < ctx.currentTime) { + event(); + Sup.clearInterval(eventInterval); + } + }); + Sup.log("scheduling"); + } + + setLogOutput(logOutput: boolean) { + this.logOutput = logOutput; + } + + log(message?: any) { + if (this.logOutput) { + Sup.log("Sup.Audio.Conductor: " + message); + } + } + + } + + export namespace Conductor { + + /* + Return the next beat time (in seconds). + */ + export function calculateNextBeatTime(beatTime: number, bpm: number): number { + return beatTime + (60 / bpm); + } + + // static bpmToMs(bpm) { + // return (60/bpm)*1000; + // } + } + } +} \ No newline at end of file diff --git a/plugins/default/sound/typescriptAPI/Sup.Audio.MultiSoundPlayer.d.ts.txt b/plugins/default/sound/typescriptAPI/Sup.Audio.MultiSoundPlayer.d.ts.txt new file mode 100644 index 000000000..76eb36aca --- /dev/null +++ b/plugins/default/sound/typescriptAPI/Sup.Audio.MultiSoundPlayer.d.ts.txt @@ -0,0 +1,60 @@ +/* + * A music player, comprised of multiple SoundPlayers, that provides methods for + * smoother and more robust game audio in the form of an "init, loop, tail(s)" + * scheme, allowing for looped sections of varying repetitions to begin and end + * seamlessly. + * + * The first time a MultiSoundPlayer is played, it should play + * the "init" sample; subsequent plays will play the "loop" sample, and the final + * time it should play the "tail" sample for the corresponding beat number. + * + */ + +declare namespace Sup { + namespace Audio { + class MultiSoundPlayer { + + // Construct a MultiSoundPlayer. + // + // init -> sample for the first time the MultiSoundPlayer is played + // loop -> sample for all subsequent repeats + // tails -> sample(s) to play when looping is done + // + // If tails is a string, then the single tail sample will be played for any + // transition, regardless of beat number. + // + // If tails is an object, it should have beat numbers as keys and + // sample paths as values, such as {0: "path/to/tail0.mp3", 6: "path/to/tail6.mp3"} + // In that case, a tail corresponding to a beat number will only be played when + // transitioning on that exact beat. + constructor(init: string | Sound, loop: string | Sound, tails: any /*object or string*/, volume: number /*1.0*/, options?: { loop?: boolean; pitch?: number; pan?: number; active?: boolean; logOutput?: boolean;}); + + // Play init or loop sample (depending on whether init has already been played) + playInitOrLoop(beatNum: number); + // Play tail sample for a given beat number + playTail(beatNum: number /*0*/); + // Set the play mode to "init", "loop", or "tail" + setPlayMode(playMode: string); + + // Activate a player so that it will play when called on + activate(); + // Deactivate a player so that it doesn't play + deactivate(); + // Reset the player and the values of its members + reset(); + + // Get or set the volume of the player + getVolume(): number; + setVolume(volume: number); + + // Increment the player's volume up or down, over a given amount of time, + // until it reaches the target volume + fade(targetVolume: number, fadeLength: number /* in ms */); + + // Set console logging on/off + setLogOutput(logOutput: boolean); + // Log to console if logOutput is true + log(message?: any); + } + } +} \ No newline at end of file diff --git a/plugins/default/sound/typescriptAPI/Sup.Audio.MultiSoundPlayer.ts.txt b/plugins/default/sound/typescriptAPI/Sup.Audio.MultiSoundPlayer.ts.txt new file mode 100644 index 000000000..acb12a942 --- /dev/null +++ b/plugins/default/sound/typescriptAPI/Sup.Audio.MultiSoundPlayer.ts.txt @@ -0,0 +1,250 @@ +/* + * A class, comprised of multiple SoundPlayers, that provides methods for + * smoother and more robust game audio in the form of an "init, loop, tail(s)" + * scheme similar to that of other game audio engines. + * + * In other words, the first time a MultiSoundPlayer is played, it should play + * the "init" sample; subsequent plays will play the "loop" sample, and the final + * time it should play the "tail" sample for the corresponding beat number. + * + */ + +namespace Sup { + export namespace Audio { + export class MultiSoundPlayer { + init: SoundPlayer; + loop: SoundPlayer; + tails: any; + originalLoopSetting: boolean; + // soundPlayers: any; + volume: number; + playMode: string; // init, loop, tail + initPlayed: boolean; + tailPlayed: boolean; + active: boolean; + logOutput: boolean; + + /* + Constructs a MultiSoundPlayer. + */ + constructor(init: string | Sound, loop: string | Sound, tails: any /* object or string */, volume = 1.0, options?: { loop?: boolean; pitch?: number; pan?: number; active?: boolean; logOutput?: boolean;}) { + this.init = new SoundPlayer(init, volume, options); + this.loop = new SoundPlayer(loop, volume, options); + // this.loop = new SoundPlayer(loop, volume, {loop: true}); + this.originalLoopSetting = false; + if ("loop" in options) { + this.originalLoopSetting = options.loop; + } + + // for multiple tails corresponding to multiple beats + this.tails = {}; + if (typeof tails == "object") { + for (var beatNum in tails) { + let tailAudio = tails[beatNum]; + this.tails[beatNum] = new SoundPlayer(tailAudio, volume, options); + } + } + else if (typeof tails == "string" || typeof tails == "Sound") { // default to beat 0 + this.tails[0] = new SoundPlayer(tails, volume, options); + } + + this.volume = volume; + + // this.soundPlayers = { + // init: this.init, + // loop: this.loop, + // tails: this.tails + // }; + + this.playMode = "init"; + this.initPlayed = false; + this.tailPlayed = false; + + // properties adjustable by optional params + this.active = true; + this.logOutput = false; + + if (options) { + if ("active" in options) { + this.active = options.active; + } + + if ("logOutput" in options) { + this.logOutput = options.logOutput; + } + } + + } + + /* + Play the appropriate sound (init, loop, or tail). + */ + play(beatNum=0) { + this.log("please use playInitOrLoop() or playTail()"); + + // if (this.active) { + // if (this.playMode == "init") { + // this.init.play(); + // } + // else if (this.playMode == "loop") { + // if (!this.loop.getLoop() || !this.loop.isPlaying()) { + // this.loop.play(); + // } + // } + // else if (this.playMode == "tail") { + // this.tails[beatNum].play(); + // } + // else { + // this.log(this.playMode + " is not a valid playMode"); + // } + // } + } + + playInitOrLoop(beatNum=0) { + if (this.active) { + if (this.initPlayed) { + if (!this.loop.getLoop()) { // {loop: false}; retrigger every time + this.loop.stop(); // calling play() on SoundPlayer won't do anything if it's already playing, so we must stop it first + this.loop.play(); + this.log("playing loop"); + } + else if (!this.loop.isPlaying) { // {loop: true}; only play if it's not already playing + this.loop.play(); + this.log("playing loop"); + } + } + else { + this.init.play(); + this.initPlayed = true; + this.log("playing init"); + this.playMode = "loop"; + } + } + else { + // this.init.stop(); + // this.loop.stop(); + } + + // else { + // if (!this.tailPlayed) { + // this.playTail(beatNum); + // } + // } + } + + playTail(beatNum=0) { + // stop init and loop + this.init.stop(); + this.loop.stop(); + this.loop.setLoop(false); // stop loop from continuing + + // play tail + if (this.active) { + if (beatNum in this.tails) { + this.tails[beatNum].play(); // play tail sample + this.tailPlayed = true; + this.log("playing tail"); + } + else { + let keys = []; + for (var key in this.tails) { + keys.push(key); + } + if (keys.length == 1 && 0 in this.tails) { + this.tails[0].play();// play default tail sample + this.tailPlayed = true; + this.log("playing default tail"); + } + } + } + } + + setPlayMode(playMode: string) { + this.playMode = playMode; + } + + reset() { + this.playMode = "init"; + this.initPlayed = false; + this.tailPlayed = false; + this.loop.setLoop(this.originalLoopSetting); + } + + activate() { + this.active = true; + this.loop.setLoop(this.originalLoopSetting); + } + + deactivate() { + this.active = false; + this.loop.setLoop(false); + } + + getVolume(): number { + return this.volume; + } + + setVolume(volume: number) { + this.init.setVolume(volume); + this.loop.setVolume(volume); + + for (var beatNum in this.tails) { + this.tails[beatNum].setVolume(volume); + } + + this.volume = volume; + } + + setLogOutput(logOutput: boolean) { + this.logOutput = logOutput; + } + + log(message?: any) { + if (this.logOutput) { + Sup.log("Sup.Audio.MultiSoundPlayer: " + message); + } + } + + fade(targetVolume: number, fadeLength: number /* in ms */) { + let intervalMs = 1; + + // let volDiff = Math.abs(this.volume - targetVolume); + let volDiff = this.volume - targetVolume; + if (volDiff < 0) { + volDiff *= -1; + } + + let step = intervalMs * volDiff / fadeLength; + + let direction = "up"; + if (targetVolume < this.volume) { + direction = "down"; + } + + let msp = this; + let timer = Sup.setInterval(intervalMs, function() { + if (direction == "up") { + if (msp.volume >= targetVolume) { + clearInterval(timer); + msp.log("fade " + timer + " done"); + } + else { + msp.setVolume(msp.volume); + msp.volume += step; + } + } + else if (direction == "down") { + if (msp.volume <= targetVolume) { + clearInterval(timer); + msp.log("fade " + timer + " done"); + } + else { + msp.setVolume(msp.volume); + msp.volume -= step; + } + } + }); + } + } + } +} \ No newline at end of file diff --git a/plugins/default/sound/typescriptAPI/index.ts b/plugins/default/sound/typescriptAPI/index.ts index cb329333f..4290b181f 100644 --- a/plugins/default/sound/typescriptAPI/index.ts +++ b/plugins/default/sound/typescriptAPI/index.ts @@ -1,18 +1,28 @@ -/// - -import * as fs from "fs"; - -SupCore.system.registerPlugin("typescriptAPI", "Sup.Sound", { - code: "namespace Sup { export class Sound extends Asset {} }", - defs: "declare namespace Sup { class Sound extends Asset { dummySoundMember; } }" -}); - -SupCore.system.registerPlugin("typescriptAPI", "Sup.Audio", { - code: fs.readFileSync(`${__dirname}/Sup.Audio.ts.txt`, { encoding: "utf8" }), - defs: fs.readFileSync(`${__dirname}/Sup.Audio.d.ts.txt`, { encoding: "utf8" }) -}); - -SupCore.system.registerPlugin("typescriptAPI", "Sup.Audio.SoundPlayer", { - code: fs.readFileSync(`${__dirname}/Sup.Audio.SoundPlayer.ts.txt`, { encoding: "utf8" }), - defs: fs.readFileSync(`${__dirname}/Sup.Audio.SoundPlayer.d.ts.txt`, { encoding: "utf8" }) -}); +/// + +import * as fs from "fs"; + +SupCore.system.registerPlugin("typescriptAPI", "Sup.Sound", { + code: "namespace Sup { export class Sound extends Asset {} }", + defs: "declare namespace Sup { class Sound extends Asset { dummySoundMember; } }" +}); + +SupCore.system.registerPlugin("typescriptAPI", "Sup.Audio", { + code: fs.readFileSync(`${__dirname}/Sup.Audio.ts.txt`, { encoding: "utf8" }), + defs: fs.readFileSync(`${__dirname}/Sup.Audio.d.ts.txt`, { encoding: "utf8" }) +}); + +SupCore.system.registerPlugin("typescriptAPI", "Sup.Audio.SoundPlayer", { + code: fs.readFileSync(`${__dirname}/Sup.Audio.SoundPlayer.ts.txt`, { encoding: "utf8" }), + defs: fs.readFileSync(`${__dirname}/Sup.Audio.SoundPlayer.d.ts.txt`, { encoding: "utf8" }) +}); + +SupCore.system.registerPlugin("typescriptAPI", "Sup.Audio.MultiSoundPlayer", { + code: fs.readFileSync(`${__dirname}/Sup.Audio.MultiSoundPlayer.ts.txt`, { encoding: "utf8" }), + defs: fs.readFileSync(`${__dirname}/Sup.Audio.MultiSoundPlayer.d.ts.txt`, { encoding: "utf8" }) +}); + +SupCore.system.registerPlugin("typescriptAPI", "Sup.Audio.Conductor", { + code: fs.readFileSync(`${__dirname}/Sup.Audio.Conductor.ts.txt`, { encoding: "utf8" }), + defs: fs.readFileSync(`${__dirname}/Sup.Audio.Conductor.d.ts.txt`, { encoding: "utf8" }) +});