diff --git a/backend/src/api/schemas/config-schema.ts b/backend/src/api/schemas/config-schema.ts index 68f9c1d0d..1ef254b94 100644 --- a/backend/src/api/schemas/config-schema.ts +++ b/backend/src/api/schemas/config-schema.ts @@ -77,7 +77,7 @@ const CONFIG_SCHEMA = joi.object({ playSoundOnError: joi.boolean(), playSoundOnClick: joi .string() - .valid("off", ..._.range(1, 12).map(_.toString)), + .valid("off", ..._.range(1, 14).map(_.toString)), soundVolume: joi.string().valid("0.1", "0.5", "1.0"), startGraphsAtZero: joi.boolean(), showOutOfFocusWarning: joi.boolean(), diff --git a/frontend/src/ts/commandline/lists/sound-on-click.ts b/frontend/src/ts/commandline/lists/sound-on-click.ts index f11ca3969..924110e85 100644 --- a/frontend/src/ts/commandline/lists/sound-on-click.ts +++ b/frontend/src/ts/commandline/lists/sound-on-click.ts @@ -145,6 +145,30 @@ const subgroup: MonkeyTypes.CommandsSubgroup = { SoundController.playNote("KeyQ", "triangle"); }, }, + { + id: "setSoundOnClick12", + display: "pentatonic", + configValue: "12", + hover: (): void => { + SoundController.scaleConfigurations["12"].preview(); + }, + exec: (): void => { + UpdateConfig.setPlaySoundOnClick("12"); + SoundController.scaleConfigurations["12"].preview(); + }, + }, + { + id: "setSoundOnClick13", + display: "wholetone", + configValue: "13", + hover: (): void => { + SoundController.scaleConfigurations["13"].preview(); + }, + exec: (): void => { + UpdateConfig.setPlaySoundOnClick("13"); + SoundController.scaleConfigurations["13"].preview(); + }, + }, ], }; diff --git a/frontend/src/ts/config.ts b/frontend/src/ts/config.ts index 0b1ca296b..0058e4dbf 100644 --- a/frontend/src/ts/config.ts +++ b/frontend/src/ts/config.ts @@ -175,7 +175,22 @@ export function setPlaySoundOnClick( ): boolean { if ( !isConfigValueValid("play sound on click", val, [ - ["off", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"], + [ + "off", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + ], ]) ) { return false; diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index ec9aef204..4e76cb4d5 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -1,7 +1,11 @@ import Config from "../config"; import Howler, { Howl } from "howler"; import * as ConfigEvent from "../observables/config-event"; -import { createErrorMessage, randomElementFromArray } from "../utils/misc"; +import { + createErrorMessage, + randomElementFromArray, + randomIntFromRange, +} from "../utils/misc"; import { leftState, rightState } from "../test/shift-tracker"; import { capsState } from "../test/caps-warning"; import * as Notifications from "../elements/notifications"; @@ -255,12 +259,15 @@ const notes = { A: [27.5, 55.0, 110.0, 220.0, 440.0, 880.0, 1760.0, 3520.0], Bb: [29.14, 58.27, 116.54, 233.08, 466.16, 932.33, 1864.66, 3729.31], 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 GetNoteFrequencyCallback = (octave: number) => number; function bindToNote( - noteFrequencies: number[], + noteFrequencies: ValidFrequencies, octaveOffset = 0 ): GetNoteFrequencyCallback { return (octave: number): number => { @@ -341,6 +348,95 @@ function initAudioContext(): void { } } +type ValidScales = "pentatonic" | "wholetone"; + +const scales: Record = { + pentatonic: ["C", "D", "E", "G", "A"], + wholetone: ["C", "D", "E", "Gb", "Ab", "Bb"], +}; + +interface 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 { + // 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, + octave: 4, + direction: 1, + }; + + return () => { + if (clickSounds === null) init(); + playScale(scaleName, scale); + }; +} + +interface ScaleMeta { + name: ValidScales; + preview: ReturnType; + meta: ScaleData; +} + +const defaultScaleData: ScaleData = { + position: 0, + octave: 4, + 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 function playScale(scale: ValidScales, 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; + } + + if (scaleMeta.octave >= 6) { + scaleMeta.direction = -1; + } + if (scaleMeta.octave <= 4) { + scaleMeta.direction = 1; + } + + const currentFrequency = notes[scales[scale][randomNote]][scaleMeta.octave]; + + const oscillatorNode = audioCtx.createOscillator(); + const gainNode = audioCtx.createGain(); + + oscillatorNode.type = "sine"; + gainNode.gain.value = parseFloat(Config.soundVolume) / 10; + oscillatorNode.connect(gainNode); + gainNode.connect(audioCtx.destination); + oscillatorNode.frequency.value = currentFrequency; + oscillatorNode.start(audioCtx.currentTime); + gainNode.gain.setTargetAtTime(0, audioCtx.currentTime, 0.3); + oscillatorNode.stop(audioCtx.currentTime + 2); +} + export function playNote( codeOverride?: string, oscillatorTypeOverride?: SupportedOscillatorTypes @@ -380,6 +476,16 @@ export function playNote( export function playClick(): void { if (Config.playSoundOnClick === "off") return; + + if (Config.playSoundOnClick in scaleConfigurations) { + const { name, meta } = + scaleConfigurations[ + Config.playSoundOnClick as keyof typeof scaleConfigurations + ]; + playScale(name, meta); + return; + } + if (Config.playSoundOnClick in clickSoundIdsToOscillatorType) { playNote(); return; diff --git a/frontend/src/ts/types/types.d.ts b/frontend/src/ts/types/types.d.ts index 065adb5db..ab32a5c02 100644 --- a/frontend/src/ts/types/types.d.ts +++ b/frontend/src/ts/types/types.d.ts @@ -93,6 +93,8 @@ declare namespace MonkeyTypes { 9 = sawtooth 10 = square 11 = triangle + 12 = pentatonic + 13 = wholetone */ type PlaySoundOnClick = | "off" @@ -106,7 +108,9 @@ declare namespace MonkeyTypes { | "8" | "9" | "10" - | "11"; + | "11" + | "12" + | "13"; type SoundVolume = "0.1" | "0.5" | "1.0"; diff --git a/frontend/static/html/pages/settings.html b/frontend/static/html/pages/settings.html index 613e3992c..0e28b9e25 100644 --- a/frontend/static/html/pages/settings.html +++ b/frontend/static/html/pages/settings.html @@ -1006,6 +1006,22 @@ > triangle +
+ pentatonic +
+
+ wholetone +