From 108c50a37f21182be345852ce824b1d24230c41e Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 29 Apr 2026 00:31:50 +0200 Subject: [PATCH 01/13] refactor: optimize sound-controller (@fehmer) --- .../src/ts/controllers/sound-controller.ts | 569 +++++------------- 1 file changed, 135 insertions(+), 434 deletions(-) diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index 24e01e6b397a..2d734d57a4ba 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -7,12 +7,64 @@ 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"; async function gethowler(): Promise { return await import("howler"); } +type ClickSoundConfig = { + numberOfSounds: number; + hasSecondVariant?: true; +}; + +type OscillatorSoundConfig = { oscillatorType: SupportedOscillatorTypes }; + +type ScaleSoundConfig = { + validNotes: ValidNotes[]; + scaleName: ValidScales; //TODO remove +}; + +type SoundConfigType = Record< + Exclude, + ClickSoundConfig | OscillatorSoundConfig | ScaleSoundConfig +>; + +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"], scaleName: "pentatonic" }, + 13: { validNotes: ["C", "D", "E", "Gb", "Ab", "Bb"], scaleName: "wholetone" }, + 14: { numberOfSounds: 8 }, + 15: { numberOfSounds: 5 }, + 16: { numberOfSounds: 11 }, //TODO 5-7 were disabled +}; + +type ClickSoundConfigType = Partial< + Record< + Exclude, + { + sounds: string[]; + counter: number; + }[] + > +>; + +export const clickSoundConfig: ClickSoundConfigType = + extractClickSounds(soundsConfig); + type ClickSounds = Record< string, { @@ -20,9 +72,8 @@ type ClickSounds = Record< counter: number; }[] >; - type ErrorSounds = Record< - string, + Exclude, { sounds: Howl[]; counter: number; @@ -58,44 +109,29 @@ async function initErrorSound(): Promise { errorSounds = { 1: [ { - sounds: [ - new Howl({ src: "../sound/error1/error1_1.wav" }), - new Howl({ src: "../sound/error1/error1_1.wav" }), - ], + sounds: [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" }), - ], + sounds: [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" }), - ], + sounds: [new Howl({ src: "../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: [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" }), - ], + sounds: [new Howl({ src: "../sound/error4/error4_2.wav" })], counter: 0, }, ], @@ -106,397 +142,30 @@ async function initErrorSound(): Promise { 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, - }, - ], - }; + clickSounds = Object.fromEntries( + Object.entries(clickSoundConfig).map(([key, value]) => [ + key, + value.map((it) => ({ + ...it, + sounds: it.sounds.map((src) => new Howl({ src })), + })), + ]), + ); Howler.volume(Config.soundVolume); } export async function previewClick(val: PlaySoundOnClick): Promise { - if (["8", "9", "10", "11"].includes(val)) { + if (val === "off") return; + + const config = soundsConfig[val]; + + if ("oscillatorType" in config) { playNote("KeyQ", clickSoundIdsToOscillatorType[val as DynamicClickSounds]); return; } - if (["12", "13"].includes(val)) { - scaleConfigurations[val as "12" | "13"].preview(); - return; + if ("validNotes" in config) { + scaleConfigurations[val]?.preview(); } if (clickSounds === null) await init(); @@ -510,7 +179,8 @@ export async function previewClick(val: PlaySoundOnClick): Promise { safeClickSounds?.[val]?.[0]?.sounds[0]?.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; @@ -667,21 +337,19 @@ 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, - }, -}; +export const scaleConfigurations: Partial> = + { + "12": { + name: "pentatonic", + preview: createPreviewScale("pentatonic"), + meta: defaultScaleData, + }, + "13": { + name: "wholetone", + preview: createPreviewScale("wholetone"), + meta: defaultScaleData, + }, + }; function playScale(scale: ValidScales, scaleMeta: ScaleData): void { if (audioCtx === undefined) { @@ -778,19 +446,23 @@ 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); return; } - if (Config.playSoundOnClick in clickSoundIdsToOscillatorType) { - playNote(codeOverride ?? undefined); + if ("validNotes" in config) { + const scaleConfig = scaleConfigurations[val]; + if (scaleConfig === undefined) { + //TODO + throw new Error("missing scale config"); + } + playScale(scaleConfig.name, scaleConfig.meta); return; } @@ -804,7 +476,9 @@ export async function playClick(codeOverride?: string): 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(); } @@ -820,7 +494,9 @@ 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(); } @@ -833,6 +509,31 @@ function setVolume(val: number): void { } } +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]; + }), + ); +} + configEvent.subscribe(({ key, newValue }) => { if (key === "playSoundOnClick" && newValue !== "off") void init(); if (key === "soundVolume") { From ce959f3289a3e373a4a3aad4e292a950f53d4c66 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 29 Apr 2026 00:45:42 +0200 Subject: [PATCH 02/13] playNote --- .../src/ts/controllers/sound-controller.ts | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index 2d734d57a4ba..3ce21f7c6162 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -21,6 +21,7 @@ type ClickSoundConfig = { hasSecondVariant?: true; }; +type SupportedOscillatorTypes = Exclude; type OscillatorSoundConfig = { oscillatorType: SupportedOscillatorTypes }; type ScaleSoundConfig = { @@ -160,7 +161,7 @@ export async function previewClick(val: PlaySoundOnClick): Promise { const config = soundsConfig[val]; if ("oscillatorType" in config) { - playNote("KeyQ", clickSoundIdsToOscillatorType[val as DynamicClickSounds]); + playNote({ codeOverride: "KeyQ", oscillatorType: config.oscillatorType }); return; } @@ -267,19 +268,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 { @@ -408,16 +396,16 @@ export async function clearAllSounds(): Promise { Howl.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; } @@ -429,11 +417,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); @@ -452,7 +436,7 @@ export async function playClick(codeOverride?: string): Promise { const config = soundsConfig[val]; if ("oscillatorType" in config) { - playNote(codeOverride); + playNote({ codeOverride, oscillatorType: config.oscillatorType }); return; } From bc2d2cb22ee51e620c751f28a163a472454174a1 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 29 Apr 2026 01:02:14 +0200 Subject: [PATCH 03/13] scales --- .../src/ts/controllers/sound-controller.ts | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index 3ce21f7c6162..331de96f2145 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -1,7 +1,6 @@ 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"; @@ -26,7 +25,6 @@ type OscillatorSoundConfig = { oscillatorType: SupportedOscillatorTypes }; type ScaleSoundConfig = { validNotes: ValidNotes[]; - scaleName: ValidScales; //TODO remove }; type SoundConfigType = Record< @@ -46,8 +44,8 @@ const soundsConfig: SoundConfigType = { 9: { oscillatorType: "sawtooth" }, 10: { oscillatorType: "square" }, 11: { oscillatorType: "triangle" }, - 12: { validNotes: ["C", "D", "E", "G", "A"], scaleName: "pentatonic" }, - 13: { validNotes: ["C", "D", "E", "Gb", "Ab", "Bb"], scaleName: "wholetone" }, + 12: { validNotes: ["C", "D", "E", "G", "A"] }, + 13: { validNotes: ["C", "D", "E", "Gb", "Ab", "Bb"] }, 14: { numberOfSounds: 8 }, 15: { numberOfSounds: 5 }, 16: { numberOfSounds: 11 }, //TODO 5-7 were disabled @@ -286,20 +284,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, @@ -309,12 +300,11 @@ function createPreviewScale(scaleName: ValidScales): () => void { return async () => { if (clickSounds === null) await init(); - playScale(scaleName, scale); + playScale(validNotes, scale); }; } type ScaleMeta = { - name: ValidScales; preview: ReturnType; meta: ScaleData; }; @@ -325,28 +315,17 @@ const defaultScaleData: ScaleData = { direction: 1, }; -export const scaleConfigurations: Partial> = - { - "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; } @@ -358,7 +337,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; @@ -446,7 +425,7 @@ export async function playClick(codeOverride?: string): Promise { //TODO throw new Error("missing scale config"); } - playScale(scaleConfig.name, scaleConfig.meta); + playScale(config.validNotes, scaleConfig.meta); return; } @@ -518,6 +497,26 @@ function extractClickSounds( ); } +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") { From 9368f639b84d729995073602a8b20027d336a168 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 29 Apr 2026 01:08:12 +0200 Subject: [PATCH 04/13] fix --- frontend/src/ts/controllers/sound-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index 331de96f2145..fc7a214f6b31 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -510,7 +510,7 @@ function extractScaleSounds( key, { preview: createPreviewScale(config.validNotes), - meta: defaultScaleData, + meta: { ...defaultScaleData }, } as ScaleMeta, ]; }), From d2d85cfca61d04bfcfdc07e2b31b6cabeadc4dcc Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 29 Apr 2026 01:58:08 +0200 Subject: [PATCH 05/13] add validation --- frontend/scripts/check-assets.ts | 54 +++++++++++ frontend/src/ts/constants/sounds.ts | 91 +++++++++++++++++++ .../src/ts/controllers/sound-controller.ts | 87 ++---------------- 3 files changed, 155 insertions(+), 77 deletions(-) create mode 100644 frontend/src/ts/constants/sounds.ts 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..5053b190f7d5 --- /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: 11 }, //TODO 5-7 were disabled +}; + +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 fc7a214f6b31..8c0d0a2ecac7 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -10,60 +10,19 @@ import { PlaySoundOnClick, PlaySoundOnError, } from "@monkeytype/schemas/configs"; +import { + clickSoundConfig, + ScaleSoundConfig, + SoundConfigType, + soundsConfig, + SupportedOscillatorTypes, + ValidNotes, +} from "../constants/sounds"; async function gethowler(): Promise { return await import("howler"); } -type ClickSoundConfig = { - numberOfSounds: number; - hasSecondVariant?: true; -}; - -type SupportedOscillatorTypes = Exclude; -type OscillatorSoundConfig = { oscillatorType: SupportedOscillatorTypes }; - -type ScaleSoundConfig = { - validNotes: ValidNotes[]; -}; - -type SoundConfigType = Record< - Exclude, - ClickSoundConfig | OscillatorSoundConfig | ScaleSoundConfig ->; - -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: 11 }, //TODO 5-7 were disabled -}; - -type ClickSoundConfigType = Partial< - Record< - Exclude, - { - sounds: string[]; - counter: number; - }[] - > ->; - -export const clickSoundConfig: ClickSoundConfigType = - extractClickSounds(soundsConfig); - type ClickSounds = Record< string, { @@ -197,7 +156,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], @@ -212,8 +171,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; @@ -472,31 +430,6 @@ function setVolume(val: number): void { } } -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]; - }), - ); -} - function extractScaleSounds( shortConfig: SoundConfigType, ): ScaleConfigurationType { From f543748f32b8ea1e2346500364d1d00f89e36ce1 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 29 Apr 2026 02:04:26 +0200 Subject: [PATCH 06/13] fix click4 --- .../sound/click4/{click4_11.wav => click4_1_2.wav} | Bin .../sound/click4/{click4_22.wav => click4_2_2.wav} | Bin .../sound/click4/{click4_33.wav => click4_3_2.wav} | Bin .../sound/click4/{click4_44.wav => click4_4_2.wav} | Bin .../sound/click4/{click4_55.wav => click4_5_2.wav} | Bin .../sound/click4/{click4_66.wav => click4_6_2.wav} | Bin 6 files changed, 0 insertions(+), 0 deletions(-) rename frontend/static/sound/click4/{click4_11.wav => click4_1_2.wav} (100%) rename frontend/static/sound/click4/{click4_22.wav => click4_2_2.wav} (100%) rename frontend/static/sound/click4/{click4_33.wav => click4_3_2.wav} (100%) rename frontend/static/sound/click4/{click4_44.wav => click4_4_2.wav} (100%) rename frontend/static/sound/click4/{click4_55.wav => click4_5_2.wav} (100%) rename frontend/static/sound/click4/{click4_66.wav => click4_6_2.wav} (100%) 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 From 954682a8dbd42a2422921f34ff6fa74a08e82447 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 29 Apr 2026 02:06:54 +0200 Subject: [PATCH 07/13] fix workflow to run for sounds --- .github/workflows/monkey-ci.yml | 1 + 1 file changed, 1 insertion(+) 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 From c29c9009020217d388c7f4830678bbf8eec509ab Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 29 Apr 2026 02:09:44 +0200 Subject: [PATCH 08/13] fix all filenames --- .../sound/click5/{click5_11.wav => click5_1_2.wav} | Bin .../sound/click5/{click5_22.wav => click5_2_2.wav} | Bin .../sound/click5/{click5_33.wav => click5_3_2.wav} | Bin .../sound/click5/{click5_44.wav => click5_4_2.wav} | Bin .../sound/click5/{click5_55.wav => click5_5_2.wav} | Bin .../sound/click5/{click5_66.wav => click5_6_2.wav} | Bin .../sound/click6/{click6_11.wav => click6_1_2.wav} | Bin .../sound/click6/{click6_22.wav => click6_2_2.wav} | Bin .../sound/click6/{click6_33.wav => click6_3_2.wav} | Bin .../sound/click7/{click7_11.wav => click7_1_2.wav} | Bin .../sound/click7/{click7_22.wav => click7_2_2.wav} | Bin .../sound/click7/{click7_33.wav => click7_3_2.wav} | Bin 12 files changed, 0 insertions(+), 0 deletions(-) rename frontend/static/sound/click5/{click5_11.wav => click5_1_2.wav} (100%) rename frontend/static/sound/click5/{click5_22.wav => click5_2_2.wav} (100%) rename frontend/static/sound/click5/{click5_33.wav => click5_3_2.wav} (100%) rename frontend/static/sound/click5/{click5_44.wav => click5_4_2.wav} (100%) rename frontend/static/sound/click5/{click5_55.wav => click5_5_2.wav} (100%) rename frontend/static/sound/click5/{click5_66.wav => click5_6_2.wav} (100%) rename frontend/static/sound/click6/{click6_11.wav => click6_1_2.wav} (100%) rename frontend/static/sound/click6/{click6_22.wav => click6_2_2.wav} (100%) rename frontend/static/sound/click6/{click6_33.wav => click6_3_2.wav} (100%) rename frontend/static/sound/click7/{click7_11.wav => click7_1_2.wav} (100%) rename frontend/static/sound/click7/{click7_22.wav => click7_2_2.wav} (100%) rename frontend/static/sound/click7/{click7_33.wav => click7_3_2.wav} (100%) 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 From d4ea34ed3cd0b1068a1fca19b045b02efc3bb1da Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 29 Apr 2026 11:05:47 +0200 Subject: [PATCH 09/13] only load needed click sounds --- .../src/ts/controllers/sound-controller.ts | 111 ++++++++++-------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index 8c0d0a2ecac7..f6ad9eee91cf 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -20,16 +20,25 @@ import { } from "../constants/sounds"; async function gethowler(): Promise { - return await import("howler"); + return import("howler"); +} + +let isInit = false; +const loadedBundles: Set = new Set(); +const howlers: Record = {}; + +async function getHowl(src: string): Promise { + const cached = howlers[src]; + + if (cached !== undefined) return cached; + + const Howl = (await gethowler()).Howl; + const howl = new Howl({ src }); + howlers[src] = howl; + + return howl; } -type ClickSounds = Record< - string, - { - sounds: Howl[]; - counter: number; - }[] ->; type ErrorSounds = Record< Exclude, { @@ -39,26 +48,19 @@ 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 { @@ -98,24 +100,36 @@ async function initErrorSound(): Promise { } async function init(): Promise { - const Howl = (await gethowler()).Howl; - if (clickSounds !== null) return; - clickSounds = Object.fromEntries( - Object.entries(clickSoundConfig).map(([key, value]) => [ - key, - value.map((it) => ({ - ...it, - sounds: it.sounds.map((src) => new Howl({ src })), - })), - ]), - ); - Howler.volume(Config.soundVolume); + if (!isInit) { + isInit = true; + const howler = await gethowler(); + howler.Howler.volume(Config.soundVolume); + } + + //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(async (it) => getHowl(it)), + ); + } + + //preload error sounds + await initErrorSound(); } -export async function previewClick(val: PlaySoundOnClick): Promise { - if (val === "off") return; +export async function previewClick(clickId: PlaySoundOnClick): Promise { + if (clickId === "off") return; - const config = soundsConfig[val]; + const config = soundsConfig[clickId]; if ("oscillatorType" in config) { playNote({ codeOverride: "KeyQ", oscillatorType: config.oscillatorType }); @@ -123,18 +137,22 @@ export async function previewClick(val: PlaySoundOnClick): Promise { } if ("validNotes" in config) { - scaleConfigurations[val]?.preview(); + scaleConfigurations[clickId]?.preview(); } - if (clickSounds === null) await init(); - - const safeClickSounds = clickSounds as ClickSounds; + await init(); - 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: PlaySoundOnError): Promise { @@ -257,7 +275,7 @@ function createPreviewScale(validNotes: ValidNotes[]): () => void { }; return async () => { - if (clickSounds === null) await init(); + await init(); playScale(validNotes, scale); }; } @@ -387,14 +405,15 @@ export async function playClick(codeOverride?: string): Promise { 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 === randomSound.sounds.length) { From 175c603656be0e4c13781aa54da42774fb13da61 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 29 Apr 2026 11:21:52 +0200 Subject: [PATCH 10/13] cleanup --- .../src/ts/controllers/sound-controller.ts | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index f6ad9eee91cf..b751c118539f 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -19,24 +19,24 @@ import { ValidNotes, } from "../constants/sounds"; -async function gethowler(): Promise { - return import("howler"); +let howlerModulePromise: Promise | null = null; +async function getHowlerModule(): Promise { + howlerModulePromise ??= import("howler"); + return howlerModulePromise; } -let isInit = false; +let initPromise: Promise | null = null; const loadedBundles: Set = new Set(); -const howlers: Record = {}; -async function getHowl(src: string): Promise { - const cached = howlers[src]; - - if (cached !== undefined) return cached; +const howlers: Record> = {}; - const Howl = (await gethowler()).Howl; - const howl = new Howl({ src }); - howlers[src] = howl; +async function getHowl(src: string): Promise { + howlers[src] ??= (async () => { + const { Howl } = await getHowlerModule(); + return new Howl({ src }); + })(); - return howl; + return howlers[src]; } type ErrorSounds = Record< @@ -64,47 +64,47 @@ async function initFartReverb(): Promise { } async function initErrorSound(): Promise { - const Howl = (await gethowler()).Howl; if (errorSounds !== null) return; errorSounds = { 1: [ { - sounds: [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" })], + sounds: [await getHowl("../sound/error2/error2_1.wav")], counter: 0, }, ], 3: [ { - sounds: [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" })], + sounds: [await getHowl("../sound/error4/error4_1.wav")], counter: 0, }, { - sounds: [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 { - if (!isInit) { - isInit = true; - const howler = await gethowler(); - howler.Howler.volume(Config.soundVolume); - } + initPromise ??= (async () => { + const { Howler } = await getHowlerModule(); + Howler.volume(Config.soundVolume); + })(); + + await initPromise; //preload sounds const clickId = Config.playSoundOnClick; @@ -117,9 +117,7 @@ async function init(): Promise { if (config === undefined) return; - await Promise.all( - config.flatMap((it) => it.sounds).map(async (it) => getHowl(it)), - ); + await Promise.all(config.flatMap((it) => it.sounds).map(getHowl)); } //preload error sounds @@ -138,6 +136,7 @@ export async function previewClick(clickId: PlaySoundOnClick): Promise { if ("validNotes" in config) { scaleConfigurations[clickId]?.preview(); + return; } await init(); @@ -347,8 +346,8 @@ 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(options: { @@ -441,8 +440,9 @@ export async function playError(): Promise { soundToPlay.play(); } -function setVolume(val: number): void { +async function setVolume(val: number): Promise { try { + const { Howler } = await getHowlerModule(); Howler.volume(val); } catch (e) { // @@ -472,6 +472,6 @@ function extractScaleSounds( configEvent.subscribe(({ key, newValue }) => { if (key === "playSoundOnClick" && newValue !== "off") void init(); if (key === "soundVolume") { - setVolume(newValue); + void setVolume(newValue); } }); From bfc0ff5b89c77d16cc05bdf796343d2cb7be1a83 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 29 Apr 2026 11:26:12 +0200 Subject: [PATCH 11/13] remove unused farts --- frontend/src/ts/constants/sounds.ts | 2 +- frontend/static/sound/click16/click16_10.wav | Bin 10496 -> 0 bytes frontend/static/sound/click16/click16_11.wav | Bin 9182 -> 0 bytes frontend/static/sound/click16/click16_5.wav | Bin 17882 -> 4630 bytes frontend/static/sound/click16/click16_6.wav | Bin 17882 -> 10496 bytes frontend/static/sound/click16/click16_7.wav | Bin 20814 -> 9182 bytes frontend/static/sound/click16/click16_9.wav | Bin 4630 -> 0 bytes 7 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 frontend/static/sound/click16/click16_10.wav delete mode 100644 frontend/static/sound/click16/click16_11.wav delete mode 100644 frontend/static/sound/click16/click16_9.wav diff --git a/frontend/src/ts/constants/sounds.ts b/frontend/src/ts/constants/sounds.ts index 5053b190f7d5..771174dd3a70 100644 --- a/frontend/src/ts/constants/sounds.ts +++ b/frontend/src/ts/constants/sounds.ts @@ -16,7 +16,7 @@ export const soundsConfig: SoundConfigType = { 13: { validNotes: ["C", "D", "E", "Gb", "Ab", "Bb"] }, 14: { numberOfSounds: 8 }, 15: { numberOfSounds: 5 }, - 16: { numberOfSounds: 11 }, //TODO 5-7 were disabled + 16: { numberOfSounds: 8 }, }; export type ClickSoundConfig = { diff --git a/frontend/static/sound/click16/click16_10.wav b/frontend/static/sound/click16/click16_10.wav deleted file mode 100644 index 23aed8d0ae55e6e6851d5bdfafe122c445aae0ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10496 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 diff --git a/frontend/static/sound/click16/click16_11.wav b/frontend/static/sound/click16/click16_11.wav deleted file mode 100644 index 7bc0996cd505f76dbc1df259971b6dd659091950..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9182 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 Tk5S#9W2?hIz*B=qJKz34!$N&w diff --git a/frontend/static/sound/click16/click16_5.wav b/frontend/static/sound/click16/click16_5.wav index 71df277a5d1805fcc4cfaba4ac5933f24d6ce852..0678eac083cd99ebb8f5dfadcef0e4330313d914 100644 GIT binary patch literal 4630 zcmW-l4^&lUy2jscpM4Mr{{W6MfGNvAOJUFy(?WA(5(iW)q12h|nnpI()vo1~g{$<= zlu1URcd{C%K&??HZPXMsy~zlqCc4l;7?lN%=zte-_!A)Koc-S4K5MUi*k|wkz2Eyj z&+|UtcgW71IkR7s$bz)_>C0BE9yUNk49Cf*Mb>>QV#xrRlbQ2GEcY|#&U`F0XXX=q zxt_D+)n${$kDWYr`~we&ycGF~-SE@`h;=~Vet$H-otB*l#K$Rpy@Pqjm* z5ciL|n_N@mu0AIRbvJK)#Q8VbsP9RUmP@A8^Y(GsD{G{`NtOSTk@AYUs$ZG?`ZFt3 z`+LSZ)9gY`57{ayF*Bk&<&Dv=`J+R#2Mxay8_2tRtMzLuwsX_HU){Ve`+ClFrgk0t zcbe|>9Sh8B+b+j@di|}wExlX1p5)#Njqz-BKK5L;V(k;=9q&cyn%op;BY&Jj0Hr=X^J4G5N z&pJn)XPp$iUsuRvr_%XYp5d4?Z34cb;>OvVO zJ)j2R$+E0MnQfi&?3I6d{_J_*^O?OS^!1Re z`(nc$x-T;17h%O#wCwNQ(EdVqPRH=x?aiBh+|V=J-_W|U>xu5>igB~Qr(sfOK5d0yXjrs*B$kdy7Kb~ZcroFAN>PL*@U z>21!HwmRV>PXt3(W0Z;EV2E?IZ*)1Rl^g10!Xs z^s}Bcr>!H_r*@%z%pPwid*^#9?N97&_F6mHPV%g^@=U3mkw0jN)wa{*4}gGK%m%MagLgN zn2ON0z(wa)AXguDo|88<-aLX53*|*K*W{UpO|A(sgH1Et^9l_9S~|dB1T&x0rP83M zaNTmaZNl4^%A*p+(@a?>he0WUE;)(I4?-7VU=e}NHiAmFOs7x1vQ>AXB`@gw93RTj zM7`|T~n z<}B8a>Dw>xcw6EmfRn#2+vSdYP517RRgy|>-|^m8`kIz7e~Aj*MT2s#{G1vWk!1?i zgvuXba}p8iaD$U{MH!j&l>>Cp4bBVbxq*ludiqNWOQ;97Uem>r=iINAfrLPAV5KwE z8L7WC&4EY3c9$;K0%xrbakl6&dSkiHlNeT%QYn^bvrl}|&wK;h1I_Q{AI!Z}D&%j} zyASNU^n^aHv1ld=gf?on&e3Mpu4@_to__+9Kgn8oh6+}K{W3UMM|b^8%H=3(`fr|Q zLAi(;n^9K+ogE8OqrvVGG}pwsTEi;ZpgWjtFAh)+ ztMxG703JnjLNUrcqQBI`tj1O36GkTQa{Q0HL-r>*zLCGf*&gcLjY~hqSWf5Y@iGRU(_{(XvdJ)BlBAYd-y(-R zR1~W(?0sOc2cbc34RT}ePsGxiiCD)j(}evV%rA<}V_ zmdh@qhCN9V*!~`l*UQJ8Z?U4*f=wNH9hDR0lqoOBcq*y~n@yl{zy4GQ!2I*%utskZ zWf=W85)8*m9vH7AiyZEZk`Qu8<|-asve5HWbX+2L>&WSMWb-@y4{+HEPm9t03uHEg z%6;sP`V#F`qHL#oYvneMx1PAGi87oXJ_6rOtO~_+%2oDeevWo}v4P!QDK#BIL8sV9 z<)MaM)E=O>6F8mc^S+TmZLrZyT}|9CC(o7ZCj|zPp_`i|l8kvt+7tcji0vBaqkuwyrj=QRy{iYtmZSst&T_ zDz%wCet$f6Fe5Y2M?Yfsu=a)Fz~R9j9}`4oG#HKLX*B#8-nq#eUHUca?k$+FL~mup zY$WR_-iV;mYg}LB7e)wi#)17XNhixp_{f68WE2*SHV5*i2Nqj{Y~NsIZ3qAB^zCjq z*a4@z@TVdw*~7DPa2QH{3t55UIMcyAk^H^)RTG(9-gw;ps>5cLl zeg7)IMy500aNY`MS6hiv8tAJK+%AmwW`JS_nWo^yT_AOuJ<69biXydjAX-@<=^35xVj? zdaXvAQAABeap~wRjqXjRf-pE7OqQcL=aOp%I&>LM3d6m_45!&X#d zgJgel7>|~Qf`Hr0Ui9VTm!b!zPQ%A;<~{{tmAu~wDk0z##;7hvdr^Up{KFZMh!Q3< zUn<=f&*4Fxb!1$MdJe+%mwcY!+KqLG_z5^|40BGx877eHVybx#WzGbb5#%vQdf~z5 zS8ET4&7Lm;^joN)im2!Dmb0K$4OT9Ku7lh_@kxwLgY$HbxnQ3IGEds%6Uj6yyR7=bjMxRHi zb&U5DGX`b`gHj?@O$6oH)H4;9$HRUCaYmtgcYHgj+zR?xLvZ&|5BjxK*GP;CvTWt5 zhvx!wu^=;nIHN&hM9_I6gO2JAy5CJOb#-_f#xA16k|2B4T)W;9PbL!>=dRyjD9qLD z9h7^4vmR7j2aDnUIBJXI%0@2+-MIhhXawn#V1Ah%adqYcw;{aa>USU*jibI4vU1nR zA!OzSb=RFD#yHIy`clS~##U&xxz4!iye)$Mt zBYXGG%$YN1CbVhXpuzL{gmkRkp??2iV{@e?gizeBb|+-U1wt4}OPV!q+b$5t8@FiC zvT@r6?f${zX1&MvuTZ)~#S*1Ul_Dg#_t@S`@F^+UtCg1cvGC*79Pj72Da}YSjwNXi z?k)VK@F+XBr;BN=}sYnM*JZ;j_ue|vNso|ibDj7MG_iz89EkJVv12DdOB z{`c=h{QinZQ944u!_lAkiO`Ywl(4_|(KwQfN0E4yOB+~gJf64kX-Y@`eQhjOA@P16 ztS(0T6C2OCv;)7t{9S=TTsT@BEB&rBkkn5I)0Ctme#Aktky^O7bX8Irf9E9yNvy7o z$Jt0*(v)-}HSl~bvVv46b;x|umE`pqyuiA(`C&9Z8bfq_K9`l z(_0^-t@x)ZHts~+ByS$8iYr#eSd~4UNe538(wQ8yF3_#I1Fb-_i|xDx_jf$xJ4HjY zurXhZ5x&MGF^-qv{rOt{kac6hY$A)KS7;M&g`}qqObYa=CnEUzbm1d)ZAHDp3|wSyfFn zRV`7=)ls!TT~vwcgUYX&&Z!sb%6hiGu21Sa*zsH>J@&d3=|QKjXj=0V#ov1 zkGv%J$-iVB*-2KBBiP|}WD|+gQ^{hooHQa!$ThsD6FG;yolJg^P|_3YJxCgoN8}c{ zK=PAUu)t2T4X=1ij*=?mG>(pd-A9q>WU~H+PYR^l^ksNZYh6%Z(o&TtRBsU1NflL< zUD5gJc)G&uO;(c0d=>LGDjBzoX(Bu8!G@RzL}AgvQC* zw5}&?DLdy~<2PF!maqJMRUZ1;LAfa!h%!Ed`2jP`QIE$u_K93#saeZNs>!cCDJ%qNkqUfcJRCDa?gdR7bi!t6*}@IW!oX;b1D?ij!tI%|s*J{Hm5 zUDOl{d63w|3h+hj6)Da~(VVmfe@V{i(lioY-Bh>4?sU;-bbVmt1np2ibc(X%4^>JP zQ3cd|SyT;{`Q&X_s+TMx6YVUroUDPr*4Yzf26e@zs+MXYqvSM|4|Xr3oob%yrZa1S z7+j${=`Ok~Nrcb0M@-cNe!0j7AlL`O=|A*7xlQ6oZJHMLC`~ia)}$OAL5tDpqzt_P zRG3DxvUc>Xo=q3irsNO#Oyhu#{b*lW6cJd7c0_E3BO2S0vm`qiO8Sy#x*VxSijuM< zOs`0_Jdn0DxvXF5BCu#tyk*rDo*#&JK?#OZlZsX1-gkNx6wui8PoUx zBhGo`pM4I~I797#&FPDpd;LTFVtH{f{GU%g)yQ`}-w5VMt(UU2`0Vyic+QX0=fAHf zWw*;k{~P_%igV9cQ$Wm(&3c4WS94wcfZO(|7e#ojnU!- zi8mUW{n;7QWx9nOt+s__iFY{)# zGs#NUM!SPu#r|qHlB1;CE+Femr)(|jsO;*e9I1|}mFl_5h#hmnL!KiJ3^I!hq%U-W z=4==;lY@OloK**kw`5C@flBhtuu2W!Qv{hqkJE4TI4MP!vq>Z)Ilu-`lh&kHK@e86 zTjl_=Tcl+@bOw=*o}e}PWM>!+BbCL!Y$~^C8oGug(I0d^8^RW|A$*(Y%t!F=#v7i7 zjS$oLb6teog$MO^PjGo2`@y5z2CK)^`*b`81a#Kj1%Sqvf0tbrwDVK z6`VVa5VN#nremE^*ty5JX3TcZ7d?$NMu@n>H}dzqKkLIP@yFyfO=8EvNlw!ubiGbc zt#mO`RcBReRRx`2U6SAAJo(l3mGkX=R!cjx{n8p@wYB=%9jzXg+v;yUwZ_`ntsZxDY|l=mW{{E1AsqjR zdd5RBU1Szd_%E?aG-b(bDstX;+MA^1)%aApkrsu=N>V}l5{(>iNv>3zmB-F0OUq^U zU3;qSYbRQztde#C`;(Q+u4aW;JM7%{SL>#=#)`HI+qLbyb`Ipay*8J=s;w+5&%v5r z8LE89OjQvaVH~-MjMb6$r&~#GnuoP#W9c+7qzQB<&CK@D!L%!#LwD2fq&sa%eW@Sy zke}oeGF}p1e}dj5ZCMuMi8@P~vAQ&o_2V6RGyVrQ{zj(r#bzJ<>|{az(nF8gcpjFkMY=kBnC^s#kZ(_KB1y2lK%4q8JKzk5IH{BCc`682lhSwrnk zst(aA2fJgGX7l+uM+sxIsN*Q==xF+z1I-?e+eQcTm1&r#jQmC#v!;;*t32chp#Q(v zVMKak_7jXJ0*H_dZnYMOmz#{&*Ypg1Uwu~LV2T%{Py=KEc>;)b&0b({weDCyt>M;g z>zXyex^88#_rX>N?5lQtJH1RRlab4&10kQvt#X~DsY#F^wo8ooYZSdX(bR`{uNS#Z!k*r|wr@`YH@gZAv0X-3HfEwL~8RLz;lt`xiSAi|l%coX2f7TkAjELC9+7_DsKgO7V$loPNE0 z=J{?k^P3B3GF#x&RE*KwbJo2Ldr&f=zbBb@jyoMb#VE%@f6n^t>#g7(6z=&`CH8wl zYgfVKo!&OyugTBsD6%x=n0mk-dykN2!U?W_js0g-b2c=87^9qLoO=zIam;6~W0HB? z++}iykC_%WltxGXoCO(+jW+P&H>?-$# zm3>qmhJ_5-4bfHCUImO?YWJ{*Slz5rwg;Y78X0S--PCSxHLz&BrII+|?X7n*S8*_~8MznY9k}9`4q8`DEKd39n9o@hw zildHg3$Ff@{DZ8+5D9bXGx`zM$;6J*wCpQ5gkb-&6F}mv*rQj7oBOmqeTUs#MJJ(} z+(_%-)d8#!YN5?|jGBaI16JN3K0v$Tq&%|XJN;Q_C!h5uJw`9lg^;D%=pSk_D#mH5 zA-pD3by6o0Wh3boW2G->Pn z;8#>+^0|;^qhl!v5RaVF9LAor^ZIo1^pp;!JGoO>CpyP*F*YzMgPxx}{Es80unvs~ z{ytG#_LHBJQo1>ICx#?W<{q`mwM?F8P30Er7@Hx|l1k!_nZd|lv^5twR~sKid1rUC zsxiWd5_iN2(chTI@A5UG2QSUjvB$I-pUfVT3v@jy9vArZ4`7FdDs~vI_qsl=+o;uY zt2&7IDk{gzvT7FW;xCUOdtC-MJZRSgT24onx(wTnw-?)KWNtZAE|p{D7ui_tN5HGhgTMqUgQXXv4|Qwc zq4~ z5EOz5>@8SHO=QONv@0HGW$W?WELNYDhA#}m4`)ZHLF+(u_(JQWmM+4-@f^G+Uk`qG zm`&yd*)Em>9$J|#r{&o!_D=030ra)?**>U_ddnvLOn&4U8lOG6xV#wmCt;PfE~RA3 zb=NQNGFjSod5U{Gs{7U#Z<1uHkGu|CjNx3*W5JFLva7{R-n_03TJ z)c9;nHx7%3VwdrX+oFWAj87Bm_*bA|9v%VzuE_S|N?pM5Pox4()GhU3vRe;OuT=?M zNKKJx)B{;ap0ZDa6|T1CSncdP)^udGtKb|ztr6BzU}9zKqQ&h)me&ffGuhkh|LhPj z{!HK+bLDqATK$A4J-dAFx3=ngd*7B31M>qOLDX%Qn&d&}96<6Xt=X zp9gl1q))+rPLc9-Dvd{NZId3b(MYnL9zo5joCbEplWf{|2HpEqKlys3TK=G!wvHdV|>{;`SdsLranj^d8#=&b*b*pnIv4ZAMnT z#JaMQ@P)7JBp<}9@XWj}tAbj#0N+dV@Rh7G&|n#T&AjBeI!7z&-qt6XB3X)0Vy={h z?)R~+Jhwd)V|KVtS-;(z-3L7VC9wzD@4dzBBkHQ1US(8^bQ_jU|I0R;(KJvf^EfYQ zRx?Ay5Obm!WE3|&JhwTZ?~l!%CxiBkp4DtTG81t+p+IoL{Z1wx4~&DsTJPCG8imS4(>{qVI}b zLbd~XZU%CeQwvplHCkN-yGf@Pff4wDLr3arsCWNG<+TSK@C=lzis0&vpeUTCadbL8 zMXST}r^1FsSX*$8qU<=T<*77*E>C4GbQ>*z2%Lm$*O4wjJ>Y=n1%SEyA<4)+i=dnI zAQ^zkb;toIE?a>1{ot_G>Wz$l?hK`&fN7*C5z z*=am>v@li~&3y_OuZ(2pb8*QCbhHqA#Aaix2!nD}Ql#h2c}aNpKrYx2GKBV^Pe=jO zNtJ;0S?E4e5p43g9;nYDFC?f5%AsCB>G+B|dY3&J3RXqg#tw#ZwZ;y!3(JG{GrOj& zEyu|ra*4bv-^rY+0&LbB8Ra1`GeIrWC3T#7q5G1P&_ZUBvCw{dK|{=dDk=t=V=btQ zM{xzypdPkHJ@yMcb1NwUU8Fwhxp*>=pNytXS_(*!n{FfpVZ&sS zj`l)TU73DHY+eCIK1GFoltkjWAGoGMy_&TT!VdiKF@lYS*_`yb4KLoN`e1O}>`+&J&ulK5=zov6M)6uf&!K>s=@1 z4cGf*s<*4(DcS9c?226HO>ayVWj)W?Ugv4Kmap`Ur!S0}j#A=?PiGM-ygr+FU8B2W zKQC_l&xqhZm`gMjJ8264#CCzxm7;HH4H~O2kl*0yd%;{>q_>Wfv-M9E2xfmzR=~|= zKd=|tEx;bu+1da`Sq#VHYzG)<7Wo>;*;_8g^|<6H^-kr~?R8%u^Ko4l zp7I*ekO^4!KPXFckZ)a3ru>k>@_>7c1saS6`W2&>!0tI13luui2(fn^>^>Y;zlFN} z6V%2j;J?G6z@3KW+mdIb98|}XB!sjizBn@GuZqG&Z$f~0u{qIwNX#jAvzsYu|?!RsGq-(jSaStIYlt9XFfBp z(Eh?Q`xx1Huy3$YjJ|d}H|Cm$9iyGIO{cgixZ{UWmH&j=d_Z_b9PL2*^2Tz5Ck+=m zx3$W%+jA=^BypejTvWEG-kuJgWrX&1_Y!B;5#{lV^G={v@^Wf9H5M_yoxyewSx63K=lbu8EM-4I1-eaYz zUpKAoR!8f*RR>kXbL+L`MD^gdrrYQ2Byd=5&qNJ)MCOBL)=yo6T2WoUQ~uz59$?wO zU@$YFsO*NqxB>cyLj7k$@9jdHL#t{JU9uk4aCeEFz=EyQF_qL0+QEC&-mJ%?qA!O^ygdJ<{sRgOQ8h)hU49124T9*p{9A%8+C&C9#6?!dj?&|R{EolGTd zG|_W(L%mrQRFUW+Jdkx@*&x}+o@W=6&+T^hNo3Aac2VgkTiQXeWfd7?Q|XZJ?B4P; zSWkBKMt+hz!J>Xc!jAnk8A$xG&r1=#3BcSynwS10MQC=~3Vh`mnF{rJ0l5AN zS{GLGk`vG%&w{6HCDD2c7;Ry4N{6HSHWS+NVbl{Du+me=nES~^D8hr0L5o0%TZ5n8 zz}*(OZ%=BX>(m&%y}{@d&V<&y9avnN+yM*ANk8g9Sn(k~yERRM3Z5U8*l6@KzLIG) zKk8T~R8U{|d~38pTZoEcg|W?1!#UK5Gs^lr@#*EP<5SOJ8MRPh2Rq)2x2%ToUNohV zbgx+H4NbPFzg6CyKk;FmL@Yj&f?AqMwD}xvxqU@(bhR0HU2L1 zCVC#3jBw73rbazplvU@NNxB2d?;@~^PUwz>l7FBXNi`aL$Rk^+^eUALwU(3Q0=o#R z`TqfrBdy!uuxG3tmXH0febVX+J{tl=-eot(&p3He`l;tK92&t4l?`hBzq%LpG8^!B z5PAsj5Dj&a&2ymw8G+7XHQ??%u#4HK30i{f%%#ELo02f_(`+;&`l2UcxmO6 zU^~l5X5{}u=p-!%v)zL@U50+ravTjJ`}9ii*6*lc1_KRDFri=Ysovx~s-gV)n;HTo zCPw8z&D9&YoKFu0LeJBwpOp)U5rA_Cu+^+G3qe#C6;H%l9$`*3nu;}MQ=^d);D|E! zB2Ke8ayvd4J&a3cUZWXzLt8G7KGFSn?w->mkk*d~0M<2bs3kNv3F<+*MUvmM#Z z>xmDh5?jc7pHYrsWR_2g8Al5_yBkYcJF}Cym38LF%|X!o4hT1xKpE881NmxN20JpE z)uTsMdhG2IL{(Q3px3GO>b)waM#?gX)rK+}vFMg3?Kbu|=mQsQf0+(Emx1G^m5uDu zPzt2|+U^5pd`q5^)l^+@-1d3`IMYpC3Ctt}g;Z^Q2GJRSZeIW}A&Ptf&)EpJlLb1; zP_UgXv?dF}GuhY@IvY0I1s438bOR4~0Lz5|S0hlRq~P&G9fT_8D|+=u5&03i1-e0t z(W!rmUTlPJMQZACK-9asq`CmRwpTro7x zV-YL<5o`H5k%x~%j)>+s7tOt$Yp`0q~ z+w<-0=-&oI=PrndoNK4Ev)hyGht><+#@LJOP^*Rg#2y9(sEGWZd=KUBt;_?Zr=v=N zW?37&sQ@Vkb`uV^TL2i}md;0H{YU51wWydPX+_kNE1~wpU{y6y4N6pWnOQir-u<*4 zxcV|O7suK`V+cZQSD^i%K`%iZcSfhc3oO|R1xSpgY;SGjrCDaW!Cj{sBVghjICsXJ^>VX2!w8{Q=m)S(6d+^D&w2H zlu3=&Jiqb5I48WuJ#(N>IY&PsooWB+8xZCw>=^B{#oTCab>4zz(~UU5XD-??YN&Qv zv7Y(%H+NWkWNe%(Hu-+cfaC*Kt+=w0d%Wwt6@CXt)$<0r)6n z{rC;KpM7HaQEmIMd29eWrH`P#eAFhUAdaJ-o*5O%9ObX($}w__yb4=QgQ`%=USV&w z58KZz%UTcnE&?VGv-{a&pbiB`An&~(wyRYSd% z6xC&wFPR91uM(8S9;6s1c_u1S$;+kN_9MML+l79?-D*UL)mg^I9p^S5H6oWpWB zg6Ue8*BozL6p#5Dqk?&Y=QYL|NxZOOqqf=#{(F-ru@8I=m{J*jkM-r{*$`Tdw_qQj zlhuIow+t~bg}&1H&|hB;)o79WiGD$(+=lELh>4jbRH~C<<6U+qJHhG&n>y_%s9o9R zEcBxuqTBo3Zl~(29deWWpK_t!8=}f0k2Tcubseym)4D0FFb4FH20!Lba2<{ACTmn796UdN7puf3vXH=ud$a|d@Y-l*@*HNS-6y>h4ax3ts5HPSV z$f$WSlhKbn!km=GtXFfg5Vdy@d93?G8-4=Yeo#eq2c1Q0?B*6Rwo9lHGI z;RPRc4^&E#dOEnd(j!qfO+$@*oW`)}@S+%6gmY+1d08w)Uya=3JLxStUThL+jb!t+ zqpow8Pj%l1zVrP|zhOR~oELr0ITxD4&6(zNOyIjVL@mNJ%_IAh{lYqoK1n4j!F$0vVTD>hz@k1{ zAFYn|HY?ofj!e7`UJz~1hOQJVf1=iT1TXob&Y)jX5e#MnrXe`GF}YEb%)w;FE#PVd zGVWq13#onNY0!`w(U0J&FVF#70#*}9TY%qO0*8JC60 z%x6UFrXp>Ub0+0XQXWsZ`;zO4*I^I0uEAu5?olw{`pEoq(4E`~1~UXJSOoS| z2Q_PT+!~D2mPUi>>PCB03PD= z%-P9t!MBEEhvS$3q%)eq>d(0<3n{I;)RW-U@{F-_pv5 zEL{nltAK6WGwlI(RoO&(>=B6QE-II54BhgE3WQcX3-;L$<<PxQXD?XuA>{BJ^fD0Jm)0kDNi@*(5gC*^S%A66wWzKd&~>Po`axk^ z13L(|g`EPoILI2&FX*5?qeti*lB6fm10)7~>o-*Da8y5obuCyjEAsJvT^qdNqFRrN zX_9V(xhxkLemo|-YVmiZH*XJBeT85mik0LUptR59A9xnbW_U!rsBgRx5qz`J-Pq0+ ziI#?kIe1n6otNbU*k!U)t-;*vOPSX*$6J72Ov)3xSg)h|;{0RE+y5h@l7eCedX~us zNhOkx>Pq&p_~ew@{G_$Pb(Pi;xjk33$LAz8vl2c%jIXqvvB3CbCX3;Gg3m+qE1TqW zn8!pGM}#;m<`@ISNA?YDe+W49DsTe_dw}^g!mnY@XA>ES9kEqiC<_BHRW=l#`4*K; zPt4K%wBOsO(Gd-@r8UcLV3$CJTfq8cWwX;*8Nss-p_db9VJblum%r_P(xHl?-*!@Q zOx0vT%mfj)KB32g@#aC+T!|^glR!{TSCSosvSx_Y3W(zUP(T|(bJ~C@tjg$tc0gV9 z9M$atSoIdPh?&qn?gI_lpoa59hv|>bfw|ue&@g&p#Up_I({wgGAFV&4Q&fTM)Y&mN z>w~Gd!BB|9p(PFiFBpWG<>ScCdBFL~gGXyU0&{hFby-wYhsiaa6_d}iffEzK1~VeF zrzJ1-VO&Z}rfww*nrRoGnK)40bX(J{Zw&a%(uRMTy1 z+Rqrz`WqE}8)eMnoa;I2T$d>&peE0YDT;;GbopIq+9A0`OiA}5`Oft#u7}&O>Ls?1 zDHDGuc~@+3R9NDXjn_BR=b z=?A9Vc;{3_!PY0M`sfyvM92ClYSYxdHFSS)ra@2%i-J4n1DpDT+G#elr8VfbRD%BD zL7d*x`H_QT(WjY+zIPKK%m2{*ZGqaWFVHkWS4M>X0|u87dX@ne_7!?a0PPQt_=jEr z6PrmNp|6CwFkIIZ(h?cFD>%R$biijolP(1;e~(XipjYWeq?(RZ14wKAMvaFLoluF$ z%SXvaofWyIH><#}&||!vSip&}`4|x-{;>U+Ms@R1qOB3bJz|sbk{2;Nd^D7Zw=9Nb zM)h(KnePPp6PK*gGTy4|8tvNY`Rr;LTR1s74ustb=CoaucT8ge#tP?c_R+8q!}*M7qA?~mL&SFBVJZ9x>&~+A zQ9$ASY$JNGGub1s+oxnVrZD_SGbj(s;q`B|6SI4((LV@Q4b)pZRKAx<)-+q$=g~1Z zZSAoO%7S(?`xUGjW@nS}sDKy9foiXckW!vdUlGSI)huY4Z7~rz7~Pzj&@;**qh&*! za?El;iy`xXsv+P(-!X?Afa+~O7}gMIhuL7m=a^`H1>PKtOkNSWxQ3pu&g%|jD*858 zz=t#Hm&l~efHxcTT>PF2>ud%3UqnO)Kv~WOwdxYOb9GUJd!aTCMh~hk$pXD&Cpuk} zoIxEWplTLGjXW8dnqZPUyG{@P*??#J;M)ZZO87gn9rN;K;e~I|wdjF9({1p$lh8%l zf$c|<^ynECM8$U<)<1~Z=D9Q+(@j~S)8C*g(8E6tTxv-AU?Qd?ZG;M95Am2jK4po+ zaY~dixPQ8T0*xGewV42nABsuW_E7b^&`X|K2|s8n&(|MclXHo;u@fTpkp609*y)(g z`hCKfsJEVa>T!Iw#9Hi{-7%@FerimHjy>N96J^v#b_g@G-&r5WCUcj_WQLeq9nZ|X zW;S!AanJBE7GWa0yHUe|l8yR*=o?+tZBr{2au$53Cn~ET#N%=DTh{>pnFpkHV&_A_sMccF zGot$a3Y(q5vp)aH!8>ni**p6YDVv(4Gy~(Vhp1{xn@m ztDkR@8}P%ikrDq=%P#nHO6(?W#Ap9wc@L;Oad+dJ@LM)bj8|Qa)0W$l7pmMoHH-h} z@PKb`G1D2HQDyBnu9_1Z=?q~obCt1=Z|7~r4{)-9tR~;Y{nXG!f6cjA_7YdLS9GmL0`W;u)$?lmQT>PABQQzzEHLgLjMZEH&&|RRaAYE z>oECS9+RUx)HodmmVO#|@JRMh^}y45s^;1abnu{0;fI)NhU)e#bP$)is@LK>Hk;Hw z#2kfI^$K_ps|uoSs)r1{4VuVal?TW@6pTAJ=FCp$8{`^vmbJu$S~eOp)-5mvu#Y}O zMQ+0%HiP|@Mm5$QeqhoD$O?@S-)?j=AH&|4!M+LXJzGCiJE88l)ELzV-+;J@jI;un z-wd}{@cks@_jITw+Tq(X9gsmH;fZJH59EiOm>fBP>Tw&bjftQl=!SMf9;t>2$gy;P zYHq;nWd_`bVA)f9=9q|Ek z(rKB7wbu`Y@V9j>B*4XoNY*1o3M$DQ;*zJR2_ZNS_Nf!xz5h! zVIm=!Z&CH=XE79$*)C|#F4@2*~EoM;f10Oo5e!z*2=r^6z(Q23M z1te|;&COD#9)ro!8!8%I%Jz6wq>7U+br`b~9Wix=839z{b)jx7#^+^%e)$X&p8GJ} znI8B|ai&l;9a(oGzD;oj`!NCNU5@NQh1Cnp?wQ_9($ZS6#|HWvoZ3ZKql zt#cl1-#@ixqBo$-RYw)S0a?aGjlU0UAse)t0;B+#{$V{6(*w1^3C9BU4`FJ302tqQ z8>|IlUoJ_rzSE^d2p4qWGsB78q}VQs2_bu zAg0lm!RrE{%3mR0ptLTB4p4|nbU@eAA7T+51}4ym2zbCM5o##?g2tnZIT-cod$wC| zv=^(ctd>0{xw-zrjO1V5!{mU6c_+yV`iZxe_p3^=2YbPuFgKGHedejOlANFqumshM z#M53-D$)bVOCbZjf`%~?-$LumY9Th<@XrCLf%7AGC6msW$Sn?Qx1;4y2j9lbrY}0W zN3n+0$XG8iw>FV{gT_XI1ZR-Q0{Rf}Dt+38;fKG%W{|jTd-s4)}NwI{iPO$HYMWFwk*& zg`Mn({i}nBEr*ybgXl|*zsjhNhky$%gh%cHKMcY5YL?*do={=NfGrMyV%iVdT|2z5 zKiGIjJXax=?V}Qyj|r5SsPUVk^6P=T-WGEPhu}dqz$2^S97T~ArsE8G@HhbPZ-u^T zaXfkl`$m9ce8ft}VU}zP<|%vQXB6;ZGyW=$>s=3}ZWw-lg+kB)lb7}IxYIm2 zv)s_~#skR;W5u_K8{aHguO{lJvNdMui`lF3jf?%>&bp6kXAje@R1mx}o$5^b>)x2R zuZ!`{H#tR$I$`fmhO&5=;j zFJiA!?YtLLJ*d#|I6)5u2l#+`y9x4yi4}GyztA7;3|`>UMX_EVk`p#B2L?I`?;L?@ zZ8`GIc6`Q2c*b0GFY@9{Wx?<>01@87KfS>J5m5P8VvX~F0~2s%tH2U_=`C36R@51v z(G5HQ_uH?R5EDQ3RiM*b?8Y|W(;}!mdw>{I5i1?gEf|iD+A{3I4t*2vc@3|Br5|C1 z0@DsHP^+ZJYI@;1MP%iANiT4&nj7psq-)p;${!u!x#H)ybJ>cN$ zz`XwlHa-=*H4F3k3$bHokT>@t%SQkyCF*Pg>PmjBs4PAu^_x2dab>x11^KYT>{xSC zaPLa|MrJIx)!hLV-(iFVDIxII9@2HC1Du!z=3CuAu zyZQs3`W4#cEu4P?Cc`gc)uX{S7Q$bbz;?^v!BY{DLvXYSeouuDR{;-iicHrSvD*b& zNFU(YUc7rLy2LBckGhTBzKi^vx;F{H2RG0%KTxz1R+{SFLt(>(@b4XD7qa3mSaJ;% z^d;Ey5XAR5{B(m~bO+wHMX$OT>cujszcRur1gsl_{$%RZ-eXw!7*=`$d$=4(g4s)) zeGASWs{g}t&u}f*;n7cUHHNlFi z!;+1GT%(Z3QtulfTDk+*8seuTtXT*CPz^gg1QFj9N7^82hQWvHp1&Xpz%gv+!SaW^MT5vz<5SNMW2l$L;iZi6lCowIMNrn zEd<^WjNCRI&n$xl)*$ltA=aK_=TpDqEwLj$uvt0SunD4cJp6L*UmNcu*TL;ipc4{_ zTK^X6>N}|aLviE?YO%3+T}SwQDhir^cOdmY7bxu7E9~!E+^)dCjw8-aLQg%3-P?tI zJd4{goc9)lX!< zrrtYY2lEr(kJ_(Dybr3QQ>56;yV{l;AQse`|=;U^2u zn-$*d#+j1;=AGME`5na1ZD=FMv2UTsaR>hH>RzDNdPGqwejdSX8~oxZyz}_qZubr# zda6es$1^vv#)t6PKluFGj?}>GD*~bN!8>aM(+lD~Wq=&1XjKaLg>ft!uHOe%Bd|@XWuL&#UtyW6 zSj9)!|0F#57NTk!{NXPA@eKUt0_^k>aeoiix{KGmM$BEr->Fgh7B+o?cfNzyxZts= zXsB@=zo0m#2ghTuVj!@mAkLl}tEh>5RRk+cj~#7^qyJ#7rC^8hcs?s$nGfqMj_W9d z=TdjQEPf|pSMp&`;(+ib@@y2&lls3g-owh@u$EVUG3`4ZeLyTG!lS<9%0A;S7wrE9 zdn=JOldu+zJj>w&skY3F^%TYK$Zd2wF_ z_vLXUFIJNQJLyJb**M!P>{2Q!UBT`hgxycTvWH;bWAKB+*s+^`vFQPxe}Slai~ah6 zmB(VAQvc5gzQK*P*Jd+VunhRc^7Jp~L85}r6>i;~kus83p`X5;P?Z4>y z0`|QLPrd{{I0dVp1WN72+IQgo3Xbf@H9o=Lx8N=3aV@X$>Qo;{tvR0HQ@-NoHLMel KW6_A^ME!qHVScgz diff --git a/frontend/static/sound/click16/click16_6.wav b/frontend/static/sound/click16/click16_6.wav index 4076a647942e8a7ac8cd201d7272097470d63f6b..23aed8d0ae55e6e6851d5bdfafe122c445aae0ad 100644 GIT binary patch literal 10496 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 literal 17882 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 F{{cp;JXrt$ diff --git a/frontend/static/sound/click16/click16_7.wav b/frontend/static/sound/click16/click16_7.wav index 44919e80e80f58aabe51508cb21409f004e20a5c..7bc0996cd505f76dbc1df259971b6dd659091950 100644 GIT binary patch literal 9182 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 Tk5S#9W2?hIz*B=qJKz34!$N&w literal 20814 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 Date: Wed, 29 Apr 2026 11:29:57 +0200 Subject: [PATCH 12/13] remove todo --- frontend/src/ts/controllers/sound-controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index b751c118539f..29e2843e32c6 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -397,7 +397,6 @@ export async function playClick(codeOverride?: string): Promise { if ("validNotes" in config) { const scaleConfig = scaleConfigurations[val]; if (scaleConfig === undefined) { - //TODO throw new Error("missing scale config"); } playScale(config.validNotes, scaleConfig.meta); From 61e46e8fe60c3f80a91b3f8600106572be981cde Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 30 Apr 2026 10:29:23 +0200 Subject: [PATCH 13/13] always init error sounds --- frontend/src/ts/controllers/sound-controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index 29e2843e32c6..639556199e14 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -106,6 +106,9 @@ async function init(): Promise { await initPromise; + //preload error sounds + await initErrorSound(); + //preload sounds const clickId = Config.playSoundOnClick; if (clickId === "off") return; @@ -119,9 +122,6 @@ async function init(): Promise { await Promise.all(config.flatMap((it) => it.sounds).map(getHowl)); } - - //preload error sounds - await initErrorSound(); } export async function previewClick(clickId: PlaySoundOnClick): Promise {