diff --git a/frontend/app.ts b/frontend/app.ts index 05b22d5..bbcc36d 100644 --- a/frontend/app.ts +++ b/frontend/app.ts @@ -1,4 +1,5 @@ import {BoardArea} from './board_area'; +import {AudioPlayer, AudioEventType} from './audio'; import {GameSelection} from './game_selection'; import {MoveList} from './movelist'; import {MultiPvView} from './multipv_view'; @@ -28,6 +29,7 @@ export class App implements WebsocketObserver { private boardArea: BoardArea; private jsHash?: string; private gameIsLive: boolean = false; + private audioPlayer: AudioPlayer; constructor() { this.gameSelection = new GameSelection( @@ -42,6 +44,7 @@ export class App implements WebsocketObserver { this.multiPvView.addObserver(this); this.websocketFeed = new WebSocketFeed(); this.websocketFeed.addObserver(this); + this.audioPlayer = new AudioPlayer(); window.addEventListener('keydown', (event: KeyboardEvent) => { if (event.key === 'Escape') this.moveList.unselectVariation(); }); @@ -78,8 +81,8 @@ export class App implements WebsocketObserver { this.gameSelection.updateGames(games); } public onPositionReceived(position: WsPositionData[]): void { - this.moveList.updatePositions( - position.filter(p => p.gameId == this.curGameId)); + const filteredPositions = position.filter(p => p.gameId == this.curGameId); + this.moveList.updatePositions(filteredPositions); } public onEvaluationReceived(evaluation: WsEvaluationData[]): void { let evals = evaluation.filter( @@ -111,6 +114,7 @@ export class App implements WebsocketObserver { public onMoveSelected( position: WsPositionData, pos_changed: boolean, isOngoling: boolean): void { + const currentPly = this.curPly ?? -1; this.curPly = position.ply; const nextMove = this.moveList.getMoveAtPly(position.ply + 1)?.moveUci; this.boardArea.changePosition( @@ -119,6 +123,11 @@ export class App implements WebsocketObserver { this.multiPvView.setPosition(position); this.websocketFeed.setPosition(position.ply); this.boardArea.resetPvVisualization(); + + // Only play move sound when the next move is 1 ply ahead of the current position + if (position.ply === currentPly + 1) { + this.audioPlayer.playAudio(AudioEventType.MOVE); + } } this.boardArea.updatePosition(position); } diff --git a/frontend/audio.ts b/frontend/audio.ts new file mode 100644 index 0000000..ff19839 --- /dev/null +++ b/frontend/audio.ts @@ -0,0 +1,65 @@ +export enum AudioEventType { + MOVE, +} + +class DebouncedAudioPlayer { + private audioContext : AudioContext; + private audio: AudioBuffer; + private cooldown: number; + private lastPlayed: number; + + constructor(context: AudioContext, audio: AudioBuffer, cooldown: number = 0) { + this.audioContext = context; + this.audio = audio; + this.cooldown = cooldown; + this.lastPlayed = 0; + } + + public play() { + const now = Date.now(); + if (now - this.lastPlayed < this.cooldown) { + return; + } + this.lastPlayed = now; + + this.audioContext.resume(); + if (this.audioContext.state !== "running") { + return; + } + + const source = this.audioContext.createBufferSource(); + source.buffer = this.audio; + source.connect(this.audioContext.destination); + source.start(0); + } +} + +export class AudioPlayer { + private sounds: Map; + + constructor() { + this.sounds = new Map(); + const context = new AudioContext(); + this.addSound(context, AudioEventType.MOVE, 'static/chess_move.mp3'); + } + + private async addSound(context: AudioContext, eventType: AudioEventType, path: string): Promise { + const moveSound = await this.loadSound(context, path); + this.sounds.set(eventType, new DebouncedAudioPlayer(context, moveSound, 50)); + } + + private async loadSound(context: AudioContext, url: string): Promise { + const response = await fetch(url); + const arrayBuffer = await response.arrayBuffer(); + const audioBuffer = await context.decodeAudioData(arrayBuffer); + return audioBuffer; + } + + public playAudio(eventType: AudioEventType): void { + try { + this.sounds.get(eventType)?.play(); + } catch (e) { + // Need user interaction to play audio + } + } +} \ No newline at end of file diff --git a/static/index.template.html b/static/index.template.html index 1796d67..85dd4bb 100644 --- a/static/index.template.html +++ b/static/index.template.html @@ -71,6 +71,7 @@ Colin M.L. Burnett . Analysis by LCZero engine. + chess pieces.wav by simone_ds (CC0 licence). Source code on GitHub. diff --git a/static/static/chess_move.mp3 b/static/static/chess_move.mp3 new file mode 100644 index 0000000..8870037 Binary files /dev/null and b/static/static/chess_move.mp3 differ