feat(sound): add play time warning (@miodec) (#6759)

### Description

<!-- Please describe the change(s) made in your PR -->

### Checks

- [ ] Adding quotes?
- [ ] Make sure to include translations for the quotes in the
description (or another comment) so we can verify their content.
- [ ] Adding a language?
- Make sure to follow the [languages
documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/LANGUAGES.md)
  - [ ] Add language to `packages/contracts/src/schemas/languages.ts`
- [ ] Add language to exactly one group in
`frontend/src/ts/constants/languages.ts`
  - [ ] Add language json file to `frontend/static/languages` 
- [ ] Adding a theme?
- Make sure to follow the [themes
documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/THEMES.md)
  - [ ] Add theme to `packages/contracts/src/schemas/themes.ts`
  - [ ] Add theme to `frontend/src/ts/constants/themes.ts`
  - [ ] Add theme css file to `frontend/static/themes`
- Also please add a screenshot of the theme, it would be extra awesome
if you do so!
- [ ] Adding a layout?
- [ ] Make sure to follow the [layouts
documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/LAYOUTS.md)
  - [ ] Add layout to `packages/contracts/src/schemas/layouts.ts`
  - [ ] Add layout json file to `frontend/static/layouts` 
- [ ] Check if any open issues are related to this PR; if so, be sure to
tag them below.
- [ ] Make sure the PR title follows the Conventional Commits standard.
(https://www.conventionalcommits.org for more info)
- [ ] Make sure to include your GitHub username prefixed with @ inside
parentheses at the end of the PR title.

<!-- label(optional scope): pull request title (@your_github_username)
-->

<!-- I know I know they seem boring but please do them, they help us and
you will find out it also helps you.-->

Closes #

<!-- the issue(s) your PR resolves if any (delete if that is not the
case) -->
<!-- please also reference any issues and or PRs related to your pull
request -->
<!-- Also remove it if you are not following any issues. -->

<!-- pro tip: you can mention an issue, PR, or discussion on GitHub by
referencing its hash number e.g:
[#1234](https://github.com/monkeytypegame/monkeytype/pull/1234) -->

<!-- pro tip: you can press . (dot or period) in the code tab of any
GitHub repo to get access to GitHub's VS Code web editor Enjoy! :) -->

---------

Co-authored-by: Christian Fehmer <cfe@sexy-developer.com>
This commit is contained in:
Jack 2025-07-21 16:40:27 +02:00 committed by GitHub
parent 27019d189f
commit 6dad5415c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 127 additions and 0 deletions

View file

@ -705,6 +705,25 @@
<button data-config-value="4">missed punch</button>
</div>
</div>
<div class="section fullWidth" data-config-name="playTimeWarning">
<div class="groupTitle">
<i class="fas fa-exclamation-triangle"></i>
<span>play time warning</span>
<button class="text" tabindex="-1">
<i class="fas fa-fw fa-link"></i>
</button>
</div>
<div class="text">
Play a short warning sound if you are close to the end of a timed test.
</div>
<div class="buttons">
<button data-config-value="off">off</button>
<button data-config-value="1">1 second</button>
<button data-config-value="3">3 seconds</button>
<button data-config-value="5">5 seconds</button>
<button data-config-value="10">10 seconds</button>
</div>
</div>
<div class="sectionSpacer"></div>
</div>

View file

@ -24,6 +24,7 @@ import QuickEndCommands from "./lists/quick-end";
import OppositeShiftModeCommands from "./lists/opposite-shift-mode";
import SoundOnErrorCommands from "./lists/sound-on-error";
import SoundVolumeCommands from "./lists/sound-volume";
import TimeWarningCommands from "./lists/time-warning";
import FlipTestColorsCommands from "./lists/flip-test-colors";
import SmoothLineScrollCommands from "./lists/smooth-line-scroll";
import AlwaysShowDecimalCommands from "./lists/always-show-decimal";
@ -242,6 +243,7 @@ export const commands: CommandsSubgroup = {
...SoundVolumeCommands,
...SoundOnClickCommands,
...SoundOnErrorCommands,
...TimeWarningCommands,
//caret
...SmoothCaretCommands,

View file

@ -0,0 +1,37 @@
import {
PlayTimeWarning,
PlayTimeWarningSchema,
} from "@monkeytype/schemas/configs";
import * as UpdateConfig from "../../config";
import * as SoundController from "../../controllers/sound-controller";
import { Command, CommandsSubgroup } from "../types";
const subgroup: CommandsSubgroup = {
title: "Time warning...",
configKey: "playTimeWarning",
list: (Object.keys(PlayTimeWarningSchema.Values) as PlayTimeWarning[]).map(
(time) => ({
id: `setPlayTimeWarning${time}`,
display:
time === "off" ? "off" : `${time} second${time !== "1" ? "s" : ""}`,
configValue: time,
exec: (): void => {
UpdateConfig.setPlayTimeWarning(time);
if (time !== "off") {
void SoundController.playTimeWarning();
}
},
})
),
};
const commands: Command[] = [
{
id: "changePlayTimeWarning",
display: "Time warning...",
icon: "fa-exclamation-triangle",
subgroup,
},
];
export default commands;

View file

@ -394,6 +394,10 @@ const configMetadata: ConfigMetadata = {
displayString: "play sound on error",
changeRequiresRestart: false,
},
playTimeWarning: {
displayString: "play time warning",
changeRequiresRestart: false,
},
// caret
smoothCaret: {
@ -855,6 +859,13 @@ export function setSoundVolume(
return genericSet("soundVolume", val, nosave);
}
export function setPlayTimeWarning(
value: ConfigSchemas.PlayTimeWarning,
nosave?: boolean
): boolean {
return genericSet("playTimeWarning", value, nosave);
}
//difficulty
export function setDifficulty(
diff: ConfigSchemas.Difficulty,

View file

@ -101,6 +101,7 @@ const obj = {
tapeMode: "off",
tapeMargin: 50,
maxLineWidth: 0,
playTimeWarning: "off",
} as Config;
export function getDefaultConfig(): Config {

View file

@ -33,6 +33,16 @@ type ErrorSounds = Record<
let errorSounds: ErrorSounds | null = null;
let clickSounds: ClickSounds | null = null;
let timeWarning: Howl | null = null;
async function initTimeWarning(): Promise<void> {
const Howl = (await gethowler()).Howl;
if (timeWarning !== null) return;
timeWarning = new Howl({
src: "../sound/timeWarning.wav",
});
}
async function initErrorSound(): Promise<void> {
const Howl = (await gethowler()).Howl;
if (errorSounds !== null) return;
@ -610,6 +620,14 @@ function playScale(scale: ValidScales, scaleMeta: ScaleData): void {
oscillatorNode.stop(audioCtx.currentTime + 2);
}
export async function playTimeWarning(): Promise<void> {
if (timeWarning === null) await initTimeWarning();
const soundToPlay = timeWarning as Howl;
soundToPlay.stop();
soundToPlay.seek(0);
soundToPlay.play();
}
export function playNote(
codeOverride?: string,
oscillatorTypeOverride?: SupportedOscillatorTypes

View file

@ -285,6 +285,14 @@ async function initGroups(): Promise<void> {
UpdateConfig.setSoundVolume,
"range"
) as SettingsGroup<ConfigValue>;
groups["playTimeWarning"] = new SettingsGroup(
"playTimeWarning",
UpdateConfig.setPlayTimeWarning,
"button",
() => {
if (Config.playTimeWarning !== "off") void Sound.playTimeWarning();
}
) as SettingsGroup<ConfigValue>;
groups["playSoundOnError"] = new SettingsGroup(
"playSoundOnError",
UpdateConfig.setPlaySoundOnError,

View file

@ -17,6 +17,7 @@ import * as Time from "../states/time";
import * as TimerEvent from "../observables/timer-event";
import * as LayoutfluidFunboxTimer from "../test/funbox/layoutfluid-funbox-timer";
import { KeymapLayout, Layout } from "@monkeytype/schemas/configs";
import * as SoundController from "../controllers/sound-controller";
type TimerStats = {
dateNow: number;
@ -175,6 +176,26 @@ function checkIfTimeIsUp(): void {
if (timerDebug) console.timeEnd("times up check");
}
function playTimeWarning(): void {
if (timerDebug) console.time("play timer warning");
let maxTime = undefined;
if (Config.mode === "time") {
maxTime = Config.time;
} else if (Config.mode === "custom" && CustomText.getLimitMode() === "time") {
maxTime = CustomText.getLimitValue();
}
if (
maxTime !== undefined &&
Time.get() === maxTime - parseInt(Config.playTimeWarning, 10)
) {
void SoundController.playTimeWarning();
}
if (timerDebug) console.timeEnd("play timer warning");
}
// ---------------------------------------
let timerStats: TimerStats[] = [];
@ -188,6 +209,7 @@ async function timerStep(): Promise<void> {
Time.increment();
premid();
updateTimer();
if (Config.playTimeWarning !== "off") playTimeWarning();
const wpmAndRaw = calculateWpmRaw();
const acc = calculateAcc();
monkey(wpmAndRaw);

Binary file not shown.

View file

@ -355,6 +355,13 @@ export const CustomBackgroundSchema = z
.or(z.literal(""));
export type CustomBackground = z.infer<typeof CustomBackgroundSchema>;
export const PlayTimeWarningSchema = z
.enum(["off", "1", "3", "5", "10"])
.describe(
"How many seconds before the end of the test to play a warning sound."
);
export type PlayTimeWarning = z.infer<typeof PlayTimeWarningSchema>;
export const ConfigSchema = z
.object({
// test
@ -402,6 +409,7 @@ export const ConfigSchema = z
soundVolume: SoundVolumeSchema,
playSoundOnClick: PlaySoundOnClickSchema,
playSoundOnError: PlaySoundOnErrorSchema,
playTimeWarning: PlayTimeWarningSchema,
// caret
smoothCaret: SmoothCaretSchema,
@ -536,6 +544,7 @@ export const ConfigGroupsLiteral = {
soundVolume: "sound",
playSoundOnClick: "sound",
playSoundOnError: "sound",
playTimeWarning: "sound",
//caret
smoothCaret: "caret",