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_
This commit is contained in:
かっこかり 2023-11-27 17:33:42 +09:00 committed by GitHub
parent 8f1da036f4
commit 2a451ebb57
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 311 additions and 36 deletions

View file

@ -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 URLIDではない
*/
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) {
// サウンドを出力しない