diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2023-11-27 17:33:42 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-11-27 17:33:42 +0900 |
| commit | 2a451ebb572531f5dfa34a3f863e2b15d29e7355 (patch) | |
| tree | 641ac9066edb00618d6e727770fd2073d974cae2 /packages/frontend/src/scripts | |
| parent | style: fix lint error of 6acaded8 (#12476) (diff) | |
| download | sharkey-2a451ebb572531f5dfa34a3f863e2b15d29e7355.tar.gz sharkey-2a451ebb572531f5dfa34a3f863e2b15d29e7355.tar.bz2 sharkey-2a451ebb572531f5dfa34a3f863e2b15d29e7355.zip | |
enhance(frontend): 通知音にドライブのファイルを使用できるように (#12447)
* (enhance) サウンドにドライブのファイルを使用できるように
* Update Changelog
* fix
* fix design
* fix design
* Update store.ts
* (fix) ファイル名表示
* refactor
* (refactor) better types
* operationTypeとsoundTypeの混同を防止
* (refactor)
* (fix)
* enhance jsdoc
* driveFile -> _driveFile_
Diffstat (limited to 'packages/frontend/src/scripts')
| -rw-r--r-- | packages/frontend/src/scripts/sound.ts | 124 |
1 files changed, 111 insertions, 13 deletions
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<string, AudioBuffer>(); 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<number> { + 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) { // サウンドを出力しない |