summaryrefslogtreecommitdiff
path: root/packages/frontend/src/scripts/sound.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/scripts/sound.ts')
-rw-r--r--packages/frontend/src/scripts/sound.ts168
1 files changed, 147 insertions, 21 deletions
diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts
index 4b0cd0bb39..2f7545ef0d 100644
--- a/packages/frontend/src/scripts/sound.ts
+++ b/packages/frontend/src/scripts/sound.ts
@@ -3,13 +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';
-const ctx = new AudioContext();
+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',
@@ -38,6 +47,8 @@ export const soundsTypes = [
'syuilo/waon',
'syuilo/popo',
'syuilo/triple',
+ 'syuilo/bubble1',
+ 'syuilo/bubble2',
'syuilo/poi1',
'syuilo/poi2',
'syuilo/pirori',
@@ -61,46 +72,161 @@ export const soundsTypes = [
'noizenecio/kick_gaba7',
] as const;
-export async function getAudio(file: string, useCache = true) {
- if (useCache && cache.has(file)) {
- return cache.get(file)!;
+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 (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 setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement {
- const masterVolume = defaultStore.state.sound_masterVolume;
- audio.volume = masterVolume - ((1 - volume) * masterVolume);
- return audio;
+/**
+ * 既定のスプライトを再生する
+ * @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).finally(() => {
+ // ごく短時間に音が重複しないように
+ setTimeout(() => {
+ canPlay = true;
+ }, 25);
+ });
}
-export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') {
- const sound = defaultStore.state[`sound_${type}`];
- if (_DEV_) console.log('play', type, sound);
- if (sound.type == null) return;
- playFile(sound.type, sound.volume);
+/**
+ * サウンド設定形式で指定された音声を再生する
+ * @param soundStore サウンド設定
+ */
+export async function playFile(soundStore: SoundStore) {
+ const buffer = await loadAudio(soundStore);
+ if (!buffer) return;
+ createSourceNode(buffer, soundStore.volume)?.start();
}
-export async function playFile(file: string, volume: number) {
+export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null {
const masterVolume = defaultStore.state.sound_masterVolume;
- if (masterVolume === 0 || volume === 0) {
- return;
+ if (isMute() || masterVolume === 0 || volume === 0) {
+ return null;
}
const gainNode = ctx.createGain();
gainNode.gain.value = masterVolume * volume;
const soundSource = ctx.createBufferSource();
- soundSource.buffer = await getAudio(file);
+ soundSource.buffer = buffer;
soundSource.connect(gainNode).connect(ctx.destination);
- soundSource.start();
+
+ 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) {
+ // サウンドを出力しない
+ return true;
+ }
+
+ // noinspection RedundantIfStatementJS
+ if (defaultStore.state.sound_useSoundOnlyWhenActive && document.visibilityState === 'hidden') {
+ // ブラウザがアクティブな時のみサウンドを出力する
+ return true;
+ }
+
+ return false;
}