soundsmith/src/ftaplayer.ts

173 lines
5.0 KiB
TypeScript

/** Copyright 2017 Sean Kasun */
interface Note {
freq: number;
time: number;
}
class Channel {
public ticks: number = 1;
public offset: number = 0;
public pos: number = 0;
public notes: Note[] = [];
public osc: number = 0;
// instrument
public pointer: number = 0;
public size: number = 0;
public volume: number = 0;
public panning: number = 0;
public control: number = 0;
public freq: number = 0;
}
class FTAPlayer {
private stereo: boolean = true;
private es5503: ES5503;
private ctx?: AudioContext;
private audioNode?: ScriptProcessorNode;
private ticksLeft: number = 0;
private channels: Channel[] = [];
private active: number[] = [];
constructor(music: Handle, wavebank: Handle, inst: Handle, delta: number) {
this.es5503 = new ES5503((osc: number): void => { this.irq(osc); });
wavebank.seek(0);
this.es5503.setRam(wavebank.read(wavebank.length));
this.es5503.setEnabled(0x3e);
for (let i: number = 0; i < 32; i++) {
this.channels.push(new Channel());
}
inst.seek(0);
let base: number = inst.r16();
while (base != 0xffff) {
const ch: number = inst.r8();
this.active.push(ch);
this.channels[ch].osc = ch;
this.channels[ch].pointer = inst.r8();
this.channels[ch].size = inst.r8();
this.channels[ch].volume = inst.r8();
this.channels[ch].panning = inst.r8();
this.channels[ch].control = inst.r8();
music.seek(base - delta);
const channelLen: number = music.r16();
for (let i: number = 0; i < channelLen; i++) {
const freq: number = music.r16();
const time: number = music.r8();
this.channels[ch].notes.push({freq, time});
}
base = inst.r16();
}
inst.seek(0x46);
this.es5503.setFrequency(31, inst.r16());
this.es5503.setControl(31, 8); // freerun + interrupt - halt
}
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 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 != 31) { // not a timer
const ch: Channel = this.channels[osc & 0xfc];
this.noteOff(ch);
this.noteOn(ch);
} else {
for (const c of this.active) {
const ch: Channel = this.channels[c];
ch.ticks--;
if (ch.ticks == 0) {
this.noteOff(ch);
ch.ticks = ch.notes[ch.pos].time;
ch.freq = ch.notes[ch.pos].freq;
ch.pos++;
if (ch.pos >= ch.notes.length) {
ch.pos = 0;
}
this.noteOn(ch);
ch.osc ^= 2; // swap oscillator pairs
}
}
}
}
private noteOn(ch: Channel): void {
this.es5503.setFrequency(ch.osc, ch.freq * 2);
this.es5503.setFrequency(ch.osc + 1, ch.freq * 2); // pair
const vol: number = ch.volume & 0xf;
this.es5503.setVolume(ch.osc, vol << 3); // scale to ch.master
this.es5503.setVolume(ch.osc + 1, vol << 3); // scale to ch.master
this.es5503.setVolume(ch.osc ^ 2, vol << 3);
this.es5503.setVolume((ch.osc ^ 2) + 1, vol << 3);
this.es5503.setPointer(ch.osc, ch.pointer);
this.es5503.setPointer(ch.osc + 1, ch.pointer);
this.es5503.setSize(ch.osc, ch.size);
this.es5503.setSize(ch.osc + 1, ch.size);
let a: number = 2; // one shot left
let b: number = 0x12; // one shot right
if (ch.panning == 0) { // pan left
b = 2;
} else if (ch.panning == 1) { // pan right
a = 0x12;
}
this.es5503.setControl(ch.osc, a | ch.control);
this.es5503.setControl(ch.osc + 1, b | ch.control);
}
private noteOff(ch: Channel): void {
this.es5503.setControl(ch.osc, 7); // halt + swap - interrupt
this.es5503.setControl(ch.osc + 1, 7); // pair
}
}