diff --git a/docs/architecture.md b/docs/architecture.md index 1dbbe01..72fb4cf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -21,8 +21,10 @@ Paths below are relative to `src/`; `@/` aliases that directory. | `visualizations/` | Pure trajectory geometry and visualization types | | `utilities/` | Stateless helpers for arrays, randomness, and CSS classes | -TypeScript and JavaScript coexist. Built-in algorithms and editable function bodies -remain JavaScript. Third-party JavaScript comes from package imports. +Application modules use TypeScript. The eight built-in algorithm implementations +remain JavaScript because their raw source populates the JavaScript editor. +Third-party JavaScript comes from package imports; `types/` describes the browser +APIs missing from the libraries' declarations. ## UI and lifecycle @@ -76,10 +78,10 @@ contain `{ key, frames }` or `{ key, error }`. Without Worker support, the same handler runs on the main thread. Custom JavaScript has no security sandbox, and the worker does not impose an execution or frame budget. -`sorting/algorithm-registry.mjs` holds frozen built-ins. Jotai stores custom +`sorting/algorithm-registry.ts` holds frozen built-ins. Jotai stores custom additions and overrides separately and derives the combined catalog. Source is compiled before updating state; duplicate IDs are rejected. -`sorting/algorithm-sources.mjs` imports readable source separately for the editor, +`sorting/algorithm-sources.ts` imports readable source separately for the editor, so source strings are not included in the worker bundle. ## Audio @@ -87,18 +89,18 @@ so source strings are not included in the worker bundle. `audio/create-transport.ts` owns timing, position, direction, looping, and pending resume invalidation. Its clock and effects are injected. Stop allows note tails to finish; suspension/disposal silences owned nodes. -`audio/create-timbre-audio.mjs` connects transport events to synthesis and previews. +`audio/create-timbre-audio.ts` connects transport events to synthesis and previews. Waveforms share the envelope in `state/envelope.ts`; the string preview is illustrative rather than a sampled live waveform. `audio/create-soundfont.ts` shares a sample cache between both players, deduplicates requests, allows retries, and aborts fetches after ten seconds. Cache misses load -without playing a late note. `audio/create-timbre-soundfont.mjs` decodes MP3s with +without playing a late note. `audio/create-timbre-soundfont.ts` decodes MP3s with the existing AudioContext and feeds buffers to Timbre. Disposal aborts requests and ignores late results. The GeneralUser GS sample bank and instrument/note mapping were retained during modernization. -`audio/timbre.mjs` imports `timbre/timbre.dev.js`, not its Node entry. +`audio/timbre.ts` imports `timbre/timbre.dev.js`, not its Node entry. `pnpm-workspace.yaml` excludes unused `speaker` and `readable-stream` dependencies; Vite maps the bundle's CommonJS `global` to `globalThis`. Timbre publishes a global as a side effect, but application code uses its import. `package.json` permits diff --git a/docs/development.md b/docs/development.md index 0485806..f7d488f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -94,8 +94,8 @@ The [sorting catalog](sorting-algorithms.md) records candidates and implementati 1. Add `src/sorting/algorithms/.mjs` with a default function and metadata, following an existing implementation. Use its `AS` argument; see the [API](api.md). -2. Register its stable ID in `src/sorting/algorithm-registry.mjs`. -3. Add a raw-source import and entry in `src/sorting/algorithm-sources.mjs`. +2. Register its stable ID in `src/sorting/algorithm-registry.ts`. +3. Add a raw-source import and entry in `src/sorting/algorithm-sources.ts`. 4. Update the catalog, then run `pnpm run check` and `pnpm run test:browser`. Keep the function body self-contained: imported helpers are unavailable when the diff --git a/docs/sorting-algorithms.md b/docs/sorting-algorithms.md index a14d2fa..14e22cb 100644 --- a/docs/sorting-algorithms.md +++ b/docs/sorting-algorithms.md @@ -7,7 +7,7 @@ keep appearing. This catalog covers the named sorts in Wikipedia's plus additional variants, networks, and external sorts. Each algorithm name links to its Wikipedia article or the relevant parent article when it has no separate page. -Implementation status checked against the [registry](../src/sorting/algorithm-registry.mjs) +Implementation status checked against the [registry](../src/sorting/algorithm-registry.ts) on 2026-09-08. Only registered built-ins count as implemented; historical files and custom editor examples do not. diff --git a/oxlint.config.ts b/oxlint.config.ts index 788ab26..712f016 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -43,7 +43,7 @@ export default defineConfig({ env: { node: false, browser: true }, }, { - files: ["src/worker.mjs"], + files: ["src/sorting/worker.ts"], env: { browser: false, worker: true }, }, ], diff --git a/src/audio/audio-settings.ts b/src/audio/audio-settings.ts index 55267db..a854a97 100644 --- a/src/audio/audio-settings.ts +++ b/src/audio/audio-settings.ts @@ -1,15 +1,20 @@ import type { useStore } from "jotai"; -import { settingsAtom } from "../state/settings"; +import { settingsAtom, type Settings } from "../state/settings"; import { selectedWaveformAtom } from "../state/waveforms"; // Audio clock callbacks read current values without subscribing or owning a store. export function createAudioSettings(store: ReturnType) { + function getSelected(key: Key): Settings[Key]; + function getSelected(key: string, fallback?: unknown): unknown; + function getSelected(key: string, fallback?: unknown) { + const selected = store.get(settingsAtom); + return Object.hasOwn(selected, key) ? selected[key as keyof Settings] : fallback; + } return { - getSelected(key: string, fallback?: unknown) { - const selected = store.get(settingsAtom); - return Object.hasOwn(selected, key) ? selected[key as keyof typeof selected] : fallback; - }, + getSelected, getSelectedWaveformInfo: () => store.get(selectedWaveformAtom), getTempoString: () => `bpm${store.get(settingsAtom).tempo} l16`, }; } + +export type AudioSettings = ReturnType; diff --git a/src/audio/create-player.mjs b/src/audio/create-player.ts similarity index 67% rename from src/audio/create-player.mjs rename to src/audio/create-player.ts index 677436f..7c5ed56 100644 --- a/src/audio/create-player.mjs +++ b/src/audio/create-player.ts @@ -1,15 +1,27 @@ -import { createTimbreAudio } from "./create-timbre-audio.mjs"; +import type { AudioSettings } from "./audio-settings"; +import type { createSoundfont } from "./create-soundfont"; +import type { PlayerState } from "../state/players"; +import type { SortFrame } from "../sorting/sort-types"; +import type { VisualizationType } from "../visualizations/visualization-types"; + +type Options = { + settings: AudioSettings; + getMidiNumber: (value: number) => number; + soundfont: ReturnType; + isLooping: () => boolean; + onUpdate: (state: PlayerState) => void; +}; +import { createTimbreAudio } from "./create-timbre-audio.ts"; import { createTransport } from "./create-transport.ts"; -import { timbre } from "./timbre.mjs"; +import { timbre } from "./timbre.ts"; import { visualizations } from "../visualizations/visualization-types.ts"; -import { createMidiBytes } from "../midi/create-midi-bytes.mjs"; +import { createMidiBytes } from "../midi/create-midi-bytes.ts"; import { drawStringPreview } from "./string-preview.ts"; // Owns transport and audio resources; React renders the published chart data. -export function createPlayer({ settings, getMidiNumber, soundfont, isLooping, onUpdate }) { - let data = []; - /** @type {import('../visualizations/visualization-types.ts').VisualizationType} */ - let renderer = "bar"; +export function createPlayer({ settings, getMidiNumber, soundfont, isLooping, onUpdate }: Options) { + let data: SortFrame[] = []; + let renderer: VisualizationType = "bar"; let disposed = false; const audio = createTimbreAudio( timbre, @@ -39,30 +51,29 @@ export function createPlayer({ settings, getMidiNumber, soundfont, isLooping, on onStart: audio.start, onSuspend: audio.suspend, onFrame(index) { - audio.playFrame(data[index]); + audio.playFrame(data[index]!); draw(); }, }); audio.refresh(); - /** @param {import('../visualizations/visualization-types.ts').VisualizationType} name */ - const setVisualization = (name) => { + const setVisualization = (name: VisualizationType) => { if (!Object.hasOwn(visualizations, name)) return; renderer = name; draw(); }; return { - setData(value) { + setData(value: SortFrame[]) { if (disposed) return; data = value; transport.setLength(data.length); draw(); }, setVisualization, - seek(value) { + seek(value: number) { transport.seek(value); draw(); }, - async action(action) { + async action(action: string) { await transport.whenReady(audio.resume(), () => { if (action === "stop") transport.stop(); else if (action === "play" || action === "reverse") transport.play(action === "reverse"); @@ -82,13 +93,13 @@ export function createPlayer({ settings, getMidiNumber, soundfont, isLooping, on setTempo: transport.setTempo, setVolume: audio.setVolume, refresh: audio.refresh, - plot(canvas) { + plot(canvas: HTMLCanvasElement | null) { if (!canvas) return; - canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height); + canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height); if (settings.getSelected("waveform") === "string") drawStringPreview(canvas); else audio.plot({ target: canvas, background: "rgba(255,255,255,0)" }); }, - getMidiBytes: (tempo, channel, instrument) => + getMidiBytes: (tempo: number, channel: number, instrument: number) => createMidiBytes(data, getMidiNumber, tempo, channel, instrument), destroy() { if (disposed) return; diff --git a/src/audio/create-timbre-audio.mjs b/src/audio/create-timbre-audio.ts similarity index 69% rename from src/audio/create-timbre-audio.mjs rename to src/audio/create-timbre-audio.ts index 3b9f192..17fa8f1 100644 --- a/src/audio/create-timbre-audio.mjs +++ b/src/audio/create-timbre-audio.ts @@ -1,8 +1,19 @@ +import type { Timbre, TimbreNode, PlotOptions } from "timbre/timbre.dev.js"; +import type { AudioSettings } from "./audio-settings"; +import type { createSoundfont } from "./create-soundfont"; +import type { SortFrame } from "../sorting/sort-types"; + // The legacy engine is injected here; this adapter has no DOM or store ownership. // Keep its synthesis, timing, and soundfont gain unchanged during UI migration. -export function createTimbreAudio(timbre, settings, getMidiNumber, isPlaying, soundfont) { - let env; - let generator; +export function createTimbreAudio( + timbre: Timbre, + settings: AudioSettings, + getMidiNumber: (value: number) => number, + isPlaying: () => boolean, + soundfont: ReturnType, +) { + let env: TimbreNode | null = null; + let generator: TimbreNode | null = null; let disposed = false; function suspend() { @@ -12,12 +23,12 @@ export function createTimbreAudio(timbre, settings, getMidiNumber, isPlaying, so return { resume: () => timbre.fn._audioContext.resume(), - createClock(tick) { + createClock(tick: () => void) { const interval = timbre("interval", { interval: settings.getTempoString() }, tick); return { start: () => interval.start(), stop: () => interval.stop(), - setTempo: (tempo) => interval.set({ interval: tempo }), + setTempo: (tempo: string) => interval.set({ interval: tempo }), dispose() { interval.removeAllListeners(); interval.removeAll(); @@ -25,23 +36,23 @@ export function createTimbreAudio(timbre, settings, getMidiNumber, isPlaying, so }; }, start() { - if (!disposed && settings.getSelected("audioType") === "waveform") generator.play(); + if (!disposed && settings.getSelected("audioType") === "waveform") generator?.play(); }, - playFrame(frame) { + playFrame(frame: SortFrame) { if (disposed) return; const audioType = settings.getSelected("audioType"); for (const item of frame.arr) { if (!item.play) continue; const midi = getMidiNumber(item.value); if (!(midi >= 0 && midi < 128)) continue; - if (audioType === "waveform") generator.noteOn(midi, 64); + if (audioType === "waveform") generator?.noteOn(midi, 64); else if (audioType === "soundfont") { soundfont.play(midi, settings.getSelected("volume") * 1.5); } } }, - setVolume(volume) { - if (!disposed) generator.set({ mul: volume }); + setVolume(volume: number) { + if (!disposed) generator?.set({ mul: volume }); }, refresh() { if (disposed) return; @@ -59,9 +70,9 @@ export function createTimbreAudio(timbre, settings, getMidiNumber, isPlaying, so if (!isPlaying()) this.pause(); }); if (wave.gen === "OscGen") generator.set("osc", timbre(settings.getSelected("waveform"))); - if (isPlaying() && settings.getSelected("audioType") === "waveform") generator.play(); + if (isPlaying() && settings.getSelected("audioType") === "waveform") generator?.play(); }, - plot(options) { + plot(options: PlotOptions) { generator?.osc?.plot(options); }, suspend, diff --git a/src/audio/create-timbre-soundfont.mjs b/src/audio/create-timbre-soundfont.ts similarity index 90% rename from src/audio/create-timbre-soundfont.mjs rename to src/audio/create-timbre-soundfont.ts index c7cabbb..059ed50 100644 --- a/src/audio/create-timbre-soundfont.mjs +++ b/src/audio/create-timbre-soundfont.ts @@ -1,14 +1,15 @@ +import type { Timbre } from "timbre/timbre.dev.js"; import { createSoundfont } from "./create-soundfont.ts"; // Native decoding replaces JSONP and the bundled JS MP3 decoder. Continue routing // samples through Timbre so they share the existing mixer with waveform playback. -export function createTimbreSoundfont(timbre) { +export function createTimbreSoundfont(timbre: Timbre) { return createSoundfont({ decode: (bytes) => timbre.fn._audioContext.decodeAudioData(bytes), createSample(decoded) { const left = decoded.getChannelData(0); const right = decoded.numberOfChannels > 1 ? decoded.getChannelData(1) : left; - const mix = Float32Array.from(left, (value, index) => (value + right[index]) / 2); + const mix = Float32Array.from(left, (value, index) => (value + right[index]!) / 2); const sample = timbre("buffer", { buffer: { samplerate: decoded.sampleRate, buffer: [mix, left, right] }, }).on("ended", function () { diff --git a/src/audio/timbre.mjs b/src/audio/timbre.ts similarity index 100% rename from src/audio/timbre.mjs rename to src/audio/timbre.ts diff --git a/src/components/dialogs/algorithm-dialog.tsx b/src/components/dialogs/algorithm-dialog.tsx index 74d4f78..6259bba 100644 --- a/src/components/dialogs/algorithm-dialog.tsx +++ b/src/components/dialogs/algorithm-dialog.tsx @@ -7,10 +7,10 @@ import { addAlgorithmAtom, editAlgorithmAtom, } from "../../state/algorithm-overrides.ts"; -import { algorithms } from "../../sorting/algorithm-registry.mjs"; -import { sources } from "../../sorting/algorithm-sources.mjs"; +import { algorithms } from "../../sorting/algorithm-registry.ts"; +import { sources } from "../../sorting/algorithm-sources.ts"; import { getFunctionBody } from "../../sorting/sort-requests.ts"; -import type { createCodeEditor } from "./create-code-editor.mjs"; +import type { createCodeEditor } from "./create-code-editor.ts"; import { PlayerDialog as Dialog } from "./player-dialog.tsx"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Input } from "@/components/ui/input"; @@ -29,7 +29,7 @@ export function AlgorithmDialog({ adding, onClose }: { adding: boolean; onClose: useEffect(() => { let cancelled = false; let instance: ReturnType | undefined; - void import("./create-code-editor.mjs") + void import("./create-code-editor.ts") .then(({ createCodeEditor }) => { if (cancelled) return; instance = createCodeEditor(host.current!); diff --git a/src/components/dialogs/create-code-editor.mjs b/src/components/dialogs/create-code-editor.ts similarity index 67% rename from src/components/dialogs/create-code-editor.mjs rename to src/components/dialogs/create-code-editor.ts index 28c1d8a..766a696 100644 --- a/src/components/dialogs/create-code-editor.mjs +++ b/src/components/dialogs/create-code-editor.ts @@ -7,7 +7,7 @@ import javascriptWorkerUrl from "ace-builds/src-min-noconflict/worker-javascript // Let Vite emit the worker and resolve it at both / and /audio-sort/. ace.config.setModuleUrl("ace/mode/javascript_worker", javascriptWorkerUrl); -export function createCodeEditor(element) { +export function createCodeEditor(element: HTMLElement) { const editor = ace.edit(element); editor.setOptions({ theme: "ace/theme/monokai", @@ -17,7 +17,13 @@ export function createCodeEditor(element) { const session = editor.getSession(); session.on("changeMode", () => { // The editor contains a function body with AS supplied by the runner. - session.$worker?.call("changeOptions", [{ esversion: 11, globals: { AS: false } }]); + // Ace creates this worker after setting the mode, but omits it from EditSession's types. + const worker = ( + session as typeof session & { + $worker?: { call(command: string, args: unknown[]): void }; + } + ).$worker; + worker?.call("changeOptions", [{ esversion: 11, globals: { AS: false } }]); }); session.setMode("ace/mode/javascript"); return editor; diff --git a/src/components/playground/playground-context.tsx b/src/components/playground/playground-context.tsx index 81c0be1..de47a6d 100644 --- a/src/components/playground/playground-context.tsx +++ b/src/components/playground/playground-context.tsx @@ -31,7 +31,7 @@ function useActions() { instrument, ); saveAs( - new Blob([Uint8Array.from(bytes as string, (char) => char.charCodeAt(0))], { + new Blob([Uint8Array.from(bytes, (char) => char.charCodeAt(0))], { type: "audio/midi", }), `${filename}.mid`, diff --git a/src/hooks/use-players.ts b/src/hooks/use-players.ts index 1a054dd..31902a1 100644 --- a/src/hooks/use-players.ts +++ b/src/hooks/use-players.ts @@ -1,10 +1,10 @@ import { useEffect, useMemo, useState } from "react"; import { useAtomValue, useStore } from "jotai"; -import { createPlayer } from "../audio/create-player.mjs"; -import { createTimbreSoundfont } from "../audio/create-timbre-soundfont.mjs"; -import { timbre } from "../audio/timbre.mjs"; +import { createPlayer } from "../audio/create-player.ts"; +import { createTimbreSoundfont } from "../audio/create-timbre-soundfont.ts"; +import { timbre } from "../audio/timbre.ts"; import { createAudioSettings } from "../audio/audio-settings"; -import { createHelpers } from "../midi/create-helpers.mjs"; +import { createHelpers } from "../midi/create-helpers.ts"; import { settingsAtom } from "../state/settings"; import { selectedWaveformAtom } from "../state/waveforms"; import { playbackPreferencesAtom } from "../state/playback-preferences"; diff --git a/src/hooks/use-sort.ts b/src/hooks/use-sort.ts index 72155e4..80d35d9 100644 --- a/src/hooks/use-sort.ts +++ b/src/hooks/use-sort.ts @@ -99,7 +99,7 @@ export function useSort(session: AudioSession | null) { accept(handleSortRequest(request)); return; } - worker = new Worker(new URL("../sorting/worker.mjs", import.meta.url), { type: "module" }); + worker = new Worker(new URL("../sorting/worker.ts", import.meta.url), { type: "module" }); worker.addEventListener("message", (event: MessageEvent) => accept(event.data)); worker.addEventListener("error", (event) => accept({ key, error: event.message })); worker.postMessage(request); diff --git a/src/midi/create-helpers.mjs b/src/midi/create-helpers.ts similarity index 56% rename from src/midi/create-helpers.mjs rename to src/midi/create-helpers.ts index d335f44..5bd4db9 100644 --- a/src/midi/create-helpers.mjs +++ b/src/midi/create-helpers.ts @@ -1,12 +1,16 @@ +import type { AudioSettings } from "../audio/audio-settings"; import { scales } from "./scales.ts"; -export function createHelpers(settings, dependencies = { scales }) { +export function createHelpers( + settings: Pick, + dependencies = { scales }, +) { return { - getMidiNumber(playValue) { - const scale = dependencies.scales[settings.getSelected("scale")]; + getMidiNumber(playValue: number) { + const scale = dependencies.scales[settings.getSelected("scale")]!; const degrees = scale.degrees; - const noteAt = (position) => - degrees[position % degrees.length] + + const noteAt = (position: number) => + degrees[position % degrees.length]! + Math.floor(position / degrees.length) * scale.pitchesPerOctave; return ( noteAt(playValue) + diff --git a/src/midi/create-midi-bytes.mjs b/src/midi/create-midi-bytes.ts similarity index 69% rename from src/midi/create-midi-bytes.mjs rename to src/midi/create-midi-bytes.ts index 56f3221..0cbf9f6 100644 --- a/src/midi/create-midi-bytes.mjs +++ b/src/midi/create-midi-bytes.ts @@ -1,7 +1,14 @@ +import type { SortFrame } from "../sorting/sort-types"; import Midi from "jsmidgen"; // Keep the original export timing and velocity independent of library defaults. -export function createMidiBytes(data, getMidiNumber, tempo, channel, instrument) { +export function createMidiBytes( + data: readonly SortFrame[], + getMidiNumber: (value: number) => number, + tempo: number, + channel: number, + instrument: number, +) { const duration = 64; let totalDuration = 0; @@ -13,13 +20,11 @@ export function createMidiBytes(data, getMidiNumber, tempo, channel, instrument) midiFile.addTrack(midiTrack); // build midi track - for (let i = 0; i < data.length; i++) { - const info = data[i]; - const play = []; + for (const info of data) { + const play: number[] = []; totalDuration += duration; // get the notes we need to play - for (let j = 0; j < info.arr.length; j++) { - const currentItem = info.arr[j]; + for (const currentItem of info.arr) { if (currentItem.play) { const midiNumber = getMidiNumber(currentItem.value); if (midiNumber >= 0 && midiNumber < 128) { @@ -30,14 +35,14 @@ export function createMidiBytes(data, getMidiNumber, tempo, channel, instrument) // note on for (let j = 0; j < play.length; j++) { if (j === 0) { - midiTrack.noteOn(channel, play[j], duration, 100); + midiTrack.noteOn(channel, play[j]!, duration, 100); } else { - midiTrack.noteOn(channel, play[j], 0, 100); + midiTrack.noteOn(channel, play[j]!, 0, 100); } } // note off for (let j = 0; j < play.length; j++) { - midiTrack.noteOff(channel, play[j], 0, 100); + midiTrack.noteOff(channel, play[j]!, 0, 100); } } diff --git a/src/sorting/algorithm-registry.mjs b/src/sorting/algorithm-registry.ts similarity index 100% rename from src/sorting/algorithm-registry.mjs rename to src/sorting/algorithm-registry.ts diff --git a/src/sorting/algorithm-sources.mjs b/src/sorting/algorithm-sources.ts similarity index 100% rename from src/sorting/algorithm-sources.mjs rename to src/sorting/algorithm-sources.ts diff --git a/src/sorting/sort-requests.ts b/src/sorting/sort-requests.ts index 7d657a2..dc26944 100644 --- a/src/sorting/sort-requests.ts +++ b/src/sorting/sort-requests.ts @@ -1,4 +1,4 @@ -import { algorithms } from "./algorithm-registry.mjs"; +import { algorithms } from "./algorithm-registry.ts"; import { createSortEngine } from "./create-sort-engine.ts"; import type { SortAlgorithm, diff --git a/src/sorting/worker.mjs b/src/sorting/worker.ts similarity index 64% rename from src/sorting/worker.mjs rename to src/sorting/worker.ts index d70a88c..64f6121 100644 --- a/src/sorting/worker.mjs +++ b/src/sorting/worker.ts @@ -1,5 +1,5 @@ import { handleSortRequest } from "./sort-requests.ts"; -globalThis.onmessage = ({ data }) => { +globalThis.onmessage = ({ data }: MessageEvent) => { globalThis.postMessage(handleSortRequest(data)); }; diff --git a/src/state/algorithm-overrides.ts b/src/state/algorithm-overrides.ts index c706b84..35a2017 100644 --- a/src/state/algorithm-overrides.ts +++ b/src/state/algorithm-overrides.ts @@ -1,5 +1,5 @@ import { atom } from "jotai"; -import { algorithms } from "../sorting/algorithm-registry.mjs"; +import { algorithms } from "../sorting/algorithm-registry.ts"; import type { SortAlgorithm } from "../sorting/sort-types.ts"; // Types for the existing function properties, not a new algorithm format. diff --git a/src/types/ace.d.ts b/src/types/ace.d.ts new file mode 100644 index 0000000..45e2e92 --- /dev/null +++ b/src/types/ace.d.ts @@ -0,0 +1,5 @@ +// Ace ships types at its package root, while the browser entry is a subpath. +declare module "ace-builds/src-noconflict/ace" { + import ace from "ace-builds"; + export default ace; +} diff --git a/src/types/jsmidgen.d.ts b/src/types/jsmidgen.d.ts new file mode 100644 index 0000000..d1dea51 --- /dev/null +++ b/src/types/jsmidgen.d.ts @@ -0,0 +1,21 @@ +// API used from jsmidgen 0.1.8. File.toBytes returns a binary string, +// unlike Track.toBytes and the return type in @types/jsmidgen. +declare module "jsmidgen" { + class MetaEvent { + static COPYRIGHT: number; + constructor(options: { type: number; data: number[]; time: number }); + } + class Track { + setTempo(tempo: number): void; + setInstrument(channel: number, instrument: number): void; + noteOn(channel: number, note: number, time: number, velocity: number): void; + noteOff(channel: number, note: number, time: number, velocity: number): void; + addEvent(event: MetaEvent): void; + } + class File { + addTrack(track: Track): void; + toBytes(): string; + } + const Midi: { File: typeof File; Track: typeof Track; MetaEvent: typeof MetaEvent }; + export default Midi; +} diff --git a/src/types/timbre.d.ts b/src/types/timbre.d.ts new file mode 100644 index 0000000..dc7b2cc --- /dev/null +++ b/src/types/timbre.d.ts @@ -0,0 +1,25 @@ +// The subset of Timbre's browser API used by the audio adapters. +declare module "timbre/timbre.dev.js" { + export type PlotOptions = { target: HTMLCanvasElement; background: string }; + export type TimbreNode = { + play(): TimbreNode; + pause(): TimbreNode; + bang(): TimbreNode; + start(): TimbreNode; + stop(): TimbreNode; + noteOn(note: number, velocity: number): TimbreNode; + set(properties: Record): TimbreNode; + set(name: string, value: unknown): TimbreNode; + on(event: string, listener: (this: TimbreNode) => void): TimbreNode; + removeAllListeners(): TimbreNode; + removeAll(): TimbreNode; + osc?: { plot(options: PlotOptions): void }; + _: { channels: number }; + }; + export type Timbre = { + (name: string, options?: Record, callback?: () => void): TimbreNode; + fn: { _audioContext: AudioContext }; + }; + const timbre: Timbre; + export default timbre; +} diff --git a/test/algorithm-overrides.test.mjs b/test/algorithm-overrides.test.mjs index 9261b54..f79ecdc 100644 --- a/test/algorithm-overrides.test.mjs +++ b/test/algorithm-overrides.test.mjs @@ -1,6 +1,6 @@ import { expect, test, vi } from "vitest"; import { createStore } from "jotai/vanilla"; -import { algorithms } from "../src/sorting/algorithm-registry.mjs"; +import { algorithms } from "../src/sorting/algorithm-registry.ts"; import { algorithmCatalogAtom, editAlgorithmAtom, diff --git a/test/algorithms.test.mjs b/test/algorithms.test.mjs index 9d3fed7..4a813f2 100644 --- a/test/algorithms.test.mjs +++ b/test/algorithms.test.mjs @@ -1,4 +1,4 @@ -import { algorithms } from "../src/sorting/algorithm-registry.mjs"; +import { algorithms } from "../src/sorting/algorithm-registry.ts"; import { describe, expect, test } from "vitest"; import { algorithmNames, runAlgorithm, seededValues } from "./helpers/algorithms.mjs"; diff --git a/test/helpers/algorithms.mjs b/test/helpers/algorithms.mjs index f10fffd..3503ed8 100644 --- a/test/helpers/algorithms.mjs +++ b/test/helpers/algorithms.mjs @@ -1,6 +1,6 @@ import { readdirSync } from "node:fs"; import { createContext, runInContext } from "node:vm"; -import { algorithms } from "../../src/sorting/algorithm-registry.mjs"; +import { algorithms } from "../../src/sorting/algorithm-registry.ts"; import { createSortEngine } from "../../src/sorting/create-sort-engine.ts"; const root = new URL("../../", import.meta.url); diff --git a/test/midi-export.test.mjs b/test/midi-export.test.mjs index b4f0c27..11a9e05 100644 --- a/test/midi-export.test.mjs +++ b/test/midi-export.test.mjs @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { createMidiBytes } from "../src/midi/create-midi-bytes.mjs"; +import { createMidiBytes } from "../src/midi/create-midi-bytes.ts"; const frames = [ { diff --git a/test/module-layout.test.mjs b/test/module-layout.test.mjs index 8c8d2f6..03fe0f2 100644 --- a/test/module-layout.test.mjs +++ b/test/module-layout.test.mjs @@ -27,7 +27,7 @@ test("application modules and directories use lowercase kebab-case names", () => expect(entry.name).toMatch( entry.isDirectory() ? /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/ - : /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*\.(?:mjs|tsx?|css)$/, + : /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*\.(?:mjs|(?:d\.)?tsx?|css)$/, ); } }); diff --git a/test/react-playground.browser.test.mjs b/test/react-playground.browser.test.mjs index d572517..87b1d09 100644 --- a/test/react-playground.browser.test.mjs +++ b/test/react-playground.browser.test.mjs @@ -10,11 +10,11 @@ import { updateEnvelopeAtom } from "../src/state/envelope.ts"; import { editAlgorithmAtom, algorithmCatalogAtom } from "../src/state/algorithm-overrides.ts"; import { toggleAutoPlayAtom } from "../src/state/playback-preferences.ts"; import { playerAtoms, sortErrorAtom, suspendedAtom } from "../src/state/players.ts"; -import { createPlayer } from "../src/audio/create-player.mjs"; -import { createTimbreSoundfont } from "../src/audio/create-timbre-soundfont.mjs"; +import { createPlayer } from "../src/audio/create-player.ts"; +import { createTimbreSoundfont } from "../src/audio/create-timbre-soundfont.ts"; -vi.mock("../src/audio/timbre.mjs", () => ({ timbre: {} })); -vi.mock("../src/audio/create-player.mjs", () => ({ +vi.mock("../src/audio/timbre.ts", () => ({ timbre: {} })); +vi.mock("../src/audio/create-player.ts", () => ({ createPlayer: vi.fn(() => ({ destroy: vi.fn(), suspend: vi.fn(), @@ -29,7 +29,7 @@ vi.mock("../src/audio/create-player.mjs", () => ({ action: vi.fn(), })), })); -vi.mock("../src/audio/create-timbre-soundfont.mjs", () => ({ +vi.mock("../src/audio/create-timbre-soundfont.ts", () => ({ createTimbreSoundfont: vi.fn(() => ({ setInstrument: vi.fn(), preload: vi.fn(), diff --git a/test/scales.test.mjs b/test/scales.test.mjs index 3ce02f6..22b9b2b 100644 --- a/test/scales.test.mjs +++ b/test/scales.test.mjs @@ -1,7 +1,7 @@ import { createHash } from "node:crypto"; import { expect, test } from "vitest"; import { scales } from "../src/midi/scales.ts"; -import { createHelpers } from "../src/midi/create-helpers.mjs"; +import { createHelpers } from "../src/midi/create-helpers.ts"; test("preserves all scale data extracted from the bundled subcollider 0.1.0", () => { // Baseline computed from ScaleInfo.names()/at() in the original bundle. diff --git a/test/timbre-audio.test.mjs b/test/timbre-audio.test.mjs index e8039ad..f9a5363 100644 --- a/test/timbre-audio.test.mjs +++ b/test/timbre-audio.test.mjs @@ -1,5 +1,5 @@ import { expect, test, vi } from "vitest"; -import { createTimbreAudio } from "../src/audio/create-timbre-audio.mjs"; +import { createTimbreAudio } from "../src/audio/create-timbre-audio.ts"; function setup() { const nodes = []; diff --git a/test/ui.test.mjs b/test/ui.test.mjs index 72f145d..48ca65f 100644 --- a/test/ui.test.mjs +++ b/test/ui.test.mjs @@ -1,6 +1,6 @@ import { expect, test } from "vitest"; -import { createHelpers } from "../src/midi/create-helpers.mjs"; -import { createPlayer } from "../src/audio/create-player.mjs"; +import { createHelpers } from "../src/midi/create-helpers.ts"; +import { createPlayer } from "../src/audio/create-player.ts"; import { visualizations } from "../src/visualizations/visualization-types.ts"; test("UI modules import without DOM initialization or first-party globals", () => { diff --git a/test/worker.test.mjs b/test/worker.test.mjs index 4db4719..1ae3103 100644 --- a/test/worker.test.mjs +++ b/test/worker.test.mjs @@ -1,7 +1,7 @@ import { createSortEngine } from "../src/sorting/create-sort-engine.ts"; import { describe, expect, test, vi } from "vitest"; -import { algorithms } from "../src/sorting/algorithm-registry.mjs"; -import { sources } from "../src/sorting/algorithm-sources.mjs"; +import { algorithms } from "../src/sorting/algorithm-registry.ts"; +import { sources } from "../src/sorting/algorithm-sources.ts"; import { createSortRequest, getFunctionBody,