// // sseqPlayer.js //-------------------- // Provides an interface for playing SSEQs onto an AudioContext. // by RHY3756547 // // window.SSEQWaveCache = new (function() { var cache = []; var sdat, ctx; this.cacheWaveArc = function(num) { if (cache[num] == null) { var warinfo = sdat.sections["$INFO"][3] if (warinfo[num] == null) return; var arc = warinfo[num].arc.samples; if (arc == null) return; cache[num] = []; for (var i=0; i0) { t.threads.splice(threadsToKill.pop(), 1); } if (t.threads.length == 0 && ctx.currentTime > t.lastNoteEnd) t.dead = true; } function startThread(pc) { var thread = new SSEQThread(sseqHead.seq.data, pc, t); t.threads.push(thread); } function terminateThread(thread) { threadsToKill.push(t.threads.indexOf(thread)); } function setTempo(bpm) { //sets tempo of threads and alters their wait times to adjust t.bpm = bpm*t.bpmMultiplier; } function loadBank(bn) { t.bank = sdat.sections["$INFO"][2][bn]; if (t.bank == null) {return;} for (var i=0; i<4; i++) { if (t.bank.waveArcs[i] != 0xFFFF) SSEQWaveCache.cacheWaveArc(t.bank.waveArcs[i]); } } function cutNoteShort(thread, note) { try { //can throw exception if note has already ended. if (note.ended) return; var time = thread.calculateCurrentTime(); var baseTime = (time == Infinity)?ctx.currentTime:time; if (baseTime > note.noteEndsAt) return; var releaseTime = note.relTime; note.note.gain.cancelScheduledValues(baseTime); note.note.gain.linearRampToValueAtTime(0, baseTime+releaseTime); //then release note.src.stop(baseTime+releaseTime); if (baseTime+releaseTime > t.lastNoteEnd) t.lastNoteEnd = baseTime+releaseTime; } catch (e) {} } function setTranspose(newT) { t.transpose = newT; for (var i=0; i>11)/1000; source.playbackRate.exponentialRampToValueAtTime(targetFreq, baseTime+time); } } //sequence the note var atk = (thread.attack != null)?thread.attack:inst.attack; var dec = (thread.decay != null)?thread.decay:inst.decay; var sus = (thread.sustain != null)?thread.sustain:inst.sustainLvl; var rel = (thread.release != null)?thread.release:inst.release; var attackTime = calculateRequiredAttackCycles(convertAttToRate(atk))*CYCLE_TIME;//(255/convertAttToRate(inst.attack))*0.016; //0.01; var decayTime = (92544/convertFallToRate(dec))*(1-sus/0x7F)*CYCLE_TIME/2; var releaseTime = (92544/convertFallToRate(rel))*(sus/0x7F)*CYCLE_TIME/2; if ((!thread.tie) || thread.lastNote == null) { note.gain.value = 0.0; note.gain.setValueAtTime(0.0, baseTime); //initially 0 note.gain.linearRampToValueAtTime(velocity, baseTime+attackTime); //attack note.gain.linearRampToValueAtTime(velocity*sus/0x7F, baseTime+attackTime+decayTime); //decay source.start(baseTime); source.onended = function(){ note.ended = true; source.disconnect(); } } if (realDur != Infinity) { if (baseTime+attackTime+decayTime < baseTime+realDur) note.gain.linearRampToValueAtTime(velocity*sus/0x7F, baseTime+realDur); //sustain until note.gain.linearRampToValueAtTime(0, baseTime+realDur+releaseTime); //then release source.stop(baseTime+realDur+releaseTime); if (baseTime+realDur+releaseTime > t.lastNoteEnd) t.lastNoteEnd = baseTime+realDur+releaseTime; } return {src: source, base: inst.freq, start:num, note: note, relTime: releaseTime, snd: snd, noteEndsAt:baseTime+realDur}; } function calculateRequiredAttackCycles(att) { var value = 92544; var ticks = 0; while (value > 0) { value = Math.floor((att*value)/255); ticks++ } return ticks; } function convertAttToRate(attack) { var table = [0x00, 0x01, 0x05, 0x0E, 0x1A, 0x26, 0x33, 0x3F, 0x49, 0x54, 0x5C, 0x64, 0x6D, 0x74, 0x7B, 0x7F, 0x84, 0x89, 0x8F]; if (attack & 0x80) return 0; else if (attack >= 0x6F) return table[0x7F-attack]; else return 0xFF-attack; } function convertFallToRate(fall) { if (fall&0x80) return 0; else if (fall == 0x7F) return 0xFFFF; else if (fall == 0x7E) return 0x3C00; else if (fall < 0x32) return ((fall<<1)+1)&0xFFFF; else return (0x1E00/(0x7E-fall))&0xFFFF; } function noteToFreq(n) { return Math.pow(2, (n-49)/12)*440; } function getInst(inst, note) { switch (inst.type) { case 0: return null; case 1: return inst; case 2: return inst.entries[Math.max(inst.lower, Math.min(inst.upper, note))-inst.lower]; case 3: for (var i=0; i 10000) { Instructions[0xFF](); console.error("audio thread locked up")}; } if (t.wait == Infinity && t.lastNote != null && t.lastNote.note.ended) Instructions[0xFF](); } function noteOn(num) { if (num == 0) return; //NOP var velocity = forcableValue(true); var length = forcableValueFunc(false, readVariableLength); if (length == 0) length = Infinity; t.lastNote = player.playNote(t, velocity, length, num); if (t.noteWait) t.wait += length; } function ticksToMs(ticks) { return (ticks/48)*(60000/player.bpm); } function readVariableLength() { var read = prog[pc++]; var value = read&0x7F; while (read & 0x80) { var read = prog[pc++]; value = (value<<7) | (read&0x7F); } return value; } function calculateCurrentTime() { return player.baseAudioTime+ticksToMs(t.wait-player.remainder)/1000; } var InstArgs = [ //starts at 0x80 [readVariableLength], [readVariableLength], [], [], [], [], [], [], [], [], [], [], [], [], [], [], //0x80-0x8F [], [], [], [read8, read24], [read24], [read24], [], [], [], [], [], [], [], [], [], [], //0x90-0x9F [read8, readSpecial, read16, read16], [read8, readSpecial], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [read8, read8], [read8, read8], [read8, read8], [read8, read8], [read8, read8], [read8, read8], [read8, read8], [], [read8, read8], [read8, read8], [read8, read8], [read8, read8], [read8, read8], [read8, read8], [], [], //0xB0-0xBF [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [read8], [], [], [], [], [], [], [], [], [], [read16], [read16], [read16], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], ] var Instructions = []; Instructions[0xFE] = function() { //track definition player.trackAlloc = read16(); } Instructions[0x93] = function() { //track definition var trackID = prog[pc++]; var newPC = prog[pc++]; newPC |= prog[pc++]<<8; newPC |= prog[pc++]<<16; var bit = 1<>7; if (bank != 0) player.loadBank(bank); } Instructions[0x94] = function() { //JUMP var newPC = prog[pc++]; newPC |= prog[pc++]<<8; newPC |= prog[pc++]<<16; pc = newPC; } Instructions[0x95] = function() { //CALL var newPC = prog[pc++]; newPC |= prog[pc++]<<8; newPC |= prog[pc++]<<16; t.stack.push(pc); pc = newPC; } Instructions[0xFD] = function() { //RETURN if (t.stack.length == 0) Instructions[0xFF](); pc = t.stack.pop(); } //LOGIC INSTRUCTIONS Instructions[0xA0] = function() { //random force = true; //this command forces the input to the next command to be a generated random number forceCommand = prog[pc++]; if (forceCommand < 0x80 || (forceCommand >= 0xB0 && forceCommand <= 0xBD)) forceSpecial = prog[pc++]; var min = reads16(); var max = reads16(); forceValue = Math.floor(Math.random()*(max-min+1))+min; } Instructions[0xA1] = function() { //from var force = true; //this command forces the input to the next command to be from a variable. use with caution probably! forceCommand = prog[pc++]; if (forceCommand < 0x80 || (forceCommand >= 0xB0 && forceCommand <= 0xBD)) forceSpecial = prog[pc++]; forceValue = player.vars[prog[pc++]]; } function varInst(inst){ var varNum = forcableValue(true); var arg = forcableValue(); if (arg & 0x80) arg -= 256; if (inst == 0xB4 && arg == 0) return; varFunc[inst-0xB0](varNum, arg) } var varFunc = [ //"=", "+=", "-=", "*=", "/=", "[Shift]", "[Rand]" function(a, b) { player.vars[a] = b }, function(a, b) { player.vars[a] += b }, function(a, b) { player.vars[a] -= b }, function(a, b) { player.vars[a] *= b }, function(a, b) { player.vars[a] = Math.floor(player.vars[a]/b) }, function(a, b) { if (b < 0) player.vars[a] = player.vars[a]>>(-b); else player.vars[a] = player.vars[a]<= b }, function(a, b) { return player.vars[a] > b }, function(a, b) { return player.vars[a] <= b }, function(a, b) { return player.vars[a] < b }, function(a, b) { return player.vars[a] != b }, ] Instructions[0xB8] = boolInst; Instructions[0xB9] = boolInst; Instructions[0xBA] = boolInst; Instructions[0xBB] = boolInst; Instructions[0xBC] = boolInst; Instructions[0xBD] = boolInst; Instructions[0xA2] = function() { //if# if (!comparisonResult) { //skip next var inst = prog[pc++]; if (inst < 0x80) { read8(); readVariableLength(); } else { var cmds = InstArgs[inst-0x80]; var last = 0; for (var i=0; i0); } //mono/poly Instructions[0xC8] = function() { t.tie = prog[pc++]; if (t.lastNote != null) player.cutNoteShort(t, t.lastNote); t.lastNote = null; } //tie Instructions[0xC9] = function() { t.portaKey = prog[pc++]; } //portamento control Instructions[0xCA] = function() { var value = forcableValue(); } //modulation depth Instructions[0xCB] = function() { var value = forcableValue(); } //modulation speed Instructions[0xCC] = function() { var value = prog[pc++]; } //modulation type Instructions[0xCD] = function() { var value = prog[pc++]; } //modulation range Instructions[0xCE] = function() { t.portaKey = (t.portaKey&0x7F)|((!prog[pc++])<<7); } //portamento on/off Instructions[0xCF] = function() { t.portaTime = forcableValue(); } //portamento time Instructions[0xD0] = function() { t.attack = forcableValue(); } //attack rate Instructions[0xD1] = function() { t.decay = forcableValue(); } //decay rate Instructions[0xD2] = function() { t.sustain = forcableValue(); } //sustain rate Instructions[0xD3] = function() { t.release = forcableValue(); } //release rate Instructions[0xD4] = function() { t.loopTimes = forcableValue(); t.loopPtr = pc; } //loop start Instructions[0xFC] = function() { if (t.loopTimes-- > 0) pc = t.loopPtr; } //loop end Instructions[0xD5] = function() { var value = forcableValue(); } //expression Instructions[0xD6] = function() { var value = prog[pc++]; } //print variable Instructions[0xE0] = function() { var value = prog[pc++]; value |= prog[pc++]<<8 } //modulation delay Instructions[0xE1] = function() { var value = prog[pc++]; value |= prog[pc++]<<8; player.setTempo(value); } //set BPM Instructions[0xE3] = function() { t.sweepPitch = forcableValueFunc(false, reads16); } //sweep pitch Instructions[0xFF] = function() { if (t.lastNote != null) player.cutNoteShort(t, t.lastNote); player.terminateThread(t); t.dead = true; } //end of track function read16() { var value = prog[pc++]; value |= prog[pc++]<<8; return value; } function reads16() { var value = read16(); if (value & 0x8000) value -= 0x10000; return value; } function read8() { return prog[pc++]; } function readSpecial(last) { if (last < 0x80 || (last >= 0xB0 && last < 0xBD)) return prog[pc++]; else return 0; } function read24() { var value = prog[pc++]; value |= prog[pc++]<<8; value |= prog[pc++]<<16; return value; } function forcableValueFunc(special, func) { if (force) return special?forceSpecial:forceValue; else return func(); } function forcableValue(special) { if (force) return special?forceSpecial:forceValue; else return prog[pc++]; } function setPan(value) { t.pan = value; if (value > 0) { gainR.gain.value = 1; gainL.gain.value = 1-value; } else { gainR.gain.value = 1+value; gainL.gain.value = 1; } } function noteToFreq(n) { return Math.pow(2, (n-49)/12)*440; } }