Audio: consolidate sound system into SoundManager, fix bootstrap + master toggle

- Move sound catalog registration off PageScene mount and into a single
  SoundManager.install() call in GlobalSettingsManager so sounds exist at
  app boot (previously silent until PageScene mounted).
- Honor the play_sounds master toggle in all five event playback sites
  via SoundManager.playEvent helpers. Settings preview keeps firing
  regardless (handled by SoundManager.playPreview).
- Replace per-callsite GetAppPreferences + SoundRegistry boilerplate in
  the four generation event hooks and the file-deleted handler with
  one-line SoundManager.* calls.
- Dedupe SOUND_OPTIONS: AudioSettingsPane now reads SoundManager.OPTIONS,
  derived from a single declarative SOUNDS array (label-bearing entries
  are user-selectable; others are still registered for code-driven use).
- Narrow the soundboard public barrel to SoundManager + SoundOption only;
  SoundRegistry and SoundEffect are now lib-internal.
This commit is contained in:
Tanish Manku committed 2026-04-30 09:02:26 -07:00
1 parent 2b5e509a29
commit bb7e631834
22 files changed
+183 -185

No files matched your search

+5
View File
@@ -13,6 +13,7 @@ import EnvironmentVariables from "~/Classes/EnvironmentVariables";
import { pageHeight, pageWidth, persistLogin } from "~/signals";
import { SyncStorytellerApiConfig } from "./api/SyncStorytellerApiConfig";
import { posthog } from "posthog-js";
import { SoundManager } from "@storyteller/soundboard";
config.autoAddCss = false; /* eslint-disable import/first */
@@ -84,6 +85,10 @@ const GlobalSettingsManager = ({ env }: { env: Record<string, string> }) => {
};
}, []);
useEffect(() => {
SoundManager.install();
}, []);
return null;
};
@@ -1,58 +0,0 @@
import { SoundRegistry, SoundEffect } from "@storyteller/soundboard";
// TODO: This is maintained in two places. Here and MiscSettingsPane.
export const InstallSounds = () => {
const r = SoundRegistry.getInstance();
if (r.hasSound("decline_normal")) {
return;
}
// Good for simple menu choices
r.setSoundOnce("click", new SoundEffect("resources/sound/smrpg_click.wav", { defaultVolume: 0.2 }));
r.setSoundOnce("scifi_menu_beep_1", new SoundEffect("resources/sound/metroidprime_UI_15.wav", { defaultVolume: 0.3 }));
r.setSoundOnce("scifi_menu_beep_2", new SoundEffect("resources/sound/metroidprime_UI_14.wav", { defaultVolume: 0.3 }));
r.setSoundOnce("scifi_menu_select", new SoundEffect("resources/sound/metroidprime_UI_18.wav", { defaultVolume: 0.3 }));
// Good for simple immediate enqueue success
r.setSoundOnce("done", new SoundEffect("resources/sound/oot_dialogue_done.wav", { defaultVolume: 0.4 })); // DEFAULT
// Good for simple immediate failure
r.setSoundOnce("error_chirp", new SoundEffect("resources/sound/goldensun_135.wav", { defaultVolume: 0.4 }));
r.setSoundOnce("spike_throw", new SoundEffect("resources/sound/smrpg_enemy_spikethrow.wav", { defaultVolume: 0.1 })); // DEFAULT
r.setSoundOnce("giant_shell_kick", new SoundEffect("resources/sound/smrpg_mario_giantshellkick.wav", { defaultVolume: 0.2 }));
r.setSoundOnce("wrong", new SoundEffect("resources/sound/smrpg_wrong.wav", { defaultVolume: 0.4 }));
// Good for simple async success
r.setSoundOnce("special_flower", new SoundEffect("resources/sound/smrpg_specialflower.wav", { defaultVolume: 0.2 })); // DEFAULT
r.setSoundOnce("extra_power", new SoundEffect("resources/sound/smrpg_character_extrapower.wav", { defaultVolume: 0.2 })); // DEFAULT
// Good for async errors
r.setSoundOnce("crumble", new SoundEffect("resources/sound/smrpg_drybones_crumble.wav", { defaultVolume: 0.1 })); // DEFAULT
r.setSoundOnce("ghost", new SoundEffect("resources/sound/smrpg_ghost.wav", { defaultVolume: 0.2 }));
r.setSoundOnce("special_alert", new SoundEffect("resources/sound/goldensun_214.wav", { defaultVolume: 0.2 }));
r.setSoundOnce("scifi_alert", new SoundEffect("resources/sound/metroidprime_UI_52.wav", { defaultVolume: 0.2 }));
r.setSoundOnce("scifi_shrill_alert", new SoundEffect("resources/sound/metroidprime_UI_51.wav", { defaultVolume: 0.2 }));
// Good for menus
r.setSoundOnce("next", new SoundEffect("resources/sound/oot_dialogue_next.wav", { defaultVolume: 0.2 }));
r.setSoundOnce("select", new SoundEffect("resources/sound/goldensun_111.wav", { defaultVolume: 0.4 }));
r.setSoundOnce("scifi_menu_open", new SoundEffect("resources/sound/metroidprime_UI_12.wav", { defaultVolume: 0.3 }));
r.setSoundOnce("scifi_menu_close", new SoundEffect("resources/sound/metroidprime_UI_13.wav", { defaultVolume: 0.3 }));
// Good for special reward / celebration
r.setSoundOnce("correct", new SoundEffect("resources/sound/smrpg_correct.wav", { defaultVolume: 0.1 }));
r.setSoundOnce("flower", new SoundEffect("resources/sound/smrpg_flower.wav", { defaultVolume: 0.1 }));
// File delete, trash, etc.
r.setSoundOnce("trash", new SoundEffect("resources/sound/oot_scrub_crumble.wav", { defaultVolume: 0.4 }));
// Misc / ungrouped
r.setSoundOnce("accept_chirp", new SoundEffect("resources/sound/goldensun_101.wav", { defaultVolume: 0.2 }));
r.setSoundOnce("accept_normal_level_1", new SoundEffect("resources/sound/goldensun_173.wav", { defaultVolume: 0.2 }));
r.setSoundOnce("accept_normal_level_2", new SoundEffect("resources/sound/goldensun_174.wav", { defaultVolume: 0.2 }));
r.setSoundOnce("accept_normal_level_3", new SoundEffect("resources/sound/goldensun_175.wav", { defaultVolume: 0.2 }));
r.setSoundOnce("decline_chirp", new SoundEffect("resources/sound/goldensun_102.wav", { defaultVolume: 0.2 }));
r.setSoundOnce("decline_normal", new SoundEffect("resources/sound/goldensun_113.wav", { defaultVolume: 0.2 }));
(window as any).sounds = r;
}
@@ -10,7 +10,6 @@ import { useEffect, useState } from "react";
import * as gpu from "detect-gpu";
import { UsersApi } from "~/Classes/ApiManager";
import { PrecisionSelector } from "./comps/PrecisionSelector/PrecisionSelector";
import { InstallSounds } from "~/pages/PageScene/InstallSounds";
import { PageEditor } from "~/pages/PageScene/PageEditor";
import { GalleryDragComponent } from "@storyteller/ui-gallery-modal";
import {
@@ -31,8 +30,7 @@ import { useGenerationEnqueueSuccessEvent } from "@storyteller/tauri-events";
import { useGenerationFailedEvent } from "@storyteller/tauri-events";
import { useTextToImageGenerationCompleteEvent } from "@storyteller/tauri-events";
import { useTextToImageStore } from "~/pages/PageImage/TextToImageStore";
import { GetAppPreferences } from "@storyteller/tauri-api";
import { SoundRegistry } from "@storyteller/soundboard";
import { SoundManager } from "@storyteller/soundboard";
export const PageScene = ({ sceneToken }: { sceneToken?: string }) => {
useSignals();
@@ -110,20 +108,10 @@ export const PageScene = ({ sceneToken }: { sceneToken?: string }) => {
useMediaFileDeletedEvent(async (event) => {
console.log("Media file deleted event received:", event);
const prefs = await GetAppPreferences();
const soundName = prefs.preferences?.delete_file_sound;
if (soundName !== undefined) {
const registry = SoundRegistry.getInstance();
registry.playSound(soundName);
}
await SoundManager.playFileDeleted();
toast.error("File deleted.");
});
useEffect(() => {
console.log("installing event listeners");
InstallSounds();
});
const currentReminderModalProps = actionReminderProps.value;
return (
+6 -12
View File
@@ -28,23 +28,14 @@
"noEmit": true
},
"references": [
{
"path": "../../libs/components/pagination"
},
{
"path": "../../libs/components/search"
},
{
"path": "../../libs/components/badge"
},
{
"path": "../../libs/components/button-icon"
},
{
"path": "../../libs/components/button-icon-select"
"path": "../../libs/components/label"
},
{
"path": "../../libs/components/label"
"path": "../../libs/components/button-icon-select"
},
{
"path": "../../libs/components/button-dropdown"
@@ -56,11 +47,14 @@
"path": "../../libs/components/login-modal"
},
{
"path": "../../libs/soundboard"
"path": "../../libs/components/badge"
},
{
"path": "../../libs/components/slider-v2"
},
{
"path": "../../libs/soundboard"
},
{
"path": "../../libs/components/pagedraw"
},
+1 -1
View File
@@ -1 +1 @@
{"fileNames":["../../node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/typescript/lib/lib.es2023.d.ts","../../node_modules/typescript/lib/lib.es2024.d.ts","../../node_modules/typescript/lib/lib.esnext.d.ts","../../node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/typescript/lib/lib.es2023.array.d.ts","../../node_modules/typescript/lib/lib.es2023.collection.d.ts","../../node_modules/typescript/lib/lib.es2023.intl.d.ts","../../node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../../node_modules/typescript/lib/lib.es2024.collection.d.ts","../../node_modules/typescript/lib/lib.es2024.object.d.ts","../../node_modules/typescript/lib/lib.es2024.promise.d.ts","../../node_modules/typescript/lib/lib.es2024.regexp.d.ts","../../node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2024.string.d.ts","../../node_modules/typescript/lib/lib.esnext.array.d.ts","../../node_modules/typescript/lib/lib.esnext.collection.d.ts","../../node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/typescript/lib/lib.esnext.promise.d.ts","../../node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../node_modules/typescript/lib/lib.esnext.iterator.d.ts","../../node_modules/typescript/lib/lib.esnext.float16.d.ts","../../node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/@remix-run/node/dist/globals.d.ts","../../node_modules/@types/cookie/index.d.ts","../../node_modules/@remix-run/server-runtime/dist/crypto.d.ts","../../node_modules/@remix-run/server-runtime/dist/cookies.d.ts","../../node_modules/@remix-run/server-runtime/dLine truncated
{"fileNames":["../../node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/typescript/lib/lib.es2023.d.ts","../../node_modules/typescript/lib/lib.es2024.d.ts","../../node_modules/typescript/lib/lib.esnext.d.ts","../../node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/typescript/lib/lib.es2023.array.d.ts","../../node_modules/typescript/lib/lib.es2023.collection.d.ts","../../node_modules/typescript/lib/lib.es2023.intl.d.ts","../../node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../../node_modules/typescript/lib/lib.es2024.collection.d.ts","../../node_modules/typescript/lib/lib.es2024.object.d.ts","../../node_modules/typescript/lib/lib.es2024.promise.d.ts","../../node_modules/typescript/lib/lib.es2024.regexp.d.ts","../../node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2024.string.d.ts","../../node_modules/typescript/lib/lib.esnext.array.d.ts","../../node_modules/typescript/lib/lib.esnext.collection.d.ts","../../node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/typescript/lib/lib.esnext.promise.d.ts","../../node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../node_modules/typescript/lib/lib.esnext.iterator.d.ts","../../node_modules/typescript/lib/lib.esnext.float16.d.ts","../../node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/@remix-run/node/dist/globals.d.ts","../../node_modules/@types/cookie/index.d.ts","../../node_modules/@remix-run/server-runtime/dist/crypto.d.ts","../../node_modules/@remix-run/server-runtime/dist/cookies.d.ts","../../node_modules/@remix-run/server-runtime/dLine truncated
@@ -2,6 +2,9 @@
"files": [],
"include": [],
"references": [
{
"path": "../../common"
},
{
"path": "../slider-v2"
},
@@ -43,6 +43,9 @@
"src/**/*.tsx"
],
"references": [
{
"path": "../../common/tsconfig.lib.json"
},
{
"path": "../slider-v2/tsconfig.lib.json"
},
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { SoundRegistry } from "@storyteller/soundboard";
import { SoundManager } from "@storyteller/soundboard";
import { Button } from "@storyteller/ui-button";
import { faPlay } from "@fortawesome/pro-solid-svg-icons";
import {
@@ -11,35 +11,6 @@ import { Select, SelectValue } from "@storyteller/ui-select";
import { Switch } from "@storyteller/ui-switch";
import { Label } from "@storyteller/ui-label";
// TODO: This is maintained in two places. Here and InstallSounds.
const SOUND_OPTIONS = [
{ value: "none", label: "None (Silent)" }, // NB: MUST BE FIRST
// Other options
{ label: "Click", value: "click" },
{ label: "Correct", value: "correct" },
{ label: "Crumble", value: "crumble" },
{ label: "Dialog Done", value: "done" },
{ label: "Dialog Next", value: "next" },
{ label: "Error Chirp", value: "error_chirp" },
{ label: "Flower", value: "flower" },
{ label: "Ghost", value: "ghost" },
{ label: "Sci-Fi Alert", value: "scifi_alert" },
{ label: "Sci-Fi Menu Beep 1", value: "scifi_menu_beep_1" },
{ label: "Sci-Fi Menu Beep 2", value: "scifi_menu_beep_2" },
{ label: "Sci-Fi Menu Close", value: "scifi_menu_close" },
{ label: "Sci-Fi Menu Open", value: "scifi_menu_open" },
{ label: "Sci-Fi Menu Select", value: "scifi_menu_select" },
{ label: "Sci-Fi Shrill Alert", value: "scifi_shrill_alert" },
{ label: "Select", value: "select" },
{ label: "Shell Kick", value: "giant_shell_kick" },
{ label: "Special Alert", value: "special_alert" },
{ label: "Special Flower", value: "special_flower" },
{ label: "Spike Throw", value: "spike_throw" },
{ label: "Trash", value: "trash" },
{ label: "Wrong", value: "wrong" },
];
interface AudioSettingsPaneProps {}
export const AudioSettingsPane = (args: AudioSettingsPaneProps) => {
@@ -83,7 +54,7 @@ export const AudioSettingsPane = (args: AudioSettingsPaneProps) => {
preference: PreferenceName.DeleteFileSound,
value: sendVal,
});
SoundRegistry.getInstance().playSound(val);
SoundManager.playPreview(val);
await reloadPreferences();
};
@@ -93,7 +64,7 @@ export const AudioSettingsPane = (args: AudioSettingsPaneProps) => {
preference: PreferenceName.EnqueueSuccessSound,
value: sendVal,
});
SoundRegistry.getInstance().playSound(val);
SoundManager.playPreview(val);
await reloadPreferences();
};
@@ -103,7 +74,7 @@ export const AudioSettingsPane = (args: AudioSettingsPaneProps) => {
preference: PreferenceName.EnqueueFailureSound,
value: sendVal,
});
SoundRegistry.getInstance().playSound(val);
SoundManager.playPreview(val);
await reloadPreferences();
};
@@ -113,7 +84,7 @@ export const AudioSettingsPane = (args: AudioSettingsPaneProps) => {
preference: PreferenceName.GenerationSuccessSound,
value: sendVal,
});
SoundRegistry.getInstance().playSound(val);
SoundManager.playPreview(val);
await reloadPreferences();
};
@@ -123,13 +94,13 @@ export const AudioSettingsPane = (args: AudioSettingsPaneProps) => {
preference: PreferenceName.GenerationFailureSound,
value: sendVal,
});
SoundRegistry.getInstance().playSound(val);
SoundManager.playPreview(val);
await reloadPreferences();
};
const playSound = (val?: string) => {
if (val !== undefined && val !== "none") {
SoundRegistry.getInstance().playSound(val);
SoundManager.playPreview(val);
}
};
@@ -150,7 +121,7 @@ export const AudioSettingsPane = (args: AudioSettingsPaneProps) => {
id="success-sound"
value={deleteFileSound}
onChange={(val: SelectValue) => setDeleteFileSound(val as string)}
options={SOUND_OPTIONS}
options={SoundManager.OPTIONS}
className="grow"
/>
<Button
@@ -169,7 +140,7 @@ export const AudioSettingsPane = (args: AudioSettingsPaneProps) => {
id="success-sound"
value={enqueueSuccessSound}
onChange={(val: SelectValue) => setEnqueueSuccessSound(val as string)}
options={SOUND_OPTIONS}
options={SoundManager.OPTIONS}
className="grow"
/>
<Button
@@ -188,7 +159,7 @@ export const AudioSettingsPane = (args: AudioSettingsPaneProps) => {
id="success-sound"
value={enqueueFailureSound}
onChange={(val: SelectValue) => setEnqueueFailureSound(val as string)}
options={SOUND_OPTIONS}
options={SoundManager.OPTIONS}
className="grow"
/>
<Button
@@ -207,7 +178,7 @@ export const AudioSettingsPane = (args: AudioSettingsPaneProps) => {
id="success-sound"
value={generationSuccessSound}
onChange={(val: SelectValue) => setSuccessSound(val as string)}
options={SOUND_OPTIONS}
options={SoundManager.OPTIONS}
className="grow"
/>
<Button
@@ -226,7 +197,7 @@ export const AudioSettingsPane = (args: AudioSettingsPaneProps) => {
id="failure-sound"
value={generationFailureSound}
onChange={(val: SelectValue) => setFailureSound(val as string)}
options={SOUND_OPTIONS}
options={SoundManager.OPTIONS}
className="grow"
/>
<Button
@@ -2,18 +2,15 @@
"files": [],
"include": [],
"references": [
{
"path": "../../analytics/google-analytics"
},
{
"path": "../modal"
},
{
"path": "../../tauri-utils"
},
{
"path": "../../model-list"
},
{
"path": "../promptbox"
},
{
"path": "../../state/subscription"
},
@@ -33,10 +30,10 @@
"path": "../../soundboard"
},
{
"path": "../../tauri-events"
"path": "../input"
},
{
"path": "../input"
"path": "../../tauri-events"
},
{
"path": "../login-modal"
@@ -45,7 +42,7 @@
"path": "../../api"
},
{
"path": "../button"
"path": "../../analytics/google-analytics"
},
{
"path": "../label"
@@ -53,6 +50,12 @@
{
"path": "../../tauri-api"
},
{
"path": "../button"
},
{
"path": "../modal"
},
{
"path": "./tsconfig.lib.json"
},
@@ -43,18 +43,15 @@
"src/**/*.tsx"
],
"references": [
{
"path": "../../analytics/google-analytics/tsconfig.lib.json"
},
{
"path": "../modal/tsconfig.lib.json"
},
{
"path": "../../tauri-utils/tsconfig.lib.json"
},
{
"path": "../../model-list/tsconfig.lib.json"
},
{
"path": "../promptbox/tsconfig.lib.json"
},
{
"path": "../../state/subscription/tsconfig.lib.json"
},
@@ -74,10 +71,10 @@
"path": "../../soundboard/tsconfig.lib.json"
},
{
"path": "../../tauri-events/tsconfig.lib.json"
"path": "../input/tsconfig.lib.json"
},
{
"path": "../input/tsconfig.lib.json"
"path": "../../tauri-events/tsconfig.lib.json"
},
{
"path": "../login-modal/tsconfig.lib.json"
@@ -86,13 +83,19 @@
"path": "../../api/tsconfig.lib.json"
},
{
"path": "../button/tsconfig.lib.json"
"path": "../../analytics/google-analytics/tsconfig.lib.json"
},
{
"path": "../label/tsconfig.lib.json"
},
{
"path": "../../tauri-api/tsconfig.lib.json"
},
{
"path": "../button/tsconfig.lib.json"
},
{
"path": "../modal/tsconfig.lib.json"
}
]
}
+2 -3
View File
@@ -1,3 +1,2 @@
export * from './lib/SoundEffect';
export * from './lib/SoundRegistry';
export { SoundManager } from './lib/SoundManager';
export type { SoundOption } from './lib/SoundManager';
@@ -1,3 +1,4 @@
// Internal to @storyteller/soundboard. Consumers go through SoundManager.
import {Howl} from 'howler';
export interface Args {
@@ -0,0 +1,107 @@
import { GetAppPreferences } from "@storyteller/tauri-api";
import { SoundEffect } from "./SoundEffect";
import { SoundRegistry } from "./SoundRegistry";
type SoundDef = {
key: string;
path: string;
volume: number;
// Present iff the sound is user-selectable in Settings → Alerts.
// Sounds without a label are still registered and can be played by key.
label?: string;
};
const SOUNDS: readonly SoundDef[] = [
// Menu choices
{ key: "click", path: "resources/sound/smrpg_click.wav", volume: 0.2, label: "Click" },
{ key: "scifi_menu_beep_1", path: "resources/sound/metroidprime_UI_15.wav", volume: 0.3, label: "Sci-Fi Menu Beep 1" },
{ key: "scifi_menu_beep_2", path: "resources/sound/metroidprime_UI_14.wav", volume: 0.3, label: "Sci-Fi Menu Beep 2" },
{ key: "scifi_menu_select", path: "resources/sound/metroidprime_UI_18.wav", volume: 0.3, label: "Sci-Fi Menu Select" },
// Immediate enqueue success
{ key: "done", path: "resources/sound/oot_dialogue_done.wav", volume: 0.4, label: "Dialog Done" },
// Immediate failure
{ key: "error_chirp", path: "resources/sound/goldensun_135.wav", volume: 0.4, label: "Error Chirp" },
{ key: "spike_throw", path: "resources/sound/smrpg_enemy_spikethrow.wav", volume: 0.1, label: "Spike Throw" },
{ key: "giant_shell_kick", path: "resources/sound/smrpg_mario_giantshellkick.wav", volume: 0.2, label: "Shell Kick" },
{ key: "wrong", path: "resources/sound/smrpg_wrong.wav", volume: 0.4, label: "Wrong" },
// Async success
{ key: "special_flower", path: "resources/sound/smrpg_specialflower.wav", volume: 0.2, label: "Special Flower" },
{ key: "extra_power", path: "resources/sound/smrpg_character_extrapower.wav", volume: 0.2 },
// Async errors
{ key: "crumble", path: "resources/sound/smrpg_drybones_crumble.wav", volume: 0.1, label: "Crumble" },
{ key: "ghost", path: "resources/sound/smrpg_ghost.wav", volume: 0.2, label: "Ghost" },
{ key: "special_alert", path: "resources/sound/goldensun_214.wav", volume: 0.2, label: "Special Alert" },
{ key: "scifi_alert", path: "resources/sound/metroidprime_UI_52.wav", volume: 0.2, label: "Sci-Fi Alert" },
{ key: "scifi_shrill_alert", path: "resources/sound/metroidprime_UI_51.wav", volume: 0.2, label: "Sci-Fi Shrill Alert" },
// Menus
{ key: "next", path: "resources/sound/oot_dialogue_next.wav", volume: 0.2, label: "Dialog Next" },
{ key: "select", path: "resources/sound/goldensun_111.wav", volume: 0.4, label: "Select" },
{ key: "scifi_menu_open", path: "resources/sound/metroidprime_UI_12.wav", volume: 0.3, label: "Sci-Fi Menu Open" },
{ key: "scifi_menu_close", path: "resources/sound/metroidprime_UI_13.wav", volume: 0.3, label: "Sci-Fi Menu Close" },
// Reward / celebration
{ key: "correct", path: "resources/sound/smrpg_correct.wav", volume: 0.1, label: "Correct" },
{ key: "flower", path: "resources/sound/smrpg_flower.wav", volume: 0.1, label: "Flower" },
// Trash / delete
{ key: "trash", path: "resources/sound/oot_scrub_crumble.wav", volume: 0.4, label: "Trash" },
// Misc — registered but not user-selectable
{ key: "accept_chirp", path: "resources/sound/goldensun_101.wav", volume: 0.2 },
{ key: "accept_normal_level_1", path: "resources/sound/goldensun_173.wav", volume: 0.2 },
{ key: "accept_normal_level_2", path: "resources/sound/goldensun_174.wav", volume: 0.2 },
{ key: "accept_normal_level_3", path: "resources/sound/goldensun_175.wav", volume: 0.2 },
{ key: "decline_chirp", path: "resources/sound/goldensun_102.wav", volume: 0.2 },
{ key: "decline_normal", path: "resources/sound/goldensun_113.wav", volume: 0.2 },
];
export type SoundOption = { value: string; label: string };
type SoundPrefKey =
| "delete_file_sound"
| "enqueue_success_sound"
| "enqueue_failure_sound"
| "generation_success_sound"
| "generation_failure_sound";
export class SoundManager {
// Dropdown options for AudioSettingsPane.
// "None (Silent)" first, then user-selectable sounds A→Z by label.
static readonly OPTIONS: SoundOption[] = [
{ value: "none", label: "None (Silent)" },
...SOUNDS
.filter((s): s is SoundDef & { label: string } => !!s.label)
.map((s) => ({ value: s.key, label: s.label }))
.sort((a, b) => a.label.localeCompare(b.label)),
];
private static installed = false;
// Register the sound catalog. Idempotent — safe to call twice.
static install() {
if (this.installed) return;
const r = SoundRegistry.getInstance();
for (const s of SOUNDS) {
r.setSoundOnce(s.key, new SoundEffect(s.path, { defaultVolume: s.volume }));
}
this.installed = true;
}
// Settings preview — fires regardless of the master toggle.
static playPreview(soundName: string) {
if (!soundName || soundName === "none") return;
SoundRegistry.getInstance().playSound(soundName);
}
// Event-driven playback — gated on `play_sounds`.
static async playFileDeleted() { await this.playEvent("delete_file_sound"); }
static async playEnqueueSuccess() { await this.playEvent("enqueue_success_sound"); }
static async playEnqueueFailure() { await this.playEvent("enqueue_failure_sound"); }
static async playGenerationSuccess() { await this.playEvent("generation_success_sound"); }
static async playGenerationFailure() { await this.playEvent("generation_failure_sound"); }
private static async playEvent(prefKey: SoundPrefKey) {
const prefs = (await GetAppPreferences()).preferences;
if (!prefs?.play_sounds) return;
const soundName = prefs[prefKey];
if (!soundName) return;
SoundRegistry.getInstance().playSound(soundName);
}
}
@@ -1,3 +1,4 @@
// Internal to @storyteller/soundboard. Consumers go through SoundManager.
import { SoundEffect } from "./SoundEffect";
export class SoundRegistry {
+3
View File
@@ -2,6 +2,9 @@
"files": [],
"include": [],
"references": [
{
"path": "../tauri-api"
},
{
"path": "./tsconfig.lib.json"
},
@@ -44,5 +44,10 @@
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
],
"references": [
{
"path": "../tauri-api/tsconfig.lib.json"
}
]
}
@@ -1,7 +1,6 @@
import { useEffect } from "react";
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { SoundRegistry } from "@storyteller/soundboard";
import { GetAppPreferences } from "@storyteller/tauri-api";
import { SoundManager } from "@storyteller/soundboard";
import { toast } from "@storyteller/ui-toaster";
import { GenerationAction, GenerationModel, GenerationServiceProvider } from "./common";
import { BasicEventWrapper } from "../../common/BasicEventWrapper";
@@ -20,12 +19,7 @@ export const useGenerationCompleteEvent = () => {
const setup = async () => {
unlisten = listen<BasicEventWrapper<GenerationCompleteEvent>>('generation-complete-event', async (event) => {
console.log("Generation complete event received:", event);
const prefs = await GetAppPreferences();
const soundName = prefs.preferences?.generation_success_sound;
if (soundName !== undefined) {
const registry = SoundRegistry.getInstance();
registry.playSound(soundName);
}
await SoundManager.playGenerationSuccess();
const message = makeMessage(event.payload.data);
toast.success(message);
});
@@ -1,7 +1,6 @@
import { useEffect } from "react";
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { SoundRegistry } from "@storyteller/soundboard";
import { GetAppPreferences } from "@storyteller/tauri-api";
import { SoundManager } from "@storyteller/soundboard";
import { toast } from "@storyteller/ui-toaster";
import { GenerationAction, GenerationModel, GenerationServiceProvider } from "./common";
import { BasicEventWrapper } from "../../common/BasicEventWrapper";
@@ -21,12 +20,7 @@ export const useGenerationEnqueueFailureEvent = () => {
const setup = async () => {
unlisten = listen<BasicEventWrapper<GenerationEnqueueFailureEvent>>('generation-enqueue-failure-event', async (event) => {
console.log("Generation enqueue failure event received:", event);
const prefs = await GetAppPreferences();
const soundName = prefs.preferences?.enqueue_failure_sound;
if (soundName !== undefined) {
const registry = SoundRegistry.getInstance();
registry.playSound(soundName);
}
await SoundManager.playEnqueueFailure();
const message = makeMessage(event.payload.data);
toast.error(message);
});
@@ -1,7 +1,6 @@
import { useEffect } from "react";
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { SoundRegistry } from "@storyteller/soundboard";
import { GetAppPreferences } from "@storyteller/tauri-api";
import { SoundManager } from "@storyteller/soundboard";
import { toast } from "@storyteller/ui-toaster";
import { GenerationAction, GenerationModel, GenerationServiceProvider } from "./common";
import { BasicEventWrapper } from "../../common/BasicEventWrapper";
@@ -20,12 +19,7 @@ export const useGenerationEnqueueSuccessEvent = () => {
const setup = async () => {
unlisten = listen<BasicEventWrapper<GenerationEnqueueSuccessEvent>>('generation-enqueue-success-event', async (event) => {
console.log("Generation enqueue success event received:", event);
const prefs = await GetAppPreferences();
const soundName = prefs.preferences?.enqueue_success_sound;
if (soundName !== undefined) {
const registry = SoundRegistry.getInstance();
registry.playSound(soundName);
}
await SoundManager.playEnqueueSuccess();
const message = makeMessage(event.payload.data);
toast.success(message);
});
@@ -1,7 +1,6 @@
import { useEffect } from "react";
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { SoundRegistry } from "@storyteller/soundboard";
import { GetAppPreferences } from "@storyteller/tauri-api";
import { SoundManager } from "@storyteller/soundboard";
import { toast } from "@storyteller/ui-toaster";
import { GenerationAction, GenerationModel, GenerationServiceProvider } from "./common";
import { BasicEventWrapper } from "../../common/BasicEventWrapper";
@@ -21,12 +20,7 @@ export const useGenerationFailedEvent = () => {
const setup = async () => {
unlisten = listen<BasicEventWrapper<GenerationFailedEvent>>('generation-failed-event', async (event) => {
console.log("Generation failed event received:", event);
const prefs = await GetAppPreferences();
const soundName = prefs.preferences?.generation_failure_sound;
if (soundName !== undefined) {
const registry = SoundRegistry.getInstance();
registry.playSound(soundName);
}
await SoundManager.playGenerationFailure();
const message = makeMessage(event.payload.data);
toast.error(message);
});
-3
View File
@@ -5,9 +5,6 @@
{
"path": "../components/toaster"
},
{
"path": "../tauri-api"
},
{
"path": "../soundboard"
},
@@ -46,9 +46,6 @@
{
"path": "../components/toaster/tsconfig.lib.json"
},
{
"path": "../tauri-api/tsconfig.lib.json"
},
{
"path": "../soundboard/tsconfig.lib.json"
},