diff --git a/.github/workflows/monkey-ci.yml b/.github/workflows/monkey-ci.yml index 7c4f7684df9a..b19099926350 100644 --- a/.github/workflows/monkey-ci.yml +++ b/.github/workflows/monkey-ci.yml @@ -242,6 +242,7 @@ jobs: - 'frontend/static/themes/**' - 'frontend/static/webfonts/**' - 'frontend/static/challenges/**' + - 'frontend/static/sounds/**' - name: Set up Node.js uses: actions/setup-node@v4 diff --git a/frontend/scripts/check-assets.ts b/frontend/scripts/check-assets.ts index 80f83819b7b5..f1246caa68fd 100644 --- a/frontend/scripts/check-assets.ts +++ b/frontend/scripts/check-assets.ts @@ -21,6 +21,7 @@ import { z } from "zod"; import { ChallengeSchema, Challenge } from "@monkeytype/schemas/challenges"; import { LayoutObject, LayoutObjectSchema } from "@monkeytype/schemas/layouts"; import { QuoteDataSchema, QuoteData } from "@monkeytype/schemas/quotes"; +import { clickSoundConfig } from "../src/ts/constants/sounds"; class Problems { private type: string; @@ -421,6 +422,54 @@ async function validateThemes(): Promise { } } +async function validateSounds(): Promise { + const problems = new Problems("Sounds", { + _additional: + "Sound files present but missing in frontend/src/ts/constants/sounds", + }); + + const soundFiles = new Set( + fs + .readdirSync("./static/sounds") + .filter((it) => it.startsWith("click")) + .flatMap((folder) => + fs + .readdirSync(`./static/sounds/${folder}`) + .map((it) => `${folder}/${it}`), + ), + ); + + //missing sound files + + Object.entries(clickSoundConfig).forEach(([key, value]) => { + value + .map((file) => file.substring("../sounds/".length)) + .filter((it) => !soundFiles.has(it)) + .forEach((file) => + problems.add( + "click" + key, + `missing file frontend/static/sounds/${file}`, + ), + ); + }); + + //additional files + const expectedSoundFiles = new Set( + Object.values(clickSoundConfig).flatMap((it) => + it.map((file) => file.substring("../sounds/".length)), + ), + ); + [...soundFiles] + .filter((name) => !expectedSoundFiles.has(name)) + .forEach((file) => problems.add("_additional", file)); + + console.log(problems.toString()); + + if (problems.hasError()) { + throw new Error("sounds with errors"); + } +} + type Validator = () => Promise; async function main(): Promise { @@ -436,11 +485,13 @@ async function main(): Promise { challenges: [validateChallenges], fonts: [validateFonts], themes: [validateThemes], + sounds: [validateSounds], others: [ validateChallenges, validateLayouts, validateFonts, validateThemes, + validateSounds, ], }; diff --git a/frontend/src/ts/constants/sounds.ts b/frontend/src/ts/constants/sounds.ts new file mode 100644 index 000000000000..122c30d4d37d --- /dev/null +++ b/frontend/src/ts/constants/sounds.ts @@ -0,0 +1,75 @@ +import { PlaySoundOnClick } from "@monkeytype/schemas/configs"; + +export const soundsConfig: SoundConfigType = { + 1: { numberOfSounds: 3 }, + 2: { numberOfSounds: 3 }, + 3: { numberOfSounds: 3 }, + 4: { numberOfSounds: 6 }, + 5: { numberOfSounds: 6 }, + 6: { numberOfSounds: 3 }, + 7: { numberOfSounds: 3 }, + 8: { oscillatorType: "sine" }, + 9: { oscillatorType: "sawtooth" }, + 10: { oscillatorType: "square" }, + 11: { oscillatorType: "triangle" }, + 12: { validNotes: ["C", "D", "E", "G", "A"] }, + 13: { validNotes: ["C", "D", "E", "Gb", "Ab", "Bb"] }, + 14: { numberOfSounds: 8 }, + 15: { numberOfSounds: 5 }, + 16: { numberOfSounds: 8 }, +}; + +export type ClickSoundConfig = { + numberOfSounds: number; +}; + +export type SupportedOscillatorTypes = Exclude; +export type OscillatorSoundConfig = { + oscillatorType: SupportedOscillatorTypes; +}; + +export type ScaleSoundConfig = { + validNotes: ValidNotes[]; +}; + +export type SoundConfigType = Record< + Exclude, + ClickSoundConfig | OscillatorSoundConfig | ScaleSoundConfig +>; + +export type ValidNotes = + | "C" + | "Db" + | "D" + | "Eb" + | "E" + | "F" + | "Gb" + | "G" + | "Ab" + | "A" + | "Bb" + | "B"; + +type ClickSoundConfigType = Partial< + Record, string[]> +>; + +export const clickSoundConfig: ClickSoundConfigType = + extractClickSounds(soundsConfig); + +function extractClickSounds( + shortConfig: SoundConfigType, +): ClickSoundConfigType { + return Object.fromEntries( + Object.entries(shortConfig) + .filter(([_, cfg]) => "numberOfSounds" in cfg) + .map(([key, cfg]) => { + const config = cfg as ClickSoundConfig; + const fullConfig = new Array(config.numberOfSounds) + .fill(0) + .map((_, index) => `../sounds/click${key}/${index + 1}.wav`); + return [key, fullConfig]; + }), + ); +} diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index 24e01e6b397a..5630efc12914 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -1,516 +1,131 @@ import { Config } from "../config/store"; import { configEvent } from "../events/config"; import { randomElementFromArray } from "../utils/arrays"; -import { randomIntFromRange } from "@monkeytype/util/numbers"; import { leftState, rightState } from "../test/shift-tracker"; import { capsState } from "../test/caps-warning"; import { showErrorNotification } from "../states/notifications"; import type { Howl } from "howler"; -import { PlaySoundOnClick } from "@monkeytype/schemas/configs"; +import { + PlaySoundOnClick, + PlaySoundOnError, +} from "@monkeytype/schemas/configs"; +import { + clickSoundConfig, + ScaleSoundConfig, + SoundConfigType, + soundsConfig, + SupportedOscillatorTypes, + ValidNotes, +} from "../constants/sounds"; + +let howlerModulePromise: Promise | null = null; +async function getHowlerModule(): Promise { + howlerModulePromise ??= import("howler"); + return howlerModulePromise; +} + +let initPromise: Promise | null = null; +const loadedBundles: Set = new Set(); + +const howlers: Record> = {}; -async function gethowler(): Promise { - return await import("howler"); +async function getHowl(src: string): Promise { + howlers[src] ??= (async () => { + const { Howl } = await getHowlerModule(); + return new Howl({ src }); + })(); + + return howlers[src]; } -type ClickSounds = Record< - string, - { - sounds: Howl[]; - counter: number; - }[] ->; - -type ErrorSounds = Record< - string, - { - sounds: Howl[]; - counter: number; - }[] ->; +type ErrorSounds = Record, Howl[]>; let errorSounds: ErrorSounds | null = null; -let clickSounds: ClickSounds | null = null; let timeWarning: Howl | null = null; let fartReverb: Howl | null = null; async function initTimeWarning(): Promise { - const Howl = (await gethowler()).Howl; if (timeWarning !== null) return; - timeWarning = new Howl({ - src: "../sound/timeWarning.wav", - }); + timeWarning = await getHowl("../sounds/timeWarning.wav"); } async function initFartReverb(): Promise { - const Howl = (await gethowler()).Howl; if (fartReverb !== null) return; - fartReverb = new Howl({ - src: "../sound/fart-reverb.wav", - }); + fartReverb = await getHowl("../sounds/fart-reverb.wav"); } async function initErrorSound(): Promise { - const Howl = (await gethowler()).Howl; if (errorSounds !== null) return; errorSounds = { - 1: [ - { - sounds: [ - new Howl({ src: "../sound/error1/error1_1.wav" }), - new Howl({ src: "../sound/error1/error1_1.wav" }), - ], - counter: 0, - }, - ], - 2: [ - { - sounds: [ - new Howl({ src: "../sound/error2/error2_1.wav" }), - new Howl({ src: "../sound/error2/error2_1.wav" }), - ], - counter: 0, - }, - ], - 3: [ - { - sounds: [ - new Howl({ src: "../sound/error3/error3_1.wav" }), - new Howl({ src: "../sound/error3/error3_1.wav" }), - ], - counter: 0, - }, - ], + 1: [await getHowl("../sounds/error1/1.wav")], + 2: [await getHowl("../sounds/error2/1.wav")], + 3: [await getHowl("../sounds/error3/1.wav")], 4: [ - { - sounds: [ - new Howl({ src: "../sound/error4/error4_1.wav" }), - new Howl({ src: "../sound/error4/error4_1.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/error4/error4_2.wav" }), - new Howl({ src: "../sound/error4/error4_2.wav" }), - ], - counter: 0, - }, + await getHowl("../sounds/error4/1.wav"), + await getHowl("../sounds/error4/2.wav"), ], }; - Howler.volume(Config.soundVolume); + (await getHowlerModule()).Howler.volume(Config.soundVolume); } async function init(): Promise { - const Howl = (await gethowler()).Howl; - if (clickSounds !== null) return; - clickSounds = { - 1: [ - { - sounds: [ - new Howl({ src: "../sound/click1/click1_1.wav" }), - new Howl({ src: "../sound/click1/click1_1.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click1/click1_2.wav" }), - new Howl({ src: "../sound/click1/click1_2.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click1/click1_3.wav" }), - new Howl({ src: "../sound/click1/click1_3.wav" }), - ], - counter: 0, - }, - ], - 2: [ - { - sounds: [ - new Howl({ src: "../sound/click2/click2_1.wav" }), - new Howl({ src: "../sound/click2/click2_1.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click2/click2_2.wav" }), - new Howl({ src: "../sound/click2/click2_2.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click2/click2_3.wav" }), - new Howl({ src: "../sound/click2/click2_3.wav" }), - ], - counter: 0, - }, - ], - 3: [ - { - sounds: [ - new Howl({ src: "../sound/click3/click3_1.wav" }), - new Howl({ src: "../sound/click3/click3_1.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click3/click3_2.wav" }), - new Howl({ src: "../sound/click3/click3_2.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click3/click3_3.wav" }), - new Howl({ src: "../sound/click3/click3_3.wav" }), - ], - counter: 0, - }, - ], - 4: [ - { - sounds: [ - new Howl({ src: "../sound/click4/click4_1.wav" }), - new Howl({ src: "../sound/click4/click4_11.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click4/click4_2.wav" }), - new Howl({ src: "../sound/click4/click4_22.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click4/click4_3.wav" }), - new Howl({ src: "../sound/click4/click4_33.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click4/click4_4.wav" }), - new Howl({ src: "../sound/click4/click4_44.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click4/click4_5.wav" }), - new Howl({ src: "../sound/click4/click4_55.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click4/click4_6.wav" }), - new Howl({ src: "../sound/click4/click4_66.wav" }), - ], - counter: 0, - }, - ], - 5: [ - { - sounds: [ - new Howl({ src: "../sound/click5/click5_1.wav" }), - new Howl({ src: "../sound/click5/click5_11.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click5/click5_2.wav" }), - new Howl({ src: "../sound/click5/click5_22.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click5/click5_3.wav" }), - new Howl({ src: "../sound/click5/click5_33.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click5/click5_4.wav" }), - new Howl({ src: "../sound/click5/click5_44.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click5/click5_5.wav" }), - new Howl({ src: "../sound/click5/click5_55.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click5/click5_6.wav" }), - new Howl({ src: "../sound/click5/click5_66.wav" }), - ], - counter: 0, - }, - ], - 6: [ - { - sounds: [ - new Howl({ src: "../sound/click6/click6_1.wav" }), - new Howl({ src: "../sound/click6/click6_11.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click6/click6_2.wav" }), - new Howl({ src: "../sound/click6/click6_22.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click6/click6_3.wav" }), - new Howl({ src: "../sound/click6/click6_33.wav" }), - ], - counter: 0, - }, - ], - 7: [ - { - sounds: [ - new Howl({ src: "../sound/click7/click7_1.wav" }), - new Howl({ src: "../sound/click7/click7_11.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click7/click7_2.wav" }), - new Howl({ src: "../sound/click7/click7_22.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click7/click7_3.wav" }), - new Howl({ src: "../sound/click7/click7_33.wav" }), - ], - counter: 0, - }, - ], - 14: [ - { - sounds: [ - new Howl({ src: "../sound/click14/click14_1.wav" }), - new Howl({ src: "../sound/click14/click14_1.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click14/click14_2.wav" }), - new Howl({ src: "../sound/click14/click14_2.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click14/click14_3.wav" }), - new Howl({ src: "../sound/click14/click14_3.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click14/click14_4.wav" }), - new Howl({ src: "../sound/click14/click14_4.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click14/click14_5.wav" }), - new Howl({ src: "../sound/click14/click14_5.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click14/click14_6.wav" }), - new Howl({ src: "../sound/click14/click14_6.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click14/click14_7.wav" }), - new Howl({ src: "../sound/click14/click14_7.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click14/click14_8.wav" }), - new Howl({ src: "../sound/click14/click14_8.wav" }), - ], - counter: 0, - }, - ], - 15: [ - { - sounds: [ - new Howl({ src: "../sound/click15/click15_1.wav" }), - new Howl({ src: "../sound/click15/click15_1.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click15/click15_2.wav" }), - new Howl({ src: "../sound/click15/click15_2.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click15/click15_3.wav" }), - new Howl({ src: "../sound/click15/click15_3.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click15/click15_4.wav" }), - new Howl({ src: "../sound/click15/click15_4.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click15/click15_5.wav" }), - new Howl({ src: "../sound/click15/click15_5.wav" }), - ], - counter: 0, - }, - ], - 16: [ - { - sounds: [ - new Howl({ src: "../sound/click16/click16_1.wav" }), - new Howl({ src: "../sound/click16/click16_1.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click16/click16_2.wav" }), - new Howl({ src: "../sound/click16/click16_2.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click16/click16_3.wav" }), - new Howl({ src: "../sound/click16/click16_3.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click16/click16_4.wav" }), - new Howl({ src: "../sound/click16/click16_4.wav" }), - ], - counter: 0, - }, - // { - // sounds: [ - // new Howl({ src: "../sound/click16/click16_5.wav" }), - // new Howl({ src: "../sound/click16/click16_5.wav" }), - // ], - // counter: 0, - // }, - // { - // sounds: [ - // new Howl({ src: "../sound/click16/click16_6.wav" }), - // new Howl({ src: "../sound/click16/click16_6.wav" }), - // ], - // counter: 0, - // }, - // { - // sounds: [ - // new Howl({ src: "../sound/click16/click16_7.wav" }), - // new Howl({ src: "../sound/click16/click16_7.wav" }), - // ], - // counter: 0, - // }, - { - sounds: [ - new Howl({ src: "../sound/click16/click16_8.wav" }), - new Howl({ src: "../sound/click16/click16_8.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click16/click16_9.wav" }), - new Howl({ src: "../sound/click16/click16_9.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click16/click16_10.wav" }), - new Howl({ src: "../sound/click16/click16_10.wav" }), - ], - counter: 0, - }, - { - sounds: [ - new Howl({ src: "../sound/click16/click16_11.wav" }), - new Howl({ src: "../sound/click16/click16_11.wav" }), - ], - counter: 0, - }, - ], - }; - Howler.volume(Config.soundVolume); + initPromise ??= (async () => { + const { Howler } = await getHowlerModule(); + Howler.volume(Config.soundVolume); + })(); + + await initPromise; + + //preload error sounds + await initErrorSound(); + + //preload sounds + const clickId = Config.playSoundOnClick; + if (clickId === "off") return; + + if (!loadedBundles.has(clickId)) { + loadedBundles.add(clickId); + + const config = clickSoundConfig[clickId]; + + if (config === undefined) return; + + await Promise.all(config.flatMap(getHowl)); + } } -export async function previewClick(val: PlaySoundOnClick): Promise { - if (["8", "9", "10", "11"].includes(val)) { - playNote("KeyQ", clickSoundIdsToOscillatorType[val as DynamicClickSounds]); +export async function previewClick(clickId: PlaySoundOnClick): Promise { + if (clickId === "off") return; + + const config = soundsConfig[clickId]; + + if ("oscillatorType" in config) { + playNote({ codeOverride: "KeyQ", oscillatorType: config.oscillatorType }); return; } - if (["12", "13"].includes(val)) { - scaleConfigurations[val as "12" | "13"].preview(); + if ("validNotes" in config) { + scaleConfigurations[clickId]?.preview(); return; } - if (clickSounds === null) await init(); + await init(); - const safeClickSounds = clickSounds as ClickSounds; - - const clickSoundIds = Object.keys(safeClickSounds); - if (!clickSoundIds.includes(val)) return; + const safeClickSounds = clickSoundConfig[clickId]; + if (safeClickSounds === undefined || safeClickSounds[0] === undefined) { + return; + } - safeClickSounds?.[val]?.[0]?.sounds[0]?.seek(0); - safeClickSounds?.[val]?.[0]?.sounds[0]?.play(); + const howl = await getHowl(safeClickSounds[0]); + howl.seek(0); + howl.play(); } -export async function previewError(val: string): Promise { +export async function previewError(val: PlaySoundOnError): Promise { + if (val === "off") return; if (errorSounds === null) await initErrorSound(); const safeErrorSounds = errorSounds as ErrorSounds; @@ -518,8 +133,8 @@ export async function previewError(val: string): Promise { const errorSoundIds = Object.keys(safeErrorSounds); if (!errorSoundIds.includes(val)) return; - errorSounds?.[val]?.[0]?.sounds[0]?.seek(0); - errorSounds?.[val]?.[0]?.sounds[0]?.play(); + errorSounds?.[val]?.[0]?.seek(0); + errorSounds?.[val]?.[0]?.play(); } let currentCode = "KeyA"; @@ -528,7 +143,7 @@ document.addEventListener("keydown", (event) => { currentCode = event.code || "KeyA"; }); -const notes = { +const notes: Record = { C: [16.35, 32.7, 65.41, 130.81, 261.63, 523.25, 1046.5, 2093.0, 4186.01], Db: [17.32, 34.65, 69.3, 138.59, 277.18, 554.37, 1108.73, 2217.46, 4434.92], D: [18.35, 36.71, 73.42, 146.83, 293.66, 587.33, 1174.66, 2349.32, 4698.64], @@ -543,8 +158,7 @@ const notes = { B: [30.87, 61.74, 123.47, 246.94, 493.88, 987.77, 1975.53, 3951.07], } as const; -type ValidNotes = keyof typeof notes; -type ValidFrequencies = (typeof notes)[ValidNotes]; +type ValidFrequencies = number[]; type GetNoteFrequencyCallback = (octave: number) => number; @@ -597,19 +211,6 @@ const codeToNote: Record = { BracketRight: bindToNote(notes.G, 2), }; -type DynamicClickSounds = Extract; -type SupportedOscillatorTypes = Exclude; - -const clickSoundIdsToOscillatorType: Record< - DynamicClickSounds, - SupportedOscillatorTypes -> = { - "8": "sine", - "9": "sawtooth", - "10": "square", - "11": "triangle", -}; - let audioCtx: AudioContext | undefined | null; function initAudioContext(): void { @@ -628,20 +229,13 @@ function initAudioContext(): void { } } -type ValidScales = "pentatonic" | "wholetone"; - -const scales: Record = { - pentatonic: ["C", "D", "E", "G", "A"], - wholetone: ["C", "D", "E", "Gb", "Ab", "Bb"], -}; - type ScaleData = { octave: number; // current octave of scale direction: number; // whether scale is ascending or descending position: number; // current position in scale }; -function createPreviewScale(scaleName: ValidScales): () => void { +function createPreviewScale(validNotes: ValidNotes[]): () => void { // We use a JavaScript closure to create a preview function that can be called multiple times and progress through the scale const scale: ScaleData = { position: 0, @@ -650,13 +244,12 @@ function createPreviewScale(scaleName: ValidScales): () => void { }; return async () => { - if (clickSounds === null) await init(); - playScale(scaleName, scale); + await init(); + playScale(validNotes, scale); }; } type ScaleMeta = { - name: ValidScales; preview: ReturnType; meta: ScaleData; }; @@ -667,30 +260,17 @@ const defaultScaleData: ScaleData = { direction: 1, }; -export const scaleConfigurations: Record< - Extract, - ScaleMeta -> = { - "12": { - name: "pentatonic", - preview: createPreviewScale("pentatonic"), - meta: defaultScaleData, - }, - "13": { - name: "wholetone", - preview: createPreviewScale("wholetone"), - meta: defaultScaleData, - }, -}; +type ScaleConfigurationType = Partial>; + +export const scaleConfigurations: ScaleConfigurationType = + extractScaleSounds(soundsConfig); -function playScale(scale: ValidScales, scaleMeta: ScaleData): void { +function playScale(validNotes: ValidNotes[], scaleMeta: ScaleData): void { if (audioCtx === undefined) { initAudioContext(); } if (!audioCtx) return; - const randomNote = randomIntFromRange(0, scales[scale].length - 1); - if (Math.random() < 0.5) { scaleMeta.octave += scaleMeta.direction; } @@ -702,7 +282,7 @@ function playScale(scale: ValidScales, scaleMeta: ScaleData): void { scaleMeta.direction = 1; } - const note = scales[scale][randomNote] as ValidNotes; + const note = randomElementFromArray(validNotes); const currentFrequency = notes[note][scaleMeta.octave] as number; @@ -736,20 +316,20 @@ export async function playFartReverb(): Promise { } export async function clearAllSounds(): Promise { - const Howl = (await gethowler()).Howler; - Howl.stop(); + const { Howler } = await getHowlerModule(); + Howler.stop(); } -function playNote( - codeOverride?: string, - oscillatorTypeOverride?: SupportedOscillatorTypes, -): void { +function playNote(options: { + codeOverride?: string; + oscillatorType: SupportedOscillatorTypes; +}): void { if (audioCtx === undefined) { initAudioContext(); } if (!audioCtx) return; - currentCode = codeOverride ?? currentCode; + currentCode = options.codeOverride ?? currentCode; if (!(currentCode in codeToNote)) { return; } @@ -761,11 +341,7 @@ function playNote( const oscillatorNode = audioCtx.createOscillator(); const gainNode = audioCtx.createGain(); - oscillatorNode.type = - oscillatorTypeOverride ?? - clickSoundIdsToOscillatorType[ - Config.playSoundOnClick as DynamicClickSounds - ]; + oscillatorNode.type = options.oscillatorType; gainNode.gain.value = Config.soundVolume / 10; oscillatorNode.connect(gainNode); @@ -778,33 +354,31 @@ function playNote( } export async function playClick(codeOverride?: string): Promise { - if (Config.playSoundOnClick === "off") return; - - if (Config.playSoundOnClick in scaleConfigurations) { - const { name, meta } = - scaleConfigurations[ - Config.playSoundOnClick as keyof typeof scaleConfigurations - ]; - playScale(name, meta); + const val = Config.playSoundOnClick; + if (val === "off") return; + + const config = soundsConfig[val]; + + if ("oscillatorType" in config) { + playNote({ codeOverride, oscillatorType: config.oscillatorType }); return; } - if (Config.playSoundOnClick in clickSoundIdsToOscillatorType) { - playNote(codeOverride ?? undefined); + if ("validNotes" in config) { + const scaleConfig = scaleConfigurations[val]; + if (scaleConfig === undefined) { + throw new Error("missing scale config"); + } + playScale(config.validNotes, scaleConfig.meta); return; } - if (clickSounds === null) await init(); - - const sounds = (clickSounds as ClickSounds)[Config.playSoundOnClick]; + await init(); + const sounds = clickSoundConfig[val]; if (sounds === undefined) throw new Error("Invalid click sound ID"); - const randomSound = randomElementFromArray(sounds); - const soundToPlay = randomSound.sounds[randomSound.counter] as Howl; - - randomSound.counter++; - if (randomSound.counter === 2) randomSound.counter = 0; + const soundToPlay = await getHowl(randomSound); soundToPlay.seek(0); soundToPlay.play(); } @@ -817,25 +391,42 @@ export async function playError(): Promise { if (sounds === undefined) throw new Error("Invalid error sound ID"); const randomSound = randomElementFromArray(sounds); - const soundToPlay = randomSound.sounds[randomSound.counter] as Howl; - - randomSound.counter++; - if (randomSound.counter === 2) randomSound.counter = 0; - soundToPlay.seek(0); - soundToPlay.play(); + randomSound.seek(0); + randomSound.play(); } -function setVolume(val: number): void { +async function setVolume(val: number): Promise { try { + const { Howler } = await getHowlerModule(); Howler.volume(val); } catch (e) { // } } +function extractScaleSounds( + shortConfig: SoundConfigType, +): ScaleConfigurationType { + return Object.fromEntries( + Object.entries(shortConfig) + .filter(([_, cfg]) => "validNotes" in cfg) + .map(([key, cfg]) => { + const config = cfg as ScaleSoundConfig; + + return [ + key, + { + preview: createPreviewScale(config.validNotes), + meta: { ...defaultScaleData }, + } as ScaleMeta, + ]; + }), + ); +} + configEvent.subscribe(({ key, newValue }) => { if (key === "playSoundOnClick" && newValue !== "off") void init(); if (key === "soundVolume") { - setVolume(newValue); + void setVolume(newValue); } }); diff --git a/frontend/static/sound/click16/click16_5.wav b/frontend/static/sound/click16/click16_5.wav deleted file mode 100644 index 71df277a5d18..000000000000 Binary files a/frontend/static/sound/click16/click16_5.wav and /dev/null differ diff --git a/frontend/static/sound/click16/click16_6.wav b/frontend/static/sound/click16/click16_6.wav deleted file mode 100644 index 4076a647942e..000000000000 Binary files a/frontend/static/sound/click16/click16_6.wav and /dev/null differ diff --git a/frontend/static/sound/click16/click16_7.wav b/frontend/static/sound/click16/click16_7.wav deleted file mode 100644 index 44919e80e80f..000000000000 Binary files a/frontend/static/sound/click16/click16_7.wav and /dev/null differ diff --git a/frontend/static/sound/click4/click4_11.wav b/frontend/static/sound/click4/click4_11.wav deleted file mode 100644 index b03acd4db372..000000000000 Binary files a/frontend/static/sound/click4/click4_11.wav and /dev/null differ diff --git a/frontend/static/sound/click4/click4_22.wav b/frontend/static/sound/click4/click4_22.wav deleted file mode 100644 index e8af2f633575..000000000000 Binary files a/frontend/static/sound/click4/click4_22.wav and /dev/null differ diff --git a/frontend/static/sound/click4/click4_33.wav b/frontend/static/sound/click4/click4_33.wav deleted file mode 100644 index 604449d5df77..000000000000 Binary files a/frontend/static/sound/click4/click4_33.wav and /dev/null differ diff --git a/frontend/static/sound/click4/click4_44.wav b/frontend/static/sound/click4/click4_44.wav deleted file mode 100644 index b2939d493ef3..000000000000 Binary files a/frontend/static/sound/click4/click4_44.wav and /dev/null differ diff --git a/frontend/static/sound/click4/click4_55.wav b/frontend/static/sound/click4/click4_55.wav deleted file mode 100644 index 145976c7c6c3..000000000000 Binary files a/frontend/static/sound/click4/click4_55.wav and /dev/null differ diff --git a/frontend/static/sound/click4/click4_66.wav b/frontend/static/sound/click4/click4_66.wav deleted file mode 100644 index ec0d5379ad15..000000000000 Binary files a/frontend/static/sound/click4/click4_66.wav and /dev/null differ diff --git a/frontend/static/sound/click5/click5_11.wav b/frontend/static/sound/click5/click5_11.wav deleted file mode 100644 index 5051ae1bec1f..000000000000 Binary files a/frontend/static/sound/click5/click5_11.wav and /dev/null differ diff --git a/frontend/static/sound/click5/click5_22.wav b/frontend/static/sound/click5/click5_22.wav deleted file mode 100644 index a34e3025c280..000000000000 Binary files a/frontend/static/sound/click5/click5_22.wav and /dev/null differ diff --git a/frontend/static/sound/click5/click5_33.wav b/frontend/static/sound/click5/click5_33.wav deleted file mode 100644 index e365207f495e..000000000000 Binary files a/frontend/static/sound/click5/click5_33.wav and /dev/null differ diff --git a/frontend/static/sound/click5/click5_44.wav b/frontend/static/sound/click5/click5_44.wav deleted file mode 100644 index a6536ff9ee01..000000000000 Binary files a/frontend/static/sound/click5/click5_44.wav and /dev/null differ diff --git a/frontend/static/sound/click5/click5_55.wav b/frontend/static/sound/click5/click5_55.wav deleted file mode 100644 index 7182d01b7d71..000000000000 Binary files a/frontend/static/sound/click5/click5_55.wav and /dev/null differ diff --git a/frontend/static/sound/click5/click5_66.wav b/frontend/static/sound/click5/click5_66.wav deleted file mode 100644 index 60a2b3a13906..000000000000 Binary files a/frontend/static/sound/click5/click5_66.wav and /dev/null differ diff --git a/frontend/static/sound/click6/click6_22.wav b/frontend/static/sound/click6/click6_22.wav deleted file mode 100644 index aacf41a62b3d..000000000000 Binary files a/frontend/static/sound/click6/click6_22.wav and /dev/null differ diff --git a/frontend/static/sound/click6/click6_3.wav b/frontend/static/sound/click6/click6_3.wav deleted file mode 100644 index aacf41a62b3d..000000000000 Binary files a/frontend/static/sound/click6/click6_3.wav and /dev/null differ diff --git a/frontend/static/sound/click6/click6_33.wav b/frontend/static/sound/click6/click6_33.wav deleted file mode 100644 index aacf41a62b3d..000000000000 Binary files a/frontend/static/sound/click6/click6_33.wav and /dev/null differ diff --git a/frontend/static/sound/click7/click7_22.wav b/frontend/static/sound/click7/click7_22.wav deleted file mode 100644 index 1521655b95b9..000000000000 Binary files a/frontend/static/sound/click7/click7_22.wav and /dev/null differ diff --git a/frontend/static/sound/click7/click7_3.wav b/frontend/static/sound/click7/click7_3.wav deleted file mode 100644 index 1521655b95b9..000000000000 Binary files a/frontend/static/sound/click7/click7_3.wav and /dev/null differ diff --git a/frontend/static/sound/click7/click7_33.wav b/frontend/static/sound/click7/click7_33.wav deleted file mode 100644 index 1521655b95b9..000000000000 Binary files a/frontend/static/sound/click7/click7_33.wav and /dev/null differ diff --git a/frontend/static/sound/click1/click1_1.wav b/frontend/static/sounds/click1/1.wav similarity index 100% rename from frontend/static/sound/click1/click1_1.wav rename to frontend/static/sounds/click1/1.wav diff --git a/frontend/static/sound/click1/click1_2.wav b/frontend/static/sounds/click1/2.wav similarity index 100% rename from frontend/static/sound/click1/click1_2.wav rename to frontend/static/sounds/click1/2.wav diff --git a/frontend/static/sound/click1/click1_3.wav b/frontend/static/sounds/click1/3.wav similarity index 100% rename from frontend/static/sound/click1/click1_3.wav rename to frontend/static/sounds/click1/3.wav diff --git a/frontend/static/sound/click14/click14_1.wav b/frontend/static/sounds/click14/1.wav similarity index 100% rename from frontend/static/sound/click14/click14_1.wav rename to frontend/static/sounds/click14/1.wav diff --git a/frontend/static/sound/click14/click14_2.wav b/frontend/static/sounds/click14/2.wav similarity index 100% rename from frontend/static/sound/click14/click14_2.wav rename to frontend/static/sounds/click14/2.wav diff --git a/frontend/static/sound/click14/click14_3.wav b/frontend/static/sounds/click14/3.wav similarity index 100% rename from frontend/static/sound/click14/click14_3.wav rename to frontend/static/sounds/click14/3.wav diff --git a/frontend/static/sound/click14/click14_4.wav b/frontend/static/sounds/click14/4.wav similarity index 100% rename from frontend/static/sound/click14/click14_4.wav rename to frontend/static/sounds/click14/4.wav diff --git a/frontend/static/sound/click14/click14_5.wav b/frontend/static/sounds/click14/5.wav similarity index 100% rename from frontend/static/sound/click14/click14_5.wav rename to frontend/static/sounds/click14/5.wav diff --git a/frontend/static/sound/click14/click14_6.wav b/frontend/static/sounds/click14/6.wav similarity index 100% rename from frontend/static/sound/click14/click14_6.wav rename to frontend/static/sounds/click14/6.wav diff --git a/frontend/static/sound/click14/click14_7.wav b/frontend/static/sounds/click14/7.wav similarity index 100% rename from frontend/static/sound/click14/click14_7.wav rename to frontend/static/sounds/click14/7.wav diff --git a/frontend/static/sound/click14/click14_8.wav b/frontend/static/sounds/click14/8.wav similarity index 100% rename from frontend/static/sound/click14/click14_8.wav rename to frontend/static/sounds/click14/8.wav diff --git a/frontend/static/sound/click15/click15_1.wav b/frontend/static/sounds/click15/1.wav similarity index 100% rename from frontend/static/sound/click15/click15_1.wav rename to frontend/static/sounds/click15/1.wav diff --git a/frontend/static/sound/click15/click15_2.wav b/frontend/static/sounds/click15/2.wav similarity index 100% rename from frontend/static/sound/click15/click15_2.wav rename to frontend/static/sounds/click15/2.wav diff --git a/frontend/static/sound/click15/click15_3.wav b/frontend/static/sounds/click15/3.wav similarity index 100% rename from frontend/static/sound/click15/click15_3.wav rename to frontend/static/sounds/click15/3.wav diff --git a/frontend/static/sound/click15/click15_4.wav b/frontend/static/sounds/click15/4.wav similarity index 100% rename from frontend/static/sound/click15/click15_4.wav rename to frontend/static/sounds/click15/4.wav diff --git a/frontend/static/sound/click15/click15_5.wav b/frontend/static/sounds/click15/5.wav similarity index 100% rename from frontend/static/sound/click15/click15_5.wav rename to frontend/static/sounds/click15/5.wav diff --git a/frontend/static/sound/click16/click16_1.wav b/frontend/static/sounds/click16/1.wav similarity index 100% rename from frontend/static/sound/click16/click16_1.wav rename to frontend/static/sounds/click16/1.wav diff --git a/frontend/static/sound/click16/click16_2.wav b/frontend/static/sounds/click16/2.wav similarity index 100% rename from frontend/static/sound/click16/click16_2.wav rename to frontend/static/sounds/click16/2.wav diff --git a/frontend/static/sound/click16/click16_3.wav b/frontend/static/sounds/click16/3.wav similarity index 100% rename from frontend/static/sound/click16/click16_3.wav rename to frontend/static/sounds/click16/3.wav diff --git a/frontend/static/sound/click16/click16_4.wav b/frontend/static/sounds/click16/4.wav similarity index 100% rename from frontend/static/sound/click16/click16_4.wav rename to frontend/static/sounds/click16/4.wav diff --git a/frontend/static/sound/click16/click16_9.wav b/frontend/static/sounds/click16/5.wav similarity index 100% rename from frontend/static/sound/click16/click16_9.wav rename to frontend/static/sounds/click16/5.wav diff --git a/frontend/static/sound/click16/click16_10.wav b/frontend/static/sounds/click16/6.wav similarity index 100% rename from frontend/static/sound/click16/click16_10.wav rename to frontend/static/sounds/click16/6.wav diff --git a/frontend/static/sound/click16/click16_11.wav b/frontend/static/sounds/click16/7.wav similarity index 100% rename from frontend/static/sound/click16/click16_11.wav rename to frontend/static/sounds/click16/7.wav diff --git a/frontend/static/sound/click16/click16_8.wav b/frontend/static/sounds/click16/8.wav similarity index 100% rename from frontend/static/sound/click16/click16_8.wav rename to frontend/static/sounds/click16/8.wav diff --git a/frontend/static/sound/click2/click2_1.wav b/frontend/static/sounds/click2/1.wav similarity index 100% rename from frontend/static/sound/click2/click2_1.wav rename to frontend/static/sounds/click2/1.wav diff --git a/frontend/static/sound/click2/click2_2.wav b/frontend/static/sounds/click2/2.wav similarity index 100% rename from frontend/static/sound/click2/click2_2.wav rename to frontend/static/sounds/click2/2.wav diff --git a/frontend/static/sound/click2/click2_3.wav b/frontend/static/sounds/click2/3.wav similarity index 100% rename from frontend/static/sound/click2/click2_3.wav rename to frontend/static/sounds/click2/3.wav diff --git a/frontend/static/sound/click3/click3_1.wav b/frontend/static/sounds/click3/1.wav similarity index 100% rename from frontend/static/sound/click3/click3_1.wav rename to frontend/static/sounds/click3/1.wav diff --git a/frontend/static/sound/click3/click3_2.wav b/frontend/static/sounds/click3/2.wav similarity index 100% rename from frontend/static/sound/click3/click3_2.wav rename to frontend/static/sounds/click3/2.wav diff --git a/frontend/static/sound/click3/click3_3.wav b/frontend/static/sounds/click3/3.wav similarity index 100% rename from frontend/static/sound/click3/click3_3.wav rename to frontend/static/sounds/click3/3.wav diff --git a/frontend/static/sound/click4/click4_1.wav b/frontend/static/sounds/click4/1.wav similarity index 100% rename from frontend/static/sound/click4/click4_1.wav rename to frontend/static/sounds/click4/1.wav diff --git a/frontend/static/sound/click4/click4_2.wav b/frontend/static/sounds/click4/2.wav similarity index 100% rename from frontend/static/sound/click4/click4_2.wav rename to frontend/static/sounds/click4/2.wav diff --git a/frontend/static/sound/click4/click4_3.wav b/frontend/static/sounds/click4/3.wav similarity index 100% rename from frontend/static/sound/click4/click4_3.wav rename to frontend/static/sounds/click4/3.wav diff --git a/frontend/static/sound/click4/click4_4.wav b/frontend/static/sounds/click4/4.wav similarity index 100% rename from frontend/static/sound/click4/click4_4.wav rename to frontend/static/sounds/click4/4.wav diff --git a/frontend/static/sound/click4/click4_5.wav b/frontend/static/sounds/click4/5.wav similarity index 100% rename from frontend/static/sound/click4/click4_5.wav rename to frontend/static/sounds/click4/5.wav diff --git a/frontend/static/sound/click4/click4_6.wav b/frontend/static/sounds/click4/6.wav similarity index 100% rename from frontend/static/sound/click4/click4_6.wav rename to frontend/static/sounds/click4/6.wav diff --git a/frontend/static/sound/click5/click5_1.wav b/frontend/static/sounds/click5/1.wav similarity index 100% rename from frontend/static/sound/click5/click5_1.wav rename to frontend/static/sounds/click5/1.wav diff --git a/frontend/static/sound/click5/click5_2.wav b/frontend/static/sounds/click5/2.wav similarity index 100% rename from frontend/static/sound/click5/click5_2.wav rename to frontend/static/sounds/click5/2.wav diff --git a/frontend/static/sound/click5/click5_3.wav b/frontend/static/sounds/click5/3.wav similarity index 100% rename from frontend/static/sound/click5/click5_3.wav rename to frontend/static/sounds/click5/3.wav diff --git a/frontend/static/sound/click5/click5_4.wav b/frontend/static/sounds/click5/4.wav similarity index 100% rename from frontend/static/sound/click5/click5_4.wav rename to frontend/static/sounds/click5/4.wav diff --git a/frontend/static/sound/click5/click5_5.wav b/frontend/static/sounds/click5/5.wav similarity index 100% rename from frontend/static/sound/click5/click5_5.wav rename to frontend/static/sounds/click5/5.wav diff --git a/frontend/static/sound/click5/click5_6.wav b/frontend/static/sounds/click5/6.wav similarity index 100% rename from frontend/static/sound/click5/click5_6.wav rename to frontend/static/sounds/click5/6.wav diff --git a/frontend/static/sound/click6/click6_1.wav b/frontend/static/sounds/click6/1.wav similarity index 100% rename from frontend/static/sound/click6/click6_1.wav rename to frontend/static/sounds/click6/1.wav diff --git a/frontend/static/sound/click6/click6_11.wav b/frontend/static/sounds/click6/2.wav similarity index 100% rename from frontend/static/sound/click6/click6_11.wav rename to frontend/static/sounds/click6/2.wav diff --git a/frontend/static/sound/click6/click6_2.wav b/frontend/static/sounds/click6/3.wav similarity index 100% rename from frontend/static/sound/click6/click6_2.wav rename to frontend/static/sounds/click6/3.wav diff --git a/frontend/static/sound/click7/click7_1.wav b/frontend/static/sounds/click7/1.wav similarity index 100% rename from frontend/static/sound/click7/click7_1.wav rename to frontend/static/sounds/click7/1.wav diff --git a/frontend/static/sound/click7/click7_11.wav b/frontend/static/sounds/click7/2.wav similarity index 100% rename from frontend/static/sound/click7/click7_11.wav rename to frontend/static/sounds/click7/2.wav diff --git a/frontend/static/sound/click7/click7_2.wav b/frontend/static/sounds/click7/3.wav similarity index 100% rename from frontend/static/sound/click7/click7_2.wav rename to frontend/static/sounds/click7/3.wav diff --git a/frontend/static/sound/error1/error1_1.wav b/frontend/static/sounds/error1/1.wav similarity index 100% rename from frontend/static/sound/error1/error1_1.wav rename to frontend/static/sounds/error1/1.wav diff --git a/frontend/static/sound/error2/error2_1.wav b/frontend/static/sounds/error2/1.wav similarity index 100% rename from frontend/static/sound/error2/error2_1.wav rename to frontend/static/sounds/error2/1.wav diff --git a/frontend/static/sound/error3/error3_1.wav b/frontend/static/sounds/error3/1.wav similarity index 100% rename from frontend/static/sound/error3/error3_1.wav rename to frontend/static/sounds/error3/1.wav diff --git a/frontend/static/sound/error4/error4_1.wav b/frontend/static/sounds/error4/1.wav similarity index 100% rename from frontend/static/sound/error4/error4_1.wav rename to frontend/static/sounds/error4/1.wav diff --git a/frontend/static/sound/error4/error4_2.wav b/frontend/static/sounds/error4/2.wav similarity index 100% rename from frontend/static/sound/error4/error4_2.wav rename to frontend/static/sounds/error4/2.wav diff --git a/frontend/static/sound/fart-reverb.wav b/frontend/static/sounds/fart-reverb.wav similarity index 100% rename from frontend/static/sound/fart-reverb.wav rename to frontend/static/sounds/fart-reverb.wav diff --git a/frontend/static/sound/timeWarning.wav b/frontend/static/sounds/timeWarning.wav similarity index 100% rename from frontend/static/sound/timeWarning.wav rename to frontend/static/sounds/timeWarning.wav