soundsmith/src/player.ts

304 lines
10 KiB
TypeScript

/** Copyright 2017 Sean Kasun */
class Player {
private es5503: ES5503;
private ctx?: AudioContext;
private audioNode?: ScriptProcessorNode;
private stereo: boolean = true;
private timer: number = 0;
private tempo: number = 0;
private curRow: number = 0;
private curPat: number = 0;
private orders: number[] = [];
private volTable: number[] = [];
private rowOffset: number = 0;
private numInst: number = 0;
private ticksLeft: number = 0;
private notes: Uint8Array;
private effects1: Uint8Array;
private effects2: Uint8Array;
private instruments: Uint8Array[] = [];
private compactTable: Uint16Array = new Uint16Array(16);
private stereoTable: Uint16Array = new Uint16Array(16);
private curInst: Uint8Array = new Uint8Array(16);
private arpeggio: Uint8Array = new Uint8Array(16);
private tone: Uint8Array = new Uint8Array(16);
private notice: (pat: number, max: number) => void;
private frequencies: number[] = [
0x0000, 0x0016, 0x0017, 0x0018, 0x001a, 0x001b, 0x001d, 0x001e,
0x0020, 0x0022, 0x0024, 0x0026, 0x0029, 0x002b, 0x002e, 0x0031,
0x0033, 0x0036, 0x003a, 0x003d, 0x0041, 0x0045, 0x0049, 0x004d,
0x0052, 0x0056, 0x005c, 0x0061, 0x0067, 0x006d, 0x0073, 0x007a,
0x0081, 0x0089, 0x0091, 0x009a, 0x00a3, 0x00ad, 0x00b7, 0x00c2,
0x00ce, 0x00d9, 0x00e6, 0x00f4, 0x0102, 0x0112, 0x0122, 0x0133,
0x0146, 0x015a, 0x016f, 0x0184, 0x019b, 0x01b4, 0x01ce, 0x01e9,
0x0206, 0x0225, 0x0246, 0x0269, 0x028d, 0x02b4, 0x02dd, 0x0309,
0x0337, 0x0368, 0x039c, 0x03d3, 0x040d, 0x044a, 0x048c, 0x04d1,
0x051a, 0x0568, 0x05ba, 0x0611, 0x066e, 0x06d0, 0x0737, 0x07a5,
0x081a, 0x0895, 0x0918, 0x09a2, 0x0a35, 0x0ad0, 0x0b75, 0x0c23,
0x0cdc, 0x0d9f, 0x0e6f, 0x0f4b, 0x1033, 0x112a, 0x122f, 0x1344,
0x1469, 0x15a0, 0x16e9, 0x1846, 0x19b7, 0x1b3f, 0x1cde, 0x1e95,
0x2066, 0x2254, 0x245e, 0x2688,
];
constructor(music: Handle, wavebank: Handle,
notice: (pat: number, max: number) => void) {
this.notice = notice;
this.es5503 = new ES5503((osc: number): void => { this.irq(osc); });
this.loadWavebank(wavebank);
music.seek(6);
const blockLen: number = music.r16();
this.tempo = music.r16();
this.es5503.setFrequency(30, 0xfa);
this.es5503.setVolume(30, 0);
this.es5503.setPointer(30, 0);
this.es5503.setSize(30, 0);
this.es5503.setEnabled(0x3c);
this.es5503.setControl(30, 8); // freerun + interrupts - halt
music.seek(0x2c);
for (let i: number = 0; i < 15; i++) {
this.volTable.push(music.r16());
music.skip(0x1c);
}
music.seek(0x1d6);
const songLen: number = music.r16() & 0xff;
for (let i: number = 0; i < songLen; i++) {
this.orders.push(music.r8() * 64 * 14);
}
music.seek(0x258);
this.notes = music.read(blockLen);
this.effects1 = music.read(blockLen);
this.effects2 = music.read(blockLen);
for (let i: number = 0; i < 16; i++) {
this.stereoTable[i] = music.r16();
}
this.rowOffset = this.orders[this.curPat];
this.notice(this.curPat + 1, this.orders.length);
}
public play(): void {
try {
this.ctx = new ((<any>window).AudioContext || (<any>window).webkitAudioContext)();
if (this.ctx == undefined) {
throw new Error("No audio support!");
}
} catch (e) {
alert(e.message);
return;
}
this.audioNode = this.ctx.createScriptProcessor(0, 0, 2);
this.audioNode.onaudioprocess = (evt: AudioProcessingEvent) => {
this.render(evt);
};
this.audioNode.connect(this.ctx.destination);
}
public stop(): void {
if (this.audioNode) {
this.audioNode.disconnect();
}
this.audioNode = undefined;
if (this.ctx) {
this.ctx.close();
}
this.ctx = undefined;
}
private loadWavebank(wavebank: Handle): void {
wavebank.seek(0);
if (wavebank.r4() == 'GSWV') { // gswv wavebank
const ofs: number = wavebank.r16();
this.numInst = wavebank.r8();
wavebank.skip(this.numInst * 10); // skip instrument names
for (let i: number = 0; i < this.numInst; i++) {
const instLen: number = (wavebank.r8() + wavebank.r8()) * 6;
this.instruments.push(wavebank.read(instLen));
}
wavebank.seek(ofs);
const tbl: Uint8Array = new Uint8Array(0x10000);
tbl.set(wavebank.read(wavebank.length - ofs));
} else { // regular wavebank
wavebank.seek(0);
this.numInst = wavebank.r16() & 0xff;
this.es5503.setRam(wavebank.read(0x10000));
wavebank.seek(0x10022);
for (let i: number = 0; i < this.numInst; i++) {
this.instruments.push(wavebank.read(12));
wavebank.skip(0x50);
}
wavebank.skip(0x3c);
for (let i: number = 0; i < 16; i++) {
this.compactTable[i] = wavebank.r16();
}
}
}
private render(evt: AudioProcessingEvent): void {
const sampleRate: number = evt.outputBuffer.sampleRate;
const leftBuf: Float32Array = evt.outputBuffer.getChannelData(0);
const rightBuf: Float32Array = evt.outputBuffer.getChannelData(1);
for (let i: number = 0; i < evt.outputBuffer.length; i++) {
// Oscillators update at 26320 Hz
this.ticksLeft -= 26320;
if (this.ticksLeft <= 0) {
this.ticksLeft += sampleRate;
this.es5503.tick();
}
const [left, right] = this.es5503.render();
if (!this.stereo) { // mix down to mono
leftBuf[i] = (left + right) * 0.707;
rightBuf[i] = leftBuf[i];
} else {
leftBuf[i] = left;
rightBuf[i] = right;
}
}
}
private irq(osc: number): void {
if (osc != 30) { // not a timer
this.es5503.go(osc);
return;
}
this.timer++;
if (this.timer == this.tempo) {
this.timer = 0;
for (let oscillator: number = 0; oscillator < 14; oscillator++) {
const semitone: number = this.notes[this.rowOffset];
if (semitone == 0 || (semitone & 0x80)) {
this.rowOffset++;
if (semitone == 0x80) {
this.es5503.setControl(oscillator * 2, 1); // halt
this.es5503.setControl(oscillator * 2 + 1, 1); // halt pair
} else if (semitone == 0x81) {
this.curRow = 0x3f;
}
} else {
let fx: number = this.effects1[this.rowOffset];
if (fx & 0xf0) { // change instrument?
this.curInst[oscillator] = (fx >> 4) - 1;
}
const inst: number = this.curInst[oscillator];
let volume: number = this.volTable[inst] >> 1;
fx &= 0xf;
if (fx == 0) {
this.arpeggio[oscillator] = this.effects2[this.rowOffset];
this.tone[oscillator] = semitone;
} else {
this.arpeggio[oscillator] = 0;
if (fx == 3) {
volume = this.effects2[this.rowOffset] >> 1;
this.es5503.setVolume(oscillator * 2, volume);
this.es5503.setVolume(oscillator * 2 + 1, volume);
} else if (fx == 6) {
volume -= this.effects2[this.rowOffset] >> 1;
volume = Math.max(volume, 0);
this.es5503.setVolume(oscillator * 2, volume);
this.es5503.setVolume(oscillator * 2 + 1, volume);
} else if (fx == 5) {
volume += this.effects2[this.rowOffset] >> 1;
volume = Math.min(volume, 0x7f);
this.es5503.setVolume(oscillator * 2, volume);
this.es5503.setVolume(oscillator * 2 + 1, volume);
} else if (fx == 0xf) {
this.tempo = this.effects2[this.rowOffset];
}
}
const addr: number = oscillator * 2;
this.es5503.stop(addr);
this.es5503.stop(addr + 1);
if (inst < this.numInst) {
let x = 0;
while (this.instruments[inst][x] < semitone) {
x += 6;
}
const oscAptr: number = this.instruments[inst][x + 1];
const oscAsiz: number = this.instruments[inst][x + 2];
let oscActl: number = this.instruments[inst][x + 3] & 0xf;
if (this.stereoTable[oscillator]) {
oscActl |= 0x10;
}
while (this.instruments[inst][x] != 0x7f) {
x += 6;
}
x += 6; // skip last
while (this.instruments[inst][x] < semitone) {
x += 6;
}
const oscBptr: number = this.instruments[inst][x + 1];
const oscBsiz: number = this.instruments[inst][x + 2];
let oscBctl: number = this.instruments[inst][x + 3] & 0xf;
if (this.stereoTable[oscillator]) {
oscBctl |= 0x10;
}
const freq: number = this.frequencies[semitone] >>
this.compactTable[inst];
this.es5503.setFrequency(addr, freq);
this.es5503.setFrequency(addr + 1, freq); // pair
this.es5503.setVolume(addr, volume);
this.es5503.setVolume(addr + 1, volume); // pair
this.es5503.setPointer(addr, oscAptr);
this.es5503.setPointer(addr + 1, oscBptr); // pair
this.es5503.setSize(addr, oscAsiz);
this.es5503.setSize(addr + 1, oscBsiz); // pair
this.es5503.setControl(addr, oscActl);
this.es5503.setControl(addr + 1, oscBctl); // pair
}
this.rowOffset++;
}
}
this.curRow++;
if (this.curRow < 0x40) {
return;
}
// advance pattern
this.curRow = 0;
this.curPat++;
if (this.curPat < this.orders.length) {
this.notice(this.curPat + 1, this.orders.length);
this.rowOffset = this.orders[this.curPat];
return;
}
// stopped
this.notice(0, 0);
this.stop();
return;
} else { // between notes. Apply arpeggio
for (let oscillator: number = 0; oscillator < 14; oscillator++) {
const a: number = this.arpeggio[oscillator];
if (a) {
switch (this.timer % 6) {
case 1: case 4:
this.tone[oscillator] += a >> 4;
break;
case 2: case 5:
this.tone[oscillator] += a & 0xf;
break;
case 0: case 3:
this.tone[oscillator] -= a >> 4;
this.tone[oscillator] -= a & 0xf;
break;
}
const freq: number = this.frequencies[this.tone[oscillator]] >>
this.compactTable[oscillator];
const addr: number = oscillator * 2;
this.es5503.setFrequency(addr, freq);
this.es5503.setFrequency(addr + 1, freq); // pair
}
}
}
}
}