{"id":51638690,"url":"https://github.com/gdamdam/mgrains","last_synced_at":"2026-07-13T17:34:01.765Z","repository":{"id":368299612,"uuid":"1284452644","full_name":"gdamdam/mgrains","owner":"gdamdam","description":"A granular instrument. Bloom any sound into clouds, or shatter it into rhythm","archived":false,"fork":false,"pushed_at":"2026-07-12T22:36:31.000Z","size":1598,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-13T17:33:59.526Z","etag":null,"topics":["granular","granular-synthesis","webaudio"],"latest_commit_sha":null,"homepage":"https://mgrains.mpump.live/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gdamdam.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-29T22:00:35.000Z","updated_at":"2026-07-12T22:36:36.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/gdamdam/mgrains","commit_stats":null,"previous_names":["gdamdam/mgrains"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/gdamdam/mgrains","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gdamdam%2Fmgrains","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gdamdam%2Fmgrains/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gdamdam%2Fmgrains/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gdamdam%2Fmgrains/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gdamdam","download_url":"https://codeload.github.com/gdamdam/mgrains/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gdamdam%2Fmgrains/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35430964,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-13T02:00:06.543Z","response_time":119,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["granular","granular-synthesis","webaudio"],"created_at":"2026-07-13T17:34:00.848Z","updated_at":"2026-07-13T17:34:01.756Z","avatar_url":"https://github.com/gdamdam.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# mgrains\n\n**A granular instrument — bloom any sound into clouds, or shatter it into rhythm.**\n\n[![version](https://img.shields.io/badge/version-1.8.0-6c8f3a)](./package.json)\n[![license](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue)](./LICENSE)\n[![tests](https://img.shields.io/badge/tests-481%20passing-2ea043)](#verification)\n[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript\u0026logoColor=white)](./tsconfig.json)\n[![React](https://img.shields.io/badge/React-19-61dafb?logo=react\u0026logoColor=white)](https://react.dev)\n[![Vite](https://img.shields.io/badge/Vite-8-646cff?logo=vite\u0026logoColor=white)](https://vite.dev)\n[![Web Audio](https://img.shields.io/badge/Web%20Audio-AudioWorklet-ff6d00)](https://developer.mozilla.org/docs/Web/API/AudioWorklet)\n[![PWA](https://img.shields.io/badge/PWA-installable-5a0fc8)](#progressive-web-app)\n\n![mgrains — granular instrument](./mgrains_screenshot.gif)\n\n\u003c/div\u003e\n\n---\n\n`mgrains` is a browser-native granular synthesizer and live effect. Feed it a generated source, an imported file, or a live capture, then perform it in one of two modes — **Bloom** (slow overlapping clouds) or **Shatter** (sample-accurate tempo-synced fragments) — through an XY surface, four performance macros, an eleven-slot effects rack, and a chromatic keyboard. All audio runs in an `AudioWorklet`; the UI never schedules grains.\n\n## Highlights\n\n- **Two modes, one muscle memory** — Bloom and Shatter share the layout but run distinct schedulers, constraints, macros, and graphics, switching through a click-free 180 ms fade-through-silence.\n- **Direct grain controls** — Grain Size, Density/Rate, Position, and Spray always on the main surface; region, timing jitter, scan speed, pitch, pitch spread, reverse probability, stereo spread, window, and output, grain filter (center + spread) in an Advanced panel — all with units.\n- **Four macros per mode** — Bloom: Cloud · Drift · Warmth · Space. Shatter: Chop · Scatter · Crush · Repeat. Each sweeps a curated parameter group; **Link/Unlink** keeps hand edits authoritative.\n- **11-effect rack** — Drive, Crush, Damp, Tape, Ring, Formant, Comb, Wow, Sub, Space (reverb), Repeat (tempo delay) — each a tile with an amount ring, opening a modal with its parameters and an SVG response curve. A stereo-linked master **limiter** is the final stage.\n- **Per-grain filter** — each grain draws its own resonant lowpass cutoff at spawn from a center ± spread (octaves) band — the classic \"every grain its own color\" move; the dial's top is **Off** for an exact, bit-identical bypass.\n- **Sources** — ten deterministic demo sounds (random pick), audio-file import, and a 20-second live rolling buffer with **Freeze** and **Clear**.\n- **Performance** — large XY surface, draggable waveform position, a multi-lane **motion recorder** — hit record and every dial or macro you move becomes a looping lane (up to 4 per take), and seeded **Mutate** with bounded **Undo**.\n- **Play it** — polyphonic chromatic playing from the computer keyboard (Ableton layout) and **Web MIDI** (note on/off + velocity), up to 8 voices with oldest-note stealing.\n- **Shatter sequencer** — BPM, straight/dotted/triplet divisions, and a deterministic 16-step lane — gate, probability, pitch offset, reverse, ratchet, plus per-step position offset and size scale — with global swing.\n- **Presets \u0026 sync** — 10 curated factory presets plus user presets in IndexedDB (versioned, motion + source-label aware, with a relink prompt); optional **Ableton Link** tempo sync via the companion **mpump** link-bridge; optional **mbus publish** — the \"Bus\" toggle next to Link offers the master output to the [mbus](https://mbus.mpump.live) patchbay as a source named `mgrains` (tab-to-tab WebRTC via the same bridge, off by default, harmless without it).\n- **PWA** — installable manifest and a network-first service worker that precaches the hashed app assets (full offline use after one visit, deploy-safe updates).\n\n## Run locally\n\n```bash\nnpm install\nnpm run dev\n```\n\nOpen the URL Vite prints and click **Start audio** (browser audio requires a user gesture). **Use headphones before enabling Live input** to avoid feedback.\n\n## Scripts\n\n| Script | Purpose |\n| --- | --- |\n| `npm run dev` | Vite dev server with HMR |\n| `npm run build` | Type-check (`tsc -b`) and production build |\n| `npm run preview` | Serve the production build locally |\n| `npm run lint` | ESLint |\n| `npm run test` | Vitest (run once) |\n| `npm run test:watch` | Vitest in watch mode |\n| `npm run typecheck` | Type-check without emit |\n| `npm run check` | **lint + test + build** (the full gate) |\n\n## Controls\n\n**Mouse / touch / pen** — drag the waveform to set Position; drag the XY surface for Position × Spray; all knobs have keyboard-accessible slider alternatives.\n\n**Computer keyboard** (when **Play keys** is on — other shortcuts are suppressed to avoid collisions):\n\n| Keys | Action |\n| --- | --- |\n| `A S D F G H J K L ;` | white notes (chromatic, polyphonic) |\n| `W E T Y U O P` | black notes |\n| `Z` / `X` | octave down / up |\n| `C` / `V` | output level down / up |\n\n**MIDI** — any connected device plays the same voices as soon as audio is running (no toggle needed); note velocity scales each voice's level. MIDI is an optional enhancement; the app is fully usable without it.\n\n## Architecture\n\n```text\nmain thread                         audio thread (AudioWorklet)\n───────────                         ───────────────────────────\nApp.tsx ── patch/notes ──▶ AudioEngine ── postMessage ──▶ granular.worklet.ts\n  │  (sanitized GrainPatch,            (AudioContext graph,        │\n  │   {offset,velocity} voices)         live-input buffer)         ▼\n  ◀────────── telemetry (~30 Hz) ─────────────────────────  GranularCore (pure DSP)\n                                                              · fixed 64-grain pool\n                                                              · seeded RNG (deterministic)\n                                                              · per-sample param smoothing\n                                                              · FX chain → master limiter\n```\n\n- **The worklet owns all scheduling.** There is no UI-timer grain scheduling.\n- **`GranularCore`** is framework-free, deterministic (seeded `XorShift32`), and allocation-conscious (effects expose an allocation-free `processInto`).\n- **Parameter ownership:** grain-local values are captured at grain birth; continuous values (output, FX amounts) are one-pole smoothed; mode changes fade through true silence.\n\n## Verification\n\n```bash\nnpm run check   # lint + 481 tests + production build\n```\n\nTests are deterministic and live next to the code (DSP core, effects, contracts, schedulers, RNG, windows, presets, instrument, transport). Note: Vitest runs in a Node environment, so React components and live audio are covered by manual QA below, not unit tests.\n\n## Permissions \u0026 privacy\n\n- **Microphone / line input** is requested only when you enable **Live input**, and degrades gracefully if denied.\n- **Web MIDI** is requested once you start audio (a user gesture), and is optional.\n- Everything is local: presets and any data live in your browser's IndexedDB. No accounts, no network, no telemetry.\n\n## Browser notes \u0026 limitations\n\n- `AudioWorklet` needs a secure context in production (`localhost` is fine for dev).\n- The engine uses the real `AudioContext.sampleRate` and never assumes 44.1/48 kHz.\n- Headless/automated browsers may expose no audio device; the app times out with an actionable error instead of hanging.\n- A PWA install does **not** provide background or lock-screen audio.\n- Ableton Link sync requires the companion **mpump link-bridge** running locally (`ws://localhost:19876`); without it the Link panel simply shows \"searching\".\n- **mbus publish** rides the same link-bridge; without it the \"Bus\" toggle just keeps retrying quietly and nothing is published. Audio flows tab-to-tab over WebRTC and never leaves the machine.\n\n## Physical-device QA checklist\n\nAutomated tests cover the DSP and logic; the following must be checked by ear on real hardware before a release:\n\n- [ ] Audible stereo playback in Chrome, Safari, and Firefox (headphones/controlled output)\n- [ ] All four direct controls (Grain Size, Density, Position, Spray) respond cleanly\n- [ ] Bloom ↔ Shatter switch is click-free; both modes are audibly distinct\n- [ ] Each macro (Cloud/Drift/Warmth/Space, Chop/Scatter/Crush/Repeat) sweeps musically\n- [ ] Each FX (Drive…Repeat) engages without artefacts; master limiter holds the ceiling\n- [ ] Polyphonic chords from the computer keyboard and from a MIDI controller (with velocity)\n- [ ] Live input: built-in mic, physical line-in, USB interface; permission denial; Freeze; Clear; device disconnect; no runaway feedback\n- [ ] Motion record → play → clear behaves and stays in sync\n- [ ] Presets: save, reload, delete; factory presets load; relink prompt on source mismatch\n- [ ] Ableton Link locks tempo with the mpump bridge + another peer\n- [ ] Mobile Safari / Android: layout usable, audio starts, no thermal/stability surprises\n- [ ] `prefers-reduced-motion`: flying particles replaced by stable markers\n- [ ] Offline: loads after first successful visit (service worker)\n\n## Repository map\n\n```text\nsrc/\n  App.tsx                       integrated UI + performance wiring\n  audio/\n    contracts.ts                canonical GrainPatch, ranges, messages, sanitize\n    AudioEngine.ts              AudioContext graph + worklet lifecycle\n    granular.worklet.ts         real-time worklet adapter\n    demoSource.ts               ten deterministic demo sources + peaks\n    macros.ts                   macro → parameter mappings + Link model\n    mutate.ts                   seeded deterministic patch variation\n    factoryPresets.ts           10 curated factory presets\n    dsp/\n      GranularCore.ts           grain engine + FX chain + master limiter\n      rng.ts, windows.ts        seeded RNG, grain envelopes\n      shatterTiming.ts          tempo/division → sample frames\n      StereoCircularBuffer.ts   live rolling buffer\n      effects.ts                drive, bitcrush, sample-rate reduce, one-pole, delay line\n      reverb.ts tempoDelay.ts tape.ts formant.ts ringMod.ts comb.ts wow.ts sub.ts limiter.ts\n  instrument/\n    qwertyKeymap.ts             Ableton computer-keyboard layout\n    voiceAllocator.ts           8-voice allocation with oldest-steal\n    midi.ts                     Web MIDI parsing + input wrapper\n  performance/motion.ts         deterministic one-lane motion recorder\n  storage/presets.ts            versioned preset serialize/migrate + IndexedDB store\n  transport/abletonLink.ts      Ableton Link bridge WebSocket client\n  transport/mbus/               vendored mbus-client (patchbay publish; see its index.ts header)\n  components/                   waveform, XY pad, parameter/macro/preset controls\n    fx/                         FX bar, modal, SVG curves, FX rack\npublic/                         manifest, service worker, app icon, CNAME\n.github/workflows/              GitHub Pages deploy\n```\n\n## Progressive Web App\n\n`public/manifest.webmanifest` + `public/sw.js` make `mgrains` installable. The service worker is **network-first for navigations** (so a deploy never serves a stale shell) and cache-first for hashed assets. At install it precaches the shell plus the content-hashed build assets listed in a generated `precache-manifest.json` (emitted by a small Vite plugin), so the full app — including the audio worklet — works offline after a single successful load.\n\n## Deployment\n\nPushes to `main` are deployed by GitHub Actions (`.github/workflows/deploy-pages.yml`) to GitHub Pages, served at the custom domain **[mgrains.mpump.live](https://mgrains.mpump.live)**. Because it's a root-domain deploy, the build is **root-relative** (no base-path override) and `public/CNAME` pins the domain across deploys. The workflow self-enables Pages (`configure-pages` with `enablement: true`); set **Settings → Pages → Source** to **GitHub Actions** if prompted.\n\n## License\n\n[GNU Affero General Public License v3.0 or later](./LICENSE). All factory sources are generated by repository code — no third-party audio is bundled.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgdamdam%2Fmgrains","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgdamdam%2Fmgrains","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgdamdam%2Fmgrains/lists"}