adventure-engine
Version:
Simple text based adventure game library.
225 lines (203 loc) • 7.24 kB
JavaScript
//=========================================================
// 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
//=========================================================
;
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;
}
}
})();