Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
at lambda-fork/main 154 lines 4.8 kB view raw
1/* 2 * Copyright (C) 2023-2025 Yomitan Authors 3 * Copyright (C) 2019-2022 Yomichan Authors 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <https://www.gnu.org/licenses/>. 17 */ 18 19import {EventDispatcher} from '../core/event-dispatcher.js'; 20import {TextToSpeechAudio} from './text-to-speech-audio.js'; 21 22/** 23 * @augments EventDispatcher<import('audio-system').Events> 24 */ 25export class AudioSystem extends EventDispatcher { 26 constructor() { 27 super(); 28 /** @type {?HTMLAudioElement} */ 29 this._fallbackAudio = null; 30 /** @type {?import('settings').FallbackSoundType} */ 31 this._fallbackSoundType = null; 32 } 33 34 /** 35 * @returns {void} 36 */ 37 prepare() { 38 // speechSynthesis.getVoices() will not be populated unless some API call is made. 39 if ( 40 typeof speechSynthesis !== 'undefined' && 41 typeof speechSynthesis.addEventListener === 'function' 42 ) { 43 speechSynthesis.addEventListener('voiceschanged', this._onVoicesChanged.bind(this), false); 44 } 45 } 46 47 /** 48 * @param {import('settings').FallbackSoundType} fallbackSoundType 49 * @returns {HTMLAudioElement} 50 */ 51 getFallbackAudio(fallbackSoundType) { 52 if (this._fallbackAudio === null || this._fallbackSoundType !== fallbackSoundType) { 53 this._fallbackSoundType = fallbackSoundType; 54 switch (fallbackSoundType) { 55 case 'click': 56 this._fallbackAudio = new Audio('/data/audio/fallback-click.mp3'); 57 break; 58 case 'bloop': 59 this._fallbackAudio = new Audio('/data/audio/fallback-bloop.mp3'); 60 break; 61 case 'none': 62 // audio handler expects audio url to always be present, empty string must be used instead of `new Audio()` 63 this._fallbackAudio = new Audio(''); 64 break; 65 } 66 } 67 return this._fallbackAudio; 68 } 69 70 /** 71 * @param {string} url 72 * @param {import('settings').AudioSourceType} sourceType 73 * @returns {Promise<HTMLAudioElement>} 74 */ 75 async createAudio(url, sourceType) { 76 const audio = new Audio(url); 77 await this._waitForData(audio); 78 if (!this._isAudioValid(audio, sourceType)) { 79 throw new Error('Could not retrieve audio'); 80 } 81 return audio; 82 } 83 84 /** 85 * @param {string} text 86 * @param {string} voiceUri 87 * @returns {TextToSpeechAudio} 88 * @throws {Error} 89 */ 90 createTextToSpeechAudio(text, voiceUri) { 91 const voice = this._getTextToSpeechVoiceFromVoiceUri(voiceUri); 92 if (voice === null) { 93 throw new Error('Invalid text-to-speech voice'); 94 } 95 return new TextToSpeechAudio(text, voice); 96 } 97 98 // Private 99 100 /** 101 * @param {Event} event 102 */ 103 _onVoicesChanged(event) { 104 this.trigger('voiceschanged', event); 105 } 106 107 /** 108 * @param {HTMLAudioElement} audio 109 * @returns {Promise<void>} 110 */ 111 _waitForData(audio) { 112 return new Promise((resolve, reject) => { 113 audio.addEventListener('loadeddata', () => resolve()); 114 audio.addEventListener('error', () => reject(audio.error)); 115 }); 116 } 117 118 /** 119 * @param {HTMLAudioElement} audio 120 * @param {import('settings').AudioSourceType} sourceType 121 * @returns {boolean} 122 */ 123 _isAudioValid(audio, sourceType) { 124 switch (sourceType) { 125 case 'jpod101': 126 { 127 const duration = audio.duration; 128 return ( 129 duration !== 5.694694 && // Invalid audio (Chrome) 130 duration !== 5.651111 // Invalid audio (Firefox) 131 ); 132 } 133 default: 134 return true; 135 } 136 } 137 138 /** 139 * @param {string} voiceUri 140 * @returns {?SpeechSynthesisVoice} 141 */ 142 _getTextToSpeechVoiceFromVoiceUri(voiceUri) { 143 try { 144 for (const voice of speechSynthesis.getVoices()) { 145 if (voice.voiceURI === voiceUri) { 146 return voice; 147 } 148 } 149 } catch (e) { 150 // NOP 151 } 152 return null; 153 } 154}