From 2a451ebb572531f5dfa34a3f863e2b15d29e7355 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Mon, 27 Nov 2023 17:33:42 +0900 Subject: enhance(frontend): 通知音にドライブのファイルを使用できるように (#12447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * (enhance) サウンドにドライブのファイルを使用できるように * Update Changelog * fix * fix design * fix design * Update store.ts * (fix) ファイル名表示 * refactor * (refactor) better types * operationTypeとsoundTypeの混同を防止 * (refactor) * (fix) * enhance jsdoc * driveFile -> _driveFile_ --- packages/frontend/src/scripts/sound.ts | 124 +++++++++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 13 deletions(-) (limited to 'packages/frontend/src/scripts') diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index a3cddba1f4..a4c6967d18 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -3,14 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import type { SoundStore } from '@/store.js'; import { defaultStore } from '@/store.js'; +import * as os from '@/os.js'; let ctx: AudioContext; const cache = new Map(); let canPlay = true; export const soundsTypes = [ + // 音声なし null, + + // ドライブの音声 + '_driveFile_', + + // プリインストール 'syuilo/n-aec', 'syuilo/n-aec-4va', 'syuilo/n-aec-4vb', @@ -64,32 +72,96 @@ export const soundsTypes = [ 'noizenecio/kick_gaba7', ] as const; -export async function loadAudio(file: string, useCache = true) { +export const operationTypes = [ + 'noteMy', + 'note', + 'antenna', + 'channel', + 'notification', + 'reaction', +] as const; + +/** サウンドの種類 */ +export type SoundType = typeof soundsTypes[number]; + +/** スプライトの種類 */ +export type OperationType = typeof operationTypes[number]; + +/** + * 音声を読み込む + * @param soundStore サウンド設定 + * @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする + */ +export async function loadAudio(soundStore: SoundStore, options?: { useCache?: boolean; }) { + if (_DEV_) console.log('loading audio. opts:', options); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { + return; + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (ctx == null) { ctx = new AudioContext(); } - if (useCache && cache.has(file)) { - return cache.get(file)!; + if (options?.useCache ?? true) { + if (soundStore.type === '_driveFile_' && cache.has(soundStore.fileId)) { + if (_DEV_) console.log('use cache'); + return cache.get(soundStore.fileId) as AudioBuffer; + } else if (cache.has(soundStore.type)) { + if (_DEV_) console.log('use cache'); + return cache.get(soundStore.type) as AudioBuffer; + } + } + + let response: Response; + + if (soundStore.type === '_driveFile_') { + try { + response = await fetch(soundStore.fileUrl); + } catch (err) { + try { + // URLが変わっている可能性があるのでドライブ側からURLを取得するフォールバック + const apiRes = await os.api('drive/files/show', { + fileId: soundStore.fileId, + }); + response = await fetch(apiRes.url); + } catch (fbErr) { + // それでも無理なら諦める + return; + } + } + } else { + try { + response = await fetch(`/client-assets/sounds/${soundStore.type}.mp3`); + } catch (err) { + return; + } } - const response = await fetch(`/client-assets/sounds/${file}.mp3`); const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await ctx.decodeAudioData(arrayBuffer); - if (useCache) { - cache.set(file, audioBuffer); + if (options?.useCache ?? true) { + if (soundStore.type === '_driveFile_') { + cache.set(soundStore.fileId, audioBuffer); + } else { + cache.set(soundStore.type, audioBuffer); + } } return audioBuffer; } -export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification' | 'reaction') { - const sound = defaultStore.state[`sound_${type}`]; - if (_DEV_) console.log('play', type, sound); +/** + * 既定のスプライトを再生する + * @param type スプライトの種類を指定 + */ +export function play(operationType: OperationType) { + const sound = defaultStore.state[`sound_${operationType}`]; + if (_DEV_) console.log('play', operationType, sound); if (sound.type == null || !canPlay) return; canPlay = false; - playFile(sound.type, sound.volume).then(() => { + playFile(sound).then(() => { // ごく短時間に音が重複しないように setTimeout(() => { canPlay = true; @@ -97,9 +169,14 @@ export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notifica }); } -export async function playFile(file: string, volume: number) { - const buffer = await loadAudio(file); - createSourceNode(buffer, volume)?.start(); +/** + * サウンド設定形式で指定された音声を再生する + * @param soundStore サウンド設定 + */ +export async function playFile(soundStore: SoundStore) { + const buffer = await loadAudio(soundStore); + if (!buffer) return; + createSourceNode(buffer, soundStore.volume)?.start(); } export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null { @@ -118,6 +195,27 @@ export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBuf return soundSource; } +/** + * 音声の長さをミリ秒で取得する + * @param file ファイルのURL(ドライブIDではない) + */ +export async function getSoundDuration(file: string): Promise { + const audioEl = document.createElement('audio'); + audioEl.src = file; + return new Promise((resolve) => { + const si = setInterval(() => { + if (audioEl.readyState > 0) { + resolve(audioEl.duration * 1000); + clearInterval(si); + audioEl.remove(); + } + }, 100); + }); +} + +/** + * ミュートすべきかどうかを判断する + */ export function isMute(): boolean { if (defaultStore.state.sound_notUseSound) { // サウンドを出力しない -- cgit v1.2.3-freya