Emscripten PCM backend

This commit is contained in:
kichikuou
2017-03-20 16:17:23 +09:00
parent 953d3115c7
commit 2850ca3152
6 changed files with 376 additions and 5 deletions
+218
View File
@@ -20,6 +20,10 @@ declare namespace FS {
function writeFile(path: string, data: ArrayBufferView | string, opts?: {encoding?: string; flags?: string; canOwn?: boolean}): void;
}
declare namespace EmterpreterAsync {
function handle(asyncOp: (resume:()=>void) => void): void;
}
namespace xsystem35 {
export const Font = { url: 'fonts/MTLc3m.ttf', fname: 'MTLc3m.ttf'};
export const xsys35rc = [
@@ -29,6 +33,12 @@ namespace xsystem35 {
].join('\n');
export var cdPlayer: CDPlayer;
export var shell: System35Shell;
export var audio: AudioManager;
export function waitPromise(p: ()=>Promise<any>) {
EmterpreterAsync.handle(function(resume) {
p().then(() => {resume()}, (reason) => { console.log(reason); resume() });
});
}
}
class System35Shell {
@@ -72,6 +82,7 @@ class System35Shell {
this.antialiasCheckbox = <HTMLInputElement>$('#antialias');
this.antialiasCheckbox.addEventListener('change', this.antialiasChanged.bind(this));
this.antialiasCheckbox.checked = localStorage.getItem('antialias') != 'false';
xsystem35.audio = new AudioManager();
}
run() {
@@ -286,4 +297,211 @@ class VolumeControl {
}
}
abstract class PCMSound {
protected context: AudioContext;
protected gain: GainNode;
protected duration_: number;
protected startTime: number;
constructor(protected dst: AudioNode) {
this.context = dst.context;
this.gain = this.context.createGain();
this.gain.connect(dst);
}
abstract start(loop: number): void;
abstract stop(): void;
setGain(gain: number) {
this.gain.gain.value = gain;
}
fadeout(msec: number) {
this.gain.gain.linearRampToValueAtTime(0, this.context.currentTime + msec / 1000);
}
getPosition(): number {
if (!this.startTime)
return 0;
return this.context.currentTime - this.startTime;
}
isPlaying(): boolean {
return !!this.startTime;
}
get duration(): number {
return this.duration_;
}
}
class PCMSoundSimple extends PCMSound {
private node: AudioBufferSourceNode;
constructor(dst: AudioNode, buf: AudioBuffer) {
super(dst);
this.node = this.context.createBufferSource();
this.node.buffer = buf;
this.node.connect(this.gain);
this.node.onended = this.onended.bind(this);
this.duration_ = buf.duration;
}
start(loop: number) {
if (loop == 0)
this.node.loop = true;
else if (loop != 1)
console.warn('Unsupported PCM loop count ' + loop);
this.node.start();
this.startTime = this.context.currentTime;
}
stop() {
if (this.startTime) {
this.node.stop();
this.startTime = null;
}
}
private onended() {
this.startTime = null;
}
}
class PCMSoundMixLR extends PCMSound {
private lsrc: AudioBufferSourceNode;
private rsrc: AudioBufferSourceNode;
private endCount = 0;
constructor(dst: AudioNode, lbuf: AudioBuffer, rbuf: AudioBuffer) {
super(dst);
this.lsrc = this.context.createBufferSource();
this.rsrc = this.context.createBufferSource();
this.lsrc.buffer = lbuf;
this.rsrc.buffer = rbuf;
var merger = this.context.createChannelMerger(2);
merger.connect(this.gain);
this.lsrc.connect(merger, 0, 0);
this.rsrc.connect(merger, 0, 1);
this.lsrc.onended = this.rsrc.onended = this.onended.bind(this);
this.duration_ = Math.max(lbuf.duration, rbuf.duration);
}
start(loop: number) {
if (loop != 1)
console.warn('PCMSoundMixLR: loop is not supported ' + loop);
this.lsrc.start();
this.rsrc.start();
this.startTime = this.context.currentTime;
}
stop() {
if (this.startTime) {
this.lsrc.stop();
this.rsrc.stop();
this.startTime = null;
}
}
private onended() {
this.endCount++;
if (this.endCount == 2)
this.startTime = null;
}
}
declare var webkitAudioContext:any;
class AudioManager {
private context: AudioContext;
private masterGain: GainNode;
private slots: PCMSound[];
private isSafari: boolean;
constructor() {
if (typeof(AudioContext) !== 'undefined') {
this.context = new AudioContext();
} else if (typeof(webkitAudioContext) !== 'undefined') {
this.context = new webkitAudioContext();
this.isSafari = true;
}
this.masterGain = this.context.createGain();
this.masterGain.connect(this.context.destination);
this.slots = [];
}
private load(ptr: number, size: number): Promise<AudioBuffer> {
var buf = Module.HEAPU8.buffer.slice(ptr, ptr + size);
if (this.isSafari) {
return new Promise((resolve, reject) => {
this.context.decodeAudioData(buf, resolve, reject);
});
}
return this.context.decodeAudioData(buf);
}
pcm_load(slot: number, ptr: number, size: number): Promise<any> {
this.pcm_stop(slot);
var start = performance.now();
return this.load(ptr, size).then((audioBuf) => {
console.log('pcm_load: ' + (performance.now() - start) + ' ms');
this.slots[slot] = new PCMSoundSimple(this.masterGain, audioBuf);
});
}
pcm_load_mixlr(slot: number, lptr: number, lsize: number, rptr: number, rsize: number): Promise<any> {
this.pcm_stop(slot);
var start = performance.now();
return Promise.all([this.load(lptr, lsize), this.load(rptr, rsize)]).then((bufs) => {
console.log('pcm_load_mixlr: ' + (performance.now() - start) + ' ms');
this.slots[slot] = new PCMSoundMixLR(this.masterGain, bufs[0], bufs[1]);
});
}
pcm_start(slot: number, loop: number): number {
if (this.slots[slot]) {
this.slots[slot].start(loop);
return 1;
}
return 0;
}
pcm_stop(slot: number): number {
if (!this.slots[slot])
return 0;
this.slots[slot].stop();
this.slots[slot] = null;
return 1;
}
pcm_fadeout(slot: number, msec: number): number {
if (!this.slots[slot])
return 0;
this.slots[slot].fadeout(msec);
return 1;
}
pcm_getpos(slot: number): number {
if (!this.slots[slot])
return 0;
return this.slots[slot].getPosition() * 1000;
}
pcm_setvol(slot: number, vol: number): number {
if (!this.slots[slot])
return 0;
this.slots[slot].setGain(vol / 100);
return 1;
}
pcm_getwavelen(slot: number): number {
if (!this.slots[slot])
return 0;
return this.slots[slot].duration * 1000;
}
pcm_isplaying(slot: number): number {
if (!this.slots[slot])
return 0;
return this.slots[slot].isPlaying() ? 1 : 0;
}
setVolume(vol: number) {
this.masterGain.gain.value = vol / 100;
}
}
xsystem35.shell = new System35Shell();