diff --git a/index.ts b/index.ts deleted file mode 100644 index f67b2c6..0000000 --- a/index.ts +++ /dev/null @@ -1 +0,0 @@ -console.log("Hello via Bun!"); \ No newline at end of file diff --git a/package.json b/package.json index f4cd02e..b3ccbad 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,13 @@ { "name": "stsheets", - "module": "index.ts", "type": "module", "private": true, + "scripts": { + "test": "bun test", + "typecheck": "bunx tsc --noEmit", + "fetch-models": "bun scripts/fetch-models.ts", + "bench": "bun src/bench/cli.ts" + }, "devDependencies": { "@types/bun": "latest" }, diff --git a/scripts/fetch-models.ts b/scripts/fetch-models.ts new file mode 100644 index 0000000..cff231b --- /dev/null +++ b/scripts/fetch-models.ts @@ -0,0 +1,60 @@ +// Downloads model files into models/ and verifies their sha256. +// Files already present with a matching hash are skipped. +import { join } from "node:path"; + +interface ModelFile { + path: string; // relative to repo root + url: string; + sha256: string; +} + +export const MODELS: ModelFile[] = [ + { + path: "models/demucs/htdemucs_6s.onnx", + url: "https://huggingface.co/Kani95/htdemucs-6s-ort/resolve/main/htdemucs_6s.onnx", + sha256: "35cb61d4d35173ff1d16df7393f41f05651ffdfe13d3c10e06abc26708bd6933", + }, + { + path: "models/basic-pitch/nmp.onnx", + url: "https://github.com/spotify/basic-pitch/raw/main/basic_pitch/saved_models/icassp_2022/nmp.onnx", + sha256: "2c3c1d144bfa61ad236e92e169c13535c880469a12a047d4e73451f2c059a0ec", + }, +]; + +const ROOT = join(import.meta.dir, ".."); + +async function sha256Of(file: string): Promise { + const hasher = new Bun.CryptoHasher("sha256"); + hasher.update(new Uint8Array(await Bun.file(file).arrayBuffer())); + return hasher.digest("hex"); +} + +export async function fetchModels(root = ROOT): Promise { + for (const m of MODELS) { + const dest = join(root, m.path); + if (await Bun.file(dest).exists()) { + const have = await sha256Of(dest); + if (have === m.sha256) { + console.log(`ok ${m.path}`); + continue; + } + console.log(`mismatch ${m.path} (have ${have}), re-downloading`); + } + console.log(`fetch ${m.url}`); + const res = await fetch(m.url, { redirect: "follow" }); + if (!res.ok) throw new Error(`download failed for ${m.url}: HTTP ${res.status}`); + const bytes = new Uint8Array(await res.arrayBuffer()); + const hasher = new Bun.CryptoHasher("sha256"); + hasher.update(bytes); + const got = hasher.digest("hex"); + if (got !== m.sha256) { + throw new Error(`sha256 mismatch for ${m.path}: expected ${m.sha256}, got ${got}`); + } + await Bun.write(dest, bytes); + console.log(`saved ${m.path}`); + } +} + +if (import.meta.main) { + await fetchModels(); +} diff --git a/src/core/rng.ts b/src/core/rng.ts new file mode 100644 index 0000000..2e597bc --- /dev/null +++ b/src/core/rng.ts @@ -0,0 +1,33 @@ +export interface Rng { + /** Uniform in [0, 1). */ + next(): number; + /** Uniform integer in [lo, hi], inclusive. */ + int(lo: number, hi: number): number; + pick(a: readonly T[]): T; + chance(p: number): boolean; +} + +// mulberry32 +export function createRng(seed: number): Rng { + let s = seed >>> 0; + const next = (): number => { + s = (s + 0x6d2b79f5) >>> 0; + let t = s; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; + return { + next, + int(lo, hi) { + return lo + Math.floor(next() * (hi - lo + 1)); + }, + pick(a: readonly T[]): T { + if (a.length === 0) throw new Error("rng.pick: empty array"); + return a[Math.floor(next() * a.length)] as T; + }, + chance(p) { + return next() < p; + }, + }; +} diff --git a/src/core/time.ts b/src/core/time.ts new file mode 100644 index 0000000..e1433a5 --- /dev/null +++ b/src/core/time.ts @@ -0,0 +1,10 @@ +import { TPQ } from "./types.ts"; + +// One quarter note = TPQ ticks. +export function ticksToSeconds(tick: number, bpm: number): number { + return (tick / TPQ) * (60 / bpm); +} + +export function secondsToTicks(s: number, bpm: number): number { + return s * (bpm / 60) * TPQ; +} diff --git a/src/core/types.ts b/src/core/types.ts new file mode 100644 index 0000000..205e244 --- /dev/null +++ b/src/core/types.ts @@ -0,0 +1,13 @@ +export type Role = "guitar" | "bass" | "keys" | "vocals" | "drums"; +export type FxPreset = "none" | "clean" | "clean-chorus" | "light-drive"; +// string numbering: 1 = highest-pitched string (alphaTex convention). Drums: pitch = GM drum number. +export interface ScoreNote { tick: number; dur: number; pitch: number; string?: number; fret?: number } +export interface Track { name: string; role: Role; program: number; tuning?: number[]; pan: number; fx: FxPreset; gainDb: number; notes: ScoreNote[] } +export interface Song { title: string; tier: string; seed: number; bpm: number; timeSig: [number, number]; bars: number; key: string; tracks: Track[] } +export interface TimedNote { onset: number; offset: number; pitch: number; velocity?: number; string?: number; fret?: number } +export interface TruthTrack { name: string; role: Role; tuning?: number[]; notes: TimedNote[] } +export interface Grid { bpm: number; firstDownbeat: number; beatsPerBar: number } +export interface Audio { sampleRate: number; channels: Float32Array[] } +export const TPQ = 960; +export const GUITAR_STANDARD = [64, 59, 55, 50, 45, 40]; +export const BASS_STANDARD = [43, 38, 33, 28]; diff --git a/tests/core/rng.test.ts b/tests/core/rng.test.ts new file mode 100644 index 0000000..2e1c523 --- /dev/null +++ b/tests/core/rng.test.ts @@ -0,0 +1,10 @@ +import { test, expect } from "bun:test"; +import { createRng } from "../../src/core/rng.ts"; + +test("rng deterministic", () => { + const a = createRng(42), b = createRng(42), c = createRng(43); + const sa = [a.next(), a.next(), a.next()]; + expect([b.next(), b.next(), b.next()]).toEqual(sa); + expect(c.next()).not.toBe(sa[0]); + const r = createRng(1); for (let i = 0; i < 1000; i++) { const v = r.int(3, 5); expect(v >= 3 && v <= 5).toBe(true); } +}); diff --git a/tests/core/time.test.ts b/tests/core/time.test.ts new file mode 100644 index 0000000..c6481b7 --- /dev/null +++ b/tests/core/time.test.ts @@ -0,0 +1,8 @@ +import { test, expect } from "bun:test"; +import { ticksToSeconds, secondsToTicks } from "../../src/core/time.ts"; + +test("ticks/seconds", () => { + expect(ticksToSeconds(960, 60)).toBeCloseTo(1); + expect(ticksToSeconds(3840, 120)).toBeCloseTo(2); + expect(secondsToTicks(0.5, 120)).toBeCloseTo(960); +});