ebt-vue3
Version:
Vue3 Library for SuttaCentral Voice EBT-Sites
1,073 lines (1,027 loc) • 35.6 kB
JavaScript
import { defineStore } from 'pinia';
import { logger } from 'log-instance/index.mjs';
import { SuttaRef, AuthorsV2 } from 'scv-esm/main.mjs';
import { useSuttasStore } from './suttas.mjs';
import { useSettingsStore } from './settings.mjs';
import { useVolatileStore } from './volatile.mjs';
import { default as EbtSettings } from '../ebt-settings.mjs';
import { default as CardFactory } from '../card-factory.mjs';
import { default as IdbAudio } from '../idb-audio.mjs';
import { default as EbtConfig } from '../../ebt-config.mjs';
import { default as EbtCard } from '../ebt-card.mjs';
import { default as Playlist } from '../playlist.mjs';
import * as VOICES from "../auto/voices.mjs";
import { Tipitaka } from 'scv-esm/main.mjs';
import { ref, nextTick } from 'vue';
import * as Idb from 'idb-keyval';
import {
DBG,
DBG_KEY, DBG_AUDIO, DBG_HIGHLIGHT_EG,
DBG_SOUND_STORE, DBG_IDB_AUDIO,
} from '../defines.mjs';
const MS_MINUTE = 60*1000;
const URL_NOAUDIO = "audio/383542__alixgaus__turn-page.mp3";
const HEADERS_JSON = { ["Accept"]: "application/json", };
const HEADERS_MPEG = { ["Accept"]: "audio/mpeg", };
const SAMPLE_RATE = 48000;
const V = ' ';
const playing = ref(false);
var segAudioDb;
var soundDb;
function SEG_AUDIO_STORE() {
if (segAudioDb === undefined) {
segAudioDb = Idb.createStore('seg-audio-db', 'seg-audio-store')
}
return segAudioDb;
}
function SOUND_STORE() {
if (soundDb === undefined) {
soundDb = Idb.createStore('sound-db', 'sound-store')
}
return soundDb;
}
function deleteDatabase(name) {
const msg = 'audio.deleteDatabase()';
const dbg = DBG_AUDIO;
dbg && console.log(msg, '[1]', name);
const DBDeleteRequest = window.indexedDB.deleteDatabase(name);
DBDeleteRequest.onerror = (event) => {
console.warn(msg, `[2]Error deleting database`, {name,event});
};
DBDeleteRequest.onsuccess = (event) => {
dbg && console.log(msg, '[3]ok', {name,event});
// event.result should be undefined
};
return DBDeleteRequest;
}
const PLAY_ONE = 'one';
const PLAY_END = 'end';
const clickElt = ref(undefined);
const audioIndex = ref(0);
const audioSutta = ref(null);
const audioScid = ref('');
const audioFocused = ref(false);
const mainContext = ref(null);
const segmentPlaying = ref(false);
const audioElapsed = ref(0);
const idbAudio = ref(undefined);
const playMode = ref(PLAY_ONE);
const playedSeconds = ref(0);
const tipitaka = new Tipitaka();
export const useAudioStore = defineStore('audio', {
state: () => {
return {
nFetch: 0,
nGet: 0,
nSet: 0,
audioIndex,
audioSutta,
audioScid,
audioFocused,
mainContext,
segmentPlaying,
audioElapsed,
idbAudio,
playing,
playMode,
playedSeconds,
clickElt,
PLAY_ONE,
PLAY_END,
}
},
getters: {
},
actions: {
keydown(evt) {
const msg = `audio.keydown(${evt.code}) `;
const dbg = DBG_KEY || DBG_AUDIO;
switch (evt.code) {
case 'ArrowUp':
if (evt.ctrlKey) {
dbg && console.log(msg, '[1]setLocation');
this.setLocation(0);
} else if (evt.shiftKey) {
dbg && console.log(msg, '[2]incrementGroup(-1)');
this.incrementGroup(-1);
} else {
dbg && console.log(msg, '[3]back');
this.back().then(incRes => {
if (!incRes) {
this.playBell();
}
});
}
break;
case 'ArrowDown':
if (evt.ctrlKey) {
dbg && console.log(msg, '[4]setLocation');
this.setLocation(-1);
} else if (evt.shiftKey) {
dbg && console.log(msg, '[5]incrementGroup(1)');
this.incrementGroup(1);
} else {
dbg && console.log(msg, '[6]next');
this.next().then(incRes => {
if (!incRes) {
this.playBell();
}
});
}
break;
case 'Space':
if (evt.altKey || evt.metaKey || evt.shiftKey || evt.ctrlKey) {
dbg && console.log(msg, '[7]clickPlayToEnd');
this.clickPlayToEnd();
} else {
dbg && console.log(msg, '[8]clickPlayOne');
this.clickPlayOne();
}
break;
case 'Enter':
dbg && console.log(msg, '[9]clickPlayToEnd');
this.clickPlayToEnd();
break;
default:
// Defer to App.vue keydown listener
return;
}
evt.preventDefault();
evt.stopPropagation();
},
playPause(playMode) {
const msg = "audio.playPause()";
const dbg = DBG.PLAY || DBG_AUDIO;
let { idbAudio, mainContext, } = this;
this.playClick();
if (idbAudio == null) {
dbg && console.log(msg, '[1]idbAudio?=>false');
playing.value = false;
} else if (idbAudio.audioSource) {
if (!idbAudio.paused) {
dbg && console.log(msg, '[2]pause=>true');
idbAudio.pause();
playing.value = false;
return true;
}
if (playMode === this.playMode) {
dbg && console.log(msg, '[3]play=>false');
idbAudio.play();
playing.value = false;
return true;
}
dbg && console.log(msg, '[4]n/a=>false');
playing.value = true;
return false;
} else {
dbg && console.log(msg, '[5]audioSource?=>false');
}
mainContext && mainContext.close();
this.playMode = playMode;
playing.value = true;
return false;
},
registerClickElt(elt) {
const msg = 'audio.registerClickElt()';
const dbg = DBG_AUDIO;
const dbgv = dbg && DBG.VERBOSE && !DBG.LOG_HTML;
if (clickElt.value === elt) { // no change
dbgv && console.log(V+msg, '[1]n/a');
return elt;
}
if (elt == null) {
console.warn(msg, '[2]elt?');
return elt;
}
dbg && console.log(msg, '[3]', {elt});
clickElt.value = elt;
let settings = useSettingsStore();
let { audioVolume } = settings;
dbg && console.log(msg, '[4]volume', audioVolume);
elt.volume = audioVolume;
return elt;
},
async playOne() {
const msg = 'audio.playOne() ';
const dbg = DBG.PLAY;
try {
playing.value = true;
dbg && console.log(msg, '[1]playSegment', this.audioScid);
let completed = await this.playSegment();
if (!completed) {
dbg && console.log(msg, '[2]interrupted');
} else if (await this.next()) {
dbg && console.log(msg, '[3]ok');
} else {
dbg && console.log(msg, '[4]end');
await this.playBell();
}
} finally {
playing.value = false;
}
},
clickPlayOne() {
const msg = 'audio.clickPlayOne() ';
const dbg = DBG.PLAY;
let settings = useSettingsStore();
settings.tutorPlay = false;
if (this.playPause(PLAY_ONE)) {
dbg && console.log(msg, '[1]playPause toggled');
return;
}
dbg && console.log(msg, '[2]playing');
this.createIdbAudio(()=>{
this.playOne()
.then(()=>playing.value = false);
});
},
async playToEnd() {
const msg = 'audio.playToEnd() ';
const dbg = DBG.PLAY;
try {
let settings = useSettingsStore();
settings.tutorPlay = false;
playedSeconds.value = 0;
playing.value = true;
do {
dbg && console.log(msg, '[1]playSegment', this.audioScid);
let segPlayed = await this.playSegment();
let playedMinutes = playedSeconds.value / 60;
let timeout = playedMinutes > settings.maxPlayMinutes;
playing.value = segPlayed && !timeout;
} while(playing.value && (await this.next()));
dbg && console.log(msg, '[2]playBell', this.audioScid);
await this.playBell();
dbg && console.log(msg, '[3]end', this.audioScid);
} finally {
playing.value = false;
}
},
clickPlayToEnd() {
const msg = 'audio.clickPlayToEnd() ';
const dbg = DBG.PLAY;
if (this.playPause(PLAY_END)) {
dbg && console.log(msg, '[1]toggle', this.audioScid);
return;
}
dbg && console.log(msg, '[2]createIdbAudio', this.audioScid);
this.createIdbAudio(()=>{
dbg && console.log(msg, '[3]playToEnd', this.audioScid);
this.playToEnd();
});
},
back() {
return this.incrementSegment(-1);
},
async syncPlaylistSutta(playlist) {
const msg = "audio.syncPlaylistSutta()";
const dbg = DBG.PLAYLIST || DBG.AUDIO_SCID;
if (!playlist) {
throw new Error(`${msg} playlist?`);
}
let incRes = null;
let suttas = useSuttasStore();
let settings = useSettingsStore();
let volatile = useVolatileStore();
let { routeCard } = volatile;
let { audioScid, audioSutta } = this;
let { sutta_uid:audioSuid, lang, author } = audioSutta;
let { index, cursor, pattern } = playlist;
let { sutta_uid, scid } = cursor;
let srNext = SuttaRef.create({sutta_uid, lang, author});
let nextSuttaRef = await suttas.getIdbSuttaRef(srNext);
routeCard.open(false);
settings.removeCard(routeCard, EbtConfig);
let nextSutta = nextSuttaRef.value;
let nextPath = [
'/play', scid, lang, author, pattern
].join('/');
dbg && console.log(msg, '[1]nextPath',
{audioSuid, audioScid, index, nextSuttaRef, nextPath});
let cardFactory = CardFactory.singleton;
let nextCard = cardFactory.pathToCard({
path:nextPath,
addCard: opts=>cardFactory.addCard(opts),
playlist,
});
if (nextCard) {
volatile.setRouteCard(nextCard);
DBG.TEST && console.log(msg, 'T1 location', nextCard.location);
this.setAudioSutta(nextSutta);
let { segments } = this.audioSutta;
nextCard.location[0] = sutta_uid;
let audioIndex = segments.findIndex(s=>s.scid===scid);
audioIndex = audioIndex < 0 ? 0 : audioIndex;
incRes = this.setLocation(audioIndex);
dbg && console.log(msg, '[2]nextCard',
{srNext, audioIndex, incRes} );
} else {
dbg && console.log(msg, '[3]!nextCard', nextpath, incRes);
}
return incRes;
},
async nextTipitaka() {
const msg = "audio.nextTipitaka()";
const dbg = DBG.PLAY;
let settings = useSettingsStore();
let volatile = useVolatileStore();
let { routeCard } = volatile;
let { audioSutta } = this;
let { sutta_uid:suid, lang, author } = audioSutta;
let sutta_uid = tipitaka.nextSuid(suid, Tipitaka.folderOfSuid);
let incRes = null;
if (sutta_uid) {
routeCard.open(false);
let sref = SuttaRef.create({sutta_uid, lang, author});
let suttas = useSuttasStore();
let nextSuttaRef = await suttas.getIdbSuttaRef(sref);
settings.removeCard(routeCard, EbtConfig);
let nextSutta = nextSuttaRef.value;
let nextPath = [
'/sutta', sutta_uid, lang, author
].join('/');
let cardFactory = CardFactory.singleton;
let addCard = (opts=>{
return cardFactory.addCard(opts);
});
let nextCard = cardFactory.pathToCard({
path: nextPath,
addCard,
});
if (nextCard) {
volatile.setRouteCard(nextCard);
this.setAudioSutta(nextSutta);
incRes = this.setLocation(0);
dbg && console.log(msg, '[2]tipitaka', sref, incRes );
} else {
dbg && console.log(msg, '[3]!nextCard', nextpath, incRes);
}
}
return incRes;
},
async nextPlaylist() {
const msg = "audio.nextPlaylist()";
const dbg = DBG.PLAY;
let settings = useSettingsStore();
let volatile = useVolatileStore();
let suttas = useSuttasStore();
let { audioSutta } = this;
let { routeCard } = volatile;
let { playlist } = routeCard;
let { pattern } = playlist;
let incRes = playlist && playlist.advance(1);
if (!incRes) {
dbg && console.log(msg, '[1]!advance', playlist, incRes);
return incRes;
}
dbg && console.log(msg, '[2]advance', playlist, incRes);
// start at top of next sutta
playlist.cursor.scid = playlist.cursor.sutta_uid;
playlist.cursor.segnum = undefined;
await this.syncPlaylistSutta(playlist);
return incRes;
},
async next() {
const msg = "audio.next()";
const dbg = DBG.PLAY;
let incRes = await this.incrementSegment(1);
if (incRes==null && playMode.value === PLAY_END) {
let settings = useSettingsStore();
let volatile = useVolatileStore();
let { routeCard } = volatile;
switch (settings.playEnd) {
case EbtSettings.END_REPEAT:
incRes = this.setLocation(0);
dbg && console.log(msg, '[1]repeat', incRes);
case EbtSettings.END_PLAYLIST:
if (routeCard.context === EbtCard.CONTEXT_PLAY) {
incRes = await this.nextPlaylist();
} else {
incRes = await this.nextTipitaka();
}
break;
case EbtSettings.END_STOP:
dbg && console.log(msg, '[3]stop', incRes);
break;
default:
dbg && console.warn(msg, `[4]${settings.playEnd}?`, incRes);
break;
}
}
return incRes;
},
setLocation(delta=0) {
const msg = `audio.setLocation(${delta}) `;
const dbg = DBG.PLAY || DBG.ROUTE;
let volatile = useVolatileStore();
let { routeCard } = volatile;
let { audioSutta, } = this;
let { segments } = audioSutta;
let incRes = routeCard.setLocation({ segments, delta, });
let { hash } = window.location;
let newHash = routeCard.routeHash();
if (incRes) {
let { iSegment } = incRes;
this.audioScid = segments[iSegment].scid;
volatile.setRoute(newHash, true, msg);
this.playSwoosh();
dbg && console.log(msg, '[1]playSwoosh', {incRes,hash, newHash});
} else {
this.playBell();
dbg && console.log(msg, '[2]incRes?', {delta,hash, newHash});
}
return incRes;
},
incrementGroup(delta=1) {
const msg = `audio.incrementGroup(${delta}) `;
const dbg = DBG_AUDIO;
let volatile = useVolatileStore();
let { routeCard } = volatile;
let { audioSutta, } = this;
let { segments } = audioSutta;
let incRes = routeCard.incrementGroup({segments, delta});
if (incRes) {
let { iSegment } = incRes;
this.audioScid = segments[iSegment].scid;
volatile.setRoute(routeCard.routeHash(), true, msg);
this.playSwoosh();
dbg && console.log(msg, '[1]playSwoosh', incRes);
} else {
this.playBell();
dbg && console.log(msg, '[2]playBell');
}
return incRes;
},
async playSegment() {
const msg = `audio.playSegment() `;
const dbg = DBG_AUDIO;
const dbgv = DBG.VERBOSE && dbg;
let settings = useSettingsStore();
let volatile = useVolatileStore();
let { routeCard } = volatile;
let audio = this;
let { idbAudio, audioScid } = audio;
// Scroll segment into view
let scrollId = routeCard.segmentElementId(audioScid);
let showId = routeCard.segmentCardId(audioScid);
DBG.SCROLL && console.log(msg, `[1]audioScid`, {
audioScid, scrollId, showId});
settings.scrollToElementId(showId, scrollId);
let segAudio = await audio.bindSegmentAudio();
let { segment:seg, langTrans } = segAudio;
let lastCurrentTime = 0;
dbg && console.log(msg, '[1]bindSegmentAudio', audioScid);
let interval;
try {
audio.audioElapsed = -2;
interval = setInterval( ()=>{
let currentTime = audio.idbAudio?.currentTime || -1;
if (currentTime > 0) {
playedSeconds.value += (currentTime - lastCurrentTime)/1000;
lastCurrentTime = currentTime;
}
audio.audioElapsed = currentTime/1000;
if (audio.audioScid !== audioScid) {
clearInterval(interval);
dbgv && console.log(msg, '[2]interrupt', interval,
`${audioScid}=>${audio.audioScid}`);
audio.segmentPlaying = false;
idbAudio.clear();
}
}, 100);
dbgv && console.log(msg + '[3]setInterval', interval);
audio.segmentPlaying = true;
if (audio.segmentPlaying && settings.speakPali && seg.pli) {
let src = await audio.pliAudioUrl;
idbAudio.src = src;
lastCurrentTime = idbAudio.currentTime;
dbgv && console.log(msg, '[4]pliUrl', src);
await idbAudio.play();
}
let speakTrans = settings.speakTranslation &&
settings.showTrans && seg[langTrans];
if (audio.segmentPlaying && speakTrans) {
let src = await audio.transAudioUrl;
idbAudio.src = src;
lastCurrentTime = idbAudio.currentTime;
dbgv && console.log(msg, '[5]transUrl:', src);
await idbAudio.play();
}
dbgv && console.log(msg, '[6]clearInterval', interval);
clearInterval(interval);
interval = undefined;
} catch(e) {
clearInterval(interval);
interval = undefined;
console.warn(msg, e);
} finally {
audio.audioElapsed = -1;
}
dbgv && console.log(msg, '[7]segmentPlaying', audio.segmentPlaying);
if (!audio.segmentPlaying) {
return false; // interrupted
}
audio.segmentPlaying = false;
return true; // completed
},
audioDuration() {
let duration = this.idbAudio?.audioBuffer?.duration;
return duration;
},
createIdbAudio(playAction) {
const msg = "audio.createIdbAudio() ";
const dbg = DBG_AUDIO || DBG_IDB_AUDIO;
//console.trace(msg);
// NOTE: Caller must be UI callback (iOS restriction)
let {
audioContext,
promiseResume,
} = this.createAudioContext();
this.mainContext = audioContext;
dbg && console.log(msg, '[1]new IdbAudio');
this.idbAudio = new IdbAudio({audioContext});
promiseResume.then(()=>{
playAction();
});
},
async incrementSegment(delta) {
const msg = `audio.incrementSegment(${delta}) `;
const dbg = DBG.PLAY || DBG_AUDIO;
const dbgv = DBG.VERBOSE && dbg;
let volatile = useVolatileStore();
let { routeCard } = volatile;
let { audioSutta, } = this;
let { segments } = audioSutta;
let incRes = routeCard.incrementLocation({ segments, delta, });
if (incRes) {
let { iSegment } = incRes;
let seg = segments[iSegment];
this.audioScid = seg.scid;
let hash = routeCard.routeHash();
volatile.setRoute(hash, true, msg);
this.playClick();
dbg && console.log(msg, '[1]playClick', incRes);
}
// sync instance
await new Promise(resolve=>nextTick(()=>resolve()));
return incRes;
},
async clearSoundCache() {
const msg = 'audio.clearSoundCache() ';
try {
logger.warn(msg);
const reqSoundDb = deleteDatabase("sound-db");
const reqSegAudioDb = deleteDatabase("seg-audio-db");
segAudioDb = null;
soundDb = null;
console.log(msg, {reqSoundDb, reqSegAudioDb});
nextTick(()=>window.location.reload()); } catch(e) {
logger.warn(msg + 'ERROR', e.message);
throw e;
}
},
playSwoosh(audioContext) {
const msg = 'audio.playSwoosh() ';
const dbg = DBG_AUDIO;
let settings = useSettingsStore();
let volume = settings.swooshVolume;
let url = volume ? `audio/swoosh${volume}.mp3` : null;
dbg && console.log(msg, '[1]playUrl', url);
return this.playUrl(url, {audioContext});
},
playBlock(audioContext) {
const msg = 'audio.playBlock() ';
const dbg = DBG_AUDIO;
let settings = useSettingsStore();
let volume = settings.blockVolume;
let url = volume ? `audio/block${volume}.mp3` : null;
dbg && console.log(msg, '[1]playURl', url);
return this.playUrl(url, {audioContext});
},
playClick() {
const msg = 'audio.playClick() ';
const dbg = DBG_AUDIO;
let { clickElt } = this;
if (clickElt) {
dbg && console.log(msg, '[1]play', clickElt?.id);
/* await */ clickElt.play();
} else {
dbg && console.warn(msg, '[1]clickElt?');
}
},
playBell(audioContext) {
const msg = 'audio.playBell() ';
const dbg = DBG.PLAY || DBG_AUDIO;
let settings = useSettingsStore();
let { ips } = settings;
let ipsChoice = EbtSettings.IPS_CHOICES.filter(c=>c.value===ips)[0];
let url = ipsChoice?.url?.substring(1);
dbg && console.log(msg, '[1]', url);
return this.playUrl(url, {audioContext});
},
async setAudioSutta(audioSutta, audioIndex=0) {
const msg = 'audio.setAudioSutta() '
const dbg = DBG_AUDIO || DBG.AUDIO_SCID;
this.audioSutta = audioSutta;
this.audioIndex = audioIndex;
let segments = audioSutta?.segments;
let audioScid = segments
? segments[audioIndex].scid
: null;
this.audioScid = audioScid;
if (audioScid) {
this.updateAudioExamples();
} else {
dbg && console.log(msg, `[2]audioScid:`, audioScid);
}
},
updateAudioExamples() {
const msg = "audio.updateAudioExamples()";
const dbg = DBG_HIGHLIGHT_EG;
let { audioSutta, audioIndex } = this;
let segments = audioSutta?.segments;
if (segments) {
let segment = segments[audioIndex];
dbg && console.log('msg', '[1]highlightEamples', segment.scid);
let updated = audioSutta.highlightExamples({segment});
if (updated) {
segment.examples = updated;
}
} else {
dbg && console.log('msg', '[2]F highlightEamples');
}
},
createAudioContext() {
const msg = "audio.createAudioContext() "
const dbg = DBG_AUDIO;
// IMPORTANT! Call this from a user-initiated non-async context
let audioContext = new AudioContext();
dbg && console.log(msg, '[1]new AudioContext()',
audioContext.state);
audioContext.onstatechange = val=>{
dbg && console.log(msg, '[2]onstatechange',
audioContext.state, val);
}
let promiseResume = audioContext.resume(); // required for iOS
dbg && promiseResume.then(val=>{
console.log(msg, '[3]resume', audioContext.state, val);
});
return {
audioContext,
promiseResume,
};
},
transVoiceName(suttaRef, settings=useSettingsStore()) {
const msg = "audio.transVoiceName()";
const dbg = DBG_AUDIO;
let vTrans = settings.vnameTrans;
let { lang:voiceLang, } = suttaRef;
let { langTrans } = settings;
if (voiceLang !== settings.langTrans) {
let voices = VOICES.default;
let langVoice = voices.filter(v=>v.langTrans===voiceLang)[0];
vTrans = langVoice.name || vTrans;
dbg && console.log(msg, '[1]vTrans', `${vTrans}/${voiceLang}`);
}
return vTrans;
},
segAudioKey(idOrRef, settings=useSettingsStore()) {
const msg = "audio.segAudioKey() ";
let { langTrans, vnameRoot } = settings;
let suttaRef = SuttaRef.create(idOrRef, langTrans);
let { lang, author, scid } = suttaRef;
author = author || AuthorsV2.langAuthor(lang);
if (author == null) {
let emsg = `${msg} author is required: ` +
JSON.stringify(idOrRef);
throw new Error(emsg);
}
let vTrans = this.transVoiceName(suttaRef, settings);
let key = `${scid}/${lang}/${author}/${vTrans}/${vnameRoot}`;
//console.log(msg, {key, idOrRef});
return key;
},
async fetchSegmentAudio(idOrRef, settings=useSettingsStore()) {
const msg = "audio.fetchSegmentAudio()";
const dbg = DBG_AUDIO;
const volatile = useVolatileStore();
let segAudio;
try {
volatile.waitBegin("ebt.loadingAudio");
let audioUrl = this.segmentAudioUrl(idOrRef, settings);
this.nFetch++;
dbg && console.log(msg, '[1]fetch', audioUrl);
let resAudio = await fetch(audioUrl, { headers: HEADERS_JSON });
segAudio = await resAudio.json();
} catch(e) {
volatile.alert(e);
throw e;
} finally {
volatile.waitEnd();
}
return segAudio;
},
async getSegmentAudio(idOrRef, settings=useSettingsStore()) {
const msg = 'audio.getSegmentAudio() ';
const dbg = DBG_AUDIO;
const dbgv = DBG.VERBOSE && dbg;
let segAudioKey = this.segAudioKey(idOrRef, settings);
dbgv && console.log(msg, "[1]Idb.get", segAudioKey);
let segAudio = await Idb.get(segAudioKey, SEG_AUDIO_STORE());
if (segAudio) {
dbg && console.log(msg, `[2]cached`, segAudioKey);
if (dbgv) {
let age = ((Date.now()-segAudio.created)/1000).toFixed(1);
console.log(msg, `[3]cached age:${age}s`, segAudio);
}
} else {
dbg && console.log(msg, "[4]fetchSegmentAudio", idOrRef);
segAudio = await this.fetchSegmentAudio(idOrRef, settings);
segAudio.created = Date.now();
dbg && console.log(msg, '[5]Idb.set', segAudioKey);
await Idb.set(segAudioKey, segAudio, SEG_AUDIO_STORE());
}
return segAudio;
},
segmentAudioUrl(idOrRef, settings=useSettingsStore()) {
const msg = 'audio.segmentAudioUrl()';
const dbg = DBG_AUDIO;
let { langTrans, serverUrl, vnameRoot } = settings;
let suttaRef = SuttaRef.create(idOrRef, langTrans);
let { sutta_uid, lang, author, scid } = suttaRef;
author = author || AuthorsV2.langAuthor(lang);
let vTrans = this.transVoiceName(suttaRef, settings);
if (author == null) {
let emsg = `${msg} author is required ${JSON.stringify(idOrRef)}`;
console.warn(emsg);
throw new Error(emsg);
}
let url = [
serverUrl,
'play',
'segment',
sutta_uid,
lang,
author,
scid,
vTrans,
vnameRoot,
].join('/');
dbg && console.log(msg, url);
return url;
},
playUrl(url, opts={}) {
const msg = "audio.playUrl() ";
const dbg = DBG_AUDIO;
let promise;
let { audioContext } = opts;
if (audioContext == null) {
let {
audioContext:tempContext,
promiseResume
} = this.createAudioContext();
dbg && console.log(msg, '[1]playUrlAsync', url);
promise = this.playUrlAsync(url, {
audioContext: tempContext});
promise.then(()=>{
tempContext.close();
});
} else {
dbg && console.log(msg, '[2]playUrlAsync', url);
promise = this.playUrlAsync(url, {audioContext});
}
return promise;
},
async playUrlAsync(url, opts) {
const msg = 'audio.playUrlAsync() ';
const dbg = DBG_AUDIO;
const dbgv = DBG.VERBOSE && dbg;
if (url == null) {
return null;
}
let { audioContext } = opts;
if (audioContext == null) {
throw new Error(`${msg} audioContext is required`);
}
dbg && console.log(msg, '[1]fetchArrayBuffer', url);
let arrayBuffer = await this.fetchArrayBuffer(url, opts);
let { byteLength } = arrayBuffer;
dbgv && console.log(msg, `[2]playArrayBuffer[${byteLength}]`,
arrayBuffer);
return this.playArrayBuffer({arrayBuffer, audioContext, });
},
async fetchArrayBuffer(url, opts={}) {
const msg = `audio.fetchArrayBuffer()`;
const dbg = DBG_AUDIO || DBG_SOUND_STORE;
const dbgv = DBG.VERBOSE && dbg;
const volatile = useVolatileStore();
let { headers=HEADERS_MPEG } = opts;
try {
let urlParts = url.split('/');
let iKey = Math.max(0, urlParts.length-4);
let idbSegKey = urlParts.slice(iKey).join('/');
dbg && console.log(msg, '[1]idbGet', idbSegKey);
let abuf = await Idb.get(idbSegKey, SOUND_STORE());
if (abuf) {
dbg && console.log(msg, `[2]cached ${abuf.byteLength}B`);
} else {
this.nFetch++;
dbgv && console.log(msg, '[3]fetch', url);
let res = await fetch(url, { headers });
switch (res.status) {
case 200:
dbg && console.log(msg, `[4]fetch ok`);
break;
default:
dbg && console.log(msg, `[5]fetch => HTTP${res.status}`);
break;
}
abuf = await res.arrayBuffer();
dbg && console.log(msg, `[6]Idb.set`, idbSegKey,
abuf.byteLength);
await Idb.set(idbSegKey, abuf, SOUND_STORE());
}
return abuf;
} catch(e) {
let eNew = new Error(`${msg} => ${e.message}`);
volatile.alert(eNew.message, 'ebt.audioError');
throw eNew;
}
},
async langAudioUrl(opts={}) {
const msg = 'audio.langAudioUrl() ';
const dbg = DBG_AUDIO;
let {idOrRef, lang, settings=useSettingsStore(), segAudio} = opts;
let { serverUrl, langTrans } = settings;
if (typeof lang !== 'string') {
if (lang) {
throw new Error(msg + `lang is required: ${lang}`);
}
lang = settings.langTrans;
}
lang = lang.toLowerCase();
let segRef = EbtSettings.segmentRef(idOrRef, settings);
//console.log(msg, {idOrRef, segRef});
segAudio = segAudio || await this.getSegmentAudio(segRef, settings);
dbg && console.log(msg, '[1]segAudio', segAudio);
let {
sutta_uid, translator, segment, vnameRoot, vnameTrans
} = segAudio;
let { audio } = segment;
let guid = audio[lang];
let text = segment[lang];
let url = null;
if (text) {
url = [
serverUrl,
'audio',
sutta_uid,
lang,
lang === 'pli' ? 'ms' : translator,
lang === langTrans ? vnameTrans : vnameRoot,
guid,
].join('/');
}
return url;
},
async createAudioBuffer({audioContext, arrayBuffer}) {
const msg = 'audio.createAudioBuffer()';
const dbg = DBG_AUDIO;
const dbgv = DBG.VERBOSE && dbg;
const volatile = useVolatileStore();
try {
if (arrayBuffer.byteLength < 500) {
let emsg = `${msg} invalid arrayBuffer`;
volatile.alert(emsg, 'ebt.audioError');
throw new Error(emsg);
}
let audioData = await new Promise((resolve, reject)=>{
audioContext.decodeAudioData(arrayBuffer, resolve, reject);
});
let numberOfChannels = Math.min(2, audioData.numberOfChannels);
let length = audioData.length;
let sampleRate = Math.max(SAMPLE_RATE, audioData.sampleRate);
dbgv && console.log(msg, '[1]',
{sampleRate, length, numberOfChannels});
let audioBuffer = audioContext.createBuffer(
numberOfChannels, length, sampleRate);
for (let iChan = 0; iChan < numberOfChannels; iChan++) {
let rawData = new Float32Array(length);
rawData.set(audioData.getChannelData(iChan), 0);
audioBuffer.getChannelData(iChan).set(rawData);
}
return audioBuffer;
} catch(e) {
let emsg = `${msg} ERROR`;
logger.warn(emsg);
dbgv && console.trace(emsg, e);
throw e;
}
},
async createAudioSource({audioContext, audioBuffer}) {
const msg = 'audio.createAudioSource()';
const dbg = DBG_AUDIO;
dbg && console.log(msg, '[1]createBufferSource');
let audioSource = audioContext.createBufferSource();
audioSource.buffer = audioBuffer;
audioSource.connect(audioContext.destination);
return audioSource;
},
async playAudioSource({audioSource}) {
const msg = 'audio.playAudioSource()';
const dbg = DBG_AUDIO;
const volatile = useVolatileStore();
return new Promise((resolve, reject) => { try {
audioSource.onended = evt => {
dbg && console.log(msg, '[1]ok', {evt});
resolve();
};
audioSource.start();
} catch(e) {
volatile.alert(e, 'ebt.audioError');
reject(e);
}}); // Promise
},
async playArrayBuffer({arrayBuffer, audioContext, }) {
const msg = 'audio.playArrayBuffer()';
const dbg = DBG_AUDIO;
const volatile = useVolatileStore();
try {
let bytes = arrayBuffer?.byteLength;
let audioBuffer = await this.createAudioBuffer(
{audioContext, arrayBuffer});
let audioSource = await this.createAudioSource(
{audioBuffer, audioContext});
dbg && console.log(msg, '[1]playAudioSource',
{bytes, audioSource});
return this.playAudioSource({audioContext, audioSource});
} catch(e) {
volatile.alert(e, 'ebt.audioError');
throw e;
}
},
async bindSegmentAudio(args={}) {
const msg = 'audio.bindSegmentAudio() ';
const dbg = DBG_AUDIO;
const dbgv = DBG.VERBOSE && dbg;
let {
volatile=useVolatileStore(),
settings=useSettingsStore(),
} = args;
let { routeCard } = volatile;
if (routeCard == null) {
return null;
}
let result;
let { langTrans, serverUrl } = settings;
let [ /*scid*/, lang=langTrans, author ] =
routeCard?.location || {};
let srefStr = routeCard.location.join('/');
let suttaRef = SuttaRef.create(srefStr, langTrans);
let { sutta_uid, } = suttaRef;
try {
volatile.waitBegin('ebt.loadingAudio');
let segAudio = await this.getSegmentAudio(suttaRef);
dbg && console.log(msg, '[1]segAudio', segAudio);
let { segment, vnameTrans, vnameRoot } = segAudio;
if (settings.speakPali) {
if (segment.pli) {
this.pliAudioUrl = [
serverUrl,
'audio',
sutta_uid,
'pli',
author,
vnameRoot,
segment.audio.pli,
].join('/');
} else {
this.pliAudioUrl = URL_NOAUDIO;
}
dbg && console.log(msg, '[2]pliAudioUrl', this.pliAudioUrl);
}
if (segment && settings.speakTranslation) {
let langText = segment[lang];
if (langText) {
this.transAudioUrl = [
serverUrl,
'audio',
sutta_uid,
lang,
author,
vnameTrans,
segment.audio[lang],
].join('/');
} else {
this.transAudioUrl = URL_NOAUDIO;
}
dbg && console.log(msg, '[3]transAudioUrl', this.transAudioUrl);
}
result = segAudio;
} finally {
volatile.waitEnd();
}
return result;
},
},
})