skynovel
Version:
webgl novelgame framework
391 lines (339 loc) • 13.4 kB
text/typescript
/* ***** 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 {CmnLib, IEvtMng} 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;
ret_ms : number;
end_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 = CmnLib.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 = CmnLib.argChk_Boolean(hArg, 'stop', (hArg.volume == 0));
if (stop) {
this.delLoopPlay(buf);
this.val.setVal_Nochk('save', 'const.sn.sound.'+ buf +'.fn', '');
// 先行して
}
this.val.flush();
if (CmnLib.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 = CmnLib.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}, CmnLib.argChk_Num(hArg, 'time', NaN))
.delay(CmnLib.argChk_Num(hArg, 'delay', 0))
.easing(ease)
.repeat(repeat == 0 ?Infinity :(repeat -1)) // 一度リピート→計二回なので
.yoyo(CmnLib.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;
CmnLib.argChk_Boolean(hArg, 'loop', true);
return this.playse(hArg);
}
// 効果音の再生
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 (CmnLib.argChk_Boolean(hArg, 'canskip', true)
&& this.evtMng.isSkipKeyDown()) return false;
const loop = CmnLib.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 ret_ms = CmnLib.argChk_Num(hArg, 'ret_ms', 0);
this.val.setVal_Nochk('save', nm +'ret_ms', ret_ms);
const end_ms = CmnLib.argChk_Num(hArg, 'end_ms', 0);
this.val.setVal_Nochk('save', nm +'end_ms', end_ms);
this.val.flush();
const o: any = {
autoPlay: true,
// autoPlay: false, // loaded が発生しない
loop : loop, // (apiには載ってないけど、ちゃんと効いた)
volume : vol,
speed : CmnLib.argChk_Num(hArg, 'speed', 1),
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;
},
};
if (! loop) o.complete = ()=> {
// [xchgbuf]をされるかもしれないので、外のoSb使用不可
const oSb = this.hSndBuf[buf];
if (oSb) {oSb.playing = ()=> false; oSb.onend();}
};
const snd = PSnd.find(fn); // バッファ
this.hSndBuf[buf] = {
snd : snd,
loop : loop,
ret_ms : ret_ms, // TODO: ret_ms未作成
end_ms : end_ms, // TODO: end_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();
}
},
};
if (snd) {snd.volume = vol; snd.play(o); return false;}
// snd.volume = ...; がないと、音量が戻らない不具合
const join = CmnLib.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);
this.initVol();
return join;
}
private playseSub(fn: string, o: any): 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; 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; 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);
const oSb = this.hSndBuf[buf];
if (oSb) oSb.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)},
CmnLib.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();
},
CmnLib.argChk_Boolean(hArg, 'canskip', false)
);
return true;
}
// 再生トラックの交換
private xchgbuf(hArg: HArg) { // TODO: xchgbuf()が未テスト
const buf = hArg.buf ?? 'SE';
const buf2 = hArg.buf2 ?? 'SE';
[this.hSndBuf[buf], this.hSndBuf[buf2]] = [this.hSndBuf[buf2], this.hSndBuf[buf]];
// const oSb = this.hSndBuf[buf];
// this.hSndBuf[buf] = this.hSndBuf[buf2];
// this.hSndBuf[buf2] = oSb;
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')),
ret_ms : Number(this.val.getVal(nm +'ret_ms')),
end_ms : Number(this.val.getVal(nm +'end_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();
}
}