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..4b7f890b547a 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,57 @@ 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/sound") + .filter((it) => it.startsWith("click")) + .flatMap((folder) => + fs + .readdirSync(`./static/sound/${folder}`) + .map((it) => `${folder}/${it}`), + ), + ); + + //missing sound files + Object.entries(clickSoundConfig).forEach(([key, value]) => { + value + .flatMap((it) => + it.sounds.map((file) => file.substring("../sound/".length)), + ) + + .filter((it) => !soundFiles.has(it)) + .forEach((file) => + problems.add( + "click" + key, + `missing file frontend/static/sound/${file}`, + ), + ); + }); + + //additional files + const expectedSoundFiles = new Set( + Object.values(clickSoundConfig).flatMap((it) => + it.flatMap((cfg) => + cfg.sounds.map((file) => file.substring("../sound/".length)), + ), + ), + ); + soundFiles + .values() + .filter((name) => !expectedSoundFiles.has(name)) + .forEach((file) => problems.add("_additional", file)); + + console.log(problems.toString()); + + return; +} + type Validator = () => Promise; async function main(): Promise { @@ -436,11 +488,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..771174dd3a70 --- /dev/null +++ b/frontend/src/ts/constants/sounds.ts @@ -0,0 +1,91 @@ +import { PlaySoundOnClick } from "@monkeytype/schemas/configs"; + +export const soundsConfig: SoundConfigType = { + 1: { numberOfSounds: 3 }, + 2: { numberOfSounds: 3 }, + 3: { numberOfSounds: 3 }, + 4: { numberOfSounds: 6, hasSecondVariant: true }, + 5: { numberOfSounds: 6, hasSecondVariant: true }, + 6: { numberOfSounds: 3, hasSecondVariant: true }, + 7: { numberOfSounds: 3, hasSecondVariant: true }, + 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; + hasSecondVariant?: true; +}; + +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< + Exclude, + { + sounds: string[]; + counter: number; + }[] + > +>; + +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) => { + const sounds = config.hasSecondVariant + ? [ + `../sound/click${key}/click${key}_${index + 1}.wav`, + `../sound/click${key}/click${key}_${index + 1}_2.wav`, + ] + : [`../sound/click${key}/click${key}_${index + 1}.wav`]; + + return { sounds, counter: 0 }; + }); + return [key, fullConfig]; + }), + ); +} diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index 24e01e6b397a..639556199e14 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -1,28 +1,46 @@ 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"; - -async function gethowler(): Promise { - return await import("howler"); +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; } -type ClickSounds = Record< - string, - { - sounds: Howl[]; - counter: number; - }[] ->; +let initPromise: Promise | null = null; +const loadedBundles: Set = new Set(); + +const howlers: Record> = {}; + +async function getHowl(src: string): Promise { + howlers[src] ??= (async () => { + const { Howl } = await getHowlerModule(); + return new Howl({ src }); + })(); + + return howlers[src]; +} type ErrorSounds = Record< - string, + Exclude, { sounds: Howl[]; counter: number; @@ -30,487 +48,114 @@ type ErrorSounds = Record< >; 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("../sound/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("../sound/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" }), - ], + sounds: [await getHowl("../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" }), - ], + sounds: [await getHowl("../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" }), - ], + sounds: [await getHowl("../sound/error3/error3_1.wav")], counter: 0, }, ], 4: [ { - sounds: [ - new Howl({ src: "../sound/error4/error4_1.wav" }), - new Howl({ src: "../sound/error4/error4_1.wav" }), - ], + sounds: [await getHowl("../sound/error4/error4_1.wav")], counter: 0, }, { - sounds: [ - new Howl({ src: "../sound/error4/error4_2.wav" }), - new Howl({ src: "../sound/error4/error4_2.wav" }), - ], + sounds: [await getHowl("../sound/error4/error4_2.wav")], counter: 0, }, ], }; - 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((it) => it.sounds).map(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]?.sounds[0] === undefined + ) { + return; + } - safeClickSounds?.[val]?.[0]?.sounds[0]?.seek(0); - safeClickSounds?.[val]?.[0]?.sounds[0]?.play(); + const howl = await getHowl(safeClickSounds[0]?.sounds[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; @@ -528,7 +173,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 +188,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 +241,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 +259,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 +274,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 +290,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>; -function playScale(scale: ValidScales, scaleMeta: ScaleData): void { +export const scaleConfigurations: ScaleConfigurationType = + extractScaleSounds(soundsConfig); + +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 +312,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 +346,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 +371,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 +384,39 @@ 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; + + const src = randomSound.sounds[randomSound.counter]; + if (src === undefined) throw new Error("Invalid click sound ID"); + const soundToPlay = await getHowl(src); randomSound.counter++; - if (randomSound.counter === 2) randomSound.counter = 0; + if (randomSound.counter === randomSound.sounds.length) { + randomSound.counter = 0; + } soundToPlay.seek(0); soundToPlay.play(); } @@ -820,22 +432,45 @@ export async function playError(): Promise { const soundToPlay = randomSound.sounds[randomSound.counter] as Howl; randomSound.counter++; - if (randomSound.counter === 2) randomSound.counter = 0; + if (randomSound.counter === randomSound.sounds.length) { + randomSound.counter = 0; + } soundToPlay.seek(0); soundToPlay.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_10.wav b/frontend/static/sound/click16/click16_10.wav deleted file mode 100644 index 23aed8d0ae55..000000000000 Binary files a/frontend/static/sound/click16/click16_10.wav and /dev/null differ diff --git a/frontend/static/sound/click16/click16_11.wav b/frontend/static/sound/click16/click16_11.wav deleted file mode 100644 index 7bc0996cd505..000000000000 Binary files a/frontend/static/sound/click16/click16_11.wav and /dev/null differ diff --git a/frontend/static/sound/click16/click16_5.wav b/frontend/static/sound/click16/click16_5.wav index 71df277a5d18..0678eac083cd 100644 Binary files a/frontend/static/sound/click16/click16_5.wav and b/frontend/static/sound/click16/click16_5.wav differ diff --git a/frontend/static/sound/click16/click16_6.wav b/frontend/static/sound/click16/click16_6.wav index 4076a647942e..23aed8d0ae55 100644 Binary files a/frontend/static/sound/click16/click16_6.wav and b/frontend/static/sound/click16/click16_6.wav differ diff --git a/frontend/static/sound/click16/click16_7.wav b/frontend/static/sound/click16/click16_7.wav index 44919e80e80f..7bc0996cd505 100644 Binary files a/frontend/static/sound/click16/click16_7.wav and b/frontend/static/sound/click16/click16_7.wav differ diff --git a/frontend/static/sound/click16/click16_9.wav b/frontend/static/sound/click16/click16_9.wav deleted file mode 100644 index 0678eac083cd..000000000000 Binary files a/frontend/static/sound/click16/click16_9.wav and /dev/null differ diff --git a/frontend/static/sound/click4/click4_11.wav b/frontend/static/sound/click4/click4_1_2.wav similarity index 100% rename from frontend/static/sound/click4/click4_11.wav rename to frontend/static/sound/click4/click4_1_2.wav diff --git a/frontend/static/sound/click4/click4_22.wav b/frontend/static/sound/click4/click4_2_2.wav similarity index 100% rename from frontend/static/sound/click4/click4_22.wav rename to frontend/static/sound/click4/click4_2_2.wav diff --git a/frontend/static/sound/click4/click4_33.wav b/frontend/static/sound/click4/click4_3_2.wav similarity index 100% rename from frontend/static/sound/click4/click4_33.wav rename to frontend/static/sound/click4/click4_3_2.wav diff --git a/frontend/static/sound/click4/click4_44.wav b/frontend/static/sound/click4/click4_4_2.wav similarity index 100% rename from frontend/static/sound/click4/click4_44.wav rename to frontend/static/sound/click4/click4_4_2.wav diff --git a/frontend/static/sound/click4/click4_55.wav b/frontend/static/sound/click4/click4_5_2.wav similarity index 100% rename from frontend/static/sound/click4/click4_55.wav rename to frontend/static/sound/click4/click4_5_2.wav diff --git a/frontend/static/sound/click4/click4_66.wav b/frontend/static/sound/click4/click4_6_2.wav similarity index 100% rename from frontend/static/sound/click4/click4_66.wav rename to frontend/static/sound/click4/click4_6_2.wav diff --git a/frontend/static/sound/click5/click5_11.wav b/frontend/static/sound/click5/click5_1_2.wav similarity index 100% rename from frontend/static/sound/click5/click5_11.wav rename to frontend/static/sound/click5/click5_1_2.wav diff --git a/frontend/static/sound/click5/click5_22.wav b/frontend/static/sound/click5/click5_2_2.wav similarity index 100% rename from frontend/static/sound/click5/click5_22.wav rename to frontend/static/sound/click5/click5_2_2.wav diff --git a/frontend/static/sound/click5/click5_33.wav b/frontend/static/sound/click5/click5_3_2.wav similarity index 100% rename from frontend/static/sound/click5/click5_33.wav rename to frontend/static/sound/click5/click5_3_2.wav diff --git a/frontend/static/sound/click5/click5_44.wav b/frontend/static/sound/click5/click5_4_2.wav similarity index 100% rename from frontend/static/sound/click5/click5_44.wav rename to frontend/static/sound/click5/click5_4_2.wav diff --git a/frontend/static/sound/click5/click5_55.wav b/frontend/static/sound/click5/click5_5_2.wav similarity index 100% rename from frontend/static/sound/click5/click5_55.wav rename to frontend/static/sound/click5/click5_5_2.wav diff --git a/frontend/static/sound/click5/click5_66.wav b/frontend/static/sound/click5/click5_6_2.wav similarity index 100% rename from frontend/static/sound/click5/click5_66.wav rename to frontend/static/sound/click5/click5_6_2.wav diff --git a/frontend/static/sound/click6/click6_11.wav b/frontend/static/sound/click6/click6_1_2.wav similarity index 100% rename from frontend/static/sound/click6/click6_11.wav rename to frontend/static/sound/click6/click6_1_2.wav diff --git a/frontend/static/sound/click6/click6_22.wav b/frontend/static/sound/click6/click6_2_2.wav similarity index 100% rename from frontend/static/sound/click6/click6_22.wav rename to frontend/static/sound/click6/click6_2_2.wav diff --git a/frontend/static/sound/click6/click6_33.wav b/frontend/static/sound/click6/click6_3_2.wav similarity index 100% rename from frontend/static/sound/click6/click6_33.wav rename to frontend/static/sound/click6/click6_3_2.wav diff --git a/frontend/static/sound/click7/click7_11.wav b/frontend/static/sound/click7/click7_1_2.wav similarity index 100% rename from frontend/static/sound/click7/click7_11.wav rename to frontend/static/sound/click7/click7_1_2.wav diff --git a/frontend/static/sound/click7/click7_22.wav b/frontend/static/sound/click7/click7_2_2.wav similarity index 100% rename from frontend/static/sound/click7/click7_22.wav rename to frontend/static/sound/click7/click7_2_2.wav diff --git a/frontend/static/sound/click7/click7_33.wav b/frontend/static/sound/click7/click7_3_2.wav similarity index 100% rename from frontend/static/sound/click7/click7_33.wav rename to frontend/static/sound/click7/click7_3_2.wav