{state === "failed" ? t.failed : state === "loading" ? t.judgeLoading : t.blindHow}
+{t.blindHow}
+{LETTERS[i]}
+ ++ {t.blindAnswer}: {t.blindWho[item.who]} · {correct ? t.blindCorrect : t.blindWrong} +
+{t.parallels} {item.report.parallels.toFixed(1)} · {t.blindChords} {item.report.chords.toFixed(0)}
+{t.loss}
+{t.lossNote}
+{t.snapshots}
+ +{t.waiting}
} +{t.metricsNote}
+ )} +{state === "loading" ? t.judgeLoading : state === "failed" ? t.failed : `${t.judgeNote} ${t.judgeHint}`}
+{state === "thinking" ? t.thinking : t.judgeReady}
+{t.candidate(i)}
+ +{stats ? `${t.choices(stats.choices)} · ${t.since}` : t.judgeHint}
+ {stats && ( ++ {names.map((n, i) => ( + + + {n} + + ))} +
+ ); +} diff --git a/content/posts/music-ai/components/player.ts b/content/posts/music-ai/components/player.ts new file mode 100644 index 0000000..1157ff0 --- /dev/null +++ b/content/posts/music-ai/components/player.ts @@ -0,0 +1,119 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; +import type { Piece } from "./music"; +import type { Reply, Request } from "./protocol"; +import type { Timbre } from "./synth"; +import { createMusicWorker } from "./worker-factory"; + +/* + * One synthesiser worker and one AudioContext for the page, shared by every figure: playing something stops whatever + * was playing. The worker is separate from the training one, so a note never waits behind a training step. The + * AudioContext is made on the first press of Play (a browser only lets a page make sound after a gesture). + */ +let synth: Worker | null = null, audio: AudioContext | null = null, source: AudioBufferSourceNode | null = null; +/** Fires when the music itself ends, a little before the buffer does: the reverb tail rings on, the controls do not wait for it. */ +let endTimer: ReturnType