From ef7281f13deca65ae71a873bc76bd22b1eb259b8 Mon Sep 17 00:00:00 2001 From: nectarboy <59830504+nectarboy@users.noreply.github.com> Date: Thu, 19 Dec 2024 02:48:41 +0200 Subject: [PATCH 01/30] Implemented "Tie-in" functionality + a few misc fixes * A sequence using bank 0 no longer causes an exception * Mono mode is now set to true by default, i think this is the correct behavior --- OptimePlayer/OptimePlayer.js | 79 +++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/OptimePlayer/OptimePlayer.js b/OptimePlayer/OptimePlayer.js index 0a9bb2c..ed6172d 100644 --- a/OptimePlayer/OptimePlayer.js +++ b/OptimePlayer/OptimePlayer.js @@ -1233,12 +1233,13 @@ class SequenceTrack { this.id = id; this.active = false; + this.activeChannels = []; this.bpm = 0; this.pc = 0; this.pan = 64; - this.mono = false; + this.mono = true; // Testing suggests this defaults to true contrary to what some sources state ..? this.volume = 0; this.priority = 0; this.program = 0; @@ -1255,7 +1256,10 @@ class SequenceTrack { this.expression = 0; + this.tie = 0; + this.portamentoEnable = 0; + this.portamentoKey = 0; this.portamentoTime = 0; this.restingFor = 0; @@ -1349,6 +1353,10 @@ class SequenceTrack { this.debugLog("Velocity: " + velocity); this.debugLog("Duration: " + duration); + if (this.mono) { + this.restingFor = duration; + } + this.sendMessage(false, MessageType.PlayNote, opcode, velocity, duration); } else { switch (opcode) { @@ -1376,12 +1384,39 @@ class SequenceTrack { break; } + case 0xA0: // Random + { + var subopcode = this.readPcInc(); + var min = this.readPcInc(2); + var max = this.readPcInc(2); + // Sign extend to s16 + min = min << 16 >> 16; + max = max << 16 >> 16; + + var rand = Math.round(Math.random() * (max - min) + min); + + break; + } case 0xC7: // Mono / Poly { let param = this.readPcInc(); this.mono = bitTest(param, 0); break; } + case 0xC8: // Tie On / Off + { + this.tie = this.readPcInc(); + this.debugLog("Tie On / Off: " + this.tie); + // TODO: implement tie + break; + } + case 0xC9: // Portamento Control + { + this.portamentoKey = this.readPcInc(); + this.portamentoEnable = true; + this.debugLog("Portamento Key: " + this.portamentoKey); + break; + } case 0xCE: // Portamento On / Off { this.portamentoEnable = this.readPcInc(); @@ -1487,9 +1522,10 @@ class SequenceTrack { } case 0x94: // Jump { + var from = this.pc; let dest = this.readPcInc(3); this.pc = dest; - this.debugLogForce(`Jump to: ${hexN(dest, 6)} Tick: ${this.sequence.ticksElapsed}`); + this.debugLogForce(`Jump from ${hexN(from, 6)} to: ${hexN(dest, 6)} Tick: ${this.sequence.ticksElapsed}`); this.sendMessage(false, MessageType.Jump); break; @@ -1978,7 +2014,7 @@ class Controller { constructor(sampleRate, sdat, sseqId) { let sseqInfo = sdat.sseqInfos[sseqId]; if (!sseqInfo) throw new Error(); - if (!sseqInfo.bank) throw new Error(); + if (sseqInfo.bank === null) throw new Error(); this.bankInfo = sdat.sbnkInfos[sseqInfo.bank]; if (!this.bankInfo) throw new Error(); this.instrumentBank = sdat.instrumentBanks[sseqInfo.bank]; @@ -2143,7 +2179,7 @@ class Controller { } if (this.sequence.ticksElapsed >= entry.endTime && !entry.fromKeyboard) { - if (entry.adsrState !== AdsrState.Release) { + if (entry.adsrState !== AdsrState.Release && !this.sequence.tracks[entry.trackNum].tie) { this.notesOn[entry.trackNum][entry.midiNote] = 0; entry.adsrState = AdsrState.Release; } @@ -2262,6 +2298,10 @@ class Controller { } if (indexToDelete !== -1) { + var note = this.activeNoteData[indexToDelete]; + var indexToDeleteInTrackChannel = this.sequence.tracks[note.trackNum].activeChannels.indexOf(note); + if (indexToDeleteInTrackChannel !== -1) + this.sequence.tracks[note.trackNum].activeChannels.splice(indexToDeleteInTrackChannel, 1); this.activeNoteData.splice(indexToDelete, 1); } @@ -2328,12 +2368,27 @@ class Controller { console.log("Release Coefficient: " + instrument.releaseCoefficient[index]); } - let initialVolume = instrument.attackCoefficient[index] === 0 ? calcChannelVolume(velocity, 0) : 0; - let synthInstrIndex = this.synthesizers[msg.trackNum].play(sample, midiNote, initialVolume, this.sequence.ticksElapsed); + if (this.sequence.tracks[msg.trackNum].tie && this.sequence.tracks[msg.trackNum].activeChannels.length > 0) { + var lastNote = this.sequence.tracks[msg.trackNum].activeChannels[this.sequence.tracks[msg.trackNum].activeChannels.length - 1]; + console.log(lastNote); + lastNote.midiNote = midiNote; + lastNote.velocity = velocity; + lastNote.endTime = this.sequence.ticksElapsed + duration; + lastNote.adsrState = AdsrState.Attack; + lastNote.adsrTimer = -92544; // idk why this number, ask gbatek + lastNote.lfoCounter = 0; + lastNote.lfoDelayCounter = 0; + lastNote.delayCounter = 0; + + var instr = this.synthesizers[msg.trackNum].instrs[lastNote.synthInstrIndex]; + instr.setNote(midiNote); + } + else { + let initialVolume = instrument.attackCoefficient[index] === 0 ? calcChannelVolume(velocity, 0) : 0; + let synthInstrIndex = this.synthesizers[msg.trackNum].play(sample, midiNote, initialVolume, this.sequence.ticksElapsed); - this.notesOn[msg.trackNum][midiNote] = 1; - this.activeNoteData.push( - { + this.notesOn[msg.trackNum][midiNote] = 1; + var note = { trackNum: msg.trackNum, midiNote: midiNote, velocity: velocity, @@ -2348,8 +2403,10 @@ class Controller { lfoCounter: 0, lfoDelayCounter: 0, delayCounter: 0, - } - ); + }; + this.activeNoteData.push(note); + this.sequence.tracks[msg.trackNum].activeChannels.push(note); + } } break; case MessageType.Jump: { From 0cce54313dc59ec324ee12701cd9d32120e4439b Mon Sep 17 00:00:00 2001 From: nectarboy <59830504+nectarboy@users.noreply.github.com> Date: Thu, 19 Dec 2024 22:46:25 +0200 Subject: [PATCH 02/30] Implemented PitchSweep & Portamento (may still be buggy) + misc fixes * Fixed pitch bend sign extension * Tie commands now stop all of a track's active channels (on or off) --- OptimePlayer/OptimePlayer.js | 131 +++++++++++++++++++++++++++-------- 1 file changed, 101 insertions(+), 30 deletions(-) diff --git a/OptimePlayer/OptimePlayer.js b/OptimePlayer/OptimePlayer.js index ed6172d..36da777 100644 --- a/OptimePlayer/OptimePlayer.js +++ b/OptimePlayer/OptimePlayer.js @@ -1196,6 +1196,12 @@ class Sequence { this.tracks[i].execute(); } this.tracks[i].restingFor--; + + for (let index in this.tracks[i].activeChannels) { + var channel = this.tracks[i].activeChannels[index]; + if (!channel.autoSweep && channel.sweepCounter) + channel.sweepCounter--; + } } } } @@ -1251,6 +1257,8 @@ class SequenceTrack { this.lfoSpeed = 16; this.lfoDelay = 0; + this.transpose = 0; + this.pitchBend = 0; this.pitchBendRange = 0; @@ -1262,6 +1270,8 @@ class SequenceTrack { this.portamentoKey = 0; this.portamentoTime = 0; + this.sweepPitch = 0; + this.restingFor = 0; this.stack = new Uint32Array(64); @@ -1277,7 +1287,7 @@ class SequenceTrack { * @param {string} _msg */ debugLog(_msg) { - // console.log(`${this.id}: ${msg}`) + console.log(`${this.id}: ${_msg}`); } /** @@ -1346,10 +1356,11 @@ class SequenceTrack { let opcode = this.readPcInc(); if (opcode <= 0x7F) { + let note = opcode + this.transpose; let velocity = this.readPcInc(); let duration = this.readVariableLength(); - this.debugLog("Note: " + opcode); + this.debugLog("Note: " + note); this.debugLog("Velocity: " + velocity); this.debugLog("Duration: " + duration); @@ -1357,7 +1368,7 @@ class SequenceTrack { this.restingFor = duration; } - this.sendMessage(false, MessageType.PlayNote, opcode, velocity, duration); + this.sendMessage(false, MessageType.PlayNote, note, velocity, duration); } else { switch (opcode) { case 0xFE: // Allocate track @@ -1407,14 +1418,21 @@ class SequenceTrack { { this.tie = this.readPcInc(); this.debugLog("Tie On / Off: " + this.tie); - // TODO: implement tie + + // Apparently when tie is turned off, all currently playing channels immediately stop + this.lastActiveChannel = null; + for (let i in this.activeChannels) { + var channel = this.activeChannels[i]; + channel.stopFlag = true; + } + break; } case 0xC9: // Portamento Control { - this.portamentoKey = this.readPcInc(); - this.portamentoEnable = true; - this.debugLog("Portamento Key: " + this.portamentoKey); + this.portamentoKey = (this.readPcInc() + this.transpose) & 0xff; + this.portamentoEnable = 1; + this.debugLog("Portamento Control: " + this.portamentoKey); break; } case 0xCE: // Portamento On / Off @@ -1507,6 +1525,12 @@ class SequenceTrack { this.debugLog("LFO Range: " + this.lfoRange); break; } + case 0xC3: // Transpose + { + this.transpose = this.readPcInc() << 24 >> 24; + this.debugLog("Transpose: " + this.transpose); + break; + } case 0xC4: // Pitch Bend { this.pitchBend = this.readPcInc(); @@ -1555,6 +1579,12 @@ class SequenceTrack { this.debugLog("LFO Delay: " + this.lfoDelay); break; } + case 0xE3: // Sweep Pitch + { + this.sweepPitch = this.readPcInc(2) << 16 >> 16; + this.debugLog("Sweep Pitch: " + this.sweepPitch); + break; + } case 0xD5: // Expression { this.expression = this.readPcInc(); @@ -2178,10 +2208,12 @@ class Controller { this.synthesizers[entry.trackNum].cutInstrument(entry.synthInstrIndex); } - if (this.sequence.ticksElapsed >= entry.endTime && !entry.fromKeyboard) { - if (entry.adsrState !== AdsrState.Release && !this.sequence.tracks[entry.trackNum].tie) { + // Stop instruments that have exceeded their duration + if (entry.stopFlag || (this.sequence.ticksElapsed >= entry.endTime && !entry.fromKeyboard)) { + if (entry.adsrState !== AdsrState.Release) { this.notesOn[entry.trackNum][entry.midiNote] = 0; entry.adsrState = AdsrState.Release; + entry.adsrTimer = -92544; } } @@ -2228,6 +2260,17 @@ class Controller { lfoValue >>= 14n; } + var finetune; + if (entry.sweepPitch && entry.sweepCounter) { + finetune = entry.sweepPitch * (entry.sweepCounter / entry.sweepLength); + if (entry.autoSweep) + entry.sweepCounter--; + console.log(finetune); + } + else { + finetune = 0; + } + if (entry.delayCounter < track.lfoDelay) { entry.delayCounter++; } else { @@ -2245,14 +2288,18 @@ class Controller { switch (track.lfoType) { case LfoType.Pitch: // LFO value is in 1/64ths of a semitone - instr.setFinetuneLfo(Number(lfoValue) / 64); + finetune += Number(lfoValue); + //instr.setFinetuneLfo(Number(lfoValue) / 64); break; default: break; } } + } + instr.setFinetuneLfo((finetune) / 64); + // all thanks to @ipatix at pret/pokediamond switch (entry.adsrState) { case AdsrState.Attack: @@ -2300,8 +2347,11 @@ class Controller { if (indexToDelete !== -1) { var note = this.activeNoteData[indexToDelete]; var indexToDeleteInTrackChannel = this.sequence.tracks[note.trackNum].activeChannels.indexOf(note); - if (indexToDeleteInTrackChannel !== -1) + if (indexToDeleteInTrackChannel !== -1) { this.sequence.tracks[note.trackNum].activeChannels.splice(indexToDeleteInTrackChannel, 1); + if (this.sequence.tracks[note.trackNum].lastActiveChannel === note) + this.sequence.tracks[note.trackNum].lastActiveChannel = null; + } this.activeNoteData.splice(indexToDelete, 1); } @@ -2329,7 +2379,8 @@ class Controller { // refers to the archive ID referred to by the corresponding SBNK entry in the INFO block /** @type {InstrumentRecord} */ - let instrument = this.instrumentBank.instruments[this.sequence.tracks[msg.trackNum].program]; + let track = this.sequence.tracks[msg.trackNum]; + let instrument = this.instrumentBank.instruments[track.program]; let index = instrument.resolveEntryIndex(midiNote); let archiveIndex = instrument.swarInfoId[index]; @@ -2368,27 +2419,28 @@ class Controller { console.log("Release Coefficient: " + instrument.releaseCoefficient[index]); } - if (this.sequence.tracks[msg.trackNum].tie && this.sequence.tracks[msg.trackNum].activeChannels.length > 0) { - var lastNote = this.sequence.tracks[msg.trackNum].activeChannels[this.sequence.tracks[msg.trackNum].activeChannels.length - 1]; - console.log(lastNote); - lastNote.midiNote = midiNote; - lastNote.velocity = velocity; - lastNote.endTime = this.sequence.ticksElapsed + duration; - lastNote.adsrState = AdsrState.Attack; - lastNote.adsrTimer = -92544; // idk why this number, ask gbatek - lastNote.lfoCounter = 0; - lastNote.lfoDelayCounter = 0; - lastNote.delayCounter = 0; - - var instr = this.synthesizers[msg.trackNum].instrs[lastNote.synthInstrIndex]; + var channel = null; + if (track.tie && track.lastActiveChannel) { + channel = track.lastActiveChannel; //track.activeChannels[track.activeChannels.length - 1]; + var instr = this.synthesizers[msg.trackNum].instrs[channel.synthInstrIndex]; instr.setNote(midiNote); + + channel.midiNote = midiNote; + channel.velocity = velocity; + channel.endTime = this.sequence.ticksElapsed + duration; + //lastNote.adsrState = AdsrState.Attack; + //lastNote.adsrTimer = -92544; // idk why this number, ask gbatek + channel.lfoCounter = 0; + channel.lfoDelayCounter = 0; + channel.delayCounter = 0; } else { let initialVolume = instrument.attackCoefficient[index] === 0 ? calcChannelVolume(velocity, 0) : 0; let synthInstrIndex = this.synthesizers[msg.trackNum].play(sample, midiNote, initialVolume, this.sequence.ticksElapsed); this.notesOn[msg.trackNum][midiNote] = 1; - var note = { + channel = { + stopFlag: false, trackNum: msg.trackNum, midiNote: midiNote, velocity: velocity, @@ -2402,11 +2454,30 @@ class Controller { fromKeyboard: msg.fromKeyboard, lfoCounter: 0, lfoDelayCounter: 0, - delayCounter: 0, + delayCounter: 0 }; - this.activeNoteData.push(note); - this.sequence.tracks[msg.trackNum].activeChannels.push(note); + this.activeNoteData.push(channel); + track.activeChannels.push(channel); + track.lastActiveChannel = channel; + } + + var sweepPitch = track.sweepPitch + (track.portamentoEnable !== 0) * ((track.portamentoKey - midiNote) << 6); + track.portamentoKey = midiNote; + var sweepLength; + var autoSweep; + if (this.portamentoTime) { + sweepLength = (track.portamentoTime * track.portamentoTime * Math.abs(track.sweepPitch)) >> 11; + autoSweep = true; } + else { + sweepLength = duration; + autoSweep = false; + } + + channel.sweepPitch = sweepPitch; + channel.sweepCounter = sweepLength; + channel.sweepLength = sweepLength; + channel.autoSweep = autoSweep; } break; case MessageType.Jump: { @@ -2436,7 +2507,7 @@ class Controller { } case MessageType.PitchBend: { let track = this.sequence.tracks[msg.trackNum]; - let pitchBend = track.pitchBend << 25 >> 25; // sign extend + let pitchBend = track.pitchBend << 24 >> 24; // sign extend pitchBend *= track.pitchBendRange / 2; // pitch bend specified in 1/64 of a semitone this.synthesizers[msg.trackNum].setFinetune(pitchBend / 64); From d4a3d5b19fb0fbd0b27d24c26d1e305db90a8ddf Mon Sep 17 00:00:00 2001 From: nectarboy <59830504+nectarboy@users.noreply.github.com> Date: Thu, 19 Dec 2024 22:55:44 +0200 Subject: [PATCH 03/30] oopsie fixed --- OptimePlayer/OptimePlayer.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/OptimePlayer/OptimePlayer.js b/OptimePlayer/OptimePlayer.js index 36da777..124af01 100644 --- a/OptimePlayer/OptimePlayer.js +++ b/OptimePlayer/OptimePlayer.js @@ -2208,14 +2208,19 @@ class Controller { this.synthesizers[entry.trackNum].cutInstrument(entry.synthInstrIndex); } - // Stop instruments that have exceeded their duration - if (entry.stopFlag || (this.sequence.ticksElapsed >= entry.endTime && !entry.fromKeyboard)) { + if (entry.stopFlag) { if (entry.adsrState !== AdsrState.Release) { this.notesOn[entry.trackNum][entry.midiNote] = 0; entry.adsrState = AdsrState.Release; entry.adsrTimer = -92544; } } + else if (this.sequence.ticksElapsed >= entry.endTime && !entry.fromKeyboard) { + if (entry.adsrState !== AdsrState.Release) { + this.notesOn[entry.trackNum][entry.midiNote] = 0; + entry.adsrState = AdsrState.Release; + } + } // LFO code based off pret/pokediamond let track = this.sequence.tracks[entry.trackNum]; @@ -2430,9 +2435,9 @@ class Controller { channel.endTime = this.sequence.ticksElapsed + duration; //lastNote.adsrState = AdsrState.Attack; //lastNote.adsrTimer = -92544; // idk why this number, ask gbatek - channel.lfoCounter = 0; - channel.lfoDelayCounter = 0; - channel.delayCounter = 0; + // channel.lfoCounter = 0; + // channel.lfoDelayCounter = 0; + // channel.delayCounter = 0; } else { let initialVolume = instrument.attackCoefficient[index] === 0 ? calcChannelVolume(velocity, 0) : 0; From 9a9669b0703c2c6c33ee3a73faa5dbcda7330e54 Mon Sep 17 00:00:00 2001 From: nectarboy <59830504+nectarboy@users.noreply.github.com> Date: Tue, 24 Dec 2024 02:19:20 +0200 Subject: [PATCH 04/30] Partially implemented random and variable sequence commands (only like 2 opcodes work with them atm because laziness) --- OptimePlayer/OptimePlayer.js | 200 ++++++++++++++++++++++++++++------- 1 file changed, 160 insertions(+), 40 deletions(-) diff --git a/OptimePlayer/OptimePlayer.js b/OptimePlayer/OptimePlayer.js index 124af01..0bbc57f 100644 --- a/OptimePlayer/OptimePlayer.js +++ b/OptimePlayer/OptimePlayer.js @@ -953,7 +953,7 @@ const MessageType = { TrackEnded: 3, VolumeChange: 4, // P0: Volume PanChange: 5, // P0: Pan (0-127) - PitchBend: 6, + PitchBend: 6 }; class Sample { @@ -1175,8 +1175,12 @@ class Sequence { this.messageBuffer = messageBuffer; /** @type {SequenceTrack[]} */ + this.vars = new Int16Array(32); this.tracks = new Array(16); + for (let i = 0; i < 32; i++) { + this.vars[i] = !(i & 7) * 0xffff; // Source: Kermalis + } for (let i = 0; i < 16; i++) { this.tracks[i] = new SequenceTrack(this, i); } @@ -1192,16 +1196,15 @@ class Sequence { if (!this.paused) { for (let i = 0; i < 16; i++) { if (this.tracks[i].active) { - while (this.tracks[i].restingFor === 0) { - this.tracks[i].execute(); - } - this.tracks[i].restingFor--; - for (let index in this.tracks[i].activeChannels) { var channel = this.tracks[i].activeChannels[index]; if (!channel.autoSweep && channel.sweepCounter) channel.sweepCounter--; } + while (this.tracks[i].restingFor === 0) { + this.tracks[i].execute(); + } + this.tracks[i].restingFor--; } } } @@ -1209,6 +1212,21 @@ class Sequence { this.ticksElapsed++; } + /** + * @param {number} id + */ + readVar(id) { + return this.vars[id & 0x1f]; // TODO: What happens when we read OOB ? + } + /** + * @param {number} id + * @param {number} val + */ + writeVar(id, val) { + this.vars[id & 0x1f] = val; + } + + /** * @param {number} num * @param {number} pc @@ -1228,6 +1246,12 @@ class Sequence { } } +const ParamOverride = { + Null: 0, + Random: 1, + Variable: 2 +}; + class SequenceTrack { /** * @param {Sequence} sequence @@ -1237,6 +1261,7 @@ class SequenceTrack { /** @type {Sequence} */ this.sequence = sequence; this.id = id; + this.paramOverride = ParamOverride.Null; this.active = false; this.activeChannels = []; @@ -1245,8 +1270,8 @@ class SequenceTrack { this.pc = 0; this.pan = 64; - this.mono = true; // Testing suggests this defaults to true contrary to what some sources state ..? - this.volume = 0; + this.mono = true; + this.volume = 0x7f; // TODO: does the synthesizer need to be updated accordingly ? this.priority = 0; this.program = 0; this.bank = 0; @@ -1260,14 +1285,14 @@ class SequenceTrack { this.transpose = 0; this.pitchBend = 0; - this.pitchBendRange = 0; + this.pitchBendRange = 2; this.expression = 0; this.tie = 0; this.portamentoEnable = 0; - this.portamentoKey = 0; + this.portamentoKey = 60; this.portamentoTime = 0; this.sweepPitch = 0; @@ -1275,19 +1300,22 @@ class SequenceTrack { this.restingFor = 0; this.stack = new Uint32Array(64); + this.loopStack = new Uint32Array(64); + this.loopStackCount = new Uint8Array(this.loopStack.length); this.sp = 0; + this.loopSp = 0; - this.attackRate = 0; - this.decayRate = 0; - this.sustainRate = 0; - this.releaseRate = 0; + this.attackRate = 0xff; + this.decayRate = 0xff; + this.sustainRate = 0xff; + this.releaseRate = 0xff; } /** * @param {string} _msg */ - debugLog(_msg) { - console.log(`${this.id}: ${_msg}`); + debugLog(msg) { + //console.log(`${this.id}: ${msg}`); } /** @@ -1310,6 +1338,23 @@ class SequenceTrack { return this.stack[--this.sp]; } + pushLoop(val, count) { + this.loopStack[this.loopSp] = val; + this.loopStackCount[this.loopSp++] = count; + if (this.loopSp >= this.loopStack.length) alert("SSEQ loop stack overflow"); + } + + popLoop() { + if (this.loopSp === 0) alert("SSEQ loop stack underflow"); + var i = this.loopSp - 1; + var val = this.loopStack[i]; + if (this.loopStackCount[i]) { + this.loopStackCount[i]--; + this.loopSp -= this.loopStackCount[i] === 0; + } + return val; + } + readPc() { return this.sequence.sseqFile.getUint8(this.pc + this.sequence.dataOffset); } @@ -1340,6 +1385,34 @@ class SequenceTrack { return num; } + readRandom() { + this.paramOverride = ParamOverride.Null; + var min = this.readPcInc(2) << 16 >> 16; + var max = this.readPcInc(2) << 16 >> 16; + return Math.round(Math.random() * (max - min) + min); + } + readVariable() { + this.paramOverride = ParamOverride.Null; + return this.sequence.readVar(this.readPcInc()); + } + + readLastPcInc(bytes = 1) { + if (!this.paramOverride) + return this.readPcInc(bytes); + else if (this.paramOverride === ParamOverride.Random) + return this.readRandom(); + else if (this.paramOverride === ParamOverride.Variable) + return this.readVariable(); + } + readLastVariableLength() { + if (!this.paramOverride) + return this.readVariableLength(); + else if (this.paramOverride === ParamOverride.Random) + return this.readRandom(); + else if (this.paramOverride === ParamOverride.Variable) + return this.readVariable(); + } + /** * @param {boolean} fromKeyboard * @param {number} type @@ -1351,14 +1424,18 @@ class SequenceTrack { this.sequence.messageBuffer.insert(new Message(fromKeyboard, this.id, type, param0, param1, param2)); } - execute() { - let opcodePc = this.pc; - let opcode = this.readPcInc(); + executeOpcode(opcode) { + //c1 cb if (opcode <= 0x7F) { let note = opcode + this.transpose; + if (note < 0) + note = 0; + else if (note > 0x7f) + note = 0x7f; + let velocity = this.readPcInc(); - let duration = this.readVariableLength(); + let duration = this.isSubOpcode ? this.subParam : this.readVariableLength(); this.debugLog("Note: " + note); this.debugLog("Velocity: " + velocity); @@ -1397,17 +1474,16 @@ class SequenceTrack { } case 0xA0: // Random { - var subopcode = this.readPcInc(); - var min = this.readPcInc(2); - var max = this.readPcInc(2); - // Sign extend to s16 - min = min << 16 >> 16; - max = max << 16 >> 16; - - var rand = Math.round(Math.random() * (max - min) + min); - + this.debugLog('RANDOM, opcode is ' + hexN(this.readPc(),2)); + this.paramOverride = ParamOverride.Random; break; - } + } + case 0xA1: // Variable + { + this.debugLog('VARIABLE, opcode is ' + hexN(this.readPc(),2)); + this.paramOverride = ParamOverride.Variable; + break; + } case 0xC7: // Mono / Poly { let param = this.readPcInc(); @@ -1430,7 +1506,12 @@ class SequenceTrack { } case 0xC9: // Portamento Control { - this.portamentoKey = (this.readPcInc() + this.transpose) & 0xff; + this.portamentoKey = (this.readPcInc() + this.transpose); + if (this.portamentoKey < 0) + this.portamentoKey = 0; + else if (this.portamentoKey > 0x7f) + this.portamentoKey = 0x7f; + this.portamentoEnable = 1; this.debugLog("Portamento Control: " + this.portamentoKey); break; @@ -1455,9 +1536,9 @@ class SequenceTrack { } case 0xC1: // Volume { - this.volume = this.readPcInc(); + this.volume = this.readLastPcInc() & 0xff; this.sendMessage(false, MessageType.VolumeChange, this.volume); - this.debugLog("Volume: " + this.volume); + //this.debugLogForce("Volume: " + this.volume); break; } case 0x81: // Set bank and program @@ -1475,6 +1556,7 @@ class SequenceTrack { { this.masterVolume = this.readPcInc(); this.debugLogForce("Master Volume: " + this.masterVolume); + console.warn('UNIMPLEMENTED MASTER VOLUME'); break; } case 0xC0: // Pan @@ -1506,7 +1588,7 @@ class SequenceTrack { } case 0xCB: // LFO Speed { - this.lfoSpeed = this.readPcInc(); + this.lfoSpeed = this.readLastPcInc() & 0xff; this.debugLog("LFO Speed: " + this.lfoSpeed); break; } @@ -1533,7 +1615,7 @@ class SequenceTrack { } case 0xC4: // Pitch Bend { - this.pitchBend = this.readPcInc(); + this.pitchBend = this.readLastPcInc(); this.debugLog("Pitch Bend: " + this.pitchBend); this.sendMessage(false, MessageType.PitchBend); break; @@ -1568,9 +1650,22 @@ class SequenceTrack { this.pc = this.pop(); break; } - case 0xB0: // TODO: According to sseq2mid: arithmetic operations? + case 0xB0: // Set Variable { - this.readPcInc(3); + var index = this.readPcInc(); + this.sequence.writeVar(index, this.readPcInc(2) << 16 >> 16); + break; + } + case 0xB1: // Add Variable + { + var index = this.readPcInc(); + this.sequence.writeVar(index, this.sequence.readVar(index) + (this.readPcInc(2) << 16 >> 16)); + break; + } + case 0xB2: // Subtract Variable + { + var index = this.readPcInc(); + this.sequence.writeVar(index, this.sequence.readVar(index) - (this.readPcInc(2) << 16 >> 16)); break; } case 0xE0: // LFO Delay @@ -1585,18 +1680,34 @@ class SequenceTrack { this.debugLog("Sweep Pitch: " + this.sweepPitch); break; } + case 0xD4: // Loop Start + { + //this.debugLogForce('Loop Start ' + this.pc); + var count = this.readPcInc(); + this.pushLoop(this.pc, count); + break; + } case 0xD5: // Expression { this.expression = this.readPcInc(); this.debugLog("Expression: " + this.expression); break; } + case 0xFC: // Loop End + { + if (this.loopSp > 0) { + this.pc = this.popLoop(); + //this.debugLogForce('Loop End, back to ' + this.pc); + } + break; + } case 0xFF: // End of Track { this.sequence.endTrack(this.id); this.sendMessage(false, MessageType.TrackEnded); // Set restingFor to non-zero since the controller checks it to stop executing this.restingFor = 1; + this.debugLogForce("Track hit a FIN, id " + this.id); break; } case 0xD0: // Attack Rate @@ -1624,10 +1735,19 @@ class SequenceTrack { break; } default: - console.error(`${this.id}: Unknown opcode: ` + hex(opcode, 2) + " PC: " + hex(opcodePc, 6)); + console.error(`${this.id}: Unknown opcode: ` + hex(opcode, 2) + " PC: " + hex(this.pc - 1, 6)); } } } + + execute() { + this.isSubOpcode = false; + + let opcodePc = this.pc; + let opcode = this.readPcInc(); + + this.executeOpcode(opcode); + } } class DelayLine { @@ -2227,7 +2347,7 @@ class Controller { let lfoValue; if (track.lfoDepth === 0) { lfoValue = BigInt(0); - } else if (entry.lfoDelayCounter < track.lfoDelay) { + } else if (entry.lfoDelayCounter++ < track.lfoDelay) { lfoValue = BigInt(0); } else { /** @@ -2250,6 +2370,7 @@ class Controller { lfoValue = BigInt(SND_SinIdx(entry.lfoCounter >>> 8) * track.lfoDepth * track.lfoRange); } + // OPTIMIZE if (lfoValue !== 0n) { switch (track.lfoType) { case LfoType.Volume: @@ -2270,7 +2391,6 @@ class Controller { finetune = entry.sweepPitch * (entry.sweepCounter / entry.sweepLength); if (entry.autoSweep) entry.sweepCounter--; - console.log(finetune); } else { finetune = 0; From 0e96c7ae934a7b344bda338fd4c01b90bee55277 Mon Sep 17 00:00:00 2001 From: nectarboy <59830504+nectarboy@users.noreply.github.com> Date: Wed, 25 Dec 2024 00:39:58 +0200 Subject: [PATCH 05/30] Added the ability to play SSAR sequences! --- OptimePlayer/OptimePlayer.js | 403 +++++++++++++++++++++++++---------- 1 file changed, 295 insertions(+), 108 deletions(-) diff --git a/OptimePlayer/OptimePlayer.js b/OptimePlayer/OptimePlayer.js index 0bbc57f..a62f14a 100644 --- a/OptimePlayer/OptimePlayer.js +++ b/OptimePlayer/OptimePlayer.js @@ -503,6 +503,9 @@ class Sdat { this.sseqInfos = []; this.sseqNameIdDict = new Map(); this.sseqIdNameDict = new Map(); + this.ssarNameIdDict = new Map(); + this.ssarIdNameDict = new Map(); + this.ssarSseqSymbols = []; this.sbnkNameIdDict = new Map(); this.sbnkIdNameDict = new Map(); @@ -599,6 +602,22 @@ class Sdat { let fileView = createRelativeDataView(view, fileOffs, fileSize); // SYMB processing + function readCString(view, start) { + let str = ''; + let offs = 0; + + // Read C string from symbol + let char; + do { + char = read8(view, start + offs++); + if (char !== 0) { + str += String.fromCharCode(char); + } + } while (char !== 0); + + return str; + } + { // SSEQ symbols let symbSseqListOffs = read32LE(symbView, 0x8); @@ -614,22 +633,9 @@ class Sdat { for (let i = 0; i < symbSseqListNumEntries; i++) { let sseqNameOffs = read32LE(symbView, symbSseqListOffs + 4 + i * 4); - let sseqNameArr = []; - let sseqNameCharOffs = 0; - - // Read C string from symbol - let char; - do { - char = read8(symbView, sseqNameOffs + sseqNameCharOffs); - sseqNameCharOffs++; - if (char !== 0) { - sseqNameArr.push(char); - } - } while (char !== 0); - // for some reason games have a ton of empty symbols -- skip them if (sseqNameOffs !== 0) { - let seqName = String.fromCharCode(...sseqNameArr); + let seqName = readCString(symbView, sseqNameOffs); sdat.sseqNameIdDict.set(seqName, i); sdat.sseqIdNameDict.set(i, seqName); @@ -643,6 +649,45 @@ class Sdat { let symbSsarListNumEntries = read32LE(symbView, symbSsarListOffs); console.log("SYMB Number of SSAR entries: " + symbSsarListNumEntries); + + sdat.ssarSseqSymbols.length = 0; + for (let i = 0; i < symbSsarListNumEntries; i++) { + let ssarNameOffs = read32LE(symbView, symbSsarListOffs + i * 8 + 4); + + // for some reason games have a ton of empty symbols -- skip them + if (ssarNameOffs !== 0) { + let ssarName = readCString(symbView, ssarNameOffs); + + sdat.ssarNameIdDict.set(ssarName, i); + sdat.ssarIdNameDict.set(i, ssarName); + } + + // Sub-SSEQ symbols for this SSAR + let symbSsarSseqListOffs = read32LE(symbView, symbSsarListOffs + i*8 + 8); + let symbSsarSseqListNumEntries = read32LE(symbView, symbSsarSseqListOffs); + if (symbSsarSseqListNumEntries) { + sdat.ssarSseqSymbols[i] = { + ssarSseqNameIdDict: new Map(), + ssarSseqIdNameDict: new Map() + }; + } + else { + sdat.ssarSseqSymbols[i] = null; + } + //console.log("SYMB Number of Sub-SSEQ entries for SSAR_" + i + ": " + symbSsarSseqListNumEntries); + + for (let ii = 0; ii < symbSsarSseqListNumEntries; ii++) { + let ssarSseqNameOffs = read32LE(symbView, symbSsarSseqListOffs + 4 + ii*4); + + // for some reason games have a ton of empty symbols -- skip them + if (ssarSseqNameOffs !== 0) { + let ssarSeqName = readCString(symbView, ssarSseqNameOffs); + + sdat.ssarSseqSymbols[i].ssarSseqNameIdDict.set(ssarSeqName, ii); + sdat.ssarSseqSymbols[i].ssarSseqIdNameDict.set(ii, ssarSeqName); + } + } + } } { @@ -654,24 +699,12 @@ class Sdat { console.log("SYMB Number of BANK entries: " + symbBankListNumEntries); for (let i = 0; i < symbBankListNumEntries; i++) { - let symbNameOffs = read32LE(symbView, symbBankListOffs + 4 + i * 4); - if (i === 0) console.log("NDS file addr of BANK list 1st entry: " + hexN(view.byteOffset + symbOffs + symbNameOffs, 8)); - - let bankNameArr = []; - let bankNameCharOffs = 0; - // Read C string from symbol - let char; - do { - char = read8(symbView, symbNameOffs + bankNameCharOffs); - bankNameCharOffs++; - if (char !== 0) { - bankNameArr.push(char); - } - } while (char !== 0); + let bankNameOffs = read32LE(symbView, symbBankListOffs + 4 + i * 4); + if (i === 0) console.log("NDS file addr of BANK list 1st entry: " + hexN(view.byteOffset + symbOffs + bankNameOffs, 8)); // for some reason games have a ton of empty symbols -- skip them - if (symbNameOffs !== 0) { - let bankName = String.fromCharCode(...bankNameArr); + if (bankNameOffs !== 0) { + let bankName = readCString(symbView, bankNameOffs); sdat.sbnkNameIdDict.set(bankName, i); sdat.sbnkIdNameDict.set(i, bankName); @@ -680,7 +713,7 @@ class Sdat { } { - // SWAR symbols + // SWAR symbols (TODO) let symbSwarListOffs = read32LE(symbView, 0x14); let symbSwarListNumEntries = read32LE(symbView, symbSwarListOffs); @@ -1538,7 +1571,7 @@ class SequenceTrack { { this.volume = this.readLastPcInc() & 0xff; this.sendMessage(false, MessageType.VolumeChange, this.volume); - //this.debugLogForce("Volume: " + this.volume); + this.debugLogForce("Volume: " + this.volume); break; } case 0x81: // Set bank and program @@ -1707,7 +1740,7 @@ class SequenceTrack { this.sendMessage(false, MessageType.TrackEnded); // Set restingFor to non-zero since the controller checks it to stop executing this.restingFor = 1; - this.debugLogForce("Track hit a FIN, id " + this.id); + this.debugLogForce("Track hit a FIN"); break; } case 0xD0: // Attack Rate @@ -2155,13 +2188,52 @@ const sLfoSinTable = [ class Controller { /** - @param {number} sampleRate - @param {Sdat} sdat - @param sampleRate - @param sdat - @param {number} sseqId + * @param {number} sampleRate */ - constructor(sampleRate, sdat, sseqId) { + constructor(sampleRate) { + + /** @type {Sample[][]} */ + this.decodedSampleArchives = []; + + /** @type {CircularBuffer} */ + this.messageBuffer = new CircularBuffer(1024); + this.sequence = null; + + /** @type {Uint8Array[]} */ + this.notesOn = []; + this.notesOnKeyboard = []; + for (let i = 0; i < 16; i++) { + this.notesOn[i] = new Uint8Array(128); + this.notesOnKeyboard[i] = new Uint8Array(128); + } + + /** @type {SampleSynthesizer[]} */ + this.synthesizers = new Array(16); + for (let i = 0; i < 16; i++) { + this.synthesizers[i] = new SampleSynthesizer(sampleRate, 16); + } + + this.jumps = 0; + this.fadingStart = false; + /** + * @type {{ trackNum: number; midiNote: number; velocity: number; synthInstrIndex: number; startTime: number; endTime: number; instrument: InstrumentRecord; instrumentEntryIndex: number; adsrState: number; adsrTimer: number; // idk why this number, ask gbatek + fromKeyboard: boolean; lfoCounter: number; lfoDelayCounter: number; delayCounter: number; }[]} + */ + this.activeNoteData = []; + this.bpmTimer = 0; + /** + * @type {number | null} + */ + this.activeKeyboardTrackNum = null; + } + + /** + * @param {Sdat} sdat + * @param {number} sseqId + */ + loadSseq(sdat, sseqId) { + this.sdat = sdat; + let sseqInfo = sdat.sseqInfos[sseqId]; if (!sseqInfo) throw new Error(); if (sseqInfo.bank === null) throw new Error(); @@ -2177,8 +2249,110 @@ class Controller { let sseqFile = sdat.fat.get(sseqInfo.fileId); if (!sseqFile) throw new Error(); - /** @type {Sample[][]} */ - this.decodedSampleArchives = []; + this.decodeSampleArchives(); + + let dataOffset = read32LE(sseqFile, 0x18); + if (dataOffset !== 0x1C) alert("SSEQ offset is not 0x1C? it is: " + hex(dataOffset, 8)); + + /** @type {CircularBuffer} */ + this.messageBuffer = new CircularBuffer(1024); + this.sequence = new Sequence(sseqFile, dataOffset, this.messageBuffer); + + /** @type {Uint8Array[]} */ + // this.notesOn = []; + // this.notesOnKeyboard = []; + // for (let i = 0; i < 16; i++) { + // this.notesOn[i] = new Uint8Array(128); + // this.notesOnKeyboard[i] = new Uint8Array(128); + // } + + /** @type {SampleSynthesizer[]} */ + // this.synthesizers = new Array(16); + // for (let i = 0; i < 16; i++) { + // this.synthesizers[i] = new SampleSynthesizer(sampleRate, 16); + // } + + this.jumps = 0; + this.fadingStart = false; + /** + * @type {{ trackNum: number; midiNote: number; velocity: number; synthInstrIndex: number; startTime: number; endTime: number; instrument: InstrumentRecord; instrumentEntryIndex: number; adsrState: number; adsrTimer: number; // idk why this number, ask gbatek + fromKeyboard: boolean; lfoCounter: number; lfoDelayCounter: number; delayCounter: number; }[]} + */ + this.activeNoteData = []; + this.bpmTimer = 0; + /** + * @type {number | null} + */ + this.activeKeyboardTrackNum = null; + } + + /** + * @param {Sdat} sdat + * @param {number} ssarId + * @param {number} subSseqId + */ + loadSsarSeq(sdat, ssarId, subSseqId) { + console.log('Loading SSAR: ' + ssarId + ', Sub-Seq: ' + subSseqId); + + this.sdat = sdat; + + let ssarInfo = sdat.ssarInfos[ssarId]; + if (!ssarInfo) throw new Error(); + let ssarFile = sdat.fat.get(ssarInfo.fileId); + if (!ssarFile) throw new Error(); + + let ssarListNumEntries = read32LE(ssarFile, 28); + let ssarListOffs = 32 + subSseqId * 12; + + let bank = read16LE(ssarFile, ssarListOffs + 4); + this.bankInfo = sdat.sbnkInfos[bank]; + if (!this.bankInfo) throw new Error(); + console.log('SSAR bank ID: ' + bank); + this.instrumentBank = sdat.instrumentBanks[bank]; + if (!this.instrumentBank) throw new Error(); + + this.decodeSampleArchives(); + + let dataOffset = read32LE(ssarFile, 24); + if (dataOffset !== ssarListNumEntries * 12 + 32) alert("SSEQ offset is not ssarListNumEntries * 12 + 32? it is: " + hex(dataOffset, 8)); + + /** @type {CircularBuffer} */ + this.messageBuffer = new CircularBuffer(1024); + this.sequence = new Sequence(ssarFile, dataOffset, this.messageBuffer); + + let trackPCOffset = read32LE(ssarFile, ssarListOffs); + this.sequence.tracks[0].pc = trackPCOffset; + + /** @type {Uint8Array[]} */ + // this.notesOn = []; + // this.notesOnKeyboard = []; + // for (let i = 0; i < 16; i++) { + // this.notesOn[i] = new Uint8Array(128); + // this.notesOnKeyboard[i] = new Uint8Array(128); + // } + + /** @type {SampleSynthesizer[]} */ + // this.synthesizers = new Array(16); + // for (let i = 0; i < 16; i++) { + // this.synthesizers[i] = new SampleSynthesizer(sampleRate, 16); + // } + + this.jumps = 0; + this.fadingStart = false; + /** + * @type {{ trackNum: number; midiNote: number; velocity: number; synthInstrIndex: number; startTime: number; endTime: number; instrument: InstrumentRecord; instrumentEntryIndex: number; adsrState: number; adsrTimer: number; // idk why this number, ask gbatek + fromKeyboard: boolean; lfoCounter: number; lfoDelayCounter: number; delayCounter: number; }[]} + */ + this.activeNoteData = []; + this.bpmTimer = 0; + /** + * @type {number | null} + */ + this.activeKeyboardTrackNum = null; + } + + decodeSampleArchives() { + this.decodedSampleArchives.length = 0; let nSamples = 0; let sSamples = 0; @@ -2186,11 +2360,11 @@ class Controller { for (let i = 0; i < 4; i++) { let decodedArchive = []; let swarId = this.bankInfo.swarId[i]; - let swarInfo = sdat.swarInfos[swarId]; + let swarInfo = this.sdat.swarInfos[swarId]; if (swarInfo != null) { console.log(`Linked archive: ${this.bankInfo.swarId[0]}`); if (swarInfo.fileId == null) throw new Error(); - let swarFile = sdat.fat.get(swarInfo.fileId); + let swarFile = this.sdat.fat.get(swarInfo.fileId); if (swarFile == null) throw new Error(); let sampleCount = read32LE(swarFile, 0x38); @@ -2272,41 +2446,6 @@ class Controller { console.log(`Program ${i}: ${typeString}\nLinked archive ${instrument.swarInfoId[0]} Sample ${instrument.swavInfoId[0]}`); } } - - let dataOffset = read32LE(sseqFile, 0x18); - if (dataOffset !== 0x1C) alert("SSEQ offset is not 0x1C? it is: " + hex(dataOffset, 8)); - - this.sdat = sdat; - /** @type {CircularBuffer} */ - this.messageBuffer = new CircularBuffer(1024); - this.sequence = new Sequence(sseqFile, dataOffset, this.messageBuffer); - - /** @type {Uint8Array[]} */ - this.notesOn = []; - this.notesOnKeyboard = []; - for (let i = 0; i < 16; i++) { - this.notesOn[i] = new Uint8Array(128); - this.notesOnKeyboard[i] = new Uint8Array(128); - } - - /** @type {SampleSynthesizer[]} */ - this.synthesizers = new Array(16); - for (let i = 0; i < 16; i++) { - this.synthesizers[i] = new SampleSynthesizer(sampleRate, 16); - } - - this.jumps = 0; - this.fadingStart = false; - /** - * @type {{ trackNum: number; midiNote: number; velocity: number; synthInstrIndex: number; startTime: number; endTime: number; instrument: InstrumentRecord; instrumentEntryIndex: number; adsrState: number; adsrTimer: number; // idk why this number, ask gbatek - fromKeyboard: boolean; lfoCounter: number; lfoDelayCounter: number; delayCounter: number; }[]} - */ - this.activeNoteData = []; - this.bpmTimer = 0; - /** - * @type {number | null} - */ - this.activeKeyboardTrackNum = null; } tick() { @@ -2652,44 +2791,20 @@ function bitTest(i, bit) { return (i & (1 << bit)) !== 0; } -/** - * @param {Sdat} sdat - * @param {number} id - */ -async function playSeqById(sdat, id) { - await playSeq(sdat, sdat.sseqIdNameDict.get(id)); -} /** - * @param {Sdat} sdat - * @param {string} name + * @param {AudioPlayer} player + * @param {Controller} controller + * @param {FsVisController} fsVisController */ -async function playSeq(sdat, name) { - g_currentlyPlayingSdat = sdat; - g_currentlyPlayingName = name; - if (g_currentController) { - await g_currentPlayer?.ctx.close(); - } - - const BUFFER_SIZE = 1024; - let player = new AudioPlayer(BUFFER_SIZE, synthesizeMore, null); - g_currentPlayer = player; +function playController(player, controller, fsVisController) { + const BUFFER_SIZE = player.bufferLength; const SAMPLE_RATE = player.sampleRate; console.log("Playing with sample rate: " + SAMPLE_RATE); - let id = sdat.sseqNameIdDict.get(name); - - g_currentlyPlayingId = id; - let bufferL = new Float64Array(BUFFER_SIZE); let bufferR = new Float64Array(BUFFER_SIZE); - let fsVisController = new FsVisController(sdat, id, 384 * 5); - let controller = new Controller(SAMPLE_RATE, sdat, id); - - g_currentController = controller; - currentFsVisController = fsVisController; - let timer = 0; function synthesizeMore() { @@ -2703,7 +2818,7 @@ async function playSeq(sdat, name) { timer -= 64 * 2728 * SAMPLE_RATE; controller.tick(); - fsVisController.tick(); + //fsVisController.tick(); TODO: what exactly does this component do ...? } let valL = 0; @@ -2722,10 +2837,82 @@ async function playSeq(sdat, name) { player.queueAudio(bufferL, bufferR); } + player.needMoreSamples = synthesizeMore; synthesizeMore(); } +/** + * @param {Sdat} sdat + * @param {string} name + */ +async function playSeq(sdat, name) { + g_currentlyPlayingSdat = sdat; + g_currentlyPlayingName = name; + if (g_currentController) { + await g_currentPlayer?.ctx.close(); + } + + const BUFFER_SIZE = 1024; + let player = new AudioPlayer(BUFFER_SIZE, null, null); + g_currentPlayer = player; + const SAMPLE_RATE = player.sampleRate; + console.log("Playing with sample rate: " + SAMPLE_RATE); + + let id = sdat.sseqNameIdDict.get(name); + + g_currentlyPlayingId = id; + + let fsVisController = new FsVisController(sdat, id, 384 * 5); + let controller = new Controller(SAMPLE_RATE); + controller.loadSseq(sdat, id); + + g_currentController = controller; + currentFsVisController = fsVisController; + + playController(player, controller, fsVisController); +} + +/** + * @param {Sdat} sdat + * @param {number} id + */ +async function playSeqById(sdat, id) { + await playSeq(sdat, sdat.sseqIdNameDict.get(id)); +} + +/** + * @param {Sdat} sdat + * @param {string} ssarName + * @param {number} seqId + */ +async function playSsarSeq(sdat, ssarName, seqId) { + g_currentlyPlayingSdat = sdat; + g_currentlyPlayingName = name; + if (g_currentController) { + await g_currentPlayer?.ctx.close(); + } + + const BUFFER_SIZE = 1024; + let player = new AudioPlayer(BUFFER_SIZE, null, null); + g_currentPlayer = player; + const SAMPLE_RATE = player.sampleRate; + console.log("Playing with sample rate: " + SAMPLE_RATE); + + let ssarId = sdat.ssarNameIdDict.get(ssarName); + + // g_currentlyPlayingId = id; // TODO: make another variable for ssar id + + let fsVisController = null; //new FsVisController(sdat, id, 384 * 5); // TODO: make this object compatible with loading SSARs + let controller = new Controller(SAMPLE_RATE); + controller.loadSsarSeq(sdat, ssarId, seqId); + + g_currentController = controller; + currentFsVisController = fsVisController; + + playController(player, controller, null); +} + /** * @param {Sample} sample */ From dfbaa731a10a2c0af1031e8c971c9411adad21df Mon Sep 17 00:00:00 2001 From: nectarboy <59830504+nectarboy@users.noreply.github.com> Date: Wed, 25 Dec 2024 00:48:56 +0200 Subject: [PATCH 06/30] Forgot to push HTML changes for SSAR playback --- index.html | 8 ++++---- index.js | 27 +++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index 01c3271..9a143e5 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - Optime Player + Optime Player DX