import { insertHTMLChild, showRelatedHTML, syncHTMLValue, updateElementPosition, updateHTMLOptions, } from '@/components/helper' import '@/components/voice.css' import buildConfig from '!/config.json' export default class Voice { #el = document.createElement('div') #parentEl #charwordTable #default = { region: buildConfig.voice_default_region, duration: { idle: 10 * 60 * 1000, next: 3 * 60 * 1000, }, language: { voice: null, }, subtitle: { x: 0, y: 100, }, } #voice = { id: { current: null, last: null, }, listener: { idle: -1, next: -1, }, lastClickToNext: false, locations: null, list: null, } #audio = { id: 'voice-audio', el: new Audio(), isPlaying: false, } #config = { useSubtitle: false, useVoice: false, useVoiceActor: false, language: null, subtitle: { language: this.#default.region, ...this.#default.subtitle, }, duration: { ...this.#default.duration }, volume: 100, } #playerObj constructor(el) { this.#parentEl = el this.#el.id = 'voice-box' this.#el.hidden = true this.#el.innerHTML = `
` insertHTMLChild(this.#parentEl, this.#el) } async init() { const res = await fetch( `${import.meta.env.BASE_URL}${buildConfig.default_assets_dir}charword_table.json` ) this.#charwordTable = await res.json() this.#voice.languages = Object.keys( this.#charwordTable.voiceLangs[this.#default.region] ) this.#audio.el.volume = this.#volume this.#default.language.voice = this.#voice.languages[0] this.#config.language = this.#default.language.voice this.#voice.locations = this.#getVoiceLocations() this.#voice.list = Object.keys(this.#getVoices()) } success() { const audioEndedFunc = () => { this.#audio.isPlaying = false this.#setCurrentSubtitle(null) this.#audio.lastClickToNext = false } this.#audio.el.addEventListener('ended', audioEndedFunc) this.#playEntryVoice() this.#initNextVoiceTimer() this.#playerObj.node.addEventListener('click', () => { this.#audio.lastClickToNext = true this.#nextVoice() }) document.addEventListener('mousemove', () => { if (this.#voice.listener.idle === -1) { this.#initIdleVoiceTimer() } }) } link(playerObj) { this.#playerObj = playerObj } resetPosition() { this.position = { ...this.#default.subtitle } document.getElementById('subtitle-padding-x-slider').value = this.#default.subtitle.x document.getElementById('subtitle-padding-x-input').value = this.#default.subtitle.x document.getElementById('subtitle-padding-y-slider').value = this.#default.subtitle.y document.getElementById('subtitle-padding-y-input').value = this.#default.subtitle.y } reset() { this.resetPosition() } #getVoiceLocations() { const folders = buildConfig.voice_folders const customVoiceName = this.#voice.languages.filter( (i) => !folders.sub.map((e) => e.lang).includes(i) )[0] folders.sub = folders.sub.map((e) => { return { name: e.name, lang: e.lang === 'CUSTOM' ? customVoiceName : e.lang, } }) return folders } #getVoices(lang = null) { return this.#charwordTable.subtitleLangs[ lang ? lang : this.#config.subtitle.language ].default } #playEntryVoice() { this.#playSpecialVoice('问候') } #playSpecialVoice(matcher) { const voices = this.#getVoices(buildConfig.voice_default_region) const voiceId = Object.keys(voices).find( (e) => voices[e].title === matcher ) this.#playVoice(voiceId) } #playVoice(id) { if (!this.useVoice) return this.#voice.id.last = this.#voice.id.current this.#voice.id.current = id this.#audio.el.src = `${import.meta.env.BASE_URL}${buildConfig.default_assets_dir}${this.#getVoiceLocation()}/${id}.ogg` let startPlayPromise = this.#audio.el.play() if (startPlayPromise !== undefined) { startPlayPromise .then(() => { this.#audio.isPlaying = true this.#setCurrentSubtitle(id) }) .catch(() => { return }) } } #getVoiceLocation() { const locations = this.#voice.locations return `${locations.main}/${locations.sub.find((e) => e.lang === this.#config.language).name}` } #setCurrentSubtitle(id) { if (id === null) { setTimeout(() => { if (this.#audio.isPlaying) return this.#toggleSubtitle(0) }, 5 * 1000) return } const subtitle = this.#getSubtitleById(id) const title = subtitle.title const content = subtitle.text const cvInfo = this.#charwordTable.voiceLangs[this.subtitleLanguage][ this.#config.language ] document.getElementById('voice-title').innerText = title document.getElementById('voice-subtitle').innerText = content document.getElementById('voice-actor-name').innerText = cvInfo.join('') if (this.#audio.isPlaying) { this.#toggleSubtitle(1) } } #toggleSubtitle(v) { this.#el.style.opacity = v ? 1 : 0 } #getSubtitleById(id) { const obj = this.#charwordTable.subtitleLangs[this.#config.subtitle.language] let key = 'default' if (obj[this.#config.language]) { key = this.#config.language } return obj[key][id] } #getSubtitleLanguages() { return Object.keys(this.#charwordTable.subtitleLangs) } #updateSubtitlePosition() { updateElementPosition(this.#el, { x: this.position.x, y: this.position.y - 100, }) } #initNextVoiceTimer() { this.#voice.listener.next = setInterval(() => { if (!this.#voice.lastClickToNext) { this.#nextVoice() } }, this.#config.duration.next) } #nextVoice() { const getVoiceId = () => { let list = this.#voice.list if ( this.#config.language === 'EN' || this.#config.language === 'KR' ) { // filter out CN_043 as this voice is not available in en and kr list = list.filter((item) => item !== 'CN_043') } const id = list[Math.floor(Math.random() * list.length)] return id === this.#voice.id.last ? getVoiceId() : id } this.#playVoice(getVoiceId()) } #initIdleVoiceTimer() { this.#voice.listener.idle = setInterval(() => { this.#playSpecialVoice('闲置') clearInterval(this.#voice.listener.idle) this.#voice.listener.idle = -1 }, this.#config.duration.idle) } set useSubtitle(show) { this.#config.useSubtitle = show this.#el.hidden = !show } get useSubtitle() { return this.#config.useSubtitle } set useVoice(show) { this.#config.useVoice = show this.#playEntryVoice() if (!show && this.#audio.isPlaying) { this.#audio.el.pause() } this.#toggleSubtitle(0) } get useVoice() { return this.#config.useVoice } set useVoiceActor(show) { this.#config.useVoiceActor = show document.getElementById('voice-actor-box').hidden = !show } get useVoiceActor() { return this.#config.useVoiceActor } set subtitleLanguage(lang) { if (this.#getSubtitleLanguages().includes(lang)) { this.#config.subtitle.language = lang } else { this.#config.subtitle.language = this.#default.region } this.#setCurrentSubtitle(this.#voice.id.current) } get subtitleLanguage() { return this.#config.subtitle.language } get subtitleLanguages() { return this.#getSubtitleLanguages() } get x() { return this.position.x } set x(v) { this.position = { x: v, } } get y() { return this.position.y } set y(v) { this.position = { y: v, } } get position() { return { x: this.#config.subtitle.x, y: this.#config.subtitle.y, } } set position(v) { if (typeof v !== 'object') return if (v.x) v.x = parseInt(v.x) if (v.y) v.y = parseInt(v.y) this.#config.subtitle = { ...this.#config.subtitle, ...v } console.log(v) this.#updateSubtitlePosition() } set language(lang) { if (this.#voice.languages.includes(lang)) { this.#config.language = lang } else { this.#config.language = this.#default.language.voice } const availableSubtitleLang = this.#getSubtitleLanguages() if (!availableSubtitleLang.includes(this.#config.subtitle.language)) { this.#config.subtitle.language = availableSubtitleLang[0] } } get language() { return this.#config.language } get languages() { return this.#voice.languages } get duration() { return { idle: this.#config.duration.idle / 60 / 1000, next: this.#config.duration.next / 60 / 1000, } } set duration(v) { if (typeof v !== 'object') return if (v.idle) { clearInterval(this.#voice.listener.idle) if (v.idle !== 0) { this.#config.duration.idle = parseInt(v.idle) * 60 * 1000 this.#initIdleVoiceTimer() } } if (v.next) { clearInterval(this.#voice.listener.next) if (v.next !== 0) { this.#config.duration.next = parseInt(v.next) * 60 * 1000 this.#initNextVoiceTimer() } } } get durationIdle() { return this.duration.idle } set durationIdle(duration) { this.duration = { idle: duration, } } set durationNext(duration) { this.duration = { next: duration, } } get durationNext() { return this.duration.next } get volume() { return this.#config.volume } get #volume() { return this.#config.volume / 100 } set volume(value) { value = value < 0 ? 0 : value > 100 ? 100 : parseInt(value) this.#config.volume = value this.#audio.el.volume = this.#volume } set subtitleX(x) { // Note: Back Compatibility this.position = { x, } } get subtitleX() { // Note: Back Compatibility return this.position.x } set subtitleY(y) { // Note: Back Compatibility this.position = { y, } } get subtitleY() { // Note: Back Compatibility return this.position.y } set idleDuration(duration) { // Note: Back Compatibility this.duration = { idle: duration, } } get idleDuration() { // Note: Back Compatibility return this.duration.idle } set nextDuration(duration) { // Note: Back Compatibility this.duration.next = duration } get nextDuration() { // Note: Back Compatibility return this.duration.next } get config() { return { ...this.#config } } get HTML() { return `
` } get listeners() { return [ { id: 'voice', event: 'click', handler: (e) => { showRelatedHTML(e.currentTarget, 'voice-realted') this.useVoice = e.currentTarget.checked }, }, { id: 'voice-lang-select', event: 'change', handler: (e) => { this.language = e.currentTarget.value }, }, { id: 'voice-volume-slider', event: 'input', handler: (e) => { syncHTMLValue(e.currentTarget, 'voice-volume-input') this.volume = e.currentTarget.value }, }, { id: 'voice-volume-input', event: 'change', handler: (e) => { syncHTMLValue(e.currentTarget, 'voice-volume-slider') this.volume = e.currentTarget.value }, }, { id: 'voice-idle-duration-input', event: 'change', handler: (e) => { this.duration = { idle: e.currentTarget.value, } }, }, { id: 'voice-next-duration-input', event: 'change', handler: (e) => { this.duration = { next: e.currentTarget.value, } }, }, { id: 'subtitle', event: 'click', handler: (e) => { showRelatedHTML(e.currentTarget, 'subtitle-realted') this.useSubtitle = e.currentTarget.checked }, }, { id: 'subtitle-lang-select', event: 'change', handler: (e) => (this.subtitleLanguage = e.currentTarget.value), }, { id: 'subtitle-padding-x-slider', event: 'input', handler: (e) => { syncHTMLValue(e.currentTarget, 'subtitle-padding-x-input') this.position = { x: e.currentTarget.value, } }, }, { id: 'subtitle-padding-x-input', event: 'change', handler: (e) => { syncHTMLValue(e.currentTarget, 'subtitle-padding-x-slider') this.position = { x: e.currentTarget.value, } }, }, { id: 'subtitle-padding-y-slider', event: 'input', handler: (e) => { syncHTMLValue(e.currentTarget, 'subtitle-padding-y-input') this.position = { y: e.currentTarget.value, } }, }, { id: 'subtitle-padding-y-input', event: 'change', handler: (e) => { syncHTMLValue(e.currentTarget, 'subtitle-padding-y-slider') this.position = { y: e.currentTarget.value, } }, }, { id: 'voice-actor', event: 'click', handler: (e) => { this.useVoiceActor = e.currentTarget.checked }, }, ] } applyConfig(key, value) { switch (key) { case 'use-voice': this.useVoice = value break case 'language': this.language = value break case 'duration': this.duration = value break case 'use-subtitle': this.useSubtitle = value break case 'subtitle-language': this.subtitleLanguage = value break case 'subtitle-position': this.position = value break case 'use-voice-actor': this.useVoiceActor = value break case 'volume': this.volume = value break default: return } } }