From 2850ca31521744c19eb40c47208a47cc2e840a40 Mon Sep 17 00:00:00 2001 From: kichikuou Date: Mon, 20 Mar 2017 14:34:51 +0900 Subject: [PATCH] Emscripten PCM backend --- Makefile.emscripten | 2 +- emterpretify_whitelist | 2 +- src/music_pcm.h | 2 - src/pcm.emscripten.c | 155 ++++++++++++++++++++++++++++ web/shell/shell.ts | 218 ++++++++++++++++++++++++++++++++++++++++ web/shell/tsconfig.json | 2 +- 6 files changed, 376 insertions(+), 5 deletions(-) create mode 100644 src/pcm.emscripten.c diff --git a/Makefile.emscripten b/Makefile.emscripten index 92b7da3..b1ca76f 100644 --- a/Makefile.emscripten +++ b/Makefile.emscripten @@ -25,6 +25,6 @@ src/xsystem35: src src/Makefile ln -sf src/xsystem35 xsystem35.bc src/Makefile: - emconfigure ./configure --disable-shared --enable-gtk=no --enable-sdl --enable-music-server=no --enable-cdrom=emscripten CFLAGS='-s USE_ZLIB=1' FT2_CONFIG=tools/freetype-config + emconfigure ./configure --disable-shared --enable-gtk=no --enable-sdl --enable-cdrom=emscripten --enable-midi=no CFLAGS='-s USE_ZLIB=1' FT2_CONFIG=tools/freetype-config .PHONY: src diff --git a/emterpretify_whitelist b/emterpretify_whitelist index c86ebd9..eb1a042 100644 --- a/emterpretify_whitelist +++ b/emterpretify_whitelist @@ -1 +1 @@ - -s EMTERPRETIFY_WHITELIST='["_ags_eCopyArea", "_ags_fadeIn", "_ags_fadeOut", "_ags_fader", "_ags_whiteIn", "_ags_whiteOut", "_checkMessage", "_check_command", "_commandCD", "_commandCE", "_commandH", "_commandHH", "_commandIK", "_commandMP", "_commandPF", "_commandPW", "_commandVA", "_commandY", "_commandZT20", "_commandZT21", "_commands2F21", "_commands2F7C", "_commands2F7D", "_commands2F7E", "_commands2F7F", "_eCopyArea1", "_eCopyArea11", "_eCopyArea12", "_eCopyArea13", "_eCopyArea14", "_eCopyArea15", "_eCopyArea16", "_eCopyArea17", "_eCopyArea18", "_eCopyArea19", "_eCopyArea2", "_eCopyArea20", "_eCopyArea21", "_eCopyArea22", "_eCopyArea23", "_eCopyArea24", "_eCopyArea25", "_eCopyArea26", "_eCopyArea3", "_eCopyArea33", "_eCopyArea34", "_eCopyArea39", "_eCopyArea4", "_eCopyArea40", "_eCopyArea48", "_eCopyArea49", "_eCopyArea5", "_eCopyArea5sp", "_eCopyArea6", "_eCopyArea7", "_eCopyArea8", "_eCopyArea9", "_main", "_mainloop", "_msg_putMessage", "_nact_main", "_sdl_keywait", "_sdl_sleep", "_sel_select", "_sys_addMsg", "_sys_hit_any_key", "_sys_key_releasewait"]' + -s EMTERPRETIFY_WHITELIST='["_ags_eCopyArea", "_ags_fadeIn", "_ags_fadeOut", "_ags_fader", "_ags_whiteIn", "_ags_whiteOut", "_checkMessage", "_check_command", "_commandCD", "_commandCE", "_commandH", "_commandHH", "_commandIK", "_commandMP", "_commandPF", "_commandPW", "_commandSM", "_commandSP", "_commandSQ", "_commandVA", "_commandY", "_commandZT20", "_commandZT21", "_commands2F1E", "_commands2F21", "_commands2F7C", "_commands2F7D", "_commands2F7E", "_commands2F7F", "_eCopyArea1", "_eCopyArea11", "_eCopyArea12", "_eCopyArea13", "_eCopyArea14", "_eCopyArea15", "_eCopyArea16", "_eCopyArea17", "_eCopyArea18", "_eCopyArea19", "_eCopyArea2", "_eCopyArea20", "_eCopyArea21", "_eCopyArea22", "_eCopyArea23", "_eCopyArea24", "_eCopyArea25", "_eCopyArea26", "_eCopyArea3", "_eCopyArea33", "_eCopyArea34", "_eCopyArea39", "_eCopyArea4", "_eCopyArea40", "_eCopyArea48", "_eCopyArea49", "_eCopyArea5", "_eCopyArea5sp", "_eCopyArea6", "_eCopyArea7", "_eCopyArea8", "_eCopyArea9", "_main", "_mainloop", "_msg_putMessage", "_mus_pcm_load", "_mus_pcm_mix", "_mus_pcm_start", "_muspcm_load_mixlr", "_muspcm_load_no", "_nact_main", "_sdl_keywait", "_sdl_sleep", "_sel_select", "_sys_addMsg", "_sys_hit_any_key", "_sys_key_releasewait"]' diff --git a/src/music_pcm.h b/src/music_pcm.h index 3e33bba..db90985 100644 --- a/src/music_pcm.h +++ b/src/music_pcm.h @@ -24,8 +24,6 @@ #ifndef __MUSIC_PCM_H__ #define __MUSIC_PCM_H__ -#include - extern int muspcm_init(); extern int muspcm_exit(); extern int muspcm_load_no(int slot, int no); diff --git a/src/pcm.emscripten.c b/src/pcm.emscripten.c new file mode 100644 index 0000000..93e7aed --- /dev/null +++ b/src/pcm.emscripten.c @@ -0,0 +1,155 @@ +/* + * pcm.emscripten.c Emscripten PCM backend + * + * Copyright (C) 2017 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +*/ + +#include + +#include "portab.h" +#include "system.h" +#include "ald_manager.h" +#include "dri.h" +#include "music_pcm.h" +#include "music_private.h" +#include "nact.h" + + +int muspcm_init() { + prv.pcm_valid = TRUE; + return OK; +} + +int muspcm_exit() { + return OK; +} + +// 番号指定のPCMファイル読み込み +int muspcm_load_no(int slot, int no) { + dridata *dfile = ald_getdata(DRIFILE_WAVE, no -1); + if (dfile == NULL) { + WARNING("DRIFILE_WAVE fail to open %d\n", no -1); + return NG; + } + + EM_ASM_({ + xsystem35.waitPromise(function(){ + return xsystem35.audio.pcm_load($0, $1, $2); + }); + }, slot, dfile->data, dfile->size); +#ifdef EMTERPRETIFY_ADVISE + emscripten_sleep(0); +#endif + + ald_freedata(dfile); + return OK; +} + +int muspcm_load_mixlr(int slot, int noL, int noR) { + dridata *dfileL = ald_getdata(DRIFILE_WAVE, noL -1); + if (dfileL == NULL) { + WARNING("DRIFILE_WAVE fail to open %d\n", noL -1); + return NG; + } + dridata *dfileR = ald_getdata(DRIFILE_WAVE, noR -1); + if (dfileR == NULL) { + ald_freedata(dfileL); + WARNING("DRIFILE_WAVE fail to open %d\n", noR -1); + return NG; + } + + EM_ASM_({ + xsystem35.waitPromise(function() { + return xsystem35.audio.pcm_load_mixlr($0, $1, $2, $3, $4); + }); + }, slot, dfileL->data, dfileL->size, dfileR->data, dfileR->size); +#ifdef EMTERPRETIFY_ADVISE + emscripten_sleep(0); +#endif + + ald_freedata(dfileL); + ald_freedata(dfileR); + return OK; +} + +// PCMデータを再生 +int muspcm_start(int slot, int loop) { + return EM_ASM_INT({ + return xsystem35.audio.pcm_start($0, $1); + }, slot, loop) ? OK : NG; +} + +// PCMデータの再生停止 +int muspcm_stop(int slot) { + return EM_ASM_INT({ + return xsystem35.audio.pcm_stop($0); + }, slot) ? OK : NG; +} + +// 指定時間のフェードアウトの後に再生停止 +int muspcm_fadeout(int slot, int msec) { + return EM_ASM_INT({ + return xsystem35.audio.pcm_fadeout($0, $1); + }, slot, msec) ? OK : NG; +} + +// PCMデータ再生一時停止 +int muspcm_pause(int slot) { + printf("%s not implemented\n", __func__); + return NG; +} + +// PCMデータ再生一時停止解除 +int muspcm_unpause(int slot) { + printf("%s not implemented\n", __func__); + return NG; +} + +// 現在の再生位置を返す +int muspcm_getpos(int slot) { + return EM_ASM_INT({ + return xsystem35.audio.pcm_getpos($0); + }, slot) ? OK : NG; +} + +// PCMオブジェクトに対してボリュームをセット +int muspcm_setvol(int dev, int slot, int lv) { + return EM_ASM_INT({ + return xsystem35.audio.pcm_setvol($0, $1); + }, slot, lv) ? OK : NG; +} + +// PCMデータの長さを取得 +int muspcm_getwavelen(int slot) { + int len = EM_ASM_INT({ + return xsystem35.audio.pcm_getwavelen($0); + }, slot); + + return len > 65535 ? 65535 : len; +} + +// 指定のスロットが現在演奏中かどうかを取得 +boolean muspcm_isplaying(int slot) { + return EM_ASM_INT({ + return xsystem35.audio.pcm_isplaying($0); + }, slot) ? TRUE : FALSE; +} + +int musfade_setvolval_all(int val) { + EM_ASM_(xsystem35.audio.setVolume($0), val); +} diff --git a/web/shell/shell.ts b/web/shell/shell.ts index 005c0f9..48b4e9e 100644 --- a/web/shell/shell.ts +++ b/web/shell/shell.ts @@ -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) { + EmterpreterAsync.handle(function(resume) { + p().then(() => {resume()}, (reason) => { console.log(reason); resume() }); + }); + } } class System35Shell { @@ -72,6 +82,7 @@ class System35Shell { this.antialiasCheckbox = $('#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 { + 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 { + 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 { + 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(); diff --git a/web/shell/tsconfig.json b/web/shell/tsconfig.json index 0b0bfa7..e0f211b 100644 --- a/web/shell/tsconfig.json +++ b/web/shell/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es6", "outDir": "../../docs", "noImplicitAny": true, "sourceMap": false