UNPKG

skynovel

Version:
477 lines (417 loc) 16.2 kB
/* ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2018-2020 Famibee (famibee.blog38.fc2.com) This software is released under the MIT License. http://opensource.org/licenses/mit-license.php ** ***** END LICENSE BLOCK ***** */ import {IEvtMng, argChk_Boolean, argChk_Num} from './CmnLib'; import {CmnTween} from './CmnTween'; import {IHTag, IVariable, IMain, HArg} from './CmnInterface'; import {Config} from './Config'; import {SysBase} from './SysBase'; const PSnd = require('pixi-sound').default; import {Loader, LoaderResource} from 'pixi.js'; const Tween = require('@tweenjs/tween.js').default; interface ISndBuf { snd : any; loop : boolean; start_ms: number; end_ms : number; ret_ms : number; resume : boolean; playing : ()=> boolean; onend : ()=> void; twFade? : TWEEN.Tween; resumeFade? : boolean; onCompleteFade? : ()=> {}; }; export class SoundMng { private hSndBuf : {[name: string]: ISndBuf} = {}; constructor(private readonly cfg: Config, hTag: IHTag, private readonly val: IVariable, private readonly main: IMain, private readonly sys: SysBase) { hTag.volume = o=> this.volume(o); // 音量設定(独自拡張) hTag.fadebgm = o=> this.fadebgm(o); // BGMのフェード hTag.fadeoutbgm = o=> this.fadeoutbgm(o); // BGMのフェードアウト hTag.fadeoutse = o=> this.fadeoutse(o); // 効果音のフェードアウト hTag.fadese = o=> this.fadese(o); // 効果音のフェード hTag.playbgm = o=> this.playbgm(o); // BGM の演奏 hTag.playse = o=> this.playse(o); // 効果音の再生 hTag.stop_allse = ()=> this.stop_allse(); // 全効果音再生の停止 hTag.stopbgm = o=> this.stopbgm(o); // BGM 演奏の停止 hTag.stopse = o=> this.stopse(o); // 効果音再生の停止 hTag.wb = o=> this.wb(o); // BGM フェードの終了待ち hTag.wf = o=> this.wf(o); // 効果音フェードの終了待ち hTag.stopfadese = o=> this.stopfadese(o); // 音声フェードの停止 hTag.wl = o=> this.wl(o); // BGM 再生の終了待ち hTag.ws = o=> this.ws(o); // 効果音再生の終了待ち hTag.xchgbuf = o=> this.xchgbuf(o); // 再生トラックの交換 val.defValTrg('sys:sn.sound.global_volume', (_name: string, val: any)=> PSnd.sound.volumeAll = Number(val)); this.val.setVal_Nochk('save', 'const.sn.loopPlaying', '{}'); val.setVal_Nochk('tmp', 'const.sn.sound.codecs', JSON.stringify(PSnd.utils.supported)); } private evtMng : IEvtMng; setEvtMng(evtMng: IEvtMng) {this.evtMng = evtMng;} // 音量設定(独自拡張) private volume(hArg: HArg) { const buf = hArg.buf ?? 'SE'; const bvn = 'const.sn.sound.'+ buf +'.volume'; const arg_vol = this.getVol(hArg, 1); if (Number(this.val.getVal('sys:'+ bvn)) === arg_vol) return false; this.val.setVal_Nochk('sys', bvn, arg_vol) // 基準音量(sys:) this.val.flush(); // fadese()内で必ずしも呼ばれないので // 再生中音声の一時的音量も変更 hArg.time = 0; hArg.volume = Number(this.val.getVal('save:'+ bvn)); // 目標音量(save:) return this.fadese(hArg); } private getVol(hArg: HArg, def: number) { const vol = argChk_Num(hArg, 'volume', def); if (vol < 0) return 0; if (vol > 1) return 1; return vol; } // BGM/効果音のフェードアウト(loadから使うのでマクロ化禁止) private fadeoutbgm(hArg: HArg) {hArg.volume = 0; return this.fadebgm(hArg);} // 効果音のフェードアウト(loadから使うのでマクロ化禁止) private fadeoutse(hArg: HArg) {hArg.volume = 0; return this.fadese(hArg);} // BGMのフェード(loadから使うのでマクロ化禁止) private fadebgm(hArg: HArg) {hArg.buf = 'BGM'; return this.fadese(hArg);} // 効果音のフェード private fadese(hArg: HArg) { this.stopfadese(hArg); const buf = hArg.buf ?? 'SE'; const oSb = this.hSndBuf[buf]; if (! oSb || ! oSb.playing()) return false; const bvn = 'const.sn.sound.'+ buf +'.volume'; const savevol = this.getVol(hArg, NaN); this.val.setVal_Nochk('save', bvn, savevol); // 目標音量(save:) const vol = savevol * Number(this.val.getVal('sys:'+ bvn, 1)) const stop = argChk_Boolean(hArg, 'stop', (savevol === 0)); // this.getVol() により savevol = hArg.volume if (stop) { this.delLoopPlay(buf); this.val.setVal_Nochk('save', 'const.sn.sound.'+ buf +'.fn', ''); // 先行して } this.val.flush(); if (argChk_Num(hArg, 'time', NaN) === 0) { oSb.snd.volume = vol; if (stop) { if (buf === 'BGM') this.stopbgm(hArg); else this.stopse(hArg); } return false; } const ease = CmnTween.ease(hArg.ease); const repeat = argChk_Num(hArg, 'repeat', 1); //console.log('fadese start from:%f to:%f', oSb.snd.volume, vol); oSb.twFade = new Tween.Tween({v: oSb.snd.volume}) .to({v: vol}, argChk_Num(hArg, 'time', NaN)) .delay(argChk_Num(hArg, 'delay', 0)) .easing(ease) .repeat(repeat ===0 ?Infinity :(repeat -1)) // 一度リピート→計二回なので .yoyo(argChk_Boolean(hArg, 'yoyo', false)) .onUpdate((o: any)=> {if (oSb.playing()) oSb.snd.volume = o.v;}) .onComplete(()=> { //console.log('fadese: onComplete'); // [xchgbuf]をされるかもしれないので、外のoSb使用不可 const oSb = this.hSndBuf[buf]; if (! oSb || ! oSb.twFade) return; delete oSb.twFade; if (stop) { if (buf === 'BGM') this.stopbgm(hArg); else this.stopse(hArg); } if (oSb.resumeFade) { this.evtMng.popLocalEvts(); // [wf]したのにキャンセルされなかった時用 this.main.resume(); } if (oSb.onCompleteFade) oSb.onCompleteFade(); }); oSb.twFade!.start(); return false; } // BGM の演奏 private playbgm(hArg: HArg) { hArg.buf = 'BGM'; hArg.canskip = false; argChk_Boolean(hArg, 'loop', true); return this.playse(hArg); } // 効果音の再生 private static MAX_END_MS = 999000; private playse(hArg: HArg) { const buf = hArg.buf ?? 'SE'; this.stopse({buf: buf}); const fn = hArg.fn; if (! fn) throw '[playse] fnは必須です(buf='+ buf +')'; // isSkipKeyDown()は此処のみとする。タイミングによって変わる if (argChk_Boolean(hArg, 'canskip', true) && this.evtMng.isSkipKeyDown()) return false; const loop = argChk_Boolean(hArg, 'loop', false); this.addLoopPlay(buf, loop); // この辺で属性を増減したら、loadFromSaveObj()にも反映する const nm = 'const.sn.sound.'+ buf +'.'; this.val.setVal_Nochk('save', nm +'fn', fn); const savevol = this.getVol(hArg, 1); this.val.setVal_Nochk('save', nm +'volume', savevol); // 目標音量(save:) const vol = savevol * Number(this.val.getVal('sys:'+ nm +'volume', 1)); const start_ms = argChk_Num(hArg, 'start_ms', 0); const end_ms = argChk_Num(hArg, 'end_ms', SoundMng.MAX_END_MS); const ret_ms = argChk_Num(hArg, 'ret_ms', 0); if (start_ms < 0) throw `[playse] start_ms:${start_ms} が負の値です`; if (ret_ms < 0) throw `[playse] ret_ms:${ret_ms} が負の値です`; if (end_ms > 0) { if (start_ms >= end_ms) throw `[playse] start_ms:${start_ms} >= end_ms:${end_ms} は異常値です`; if (ret_ms >= end_ms) throw `[playse] ret_ms:${ret_ms} >= end_ms:${end_ms} は異常値です`; } this.val.setVal_Nochk('save', nm +'start_ms', start_ms); this.val.setVal_Nochk('save', nm +'end_ms', end_ms); this.val.setVal_Nochk('save', nm +'ret_ms', ret_ms); this.val.flush(); // pixi-sound用基本パラメータ const o: any = { loop : loop, volume : vol, speed : argChk_Num(hArg, 'speed', 1), sprites : {}, loaded : (e: Error, snd: any)=> { if (e) {this.main.errScript(`Sound ロード失敗です fn:${fn} ${e}`, false); return;} const oSb = this.hSndBuf[buf]; if (oSb) oSb.snd = snd; }, }; // start_ms・end_ms機能→pixi-sound準備 let sp_nm = ''; if (start_ms > 0 || end_ms < SoundMng.MAX_END_MS) { sp_nm = `${fn};${start_ms};${end_ms};${ret_ms}`; const os = o.sprites[sp_nm] = { start : start_ms /1000, end : end_ms /1000, }; o.preload = true; // loaded発生用 const old = o.loaded; o.loaded = (e: Error, snd: any)=> { const d = snd.duration; old(e, snd); if (os.end < 0) { // 負の値は末尾から os.end += d; snd.removeSprites(sp_nm); snd.addSprites(sp_nm, os); if (os.start >= os.end) throw `[playse] start_ms:${start_ms} >= end_ms:${end_ms}(${os.end *1000}) は異常値です`; if (ret_ms >= os.end *1000) throw `[playse] ret_ms:${ret_ms} >= end_ms:${end_ms}(${os.end *1000}) は異常値です`; } if (os.start >= d) throw`[playse] start_ms:${start_ms} >= 音声ファイル再生時間:${d} は異常値です`; if (end_ms !== SoundMng.MAX_END_MS && os.end >= d) throw`[playse] end_ms:${end_ms} >= 音声ファイル再生時間:${d} は異常値です`; snd.play(sp_nm, o.complete); // completeがundefinedでもいい }; } else o.autoPlay = true; // ループなし ... 再生完了イベント if (! loop) o.complete = ()=> { // [xchgbuf]をされるかもしれないので、外のoSb使用不可 const oSb2 = this.hSndBuf[buf]; if (oSb2) {oSb2.playing = ()=> false; oSb2.onend();} }; // ループあり ... ret_ms処理 else if (ret_ms !== 0) { o.loop = false; // 一周目はループなしとする o.complete = (snd: any)=> { const d = snd.duration; const sp_nm2 = `${fn};loop2;${end_ms};${ret_ms}`; const o2: any = { // oのコピーからやるとトラブルの元だった preload : true, // loaded発生用 loop : true, volume : vol, speed : o.speed, sprites : {}, loaded : (_: Error, snd2: any)=> { // [xchgbuf]をされるかもしれないので、外のoSb使用不可 const oSb2 = this.hSndBuf[buf]; if (oSb2) oSb2.snd = snd2; snd2.play(sp_nm2) }, }; const o2s = o2.sprites[sp_nm2] = { start : ret_ms /1000, end : end_ms /1000, }; if (o2s.end < 0) { // 負の値は末尾から o2s.end += d; snd.removeSprites(sp_nm2); snd.addSprites(sp_nm2, o2s); } if (o2s.start >= d) throw`[playse] ret_ms:${ret_ms} >= 音声ファイル再生時間:${d} は異常値です`; this.playseSub(fn, o2, sp_nm2); } } const snd = PSnd.find(fn); // バッファにあるか this.hSndBuf[buf] = { snd : snd, loop : loop, start_ms: start_ms, end_ms : end_ms, ret_ms : ret_ms, resume : false, playing : ()=> true, // [ws]的にはここでtrueが欲しい onend : ()=> { // [xchgbuf]をされるかもしれないので、外のoSb使用不可 const oSb = this.hSndBuf[buf]; if (! oSb) return; // if (CmnLib.isFirefox) oSb.playing = ()=> false; //delete this.hSndBuf[buf]; // [xchgbuf]をされるかもしれないので、delete不可 // 【2018/06/25】cache=falseならここでunload()? this.stopfadese(hArg); // 止めた方が良いかなと if (oSb.resume) { this.evtMng.popLocalEvts(); // [ws]中にキャンセルされなかった時用 this.main.resume(); } }, }; this.initVol(); if (snd) { snd.volume = vol; // 再生のたびに音量を戻す if (sp_nm) this.playseSub(fn, o, sp_nm); else snd.play(o); return false; } const join = argChk_Boolean(hArg, 'join', true); if (join) { const old = o.loaded; o.loaded = (e: Error, snd: any)=> {this.main.resume(); old(e, snd)}; } this.playseSub(fn, o, sp_nm); return join; } private playseSub(fn: string, o: any, sp_nm: string): void { const url = this.cfg.searchPath(fn, Config.EXT_SOUND); // const url = 'http://localhost:8080/prj/audio/title.{ogg,mp3}'; if (url.slice(-4) !== '.bin') { o.url = url; if (sp_nm) PSnd.Sound.from(o); else PSnd.add(fn, o); return; } (new Loader()).add(fn, url, {xhrType: 'arraybuffer'}) .pre((res: LoaderResource, next: Function)=> res.load(()=> { this.sys.pre(res.extension, res.data) .then(r=> {res.data = r; next();}) .catch(e=> this.main.errScript(`Sound ロード失敗です fn:${res.name} ${e}`, false)); })) .load((_ldr, hRes)=> { o.source = hRes[fn]?.data; if (sp_nm) PSnd.Sound.from(o); else PSnd.add(fn, o); }); } private initVol = ()=> { PSnd.sound.volumeAll =Number(this.val.getVal('sys:sn.sound.global_volume',1)); this.initVol = ()=> {}; }; // 全効果音再生の停止 private stop_allse() { for (const buf in this.hSndBuf) this.stopse({buf: buf}); this.hSndBuf = {}; return false; } // BGM 演奏の停止(loadから使うのでマクロ化禁止) private stopbgm(hArg: HArg) {hArg.buf = 'BGM'; return this.stopse(hArg);} // 効果音再生の停止 private stopse(hArg: HArg) { const buf = hArg.buf ?? 'SE'; this.stopfadese(hArg); this.delLoopPlay(buf); this.hSndBuf[buf]?.snd.stop(); return false; } // BGM フェードの終了待ち private wb(hArg: HArg) {hArg.buf = 'BGM'; return this.wf(hArg);} // 効果音フェードの終了待ち private wf(hArg: HArg) { const buf = hArg.buf ?? 'SE'; const oSb = this.hSndBuf[buf]; if (! oSb || ! oSb.twFade) return false; if (! oSb.playing()) return false; oSb.resumeFade = true; this.evtMng.stdWait( ()=> {this.stopfadese(hArg)}, argChk_Boolean(hArg, 'canskip', true) ); return true; } // 音声フェードの停止 private stopfadese(hArg: HArg) { const buf = hArg.buf ?? 'SE'; const oSb = this.hSndBuf[buf]; if (! oSb || ! oSb.twFade) return false; oSb.twFade.stop().end(); // stop()とend()は別 return false; } // BGM 再生の終了待ち private wl(hArg: HArg) {hArg.buf = 'BGM'; return this.ws(hArg);} // 効果音再生の終了待ち private ws(hArg: HArg) { const buf = hArg.buf ?? 'SE'; const oSb = this.hSndBuf[buf]; if (! oSb || ! oSb.playing() || oSb.loop) return false; oSb.resume = true; this.evtMng.stdWait( ()=> { this.stopse(hArg); // [xchgbuf]をされるかもしれないので、外のoSb使用不可 const oSb = this.hSndBuf[buf]; if (! oSb || ! oSb.playing() || oSb.loop) return; oSb.onend(); }, argChk_Boolean(hArg, 'canskip', false) ); return true; } // 再生トラックの交換 private xchgbuf(hArg: HArg) { const buf = hArg.buf ?? 'SE'; const buf2 = hArg.buf2 ?? 'SE'; [this.hSndBuf[buf], this.hSndBuf[buf2]] = [this.hSndBuf[buf2], this.hSndBuf[buf]]; return false; } // レスポンス向上のため音声ファイルを先読み loadAheadSnd(hArg: HArg): void { [hArg.clickse, hArg.enterse, hArg.leavese].forEach(fn=> { if (! fn || PSnd.exists(fn)) return; this.playseSub(fn, {preload: true, autoPlay: false}, ''); }); } // しおりの読込(BGM状態復元) playLoopFromSaveObj(): void { const loopPlaying = String(this.val.getVal('save:const.sn.loopPlaying', '{}')); this.val.flush(); if (loopPlaying === '{}') {this.stop_allse(); return;} const aFnc: {(): void}[] = []; const hBuf = JSON.parse(loopPlaying); for (const buf in hBuf) { const nm = 'save:const.sn.sound.'+ buf +'.'; const hArg = { fn : String(this.val.getVal(nm +'fn')), buf : buf, join : false, loop : true, volume : Number(this.val.getVal(nm +'volume')), start_ms: Number(this.val.getVal(nm +'start_ms')), end_ms : Number(this.val.getVal(nm +'end_ms')), ret_ms : Number(this.val.getVal(nm +'ret_ms')), }; aFnc.push(()=> { if (hArg.buf === 'BGM') this.playbgm(hArg); else this.playse(hArg); }); } this.stop_allse(); aFnc.forEach(f=> f()); } private addLoopPlay(buf: string, is_loop: Boolean): void { if (! is_loop) {this.delLoopPlay(buf); return;} const hBuf = JSON.parse(String(this.val.getVal('save:const.sn.loopPlaying', '{}'))); hBuf[buf] = 0; this.val.setVal_Nochk('save', 'const.sn.loopPlaying', JSON.stringify(hBuf)); this.val.flush(); } private delLoopPlay(buf: string): void { const hBuf = JSON.parse(String(this.val.getVal('save:const.sn.loopPlaying', '{}'))); delete hBuf[buf]; this.val.setVal_Nochk('save', 'const.sn.loopPlaying', JSON.stringify(hBuf)); this.val.flush(); } }