UNPKG

adventure-engine

Version:

Simple text based adventure game library.

225 lines (203 loc) 7.24 kB
//========================================================= // lo-fi-player.js: Primary module for adventure-engine audio/synthesizer //========================================================= // ______ _ ______ // (____ \ | | (_____ \ // ____) )| | _____) ) // | __ ( | | | ____/ // | |__) )| |_____ _| | // |______(_)_______|_)_| // // Description: // Simple synth for playing audio text files based on adventure-engine // music spec. // // Author: Brian Piltin // // Copyright: (C) 2019 Brian Piltin. // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY // SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // Version: 0.1.2 // History: // 0.1.0: Initial version // 0.1.1: Add support for multi-timbral notes and chords // 0.1.2: Fix misc. bugs // // TODO: // - Add support for control characters //========================================================= "use strict"; var loFiPlayer = (function () { const DEBUG = false; const STATE_STOPPED = 0, STATE_PLAYING = 1, STATE_STOPPING = 2; let instance; let channels = []; function init(numChannels = 2) { for (var i = 0; i < numChannels; i++) { channels.push(playerChannel()); } function playerChannel(config = {}) { //--------------------------------------------------------- // Audio //--------------------------------------------------------- let audioCtx = new (window.AudioContext || window.webkitAudioContext)(), state = STATE_STOPPED, oscState = STATE_STOPPED, tone = config.tone || "sine", speed = config.speed || 1.0, muted = config.muted || false; //--------------------------------------------------------- // Main // [note-frequency] [note-duration-ms], OR // [note-frequency]-[note-frequency].. [note-duration], //--------------------------------------------------------- async function play(notes = "") { let lastSpeed = speed, repeats = 0; await stop(); state = STATE_PLAYING; notes = notes.replace("\t", " ").split(","); for (let i = 0; i < notes.length; i++) { if (state === STATE_STOPPING) { break; }; var note = notes[i].trim().split(" "); if (note[0] && note[0][0] === "%") { // We have a control code if (note[0][1] === "r") { let numRepeats = parseInt(note[0][2]); if (!numRepeats || repeats < numRepeats) { // Get the repeat times i = -1; repeats++; } } else if (note[0][1] === "t") { // Change the tone let toneNumber = parseInt(note[0][2]); if (toneNumber === 0) { tone = "sine"; } else if (toneNumber === 1) { tone = "square"; } else if (toneNumber === 2) { tone = "sawtooth"; } else if (toneNumber === 3) { tone = "triangle"; } } else if (note[0][1] === "s") { stop(); } } else { // We have either a single frequency or a group of frequencies let freqs = note[0].split("-"); // Convert the freqs to ints and scale for (let i = 0; i < freqs.length; i++) { freqs[i] = parseInt(freqs[i]) * lastSpeed; } let duration = parseInt(note[1]) / (lastSpeed || 1.0); if (freqs[0] && !muted) { await playNotes(freqs, duration, tone); } else { await playRest(duration); } } } state = STATE_STOPPED; } function playNotes(freqs, duration, tone) { let oscCount = freqs.length; oscState = STATE_PLAYING; return new Promise(function (resolve) { for (let freq of freqs) { let osc = audioCtx.createOscillator(); osc.type = tone || "sine"; osc.frequency.setValueAtTime(freq, audioCtx.currentTime); osc.connect(audioCtx.destination); osc.start(); setTimeout(function () { osc.onended = function () { oscCount--; if (oscCount === 0) { oscState = STATE_STOPPED; resolve(); } }; osc.stop(); }, audioCtx.currentTime + duration); } }); } function playRest(timeMs) { return new Promise(function (resolve) { setTimeout(function () { resolve(); }, timeMs); }); } function stop() { state = state !== STATE_STOPPED ? STATE_STOPPING : STATE_STOPPED; return new Promise(function (resolve) { let stoppedChecker = setInterval(function () { if (state === STATE_STOPPED && oscState === STATE_STOPPED) { clearInterval(stoppedChecker); resolve(); } }, 100); }); } return { play, stop, mute: function () { muted = true; }, unMute: function () { muted = false; } } } //--------------------------------------------------------- // Public API //--------------------------------------------------------- return { play: function (song, channel) { DEBUG && console.log("Playing"); channels[channel || 0].play(song); }, stop: function (channel) { if (channel) { channels[channel || 0].stop(); } else { for (const channel of channels) { channel.stop(); } } }, mute: function (channel) { DEBUG && console.log("Muting"); if (channel) { channels[channel || 0].mute(); } else { for (const channel of channels) { channel.mute(); } } }, unMute: function (channel) { DEBUG && console.log("Unmuting"); if (channel) { channels[channel || 0].unMute(); } else { for (const channel of channels) { channel.unMute(); } } } } } return { getInstance: function () { if (!instance) { instance = init(); } return instance; } } })();