diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html
index 07919efa3..c77a28756 100644
--- a/frontend/src/html/pages/settings.html
+++ b/frontend/src/html/pages/settings.html
@@ -705,6 +705,25 @@
+
+
+
+ play time warning
+
+
+
+ Play a short warning sound if you are close to the end of a timed test.
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts
index a05e1b584..ef4f1bc18 100644
--- a/frontend/src/ts/commandline/lists.ts
+++ b/frontend/src/ts/commandline/lists.ts
@@ -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,
diff --git a/frontend/src/ts/commandline/lists/time-warning.ts b/frontend/src/ts/commandline/lists/time-warning.ts
new file mode 100644
index 000000000..4a021ca39
--- /dev/null
+++ b/frontend/src/ts/commandline/lists/time-warning.ts
@@ -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;
diff --git a/frontend/src/ts/config.ts b/frontend/src/ts/config.ts
index 5738b650f..8d15c52b7 100644
--- a/frontend/src/ts/config.ts
+++ b/frontend/src/ts/config.ts
@@ -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,
diff --git a/frontend/src/ts/constants/default-config.ts b/frontend/src/ts/constants/default-config.ts
index 7ae2db109..4ab407952 100644
--- a/frontend/src/ts/constants/default-config.ts
+++ b/frontend/src/ts/constants/default-config.ts
@@ -101,6 +101,7 @@ const obj = {
tapeMode: "off",
tapeMargin: 50,
maxLineWidth: 0,
+ playTimeWarning: "off",
} as Config;
export function getDefaultConfig(): Config {
diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts
index aa2a01275..6d2e140d4 100644
--- a/frontend/src/ts/controllers/sound-controller.ts
+++ b/frontend/src/ts/controllers/sound-controller.ts
@@ -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 {
+ const Howl = (await gethowler()).Howl;
+ if (timeWarning !== null) return;
+ timeWarning = new Howl({
+ src: "../sound/timeWarning.wav",
+ });
+}
+
async function initErrorSound(): Promise {
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 {
+ if (timeWarning === null) await initTimeWarning();
+ const soundToPlay = timeWarning as Howl;
+ soundToPlay.stop();
+ soundToPlay.seek(0);
+ soundToPlay.play();
+}
+
export function playNote(
codeOverride?: string,
oscillatorTypeOverride?: SupportedOscillatorTypes
diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts
index 6ce4190b5..260b18c7b 100644
--- a/frontend/src/ts/pages/settings.ts
+++ b/frontend/src/ts/pages/settings.ts
@@ -285,6 +285,14 @@ async function initGroups(): Promise {
UpdateConfig.setSoundVolume,
"range"
) as SettingsGroup;
+ groups["playTimeWarning"] = new SettingsGroup(
+ "playTimeWarning",
+ UpdateConfig.setPlayTimeWarning,
+ "button",
+ () => {
+ if (Config.playTimeWarning !== "off") void Sound.playTimeWarning();
+ }
+ ) as SettingsGroup;
groups["playSoundOnError"] = new SettingsGroup(
"playSoundOnError",
UpdateConfig.setPlaySoundOnError,
diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts
index b9b2db08a..60dbd97ae 100644
--- a/frontend/src/ts/test/test-timer.ts
+++ b/frontend/src/ts/test/test-timer.ts
@@ -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 {
Time.increment();
premid();
updateTimer();
+ if (Config.playTimeWarning !== "off") playTimeWarning();
const wpmAndRaw = calculateWpmRaw();
const acc = calculateAcc();
monkey(wpmAndRaw);
diff --git a/frontend/static/sound/timeWarning.wav b/frontend/static/sound/timeWarning.wav
new file mode 100644
index 000000000..85c272711
Binary files /dev/null and b/frontend/static/sound/timeWarning.wav differ
diff --git a/packages/schemas/src/configs.ts b/packages/schemas/src/configs.ts
index 932482d98..340f8e8be 100644
--- a/packages/schemas/src/configs.ts
+++ b/packages/schemas/src/configs.ts
@@ -355,6 +355,13 @@ export const CustomBackgroundSchema = z
.or(z.literal(""));
export type CustomBackground = z.infer;
+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;
+
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",