From 7efa5a708f472b1a89685be22085b4e8f25c1fbb Mon Sep 17 00:00:00 2001 From: brsbl <57682038+brsbl@users.noreply.github.com> Date: Sun, 4 Oct 2026 04:06:59 -0700 Subject: [PATCH] Fix LM-919 review follow-ups Engine - Start and Stop share one pending start; Stop cancels hits queued in the lookahead, and resume failures no longer escape. - Choked kicks and hats stop their sources; a stalled tick skips missed steps instead of stacking them; the hidden-tab playhead queue stays small; a held chord stops droning when no synth step can fire. - Firefox keeps the synth's decay when a note is released early. Controls and session - Changes that alter nothing no longer add undo steps. - The first held key fills a rhythm that plays at the current SPEED. - LED groups use arrow keys; Undo keeps focus; Escape cancels a typed tempo; trackpad swipes move knobs smoothly; drags don't select text. - Screen readers hear Start/Stop once and the octave-shifted key names. - The share link trails edits by 250 ms and survives browser limits. Docs and assets - Remove the unused 808 samples and fix stale README, docs, and comments. Co-Authored-By: Claude Opus 5.5 --- .claude/CLAUDE.md | 2 +- README.md | 9 ++-- docs/ROADMAP.md | 2 + docs/TECH_STACK.md | 2 + docs/design/DESIGN_SYSTEM.md | 2 + eslint.config.js | 2 +- public/808 Samples/bass (1).wav | 3 -- public/808 Samples/bass (10).wav | 3 -- public/808 Samples/bass (11).wav | 3 -- public/808 Samples/bass (12).wav | 3 -- public/808 Samples/bass (13).wav | 3 -- public/808 Samples/bass (14).wav | 3 -- public/808 Samples/bass (15).wav | 3 -- public/808 Samples/bass (16).wav | 3 -- public/808 Samples/bass (17).wav | 3 -- public/808 Samples/bass (18).wav | 3 -- public/808 Samples/bass (19).WAV | 3 -- public/808 Samples/bass (2).WAV | 3 -- public/808 Samples/bass (20).WAV | 3 -- public/808 Samples/bass (21).wav | 3 -- public/808 Samples/bass (22).wav | 3 -- public/808 Samples/bass (23).wav | 3 -- public/808 Samples/bass (3).WAV | 3 -- public/808 Samples/bass (4).WAV | 3 -- public/808 Samples/bass (5).WAV | 3 -- public/808 Samples/bass (6).WAV | 3 -- public/808 Samples/bass (7).wav | 3 -- public/808 Samples/bass (8).wav | 3 -- public/808 Samples/bass (9).wav | 3 -- public/808 Samples/bass.wav | 3 -- public/808 Samples/clap (1).wav | 3 -- public/808 Samples/clap (10).wav | 3 -- public/808 Samples/clap (11).wav | 3 -- public/808 Samples/clap (12).wav | 3 -- public/808 Samples/clap (13).WAV | 3 -- public/808 Samples/clap (14).WAV | 3 -- public/808 Samples/clap (15).wav | 3 -- public/808 Samples/clap (2).wav | 3 -- public/808 Samples/clap (3).WAV | 3 -- public/808 Samples/clap (4).WAV | 3 -- public/808 Samples/clap (5).WAV | 3 -- public/808 Samples/clap (6).WAV | 3 -- public/808 Samples/clap (7).WAV | 3 -- public/808 Samples/clap (8).wav | 3 -- public/808 Samples/clap (9).wav | 3 -- public/808 Samples/clap.wav | 3 -- public/808 Samples/cymbal (1).wav | 3 -- public/808 Samples/cymbal (2).wav | 3 -- public/808 Samples/cymbal (3).wav | 3 -- public/808 Samples/cymbal (4).WAV | 3 -- public/808 Samples/cymbal.wav | 3 -- public/808 Samples/hi hat (1).WAV | 3 -- public/808 Samples/hi hat (10).wav | 3 -- public/808 Samples/hi hat (11).wav | 3 -- public/808 Samples/hi hat (12).wav | 3 -- public/808 Samples/hi hat (13).WAV | 3 -- public/808 Samples/hi hat (14).wav | 3 -- public/808 Samples/hi hat (15).wav | 3 -- public/808 Samples/hi hat (16).wav | 3 -- public/808 Samples/hi hat (17).wav | 3 -- public/808 Samples/hi hat (18).wav | 3 -- public/808 Samples/hi hat (19).wav | 3 -- public/808 Samples/hi hat (2).WAV | 3 -- public/808 Samples/hi hat (20).wav | 3 -- public/808 Samples/hi hat (21).WAV | 3 -- public/808 Samples/hi hat (22).wav | 3 -- public/808 Samples/hi hat (23).wav | 3 -- public/808 Samples/hi hat (24).wav | 3 -- public/808 Samples/hi hat (25).wav | 3 -- public/808 Samples/hi hat (26).wav | 3 -- public/808 Samples/hi hat (27).wav | 3 -- public/808 Samples/hi hat (28).WAV | 3 -- public/808 Samples/hi hat (29).WAV | 3 -- public/808 Samples/hi hat (3).wav | 3 -- public/808 Samples/hi hat (30).wav | 3 -- public/808 Samples/hi hat (31).wav | 3 -- public/808 Samples/hi hat (32).wav | 3 -- public/808 Samples/hi hat (33).wav | 3 -- public/808 Samples/hi hat (34).wav | 3 -- public/808 Samples/hi hat (35).wav | 3 -- public/808 Samples/hi hat (36).wav | 3 -- public/808 Samples/hi hat (37).WAV | 3 -- public/808 Samples/hi hat (38).wav | 3 -- public/808 Samples/hi hat (39).wav | 3 -- public/808 Samples/hi hat (4).wav | 3 -- public/808 Samples/hi hat (5).WAV | 3 -- public/808 Samples/hi hat (6).wav | 3 -- public/808 Samples/hi hat (7).WAV | 3 -- public/808 Samples/hi hat (8).WAV | 3 -- public/808 Samples/hi hat (9).wav | 3 -- public/808 Samples/hi hat.wav | 3 -- public/808 Samples/kick (1).WAV | 3 -- public/808 Samples/kick (10).wav | 3 -- public/808 Samples/kick (11).wav | 3 -- public/808 Samples/kick (12).wav | 3 -- public/808 Samples/kick (13).wav | 3 -- public/808 Samples/kick (14).wav | 3 -- public/808 Samples/kick (15).wav | 3 -- public/808 Samples/kick (16).wav | 3 -- public/808 Samples/kick (17).wav | 3 -- public/808 Samples/kick (18).WAV | 3 -- public/808 Samples/kick (19).WAV | 3 -- public/808 Samples/kick (2).wav | 3 -- public/808 Samples/kick (20).WAV | 3 -- public/808 Samples/kick (21).WAV | 3 -- public/808 Samples/kick (22).WAV | 3 -- public/808 Samples/kick (3).wav | 3 -- public/808 Samples/kick (4).wav | 3 -- public/808 Samples/kick (5).wav | 3 -- public/808 Samples/kick (6).wav | 3 -- public/808 Samples/kick (7).wav | 3 -- public/808 Samples/kick (8).wav | 3 -- public/808 Samples/kick (9).wav | 3 -- public/808 Samples/kick.WAV | 3 -- public/808 Samples/snare (1).WAV | 3 -- public/808 Samples/snare (10).wav | 3 -- public/808 Samples/snare (11).wav | 3 -- public/808 Samples/snare (12).wav | 3 -- public/808 Samples/snare (13).wav | 3 -- public/808 Samples/snare (14).wav | 3 -- public/808 Samples/snare (15).wav | 3 -- public/808 Samples/snare (16).wav | 3 -- public/808 Samples/snare (17).wav | 3 -- public/808 Samples/snare (18).wav | 3 -- public/808 Samples/snare (19).wav | 3 -- public/808 Samples/snare (2).WAV | 3 -- public/808 Samples/snare (20).wav | 3 -- public/808 Samples/snare (21).wav | 3 -- public/808 Samples/snare (22).wav | 3 -- public/808 Samples/snare (23).wav | 3 -- public/808 Samples/snare (24).WAV | 3 -- public/808 Samples/snare (25).WAV | 3 -- public/808 Samples/snare (26).WAV | 3 -- public/808 Samples/snare (27).WAV | 3 -- public/808 Samples/snare (28).WAV | 3 -- public/808 Samples/snare (29).WAV | 3 -- public/808 Samples/snare (3).WAV | 3 -- public/808 Samples/snare (30).WAV | 3 -- public/808 Samples/snare (31).WAV | 3 -- public/808 Samples/snare (32).WAV | 3 -- public/808 Samples/snare (33).WAV | 3 -- public/808 Samples/snare (34).WAV | 3 -- public/808 Samples/snare (35).wav | 3 -- public/808 Samples/snare (36).wav | 3 -- public/808 Samples/snare (37).wav | 3 -- public/808 Samples/snare (38).wav | 3 -- public/808 Samples/snare (39).wav | 3 -- public/808 Samples/snare (4).WAV | 3 -- public/808 Samples/snare (40).wav | 3 -- public/808 Samples/snare (41).wav | 3 -- public/808 Samples/snare (42).wav | 3 -- public/808 Samples/snare (43).wav | 3 -- public/808 Samples/snare (44).wav | 3 -- public/808 Samples/snare (45).wav | 3 -- public/808 Samples/snare (46).WAV | 3 -- public/808 Samples/snare (47).wav | 3 -- public/808 Samples/snare (48).wav | 3 -- public/808 Samples/snare (5).WAV | 3 -- public/808 Samples/snare (6).WAV | 3 -- public/808 Samples/snare (7).WAV | 3 -- public/808 Samples/snare (8).wav | 3 -- public/808 Samples/snare (9).wav | 3 -- public/808 Samples/snare.WAV | 3 -- public/808 Samples/tom (1).WAV | 3 -- public/808 Samples/tom (10).WAV | 3 -- public/808 Samples/tom (11).wav | 3 -- public/808 Samples/tom (12).wav | 3 -- public/808 Samples/tom (13).wav | 3 -- public/808 Samples/tom (2).WAV | 3 -- public/808 Samples/tom (3).wav | 3 -- public/808 Samples/tom (4).wav | 3 -- public/808 Samples/tom (5).wav | 3 -- public/808 Samples/tom (6).wav | 3 -- public/808 Samples/tom (7).wav | 3 -- public/808 Samples/tom (8).WAV | 3 -- public/808 Samples/tom (9).WAV | 3 -- public/808 Samples/tom.WAV | 3 -- public/README.md | 5 --- src/App.tsx | 33 ++++++++++----- src/engine/clock.test.ts | 61 +++++++++++++++++++++++++++ src/engine/clock.ts | 19 ++++++++- src/engine/drums.ts | 59 +++++++++++++++++++------- src/engine/engine.ts | 50 +++++++++++++++++++--- src/engine/mixer.ts | 2 +- src/engine/synth.test.ts | 27 ++++++++++++ src/engine/synth.ts | 38 +++++++++++++++-- src/session/commands.test.ts | 8 +++- src/session/commands.ts | 6 +-- src/session/library.ts | 12 ++++++ src/session/store.test.ts | 21 ++++++++++ src/session/store.ts | 4 +- src/session/url.test.ts | 27 +++++++++++- src/session/url.ts | 10 ++++- src/ui/Keyboard.tsx | 6 +-- src/ui/LoopMachine.tsx | 3 +- src/ui/TempoControl.tsx | 10 +++-- src/ui/Transport.module.css | 2 +- src/ui/controls/Fader.module.css | 3 ++ src/ui/controls/Knob.module.css | 3 ++ src/ui/controls/LedButton.module.css | 4 +- src/ui/controls/LedButton.tsx | 62 +++++++++++++++++++++++++--- src/ui/controls/useNotchedControl.ts | 25 ++++++++++- 202 files changed, 445 insertions(+), 587 deletions(-) delete mode 100644 public/808 Samples/bass (1).wav delete mode 100644 public/808 Samples/bass (10).wav delete mode 100644 public/808 Samples/bass (11).wav delete mode 100644 public/808 Samples/bass (12).wav delete mode 100644 public/808 Samples/bass (13).wav delete mode 100644 public/808 Samples/bass (14).wav delete mode 100644 public/808 Samples/bass (15).wav delete mode 100644 public/808 Samples/bass (16).wav delete mode 100644 public/808 Samples/bass (17).wav delete mode 100644 public/808 Samples/bass (18).wav delete mode 100644 public/808 Samples/bass (19).WAV delete mode 100644 public/808 Samples/bass (2).WAV delete mode 100644 public/808 Samples/bass (20).WAV delete mode 100644 public/808 Samples/bass (21).wav delete mode 100644 public/808 Samples/bass (22).wav delete mode 100644 public/808 Samples/bass (23).wav delete mode 100644 public/808 Samples/bass (3).WAV delete mode 100644 public/808 Samples/bass (4).WAV delete mode 100644 public/808 Samples/bass (5).WAV delete mode 100644 public/808 Samples/bass (6).WAV delete mode 100644 public/808 Samples/bass (7).wav delete mode 100644 public/808 Samples/bass (8).wav delete mode 100644 public/808 Samples/bass (9).wav delete mode 100644 public/808 Samples/bass.wav delete mode 100644 public/808 Samples/clap (1).wav delete mode 100644 public/808 Samples/clap (10).wav delete mode 100644 public/808 Samples/clap (11).wav delete mode 100644 public/808 Samples/clap (12).wav delete mode 100644 public/808 Samples/clap (13).WAV delete mode 100644 public/808 Samples/clap (14).WAV delete mode 100644 public/808 Samples/clap (15).wav delete mode 100644 public/808 Samples/clap (2).wav delete mode 100644 public/808 Samples/clap (3).WAV delete mode 100644 public/808 Samples/clap (4).WAV delete mode 100644 public/808 Samples/clap (5).WAV delete mode 100644 public/808 Samples/clap (6).WAV delete mode 100644 public/808 Samples/clap (7).WAV delete mode 100644 public/808 Samples/clap (8).wav delete mode 100644 public/808 Samples/clap (9).wav delete mode 100644 public/808 Samples/clap.wav delete mode 100644 public/808 Samples/cymbal (1).wav delete mode 100644 public/808 Samples/cymbal (2).wav delete mode 100644 public/808 Samples/cymbal (3).wav delete mode 100644 public/808 Samples/cymbal (4).WAV delete mode 100644 public/808 Samples/cymbal.wav delete mode 100644 public/808 Samples/hi hat (1).WAV delete mode 100644 public/808 Samples/hi hat (10).wav delete mode 100644 public/808 Samples/hi hat (11).wav delete mode 100644 public/808 Samples/hi hat (12).wav delete mode 100644 public/808 Samples/hi hat (13).WAV delete mode 100644 public/808 Samples/hi hat (14).wav delete mode 100644 public/808 Samples/hi hat (15).wav delete mode 100644 public/808 Samples/hi hat (16).wav delete mode 100644 public/808 Samples/hi hat (17).wav delete mode 100644 public/808 Samples/hi hat (18).wav delete mode 100644 public/808 Samples/hi hat (19).wav delete mode 100644 public/808 Samples/hi hat (2).WAV delete mode 100644 public/808 Samples/hi hat (20).wav delete mode 100644 public/808 Samples/hi hat (21).WAV delete mode 100644 public/808 Samples/hi hat (22).wav delete mode 100644 public/808 Samples/hi hat (23).wav delete mode 100644 public/808 Samples/hi hat (24).wav delete mode 100644 public/808 Samples/hi hat (25).wav delete mode 100644 public/808 Samples/hi hat (26).wav delete mode 100644 public/808 Samples/hi hat (27).wav delete mode 100644 public/808 Samples/hi hat (28).WAV delete mode 100644 public/808 Samples/hi hat (29).WAV delete mode 100644 public/808 Samples/hi hat (3).wav delete mode 100644 public/808 Samples/hi hat (30).wav delete mode 100644 public/808 Samples/hi hat (31).wav delete mode 100644 public/808 Samples/hi hat (32).wav delete mode 100644 public/808 Samples/hi hat (33).wav delete mode 100644 public/808 Samples/hi hat (34).wav delete mode 100644 public/808 Samples/hi hat (35).wav delete mode 100644 public/808 Samples/hi hat (36).wav delete mode 100644 public/808 Samples/hi hat (37).WAV delete mode 100644 public/808 Samples/hi hat (38).wav delete mode 100644 public/808 Samples/hi hat (39).wav delete mode 100644 public/808 Samples/hi hat (4).wav delete mode 100644 public/808 Samples/hi hat (5).WAV delete mode 100644 public/808 Samples/hi hat (6).wav delete mode 100644 public/808 Samples/hi hat (7).WAV delete mode 100644 public/808 Samples/hi hat (8).WAV delete mode 100644 public/808 Samples/hi hat (9).wav delete mode 100644 public/808 Samples/hi hat.wav delete mode 100644 public/808 Samples/kick (1).WAV delete mode 100644 public/808 Samples/kick (10).wav delete mode 100644 public/808 Samples/kick (11).wav delete mode 100644 public/808 Samples/kick (12).wav delete mode 100644 public/808 Samples/kick (13).wav delete mode 100644 public/808 Samples/kick (14).wav delete mode 100644 public/808 Samples/kick (15).wav delete mode 100644 public/808 Samples/kick (16).wav delete mode 100644 public/808 Samples/kick (17).wav delete mode 100644 public/808 Samples/kick (18).WAV delete mode 100644 public/808 Samples/kick (19).WAV delete mode 100644 public/808 Samples/kick (2).wav delete mode 100644 public/808 Samples/kick (20).WAV delete mode 100644 public/808 Samples/kick (21).WAV delete mode 100644 public/808 Samples/kick (22).WAV delete mode 100644 public/808 Samples/kick (3).wav delete mode 100644 public/808 Samples/kick (4).wav delete mode 100644 public/808 Samples/kick (5).wav delete mode 100644 public/808 Samples/kick (6).wav delete mode 100644 public/808 Samples/kick (7).wav delete mode 100644 public/808 Samples/kick (8).wav delete mode 100644 public/808 Samples/kick (9).wav delete mode 100644 public/808 Samples/kick.WAV delete mode 100644 public/808 Samples/snare (1).WAV delete mode 100644 public/808 Samples/snare (10).wav delete mode 100644 public/808 Samples/snare (11).wav delete mode 100644 public/808 Samples/snare (12).wav delete mode 100644 public/808 Samples/snare (13).wav delete mode 100644 public/808 Samples/snare (14).wav delete mode 100644 public/808 Samples/snare (15).wav delete mode 100644 public/808 Samples/snare (16).wav delete mode 100644 public/808 Samples/snare (17).wav delete mode 100644 public/808 Samples/snare (18).wav delete mode 100644 public/808 Samples/snare (19).wav delete mode 100644 public/808 Samples/snare (2).WAV delete mode 100644 public/808 Samples/snare (20).wav delete mode 100644 public/808 Samples/snare (21).wav delete mode 100644 public/808 Samples/snare (22).wav delete mode 100644 public/808 Samples/snare (23).wav delete mode 100644 public/808 Samples/snare (24).WAV delete mode 100644 public/808 Samples/snare (25).WAV delete mode 100644 public/808 Samples/snare (26).WAV delete mode 100644 public/808 Samples/snare (27).WAV delete mode 100644 public/808 Samples/snare (28).WAV delete mode 100644 public/808 Samples/snare (29).WAV delete mode 100644 public/808 Samples/snare (3).WAV delete mode 100644 public/808 Samples/snare (30).WAV delete mode 100644 public/808 Samples/snare (31).WAV delete mode 100644 public/808 Samples/snare (32).WAV delete mode 100644 public/808 Samples/snare (33).WAV delete mode 100644 public/808 Samples/snare (34).WAV delete mode 100644 public/808 Samples/snare (35).wav delete mode 100644 public/808 Samples/snare (36).wav delete mode 100644 public/808 Samples/snare (37).wav delete mode 100644 public/808 Samples/snare (38).wav delete mode 100644 public/808 Samples/snare (39).wav delete mode 100644 public/808 Samples/snare (4).WAV delete mode 100644 public/808 Samples/snare (40).wav delete mode 100644 public/808 Samples/snare (41).wav delete mode 100644 public/808 Samples/snare (42).wav delete mode 100644 public/808 Samples/snare (43).wav delete mode 100644 public/808 Samples/snare (44).wav delete mode 100644 public/808 Samples/snare (45).wav delete mode 100644 public/808 Samples/snare (46).WAV delete mode 100644 public/808 Samples/snare (47).wav delete mode 100644 public/808 Samples/snare (48).wav delete mode 100644 public/808 Samples/snare (5).WAV delete mode 100644 public/808 Samples/snare (6).WAV delete mode 100644 public/808 Samples/snare (7).WAV delete mode 100644 public/808 Samples/snare (8).wav delete mode 100644 public/808 Samples/snare (9).wav delete mode 100644 public/808 Samples/snare.WAV delete mode 100644 public/808 Samples/tom (1).WAV delete mode 100644 public/808 Samples/tom (10).WAV delete mode 100644 public/808 Samples/tom (11).wav delete mode 100644 public/808 Samples/tom (12).wav delete mode 100644 public/808 Samples/tom (13).wav delete mode 100644 public/808 Samples/tom (2).WAV delete mode 100644 public/808 Samples/tom (3).wav delete mode 100644 public/808 Samples/tom (4).wav delete mode 100644 public/808 Samples/tom (5).wav delete mode 100644 public/808 Samples/tom (6).wav delete mode 100644 public/808 Samples/tom (7).wav delete mode 100644 public/808 Samples/tom (8).WAV delete mode 100644 public/808 Samples/tom (9).WAV delete mode 100644 public/808 Samples/tom.WAV delete mode 100644 public/README.md create mode 100644 src/engine/clock.test.ts create mode 100644 src/engine/synth.test.ts diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 4bd7140..11f3ec4 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -17,7 +17,7 @@ src/ui/ React components, CSS Modules, tokens.css, vendored fonts - **One session document** is the source of truth. UI dispatches `Command`s; `applyCommand` is pure and tested. The store keeps undo history (knob drags coalesce into one step), notifies the engine (`engine.apply`), and rewrites the share link. - **Exploring:** `src/session/library.ts` holds the ready-made patterns (the app opens on the first) and the chords. Holding the first key with no synth steps fills in a default rhythm. -- **Engine:** `Clock` schedules 100 ms ahead, ticked from a Worker so background tabs keep time. The sound is aimed at dirty French electro (Justice, Daft Punk, a little disco). `mixer.ts` builds the whole signal chain once, shared by the live engine and offline renders. Drums are synthesized per hit in `drums.ts` (808 kick with a 909-style snap, driven 909 snare, clean disco open hat; no samples), each with a DECAY tail multiplier, and run through a `ChannelStrip` (volume → mix bus, with a send to the stereo ping-pong delay in `delay.ts`). The `Synth` is a seven-voice detuned stereo unison plus a sub, through a plucked low-pass, light drive, and a 7 kHz high cut; it starts voices at scheduled times and releases the previous arp note at the next note's start. The synth, delay, and reverb run through the `Pump`, which ducks on every kick like a sidechain; drums bypass it. Everything meets in the mix bus (gentle glue compressor with headroom) before a safety limiter. The playhead (`step`, fired `note`) is published when each step actually sounds. +- **Engine:** `Clock` schedules 100 ms ahead, ticked from a Worker so background tabs keep time. The sound is aimed at dirty French electro (Justice, Daft Punk, a little disco). `mixer.ts` builds the whole signal chain once. Drums are synthesized per hit in `drums.ts` (808 kick with a 909-style snap, driven 909 snare, clean disco open hat; no samples), each with a DECAY tail multiplier, and run through a `ChannelStrip` (volume → mix bus, with a send to the stereo ping-pong delay in `delay.ts`). The `Synth` is a seven-voice detuned stereo unison plus a sub, through a plucked low-pass, light drive, and a 7 kHz high cut; it starts voices at scheduled times and releases the previous arp note at the next note's start. The synth, delay, and reverb run through the `Pump`, which ducks on every kick like a sidechain; drums bypass it. Everything meets in the mix bus (gentle glue compressor with headroom) before a safety limiter. The playhead (`step`, fired `note`) is published when each step actually sounds. - **Share link (v2):** `?s=2~~~…~syn.<…>`, keyed by drum id. See `src/session/url.ts`. Malformed parts fall back to defaults. ## Adding a drum diff --git a/README.md b/README.md index e687fc0..9a4a57e 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,10 @@ A drum machine + synth in your browser, styled after the Roland TR-909. Program ## Features -- **Drums:** 16-step sequencer for hi-hat, snare, and kick, synthesized 909-style in the browser (no samples), each with volume, reverb, and tone knobs -- **Synth:** arpeggiator over a two-octave keyboard, with a detuned two-oscillator voice, a sub, and a plucked filter. Keys choose which notes play; the synth's step row chooses when. Direction (up, down, up-down), speed (1/4, 1/8, 1/16), and four waveforms -- **Playback:** on steps carry a soft tint of their instrument's color and light up fully when they fire; a beat band marks the four beats; the synth's key flashes with its step +- **Drums:** 16-step sequencer for an 808 kick, a 909 snare, and a disco open hat, synthesized in the browser (no samples), each with volume, delay, and decay knobs +- **Synth:** arpeggiator over a two-octave keyboard, with a seven-voice detuned stereo unison, a sub, and a plucked filter. Keys choose which notes play; the synth's step row chooses when. Direction (up, down, up-down), speed (1/4, 1/8, 1/16), four waveforms, octave, and chord keys +- **Exploring:** six house patterns to start from, and undo (⌘Z / Ctrl+Z) for every change +- **Playback:** on steps carry a soft tint of their instrument's color and light up fully when they fire; a beat band marks the four beats; the note playing lights its key's LED - **Controls:** knobs click to 11 notches and the fader to 9. Drag, click a notch, scroll, or use the arrow keys - **Computer keyboard:** A–C and W–\ toggle notes, like a piano layout - **Share links:** the URL holds the whole loop, drums and synth included @@ -25,7 +26,7 @@ Then open http://localhost:3000. ``` src/ instruments.ts registry: drums (id, label, voice, color) and keyboard notes - engine/ audio engine, no React: clock, channel strips, reverb, drum voices, synth, arpeggiator + engine/ audio engine, no React: clock, mixer, channel strips, delay, pump, reverb, drum voices, synth, arpeggiator session/ session types, commands, store, and share-link format ui/ React components, design tokens, and fonts ``` diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index cf6b73b..d80297e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,5 +1,7 @@ # Loop Machine Style Guide Implementation Roadmap +> **Superseded.** This plan predates the LM-919 rewrite (TypeScript, CSS Modules, no Tailwind). See `README.md` and `.claude/CLAUDE.md` for the current stack and design. + ## Overview This roadmap outlines the implementation strategy for transforming the Loop Machine into a fully polished, accessible, and responsive application based on **The 1984 Digital Control System** documented in `./design/DESIGN_SYSTEM.md`. diff --git a/docs/TECH_STACK.md b/docs/TECH_STACK.md index 2fedc0f..c4b2d63 100644 --- a/docs/TECH_STACK.md +++ b/docs/TECH_STACK.md @@ -1,5 +1,7 @@ # Tech Stack Evaluation +> **Superseded.** This plan predates the LM-919 rewrite (TypeScript, CSS Modules, no Tailwind). See `README.md` and `.claude/CLAUDE.md` for the current stack and design. + ## Current Stack | Layer | Choice | Status | |-------|--------|--------| diff --git a/docs/design/DESIGN_SYSTEM.md b/docs/design/DESIGN_SYSTEM.md index 6f8ee80..f4bb629 100644 --- a/docs/design/DESIGN_SYSTEM.md +++ b/docs/design/DESIGN_SYSTEM.md @@ -1,5 +1,7 @@ # The 1984 Digital Control System +> **Superseded.** This plan predates the LM-919 rewrite (TypeScript, CSS Modules, no Tailwind). See `README.md` and `.claude/CLAUDE.md` for the current stack and design. + A unified design language for retro-inspired musical interfaces—drum machines, synth modules, sequencers, and hybrid workstations. It blends the visual DNA of Roland TR-707, Oberheim OB-series, and early digital lab equipment into a clean, modern, production-ready UI system. ![1984 Digital Control System](./1984-digital-control-system.png) diff --git a/eslint.config.js b/eslint.config.js index ec3ef7a..518bbb4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -20,7 +20,7 @@ export default tseslint.config( }, }, { - // The engine stays framework-free so the web app, Mixtape, and tests can share it. + // The engine stays framework-free so the web app and tests can share it. files: ['src/engine/**/*.ts'], rules: { 'no-restricted-imports': ['error', { patterns: [{ group: ['react', 'react-dom', 'react/*'], message: 'The engine must not depend on React.' }] }], diff --git a/public/808 Samples/bass (1).wav b/public/808 Samples/bass (1).wav deleted file mode 100644 index 2c72cc8..0000000 --- a/public/808 Samples/bass (1).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:611a6fe4c0a67f7d9f5d119a4cb242dbd8ee6921672387ed17ffbc4c4d3d253a -size 184448 diff --git a/public/808 Samples/bass (10).wav b/public/808 Samples/bass (10).wav deleted file mode 100644 index a8c431d..0000000 --- a/public/808 Samples/bass (10).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b3825ea73d44be346bf6fc8f5d2d0742c6170b19a3bbee213fc5692615af4ca0 -size 341928 diff --git a/public/808 Samples/bass (11).wav b/public/808 Samples/bass (11).wav deleted file mode 100644 index 187aa36..0000000 --- a/public/808 Samples/bass (11).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:60f4fdf0bb0766776cbbfb10582e8c119ca0d00014f9072ed213f56fdee30f13 -size 6256 diff --git a/public/808 Samples/bass (12).wav b/public/808 Samples/bass (12).wav deleted file mode 100644 index 1227fd3..0000000 --- a/public/808 Samples/bass (12).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a80441b517bf8779b4d34dcee91ef9125e96ea527e0aacb02c5a6e4fd2288d66 -size 22228 diff --git a/public/808 Samples/bass (13).wav b/public/808 Samples/bass (13).wav deleted file mode 100644 index 02f04a5..0000000 --- a/public/808 Samples/bass (13).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cf932c8ea89d61f811645bccc884a865a7d0f19774feede822053061ad8eedbe -size 8984 diff --git a/public/808 Samples/bass (14).wav b/public/808 Samples/bass (14).wav deleted file mode 100644 index 6a13141..0000000 --- a/public/808 Samples/bass (14).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e5b51459641da17eeb9956ced1366407527d78c83d1fdf8b32e018f53dbb54ca -size 16144 diff --git a/public/808 Samples/bass (15).wav b/public/808 Samples/bass (15).wav deleted file mode 100644 index c9f45e5..0000000 --- a/public/808 Samples/bass (15).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:342b96023bc8f5f7c19e44b616f8659b2dbc70861ca799a23d370d3411ed9849 -size 119980 diff --git a/public/808 Samples/bass (16).wav b/public/808 Samples/bass (16).wav deleted file mode 100644 index 74ce904..0000000 --- a/public/808 Samples/bass (16).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2ae501241cc34927f2e8d37f01b9921f1bb4eba099ab857d9475107203800c2a -size 6204 diff --git a/public/808 Samples/bass (17).wav b/public/808 Samples/bass (17).wav deleted file mode 100644 index 0158bc6..0000000 --- a/public/808 Samples/bass (17).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1500ac0322daf3d8e9671941f6c7b44c6220310d3107a0f7d3e2f8e03a18ec56 -size 393320 diff --git a/public/808 Samples/bass (18).wav b/public/808 Samples/bass (18).wav deleted file mode 100644 index 9586624..0000000 --- a/public/808 Samples/bass (18).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:86e7c96f4bef70a60c3d70016270da40c5b21fd926bf270c644d37028505b5e9 -size 46764 diff --git a/public/808 Samples/bass (19).WAV b/public/808 Samples/bass (19).WAV deleted file mode 100644 index 5496b32..0000000 --- a/public/808 Samples/bass (19).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fbc980f0e306afccabba3f375dc0e530a91ba5cdda33e030058205c5a93f241a -size 63720 diff --git a/public/808 Samples/bass (2).WAV b/public/808 Samples/bass (2).WAV deleted file mode 100644 index 7e0bb23..0000000 --- a/public/808 Samples/bass (2).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2929f825db405543f8f1e1ef09e368d83421f030e249cb6a17e5a5ff34a882d6 -size 264646 diff --git a/public/808 Samples/bass (20).WAV b/public/808 Samples/bass (20).WAV deleted file mode 100644 index 0360f1b..0000000 --- a/public/808 Samples/bass (20).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:548df7e1658123fa5de21196c375e186f01fc32f3028ac65340b45246bd9e261 -size 136102 diff --git a/public/808 Samples/bass (21).wav b/public/808 Samples/bass (21).wav deleted file mode 100644 index 2cb801d..0000000 --- a/public/808 Samples/bass (21).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b6db3c09834270e1f7800d4f086c577972f1832e7dad2fe5a848f67e59d6473f -size 211468 diff --git a/public/808 Samples/bass (22).wav b/public/808 Samples/bass (22).wav deleted file mode 100644 index 112b475..0000000 --- a/public/808 Samples/bass (22).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:45856a27118c58ffcc4e813060270bfa03dd7d339671cf720edf61ebf080ca6d -size 246016 diff --git a/public/808 Samples/bass (23).wav b/public/808 Samples/bass (23).wav deleted file mode 100644 index 20b767e..0000000 --- a/public/808 Samples/bass (23).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:75b6cf2c133b3e799b349cb2fbcc5cc693ed870043986e2968627616b33c1c01 -size 227008 diff --git a/public/808 Samples/bass (3).WAV b/public/808 Samples/bass (3).WAV deleted file mode 100644 index 2a96f96..0000000 --- a/public/808 Samples/bass (3).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:77b2648133000406d5add8f5890051d2300c7a1b5015d9800ff954b7a77a8cea -size 44146 diff --git a/public/808 Samples/bass (4).WAV b/public/808 Samples/bass (4).WAV deleted file mode 100644 index caa21be..0000000 --- a/public/808 Samples/bass (4).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:75ab3c2b179c47c718a740dbae97f792442b739ca28df9e2ab59aea6c26b0496 -size 176446 diff --git a/public/808 Samples/bass (5).WAV b/public/808 Samples/bass (5).WAV deleted file mode 100644 index 6bd9f63..0000000 --- a/public/808 Samples/bass (5).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7ef0803c4dc34bef9740be40bc42d7a0bb98a6951be9c3e97e4d1b184eed3e78 -size 264646 diff --git a/public/808 Samples/bass (6).WAV b/public/808 Samples/bass (6).WAV deleted file mode 100644 index 093c808..0000000 --- a/public/808 Samples/bass (6).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:023f572ab99c32a2574d9dcbc2a73e398c95ac690ff87f8cd421684e22f8118a -size 176446 diff --git a/public/808 Samples/bass (7).wav b/public/808 Samples/bass (7).wav deleted file mode 100644 index 952fc24..0000000 --- a/public/808 Samples/bass (7).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6bc5d6ff5687cd68f69b53432dee8851ceae3e15fa428a23d75da2c6e2cf6cfb -size 186184 diff --git a/public/808 Samples/bass (8).wav b/public/808 Samples/bass (8).wav deleted file mode 100644 index 0fa07b1..0000000 --- a/public/808 Samples/bass (8).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:15f5c51fc5901df29335489c25819a68495247c8beff730d20dc181b33facbd7 -size 191096 diff --git a/public/808 Samples/bass (9).wav b/public/808 Samples/bass (9).wav deleted file mode 100644 index 795aeed..0000000 --- a/public/808 Samples/bass (9).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6778ef87cc8a9ceb0eaf71bb3f4f67bb0cff274ba145edc7036cd1ad716459e4 -size 5048 diff --git a/public/808 Samples/bass.wav b/public/808 Samples/bass.wav deleted file mode 100644 index 4f2c076..0000000 --- a/public/808 Samples/bass.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:064b885a5283f1751177df12cdb7a3502ecee132973d97030277a0c3baa44769 -size 225156 diff --git a/public/808 Samples/clap (1).wav b/public/808 Samples/clap (1).wav deleted file mode 100644 index 889e4e5..0000000 --- a/public/808 Samples/clap (1).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:475e818f75e4f4b516913e9b8ae862d1ef142120c175b69454b6888b9e93854e -size 22680 diff --git a/public/808 Samples/clap (10).wav b/public/808 Samples/clap (10).wav deleted file mode 100644 index 82512a7..0000000 --- a/public/808 Samples/clap (10).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c0b20196cdb6226e6a67649ba78d988ad209a8eb4df4d4df84e02b1e71e91728 -size 30180 diff --git a/public/808 Samples/clap (11).wav b/public/808 Samples/clap (11).wav deleted file mode 100644 index e6ac358..0000000 --- a/public/808 Samples/clap (11).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9ac9bd23224696386f8c9a5ad8b4c0ca6efe3e5185f58bd3b0ec9d2598d87064 -size 29300 diff --git a/public/808 Samples/clap (12).wav b/public/808 Samples/clap (12).wav deleted file mode 100644 index 8a05a2d..0000000 --- a/public/808 Samples/clap (12).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8dda878e81777e0aae7e468fafbca21a26f944646b04bb3e8252cc255e5c15fe -size 20192 diff --git a/public/808 Samples/clap (13).WAV b/public/808 Samples/clap (13).WAV deleted file mode 100644 index b1c98ea..0000000 --- a/public/808 Samples/clap (13).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9fba5e86963afa2bb771778b7437a7d50d9a7e56d1515c85f288d92130325069 -size 38188 diff --git a/public/808 Samples/clap (14).WAV b/public/808 Samples/clap (14).WAV deleted file mode 100644 index 95ffb61..0000000 --- a/public/808 Samples/clap (14).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:376429bb81cb48d1f392a11cd066c32ffb7883d445b2488704ea52e46bb08286 -size 176446 diff --git a/public/808 Samples/clap (15).wav b/public/808 Samples/clap (15).wav deleted file mode 100644 index 86d8bcf..0000000 --- a/public/808 Samples/clap (15).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8cfa04116d9c560cbe45d6a64890976e0950913ecb73fb4c2e8f342a577674ff -size 174096 diff --git a/public/808 Samples/clap (2).wav b/public/808 Samples/clap (2).wav deleted file mode 100644 index 42af3c0..0000000 --- a/public/808 Samples/clap (2).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2e0488aef66cfd2f9464b82a27a7ca88cf1713094a44f5048c849852e08bb64e -size 86144 diff --git a/public/808 Samples/clap (3).WAV b/public/808 Samples/clap (3).WAV deleted file mode 100644 index 9b3f2b1..0000000 --- a/public/808 Samples/clap (3).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2e480fba58d7b3bbbcaac36dc5303a4e03d4dccd116dbe5e5463ecb18a094caa -size 49548 diff --git a/public/808 Samples/clap (4).WAV b/public/808 Samples/clap (4).WAV deleted file mode 100644 index 1a56ed1..0000000 --- a/public/808 Samples/clap (4).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a2ece2764f66491597b5c76b8d0e740141c08b32dc6092c2a76e3372e76d04a5 -size 47658 diff --git a/public/808 Samples/clap (5).WAV b/public/808 Samples/clap (5).WAV deleted file mode 100644 index 6546f18..0000000 --- a/public/808 Samples/clap (5).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:32b42b8767d44a1f22f1084e6cea6efe6f547851231802adfe8790689d917e4e -size 39854 diff --git a/public/808 Samples/clap (6).WAV b/public/808 Samples/clap (6).WAV deleted file mode 100644 index 6b3cdec..0000000 --- a/public/808 Samples/clap (6).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:be27b4a33496bfa3bba7294073915b83e5bcb6317cd0b05c09e029ee5b976c46 -size 32410 diff --git a/public/808 Samples/clap (7).WAV b/public/808 Samples/clap (7).WAV deleted file mode 100644 index 467922e..0000000 --- a/public/808 Samples/clap (7).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e2f2de59f3834f3691ac9b6593b3da8056fd965eab81aaafa7e1da89c06665bf -size 22162 diff --git a/public/808 Samples/clap (8).wav b/public/808 Samples/clap (8).wav deleted file mode 100644 index 42caa34..0000000 --- a/public/808 Samples/clap (8).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:af8b805d8bdb79ca98efc5675730941b0db692a0a169cee1600729a241d9587d -size 22034 diff --git a/public/808 Samples/clap (9).wav b/public/808 Samples/clap (9).wav deleted file mode 100644 index c6dae52..0000000 --- a/public/808 Samples/clap (9).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:93656765719c9673a334796d025fc0efa774e0c1f30af007f68db55178108511 -size 37240 diff --git a/public/808 Samples/clap.wav b/public/808 Samples/clap.wav deleted file mode 100644 index 44951f4..0000000 --- a/public/808 Samples/clap.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ee773b0ec60a583c3cd16ee0af4b20facb17ab59da62379a3e4adc95731df090 -size 51472 diff --git a/public/808 Samples/cymbal (1).wav b/public/808 Samples/cymbal (1).wav deleted file mode 100644 index a699004..0000000 --- a/public/808 Samples/cymbal (1).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8bb67227232af019a77453398b7795c8f5670d8683923557774174c49f02f24a -size 64332 diff --git a/public/808 Samples/cymbal (2).wav b/public/808 Samples/cymbal (2).wav deleted file mode 100644 index b91f250..0000000 --- a/public/808 Samples/cymbal (2).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d77d609073fe711b85f63333415c628f1cd5e5bb1efdff7d0caf2fc20759a718 -size 193700 diff --git a/public/808 Samples/cymbal (3).wav b/public/808 Samples/cymbal (3).wav deleted file mode 100644 index a50d79c..0000000 --- a/public/808 Samples/cymbal (3).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f6e8129ace4c3cb7f1648d0be38b56b5c2cef63f8aa215164b32aa3b83ac727a -size 148270 diff --git a/public/808 Samples/cymbal (4).WAV b/public/808 Samples/cymbal (4).WAV deleted file mode 100644 index d83fd6e..0000000 --- a/public/808 Samples/cymbal (4).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4cd701b60d391870de13f3b5bce71a135a2124610486a4b2adfc0cca6669c298 -size 281928 diff --git a/public/808 Samples/cymbal.wav b/public/808 Samples/cymbal.wav deleted file mode 100644 index 702f25e..0000000 --- a/public/808 Samples/cymbal.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:52b8c241b68d8cf6a4a4360964ee3904633fd47e05f9298a972626e94f053e19 -size 77770 diff --git a/public/808 Samples/hi hat (1).WAV b/public/808 Samples/hi hat (1).WAV deleted file mode 100644 index 3bc40bd..0000000 --- a/public/808 Samples/hi hat (1).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c4a7a38569f65f1f22e503d7da175260037c44726305caeec282cda2141aa664 -size 9512 diff --git a/public/808 Samples/hi hat (10).wav b/public/808 Samples/hi hat (10).wav deleted file mode 100644 index 65d5ecc..0000000 --- a/public/808 Samples/hi hat (10).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c3602751040ce51c424da24368bae7a60d5a70188e8a68f17ee37ae244488015 -size 6660 diff --git a/public/808 Samples/hi hat (11).wav b/public/808 Samples/hi hat (11).wav deleted file mode 100644 index 6276bfa..0000000 --- a/public/808 Samples/hi hat (11).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:990ffee71ab42edcb5c0dc6a9d3a0a6f95997c5a63121c4c04f71460ad187c2d -size 7850 diff --git a/public/808 Samples/hi hat (12).wav b/public/808 Samples/hi hat (12).wav deleted file mode 100644 index 73e0b69..0000000 --- a/public/808 Samples/hi hat (12).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ffb7716b25109dcc34abdd1e7dff60b9a9b11d8c5873b73a8f96b43dce1d8bfd -size 12828 diff --git a/public/808 Samples/hi hat (13).WAV b/public/808 Samples/hi hat (13).WAV deleted file mode 100644 index d18ddd9..0000000 --- a/public/808 Samples/hi hat (13).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:efd236abe8eb4e2a3ce517a853baf1a5b2ce1128745bdfc4ca2c3f4bd3ce84cd -size 22158 diff --git a/public/808 Samples/hi hat (14).wav b/public/808 Samples/hi hat (14).wav deleted file mode 100644 index faf79b0..0000000 --- a/public/808 Samples/hi hat (14).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9e074bb6db9d844092ed39b49a4db4fc3991adcf2364029788b02612bf47867d -size 9058 diff --git a/public/808 Samples/hi hat (15).wav b/public/808 Samples/hi hat (15).wav deleted file mode 100644 index 146fedc..0000000 --- a/public/808 Samples/hi hat (15).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b83b9a94ea0c59499264982e3591fbeb7ecc65ac4cd1be37c03d1706edd5910d -size 26926 diff --git a/public/808 Samples/hi hat (16).wav b/public/808 Samples/hi hat (16).wav deleted file mode 100644 index 750560e..0000000 --- a/public/808 Samples/hi hat (16).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b0bbb825873b24d2ac4747e4f9590e64270ce8f174aee006515435b07d933c58 -size 36480 diff --git a/public/808 Samples/hi hat (17).wav b/public/808 Samples/hi hat (17).wav deleted file mode 100644 index eb31e91..0000000 --- a/public/808 Samples/hi hat (17).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5046d42122264015efcb96ef0709673464c61728e847aa2c648fff279b81c5b1 -size 60824 diff --git a/public/808 Samples/hi hat (18).wav b/public/808 Samples/hi hat (18).wav deleted file mode 100644 index 729d781..0000000 --- a/public/808 Samples/hi hat (18).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ab9c2683db264fc060775e56845e45920d84ec0b06fb6a0cce06a9d75dbc73af -size 85900 diff --git a/public/808 Samples/hi hat (19).wav b/public/808 Samples/hi hat (19).wav deleted file mode 100644 index 83462bb..0000000 --- a/public/808 Samples/hi hat (19).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f863ea1f1cfa279e467475747123bb392f450a9dfa68f0b4a33c18e1775fc625 -size 109248 diff --git a/public/808 Samples/hi hat (2).WAV b/public/808 Samples/hi hat (2).WAV deleted file mode 100644 index dbc5567..0000000 --- a/public/808 Samples/hi hat (2).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:adce03f02363c91c1d69a740a4700289dc67b6905ff5312a193ac2d47f2a1a52 -size 52294 diff --git a/public/808 Samples/hi hat (20).wav b/public/808 Samples/hi hat (20).wav deleted file mode 100644 index 4531c64..0000000 --- a/public/808 Samples/hi hat (20).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e94c0b9ed43f2bfb2e68e30da94f02af0e5358405d09aebe0369bf42b0d48d94 -size 103556 diff --git a/public/808 Samples/hi hat (21).WAV b/public/808 Samples/hi hat (21).WAV deleted file mode 100644 index 493457d..0000000 --- a/public/808 Samples/hi hat (21).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3d657472b1dfba44be11d4695effebabc75e039a8a959bf31d1992f69a01b051 -size 61464 diff --git a/public/808 Samples/hi hat (22).wav b/public/808 Samples/hi hat (22).wav deleted file mode 100644 index bfd9791..0000000 --- a/public/808 Samples/hi hat (22).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8eaca4ca151b0cfdedfff625a73299ddb4fd73d3b8030a28cfe9a53f4824b266 -size 7982 diff --git a/public/808 Samples/hi hat (23).wav b/public/808 Samples/hi hat (23).wav deleted file mode 100644 index 33f83ae..0000000 --- a/public/808 Samples/hi hat (23).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:017060746abe88f9ffd50afb23b5867301852c56f93ebb6511214dfff78307b5 -size 17036 diff --git a/public/808 Samples/hi hat (24).wav b/public/808 Samples/hi hat (24).wav deleted file mode 100644 index c485da5..0000000 --- a/public/808 Samples/hi hat (24).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:31e93d69c853195009b4998e6343fb225163074d896a7c7836f5637722ad2088 -size 34796 diff --git a/public/808 Samples/hi hat (25).wav b/public/808 Samples/hi hat (25).wav deleted file mode 100644 index 47a2400..0000000 --- a/public/808 Samples/hi hat (25).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bbdbb49ddd6f96398f7e574e94af47d31698ca49413f54145fd6b2530de3e398 -size 56324 diff --git a/public/808 Samples/hi hat (26).wav b/public/808 Samples/hi hat (26).wav deleted file mode 100644 index 50d00cf..0000000 --- a/public/808 Samples/hi hat (26).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:63780ae3923af3730739bfbf9863cf708ccb524c051527e9048eace577fdde21 -size 35214 diff --git a/public/808 Samples/hi hat (27).wav b/public/808 Samples/hi hat (27).wav deleted file mode 100644 index 81eb578..0000000 --- a/public/808 Samples/hi hat (27).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e415f3c1c9251a30136e92b2dafeab3aae7c2ca0f71500cda4017d8205a929bd -size 90652 diff --git a/public/808 Samples/hi hat (28).WAV b/public/808 Samples/hi hat (28).WAV deleted file mode 100644 index 357b862..0000000 --- a/public/808 Samples/hi hat (28).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:09a510afa4142a4c819ddddab93bf4506cddad130b679d734f5bde94a0de6380 -size 6964 diff --git a/public/808 Samples/hi hat (29).WAV b/public/808 Samples/hi hat (29).WAV deleted file mode 100644 index 632761f..0000000 --- a/public/808 Samples/hi hat (29).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:abfe6b393dfd4268dc631ab845794de16e31a5bafa3b349eaa45dfb4fecf301e -size 7820 diff --git a/public/808 Samples/hi hat (3).wav b/public/808 Samples/hi hat (3).wav deleted file mode 100644 index a15afcc..0000000 --- a/public/808 Samples/hi hat (3).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5313281b437a82744b2c6a8388dffc71e542f1b0bc370ce7553c913482dc4a2d -size 52148 diff --git a/public/808 Samples/hi hat (30).wav b/public/808 Samples/hi hat (30).wav deleted file mode 100644 index ea5e608..0000000 --- a/public/808 Samples/hi hat (30).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c3bfca0159e0e03c97fb9e1fa1ced31b9b8ef034e391e398afac8255128a6d16 -size 42492 diff --git a/public/808 Samples/hi hat (31).wav b/public/808 Samples/hi hat (31).wav deleted file mode 100644 index 746f6ee..0000000 --- a/public/808 Samples/hi hat (31).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b4d0a5d483cb05121f8dd9a4a43fcaf73b2b4d12d7820d10b75c70a758ced603 -size 39992 diff --git a/public/808 Samples/hi hat (32).wav b/public/808 Samples/hi hat (32).wav deleted file mode 100644 index ad9ec4e..0000000 --- a/public/808 Samples/hi hat (32).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e5d4abc16866c7f2eb458f8d3ff17d703285e6704864df4ae417d8b4ea798e29 -size 40892 diff --git a/public/808 Samples/hi hat (33).wav b/public/808 Samples/hi hat (33).wav deleted file mode 100644 index 637d564..0000000 --- a/public/808 Samples/hi hat (33).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8b1c53939efef54b9eaab9fdbefd258b843f1edef9520ee1e15a4abd8f4ad7bb -size 146208 diff --git a/public/808 Samples/hi hat (34).wav b/public/808 Samples/hi hat (34).wav deleted file mode 100644 index 4f1d3a2..0000000 --- a/public/808 Samples/hi hat (34).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:92c672bf34c508a68398b571fe0f4777c8f5c7d784ceef958b7146cbc5bc2ab7 -size 118304 diff --git a/public/808 Samples/hi hat (35).wav b/public/808 Samples/hi hat (35).wav deleted file mode 100644 index c5bbf7d..0000000 --- a/public/808 Samples/hi hat (35).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c47ee5438f0a0216edac0f7718a7cdd7d742768fd2284692de1ec8d3b01a7069 -size 6424 diff --git a/public/808 Samples/hi hat (36).wav b/public/808 Samples/hi hat (36).wav deleted file mode 100644 index 4f63e09..0000000 --- a/public/808 Samples/hi hat (36).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4df5dc3c3052c4de40930260d455023971ccefa19e3e8ca2c0baac56e178067e -size 49012 diff --git a/public/808 Samples/hi hat (37).WAV b/public/808 Samples/hi hat (37).WAV deleted file mode 100644 index e8c9c85..0000000 --- a/public/808 Samples/hi hat (37).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ec6c02c6449a843c348342a412c68d8cda4c41ab936e9ef2d449e407e853c300 -size 144456 diff --git a/public/808 Samples/hi hat (38).wav b/public/808 Samples/hi hat (38).wav deleted file mode 100644 index 8a6b0f6..0000000 --- a/public/808 Samples/hi hat (38).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ca1baf92dfc85f48a5f973ffeb499f771ee9a342c7338e3107773e2f641e7e6a -size 7184 diff --git a/public/808 Samples/hi hat (39).wav b/public/808 Samples/hi hat (39).wav deleted file mode 100644 index a0e2aab..0000000 --- a/public/808 Samples/hi hat (39).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:852f3b485137038162b457ba4fa9117285e472b8e29cad3e31f55f0a778eaea5 -size 8608 diff --git a/public/808 Samples/hi hat (4).wav b/public/808 Samples/hi hat (4).wav deleted file mode 100644 index d3620d7..0000000 --- a/public/808 Samples/hi hat (4).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d7653df53d9aecd2768b7b6c826c8b8cef2b28862a758bdfd0d527a2ba0c6c76 -size 9028 diff --git a/public/808 Samples/hi hat (5).WAV b/public/808 Samples/hi hat (5).WAV deleted file mode 100644 index 19ff6c0..0000000 --- a/public/808 Samples/hi hat (5).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:238e50007bed75d6367858fb0b5299eb6b278ae793bbea0c662bdde753041716 -size 21648 diff --git a/public/808 Samples/hi hat (6).wav b/public/808 Samples/hi hat (6).wav deleted file mode 100644 index 32b398f..0000000 --- a/public/808 Samples/hi hat (6).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2b278d38281d0784281f41df8d90bb49dd0ca69ba69a170960785376b8878157 -size 6660 diff --git a/public/808 Samples/hi hat (7).WAV b/public/808 Samples/hi hat (7).WAV deleted file mode 100644 index c67afc2..0000000 --- a/public/808 Samples/hi hat (7).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4bdee653d6a489afb62ecdd2796c071b6ca9b208a66b2243b45457d7175d649b -size 7616 diff --git a/public/808 Samples/hi hat (8).WAV b/public/808 Samples/hi hat (8).WAV deleted file mode 100644 index c29a2f3..0000000 --- a/public/808 Samples/hi hat (8).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2491495c0d3c10bcd112cd093f41a10c6b43a44ddd4140c50d678a704dac3873 -size 29680 diff --git a/public/808 Samples/hi hat (9).wav b/public/808 Samples/hi hat (9).wav deleted file mode 100644 index c9c1b1f..0000000 --- a/public/808 Samples/hi hat (9).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1974b60e82fd5461ee84e5761416d9f0d393421959aaa490e286a05f54a33202 -size 5880 diff --git a/public/808 Samples/hi hat.wav b/public/808 Samples/hi hat.wav deleted file mode 100644 index 8edf432..0000000 --- a/public/808 Samples/hi hat.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:877a5ec308030cc076ac19ec647894d34345be92df3e1aa2df633c7edc7196c1 -size 30632 diff --git a/public/808 Samples/kick (1).WAV b/public/808 Samples/kick (1).WAV deleted file mode 100644 index a60a2a2..0000000 --- a/public/808 Samples/kick (1).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eeabdb382b122b06b14d2d83cc7dd77c33daebf0d8c124237d5442f1db5725e3 -size 44368 diff --git a/public/808 Samples/kick (10).wav b/public/808 Samples/kick (10).wav deleted file mode 100644 index c427b1c..0000000 --- a/public/808 Samples/kick (10).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fc455f4c66dfaea6df28b73f35456bb50593af934cea62a8669d5b74b041ebaf -size 23084 diff --git a/public/808 Samples/kick (11).wav b/public/808 Samples/kick (11).wav deleted file mode 100644 index 94ef953..0000000 --- a/public/808 Samples/kick (11).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b8b9345d21cad1427d14198d3e05a0e88beed7447f4b97199dc35236534b17a7 -size 32140 diff --git a/public/808 Samples/kick (12).wav b/public/808 Samples/kick (12).wav deleted file mode 100644 index 3e9bb18..0000000 --- a/public/808 Samples/kick (12).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:07dc35af26a377d334455d7762c32dac8dbc93cdef4be501e5b40ff4862df8b6 -size 208160 diff --git a/public/808 Samples/kick (13).wav b/public/808 Samples/kick (13).wav deleted file mode 100644 index 956b70d..0000000 --- a/public/808 Samples/kick (13).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d0266804b8534d362c90d010bfce32e1cedafdc9f9f4a6bfd2e82cf437f52682 -size 150816 diff --git a/public/808 Samples/kick (14).wav b/public/808 Samples/kick (14).wav deleted file mode 100644 index 532ab2c..0000000 --- a/public/808 Samples/kick (14).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6f8b8194b11d481c0d473333022fdd61cdead6fc9e9e8e24b2cfcae5cc8d6136 -size 264690 diff --git a/public/808 Samples/kick (15).wav b/public/808 Samples/kick (15).wav deleted file mode 100644 index 1f15eed..0000000 --- a/public/808 Samples/kick (15).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0b4b6a299006683b66f94ab78b460e1769820adc23bc6ba87f51d040e8d17ddb -size 177614 diff --git a/public/808 Samples/kick (16).wav b/public/808 Samples/kick (16).wav deleted file mode 100644 index bd53456..0000000 --- a/public/808 Samples/kick (16).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b3623587b2618e1556649076df170c3c3c4c98fffeed127a1272a98f0a13316d -size 57612 diff --git a/public/808 Samples/kick (17).wav b/public/808 Samples/kick (17).wav deleted file mode 100644 index 20448a5..0000000 --- a/public/808 Samples/kick (17).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b9f0cdbb5fb51ddf1346f16448a8b4721460a1500328859e6a12304174f4f77d -size 134200 diff --git a/public/808 Samples/kick (18).WAV b/public/808 Samples/kick (18).WAV deleted file mode 100644 index 19d7b46..0000000 --- a/public/808 Samples/kick (18).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:74fb0b09463ee228a5feb6092109e346172781b447176aa163a34a87174b76c6 -size 50398 diff --git a/public/808 Samples/kick (19).WAV b/public/808 Samples/kick (19).WAV deleted file mode 100644 index c552f6d..0000000 --- a/public/808 Samples/kick (19).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ec73b7d379d7bde2554d8c80b9b7417ba0d6308268e37e6c60781b4dc9716177 -size 32912 diff --git a/public/808 Samples/kick (2).wav b/public/808 Samples/kick (2).wav deleted file mode 100644 index 5ce2c41..0000000 --- a/public/808 Samples/kick (2).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4b09814b69bb44e7a7ba2deedbd6b98d5b9908b7e6ff0e00555c58fcb7bd2565 -size 29144 diff --git a/public/808 Samples/kick (20).WAV b/public/808 Samples/kick (20).WAV deleted file mode 100644 index 936707d..0000000 --- a/public/808 Samples/kick (20).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:469e599617a380b5fbee9671e0f6eb178e736491840b4dfa590b96b7d8c11827 -size 196882 diff --git a/public/808 Samples/kick (21).WAV b/public/808 Samples/kick (21).WAV deleted file mode 100644 index e95339b..0000000 --- a/public/808 Samples/kick (21).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:deedc587c7095599f269526f9aa17c602d381cbecbf60f4a3a746eb7b5094d06 -size 23994 diff --git a/public/808 Samples/kick (22).WAV b/public/808 Samples/kick (22).WAV deleted file mode 100644 index 7d14c6d..0000000 --- a/public/808 Samples/kick (22).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:95acf118ec2bd2a4ade82bd57f3061d041545206bd595bf93a6260d8e9ce8326 -size 44262 diff --git a/public/808 Samples/kick (3).wav b/public/808 Samples/kick (3).wav deleted file mode 100644 index 20c6fa1..0000000 --- a/public/808 Samples/kick (3).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:33de77fb8ad3f9f67092f31dcf5ed24692726a90a8627fae0d08d3e99c7d4f07 -size 165932 diff --git a/public/808 Samples/kick (4).wav b/public/808 Samples/kick (4).wav deleted file mode 100644 index 7dbdf74..0000000 --- a/public/808 Samples/kick (4).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:54f00d9e1ba9503d9f49afcfb69321b90473197b191cdfdecd41b803b88d0ec2 -size 115244 diff --git a/public/808 Samples/kick (5).wav b/public/808 Samples/kick (5).wav deleted file mode 100644 index 208043e..0000000 --- a/public/808 Samples/kick (5).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:56249d1ff5e455380d49870e7980cfac80e67ce4ceda906354a4ee8d68f1f480 -size 46124 diff --git a/public/808 Samples/kick (6).wav b/public/808 Samples/kick (6).wav deleted file mode 100644 index f892914..0000000 --- a/public/808 Samples/kick (6).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de26d7b73e719d4bca58b2d26a1c302989072c53d5eccb32fb71ed9beb7aa6c9 -size 59948 diff --git a/public/808 Samples/kick (7).wav b/public/808 Samples/kick (7).wav deleted file mode 100644 index 80d480f..0000000 --- a/public/808 Samples/kick (7).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d2983f5d4a4a7809f43b941401cb0f456244ffd86509cfa10817e407f42ee99b -size 336428 diff --git a/public/808 Samples/kick (8).wav b/public/808 Samples/kick (8).wav deleted file mode 100644 index 07f5f18..0000000 --- a/public/808 Samples/kick (8).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:89a5592b49101f8519995ffcaf0bb2dba9b759e597872f39e2c2439e610f7b4f -size 165932 diff --git a/public/808 Samples/kick (9).wav b/public/808 Samples/kick (9).wav deleted file mode 100644 index 7e5db83..0000000 --- a/public/808 Samples/kick (9).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:feb295214608795bddad349dfc10dbc8203645bcbc72ca2148d626d391d50d94 -size 193580 diff --git a/public/808 Samples/kick.WAV b/public/808 Samples/kick.WAV deleted file mode 100644 index 8fbb3f3..0000000 --- a/public/808 Samples/kick.WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c5e72366dada7e3e1b413cab753f02723e85a6ff237d440d5ca1a48d21340ba -size 17104 diff --git a/public/808 Samples/snare (1).WAV b/public/808 Samples/snare (1).WAV deleted file mode 100644 index dcad208..0000000 --- a/public/808 Samples/snare (1).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:70bed658454165ea6afa27bf6cff4fdb91dbdb69be7f2392682822e0360f5f15 -size 16066 diff --git a/public/808 Samples/snare (10).wav b/public/808 Samples/snare (10).wav deleted file mode 100644 index fcb10f4..0000000 --- a/public/808 Samples/snare (10).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:64182e2c9e60f70e6b9547de46eb5a2fdac2bf5cd470037cb4f09c177e84ab80 -size 16314 diff --git a/public/808 Samples/snare (11).wav b/public/808 Samples/snare (11).wav deleted file mode 100644 index 8ba0601..0000000 --- a/public/808 Samples/snare (11).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e9463283fa98979ed3f8bff38aa797e1432ecf046dcba761cae4e42e9dbd8d1d -size 16290 diff --git a/public/808 Samples/snare (12).wav b/public/808 Samples/snare (12).wav deleted file mode 100644 index 23621da..0000000 --- a/public/808 Samples/snare (12).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fb1427c82cf2fb0225053b74782202665e28fa24bda46851edad0cac416f8733 -size 20418 diff --git a/public/808 Samples/snare (13).wav b/public/808 Samples/snare (13).wav deleted file mode 100644 index 2c08e3e..0000000 --- a/public/808 Samples/snare (13).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:71506ade029e51ff9e65c6b51b635ad984ce49ef2bafec8b7c93f9253a213fb8 -size 19554 diff --git a/public/808 Samples/snare (14).wav b/public/808 Samples/snare (14).wav deleted file mode 100644 index 67cb569..0000000 --- a/public/808 Samples/snare (14).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:84edfb96cbf8fb7ea065327e7a05e34e0261868787a2dc7e0c38fb210059af03 -size 19230 diff --git a/public/808 Samples/snare (15).wav b/public/808 Samples/snare (15).wav deleted file mode 100644 index 0a60ec2..0000000 --- a/public/808 Samples/snare (15).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c7be59a7b2e75e60eab817b48adf431e538faa4212fbfbb2b99e97a6bb2d56c -size 21250 diff --git a/public/808 Samples/snare (16).wav b/public/808 Samples/snare (16).wav deleted file mode 100644 index 8e1d9b4..0000000 --- a/public/808 Samples/snare (16).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ba973b66fdb68306e39f8d716ab5c838719435c32d895438504ed3befae3ea37 -size 22218 diff --git a/public/808 Samples/snare (17).wav b/public/808 Samples/snare (17).wav deleted file mode 100644 index d691f99..0000000 --- a/public/808 Samples/snare (17).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a50636e45043f47a5e3d56e624efb27774a1523db472078d1c701793ac9e100a -size 20114 diff --git a/public/808 Samples/snare (18).wav b/public/808 Samples/snare (18).wav deleted file mode 100644 index 8aef004..0000000 --- a/public/808 Samples/snare (18).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9bd606f3bd2e8e1fc37e71d704e79b787f28465c6c28236363076ea1dc041f53 -size 18278 diff --git a/public/808 Samples/snare (19).wav b/public/808 Samples/snare (19).wav deleted file mode 100644 index e8d4d61..0000000 --- a/public/808 Samples/snare (19).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2d103529d8c36c00fbd2f1e2446846d77eb2f1341691d3784d9ab4073d84db74 -size 19370 diff --git a/public/808 Samples/snare (2).WAV b/public/808 Samples/snare (2).WAV deleted file mode 100644 index f7de438..0000000 --- a/public/808 Samples/snare (2).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ef7ab5c603fd63e19b7fc2ed0c2a8e9c0d1ab7587a389386f1edc15c30770be0 -size 12238 diff --git a/public/808 Samples/snare (20).wav b/public/808 Samples/snare (20).wav deleted file mode 100644 index c9dc845..0000000 --- a/public/808 Samples/snare (20).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:558b86c84c0eab9ca35ece20696990d8a8953daf60e3039cca9b9ef240ac9c3a -size 21138 diff --git a/public/808 Samples/snare (21).wav b/public/808 Samples/snare (21).wav deleted file mode 100644 index ca5ad4e..0000000 --- a/public/808 Samples/snare (21).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:29c808c4e86547a43bb5c6e2a9afc562fca8d1d2d7ce5c174c6a444c2281fd72 -size 21078 diff --git a/public/808 Samples/snare (22).wav b/public/808 Samples/snare (22).wav deleted file mode 100644 index 237318e..0000000 --- a/public/808 Samples/snare (22).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d295a4fcc7ebc202d32d639fd95a1faa1ca8aedcf219499611fc41112a24a29b -size 18422 diff --git a/public/808 Samples/snare (23).wav b/public/808 Samples/snare (23).wav deleted file mode 100644 index 03f7eb3..0000000 --- a/public/808 Samples/snare (23).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8a24fb68aa268b98f65b911421086ce35fb82b61ff8ca62b3f85f1c6ce2ea6a4 -size 20494 diff --git a/public/808 Samples/snare (24).WAV b/public/808 Samples/snare (24).WAV deleted file mode 100644 index 87fc920..0000000 --- a/public/808 Samples/snare (24).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d65a8d719ba4ecfd1dee83ac15da11e3d1d5adbb7ced2495f1fcb7d90cf65016 -size 7290 diff --git a/public/808 Samples/snare (25).WAV b/public/808 Samples/snare (25).WAV deleted file mode 100644 index 87fc920..0000000 --- a/public/808 Samples/snare (25).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d65a8d719ba4ecfd1dee83ac15da11e3d1d5adbb7ced2495f1fcb7d90cf65016 -size 7290 diff --git a/public/808 Samples/snare (26).WAV b/public/808 Samples/snare (26).WAV deleted file mode 100644 index 0b8576e..0000000 --- a/public/808 Samples/snare (26).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d716206fcb671f17c7b4058d5ac66b226f0006a5686bfcaa9a341cc9ab77764b -size 7290 diff --git a/public/808 Samples/snare (27).WAV b/public/808 Samples/snare (27).WAV deleted file mode 100644 index 111de33..0000000 --- a/public/808 Samples/snare (27).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a1a106fcb4009581b94037cebe7d14e668275f646179db65f29e8190d53bf128 -size 12598 diff --git a/public/808 Samples/snare (28).WAV b/public/808 Samples/snare (28).WAV deleted file mode 100644 index d22d2e3..0000000 --- a/public/808 Samples/snare (28).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8f0d6f3f869d757c5892f88404527c12a0ac3461547af0b70a467a79bbf1d68e -size 2252 diff --git a/public/808 Samples/snare (29).WAV b/public/808 Samples/snare (29).WAV deleted file mode 100644 index fda2028..0000000 --- a/public/808 Samples/snare (29).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2128c97dde77df868878e2d05f063632423c425425934568d90978e65076be5b -size 16402 diff --git a/public/808 Samples/snare (3).WAV b/public/808 Samples/snare (3).WAV deleted file mode 100644 index c105860..0000000 --- a/public/808 Samples/snare (3).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b39a1f4212db1d13d946de1831d95b690b7707d21950589df85d4aa33c93d1aa -size 35500 diff --git a/public/808 Samples/snare (30).WAV b/public/808 Samples/snare (30).WAV deleted file mode 100644 index 92fef9d..0000000 --- a/public/808 Samples/snare (30).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:69dba4359086f8ea65160ecad3f146ba0bb39273cda0757ea61d92f92d2a25a7 -size 29572 diff --git a/public/808 Samples/snare (31).WAV b/public/808 Samples/snare (31).WAV deleted file mode 100644 index 28b9dd3..0000000 --- a/public/808 Samples/snare (31).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:22f048dd7bad7f2b5240b523b1a122b75b5e056710b4b87766bccbf4e0c9791e -size 15700 diff --git a/public/808 Samples/snare (32).WAV b/public/808 Samples/snare (32).WAV deleted file mode 100644 index 8f82dbc..0000000 --- a/public/808 Samples/snare (32).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bdbd9d8c9537d99ae1caea9685ab0c2f48e0df67db8b84467231106f6086462f -size 26066 diff --git a/public/808 Samples/snare (33).WAV b/public/808 Samples/snare (33).WAV deleted file mode 100644 index fd1d625..0000000 --- a/public/808 Samples/snare (33).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7421ae6c9d9ed4d43e05f1ef805c37a7655edfa14d81e51e412c31208cf163dd -size 6228 diff --git a/public/808 Samples/snare (34).WAV b/public/808 Samples/snare (34).WAV deleted file mode 100644 index 35dca6f..0000000 --- a/public/808 Samples/snare (34).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:804f3d5524f8602d613a186607ff2bd84800033c608acd9c1eb16df6d33560b9 -size 51484 diff --git a/public/808 Samples/snare (35).wav b/public/808 Samples/snare (35).wav deleted file mode 100644 index 67da583..0000000 --- a/public/808 Samples/snare (35).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:898d9972625dd485b67b809eb37bf03c51a288185907e32941a668c9fb384bb8 -size 8128 diff --git a/public/808 Samples/snare (36).wav b/public/808 Samples/snare (36).wav deleted file mode 100644 index 48ffa49..0000000 --- a/public/808 Samples/snare (36).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fd59b74a4ccc88f1a2fc0f34aac6c4f46ae17f0540f6ce50dcefd49ae08be3a3 -size 7916 diff --git a/public/808 Samples/snare (37).wav b/public/808 Samples/snare (37).wav deleted file mode 100644 index 8c0a91b..0000000 --- a/public/808 Samples/snare (37).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d85777bb9d311e248deca4e6bb69382e59491e71e7272fe1be84a3fc95c3fbeb -size 6752 diff --git a/public/808 Samples/snare (38).wav b/public/808 Samples/snare (38).wav deleted file mode 100644 index 49919dc..0000000 --- a/public/808 Samples/snare (38).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c879cd1608ae8bf777d4f17ba30a113f58033f8f9e9c521a74fe15287f4d9218 -size 7160 diff --git a/public/808 Samples/snare (39).wav b/public/808 Samples/snare (39).wav deleted file mode 100644 index 1f97adb..0000000 --- a/public/808 Samples/snare (39).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0eed63409ba448e8e19915ed9136c8e331d6dac95493350287f176035317cf36 -size 6824 diff --git a/public/808 Samples/snare (4).WAV b/public/808 Samples/snare (4).WAV deleted file mode 100644 index d22c161..0000000 --- a/public/808 Samples/snare (4).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5d23f6a50d8af1ecf0882ec73d962f739fd79f71793f9254975e6b49a03a652b -size 12004 diff --git a/public/808 Samples/snare (40).wav b/public/808 Samples/snare (40).wav deleted file mode 100644 index 4bdde98..0000000 --- a/public/808 Samples/snare (40).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e4cdcf06b6e9916f7c36e121733107c7e1a006889a4880c4421d6940ea962cb9 -size 6764 diff --git a/public/808 Samples/snare (41).wav b/public/808 Samples/snare (41).wav deleted file mode 100644 index 19d7062..0000000 --- a/public/808 Samples/snare (41).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e1f6994db18ca389bcec504bf90251f4a394d3044d5073f67177e39fe90f6340 -size 5020 diff --git a/public/808 Samples/snare (42).wav b/public/808 Samples/snare (42).wav deleted file mode 100644 index 7506614..0000000 --- a/public/808 Samples/snare (42).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5971910b3cd2aee1f24694809a9f8a9e076951bc4785f58d7532e2342f51cb5d -size 6260 diff --git a/public/808 Samples/snare (43).wav b/public/808 Samples/snare (43).wav deleted file mode 100644 index 55ffeb8..0000000 --- a/public/808 Samples/snare (43).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b250d2b533bb42d9cdc8c3bdf30d658bd275095c5a74d05a98f9b3544be6d51d -size 132896 diff --git a/public/808 Samples/snare (44).wav b/public/808 Samples/snare (44).wav deleted file mode 100644 index 81753f0..0000000 --- a/public/808 Samples/snare (44).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2eaee6fc0201e199fd602654293787fa604ff012551c4aec0b86871c35d24469 -size 11344 diff --git a/public/808 Samples/snare (45).wav b/public/808 Samples/snare (45).wav deleted file mode 100644 index 273a04b..0000000 --- a/public/808 Samples/snare (45).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:204f27c9cd859610ee37280bd6121579ba11614e5ebcf377d61cea3cdc47ceb2 -size 14356 diff --git a/public/808 Samples/snare (46).WAV b/public/808 Samples/snare (46).WAV deleted file mode 100644 index ab88b84..0000000 --- a/public/808 Samples/snare (46).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c109fafc4a5a97318298dc5135fef005b252d241a156623478f9a949e5fe76de -size 13364 diff --git a/public/808 Samples/snare (47).wav b/public/808 Samples/snare (47).wav deleted file mode 100644 index b9c02b0..0000000 --- a/public/808 Samples/snare (47).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fd6d51f34482386fc41c0b69f30de6dfd88e0f64751cb2cd072eb3d061a4358c -size 19264 diff --git a/public/808 Samples/snare (48).wav b/public/808 Samples/snare (48).wav deleted file mode 100644 index 06d06d5..0000000 --- a/public/808 Samples/snare (48).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:083e518bb3af1947fc02aae2b8ab83cf5be347f13ac3fbf2c935f37835d8948f -size 21604 diff --git a/public/808 Samples/snare (5).WAV b/public/808 Samples/snare (5).WAV deleted file mode 100644 index d09b2db..0000000 --- a/public/808 Samples/snare (5).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2e1d0a8d5860eef63f46279ca1b9a3b100b2270e7f1d3669db8c0acaa2d82c48 -size 6776 diff --git a/public/808 Samples/snare (6).WAV b/public/808 Samples/snare (6).WAV deleted file mode 100644 index 8f93dfc..0000000 --- a/public/808 Samples/snare (6).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:38a3067caa7a72be9bdfeb7dc9527192fc5c8e14cb56506ab4f25ad5e9c86ac4 -size 12224 diff --git a/public/808 Samples/snare (7).WAV b/public/808 Samples/snare (7).WAV deleted file mode 100644 index a709dc6..0000000 --- a/public/808 Samples/snare (7).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:679fa5f00d0d885bb5fd3ce8d6242f869a9710365faa0411375c0d05ff89a983 -size 12288 diff --git a/public/808 Samples/snare (8).wav b/public/808 Samples/snare (8).wav deleted file mode 100644 index 7a475ef..0000000 --- a/public/808 Samples/snare (8).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:644ec759b27db8ed9a4bf56b13801bbd85dc3ea49070d351dc831d3669f2c2cd -size 30836 diff --git a/public/808 Samples/snare (9).wav b/public/808 Samples/snare (9).wav deleted file mode 100644 index 1e43a22..0000000 --- a/public/808 Samples/snare (9).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:614581919828a6753e43d38c27331cf1991ca94dd5145823985128323b7cc4ee -size 22274 diff --git a/public/808 Samples/snare.WAV b/public/808 Samples/snare.WAV deleted file mode 100644 index 6b3b1c8..0000000 --- a/public/808 Samples/snare.WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1963372b7d3fda48156cd6cdb18aa65d4930ef3b06058ab6792e86571cb2fc2c -size 23094 diff --git a/public/808 Samples/tom (1).WAV b/public/808 Samples/tom (1).WAV deleted file mode 100644 index dd88afc..0000000 --- a/public/808 Samples/tom (1).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7846a9820e3ed8e4fb91856f0eed2c040fc81c2eacd18d1cab9396891275c0da -size 52176 diff --git a/public/808 Samples/tom (10).WAV b/public/808 Samples/tom (10).WAV deleted file mode 100644 index a37a104..0000000 --- a/public/808 Samples/tom (10).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3687a4a59346bf2733e84cc92b9b3c121022156eb0ea9d2f0f7fb70f0b2eb39e -size 122064 diff --git a/public/808 Samples/tom (11).wav b/public/808 Samples/tom (11).wav deleted file mode 100644 index b16ce4d..0000000 --- a/public/808 Samples/tom (11).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:11d1fa52ae74ba4721a869ce2baeb62384b55aedc8dae2acecbae49a1582b6f6 -size 14914 diff --git a/public/808 Samples/tom (12).wav b/public/808 Samples/tom (12).wav deleted file mode 100644 index 859751b..0000000 --- a/public/808 Samples/tom (12).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:779862831e77fc4c719ca3987ff5055f429c6a899a09894b0fe7668cae449dce -size 27680 diff --git a/public/808 Samples/tom (13).wav b/public/808 Samples/tom (13).wav deleted file mode 100644 index 0be2e16..0000000 --- a/public/808 Samples/tom (13).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:37afaf5758e255b20090b27c49b4c45ecc0e4ee02d2598b77d50ec13ca5b8708 -size 18122 diff --git a/public/808 Samples/tom (2).WAV b/public/808 Samples/tom (2).WAV deleted file mode 100644 index 7469f74..0000000 --- a/public/808 Samples/tom (2).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c11bc4f10ece1e0d2ec04db8bc596d729bb35daa1a3cf600378fef078e72e5a2 -size 27058 diff --git a/public/808 Samples/tom (3).wav b/public/808 Samples/tom (3).wav deleted file mode 100644 index 4b1a5f4..0000000 --- a/public/808 Samples/tom (3).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6658ad68c8329040f7062f2ff4373cf6c49d6518349f4882e3f3fd566964406e -size 31216 diff --git a/public/808 Samples/tom (4).wav b/public/808 Samples/tom (4).wav deleted file mode 100644 index e75e842..0000000 --- a/public/808 Samples/tom (4).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:75064c2d4b9fa62de49da24c3e4f23da088337060ac0766e5557baa108666f90 -size 72390 diff --git a/public/808 Samples/tom (5).wav b/public/808 Samples/tom (5).wav deleted file mode 100644 index 4b3dda3..0000000 --- a/public/808 Samples/tom (5).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:46fec81da1179e8e9701bee3a48179cec9aaf4d821b64356d47f044a7ceb143b -size 10896 diff --git a/public/808 Samples/tom (6).wav b/public/808 Samples/tom (6).wav deleted file mode 100644 index 679175c..0000000 --- a/public/808 Samples/tom (6).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3ee9936277b9748b9584a41263750e64b9a3816dcef258a0915dc58b30158901 -size 29066 diff --git a/public/808 Samples/tom (7).wav b/public/808 Samples/tom (7).wav deleted file mode 100644 index 399db1e..0000000 --- a/public/808 Samples/tom (7).wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b2be5cd6c992e393adbb953a34b2092af674013d091a32ad7f847f904f481868 -size 15628 diff --git a/public/808 Samples/tom (8).WAV b/public/808 Samples/tom (8).WAV deleted file mode 100644 index 4257d1b..0000000 --- a/public/808 Samples/tom (8).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c3cb77d952a27f41b7f073e9ed4d7fe7c16108a00600775073befd36b9ed4541 -size 63292 diff --git a/public/808 Samples/tom (9).WAV b/public/808 Samples/tom (9).WAV deleted file mode 100644 index 5b32291..0000000 --- a/public/808 Samples/tom (9).WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:60930b8f698ebe4789d890d2ffa93942709a102e089c07bb27781152d6b14d56 -size 94322 diff --git a/public/808 Samples/tom.WAV b/public/808 Samples/tom.WAV deleted file mode 100644 index d218aaf..0000000 --- a/public/808 Samples/tom.WAV +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:61c71956fbd31ab90c50b8e6a65cf1c66be28e82c80d1357aef645c9efd099e2 -size 26816 diff --git a/public/README.md b/public/README.md deleted file mode 100644 index 207dce2..0000000 --- a/public/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# public/ - -Static assets served directly. - -- `808 Samples/` - Drum machine audio samples diff --git a/src/App.tsx b/src/App.tsx index b9bdec1..fcc1867 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,9 @@ import styles from './App.module.css' const PANEL_WIDTH = 1128 const PAGE_GUTTER = 48 +/** How long the share link may trail the session. Browsers rate-limit address rewrites, and a knob drag changes the session many times a second. */ +const LINK_DELAY_MS = 250 + export function App() { const [store] = useState(() => createSessionStore(readSessionFromLocation())) const [engine] = useState(() => new Engine(DRUMS)) @@ -23,16 +26,26 @@ export function App() { return () => engine.dispose() }, [engine, store]) - // The session drives both the engine and the share link. - useEffect( - () => - store.subscribe(() => { - const session = store.getState() - engine.apply(session) - writeSessionToLocation(session) - }), - [engine, store], - ) + // The session drives the engine on every change; the share link catches up once per LINK_DELAY_MS, and before the page goes away. + useEffect(() => { + let pending: ReturnType | undefined + const writeLink = () => { + if (pending === undefined) return + clearTimeout(pending) + pending = undefined + writeSessionToLocation(store.getState()) + } + const unsubscribe = store.subscribe(() => { + engine.apply(store.getState()) + pending ??= setTimeout(writeLink, LINK_DELAY_MS) + }) + window.addEventListener('pagehide', writeLink) + return () => { + unsubscribe() + window.removeEventListener('pagehide', writeLink) + writeLink() + } + }, [engine, store]) // The panel has a fixed hardware layout; scale it down to fit narrower windows, like the prototype in the proposal. const [zoom, setZoom] = useState(1) diff --git a/src/engine/clock.test.ts b/src/engine/clock.test.ts new file mode 100644 index 0000000..67f8ec0 --- /dev/null +++ b/src/engine/clock.test.ts @@ -0,0 +1,61 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { catchUp, Clock, LOOKAHEAD, TICK_MS } from './clock' + +const SPS = 0.125 + +describe('catchUp', () => { + it('leaves an on-time grid alone', () => { + expect(catchUp(3, 1.2, 1, SPS)).toEqual({ step: 3, nextTime: 1.2 }) + expect(catchUp(3, 1, 1, SPS)).toEqual({ step: 3, nextTime: 1 }) + }) + + it('skips the steps that have already passed and lands on the next grid point', () => { + const { step, nextTime } = catchUp(2, 1, 1.3, SPS) + expect(step).toBe(5) // 1.0, 1.125, 1.25 passed; the grid resumes at 1.375 + expect(nextTime).toBeCloseTo(1.375) + }) + + it('wraps the step around the pattern', () => { + expect(catchUp(15, 0, 0.2, SPS).step).toBe(1) + expect(catchUp(0, 0, 100, SPS).step).toBe(Math.ceil(100 / SPS) % 16) + }) +}) + +describe('Clock', () => { + afterEach(() => vi.useRealTimers()) + + it('leaves a gap, not stacked hits, after the main thread stalls', () => { + vi.useFakeTimers() + const ctx = { currentTime: 0 } + const hits: Array<{ step: number; time: number }> = [] + const clock = new Clock(ctx as BaseAudioContext, () => SPS, (step, time) => hits.push({ step, time })) + + clock.start() + expect(hits.map((h) => h.step)).toEqual([0]) + + ctx.currentTime = 10 + hits.length = 0 + vi.advanceTimersByTime(TICK_MS) + expect(hits.length).toBeGreaterThan(0) + expect(hits.length).toBeLessThanOrEqual(Math.ceil(LOOKAHEAD / SPS) + 1) + expect(hits.every((h) => h.time >= 10)).toBe(true) + // The skipped steps keep the pattern on the grid: step n sounds at n × SPS + 0.05 (mod the pattern). + for (const h of hits) expect(h.step).toBe(Math.round((h.time - 0.05) / SPS) % 16) + + clock.stop() + }) + + it('stops ticking once stopped', () => { + vi.useFakeTimers() + const ctx = { currentTime: 0 } + const onStep = vi.fn() + const clock = new Clock(ctx as BaseAudioContext, () => SPS, onStep) + clock.start() + clock.stop() + expect(clock.running).toBe(false) + onStep.mockClear() + ctx.currentTime = 5 + vi.advanceTimersByTime(TICK_MS * 4) + expect(onStep).not.toHaveBeenCalled() + }) +}) diff --git a/src/engine/clock.ts b/src/engine/clock.ts index 65083f6..c3d2dc6 100644 --- a/src/engine/clock.ts +++ b/src/engine/clock.ts @@ -6,6 +6,17 @@ export const TICK_MS = 25 export const stepSeconds = (bpm: number): number => 60 / bpm / 4 +/** + * Where the grid resumes when the scheduler wakes up late (the main thread + * stalled). Steps whose time has already passed are skipped, not played + * stacked on top of each other in the past: a stall leaves a gap. + */ +export function catchUp(step: number, nextTime: number, now: number, secondsPerStep: number): { step: number; nextTime: number } { + if (nextTime >= now) return { step, nextTime } + const missed = Math.ceil((now - nextTime) / secondsPerStep) + return { step: (step + missed) % STEPS, nextTime: nextTime + missed * secondsPerStep } +} + /** * Lookahead scheduler. A Worker drives the tick so playback stays steady when * the tab is in the background, where main-thread timers are throttled. @@ -39,7 +50,11 @@ export class Clock { } private readonly tick = (): void => { - const horizon = this.ctx.currentTime + LOOKAHEAD + const now = this.ctx.currentTime + const resumed = catchUp(this.step, this.nextTime, now, this.secondsPerStep()) + this.step = resumed.step + this.nextTime = resumed.nextTime + const horizon = now + LOOKAHEAD while (this.nextTime < horizon) { this.onStep(this.step, this.nextTime) this.nextTime += this.secondsPerStep() @@ -56,6 +71,8 @@ function startTicker(tick: () => void): { stop(): void } { worker.onmessage = tick return { stop() { + // A message already queued must not tick a stopped clock. + worker.onmessage = null worker.terminate() URL.revokeObjectURL(url) }, diff --git a/src/engine/drums.ts b/src/engine/drums.ts index 46a618e..a213384 100644 --- a/src/engine/drums.ts +++ b/src/engine/drums.ts @@ -43,20 +43,35 @@ function envelope(ctx: BaseAudioContext, time: number, peak: number, decay: numb return g } +type Source = AudioScheduledSourceNode + +/** The latest hit of a choke group: its gain, and the sources feeding it so the next hit can stop them. */ +interface Choke { + gain: GainNode + sources: Source[] +} + /** * A gain into `out` that the next hit in the same group cuts off at its start - * time, like the 808's kick or a hi-hat pedal closing an open hat. + * time, like the 808's kick or a hi-hat pedal closing an open hat. Once the + * gain has faded the old hit's sources are silent, so they are stopped too + * instead of running out their full tail (several seconds at long DECAY). */ -function choked(ctx: BaseAudioContext, out: AudioNode, time: number, group: WeakMap): GainNode { - group.get(out)?.gain.setTargetAtTime(0, time, 0.008) +function choked(ctx: BaseAudioContext, out: AudioNode, time: number, group: WeakMap): Choke { + const previous = group.get(out) + if (previous) { + previous.gain.gain.setTargetAtTime(0, time, 0.008) + previous.sources.forEach((src) => src.stop(time + 0.05)) + } const gain = ctx.createGain() gain.connect(out) - group.set(out, gain) - return gain + const choke: Choke = { gain, sources: [] } + group.set(out, choke) + return choke } -const lastKick = new WeakMap() -const lastHat = new WeakMap() +const lastKick = new WeakMap() +const lastHat = new WeakMap() /** * 808 kick: a sine that drops 110 → 52 Hz in 50 ms, then sags toward 45 Hz @@ -64,8 +79,9 @@ const lastHat = new WeakMap() * in 25 ms) and a noise tick give the attack its punch. Light saturation adds * harmonics so the low end carries on small speakers without losing its roundness. */ -function kick(ctx: BaseAudioContext, out: AudioNode, time: number, length: number): void { - const dirt = drive(ctx, choked(ctx, out, time, lastKick), 2.5, 0.95) +function kick(ctx: BaseAudioContext, out: AudioNode, time: number, length: number): Source[] { + const choke = choked(ctx, out, time, lastKick) + const dirt = drive(ctx, choke.gain, 2.5, 0.95) const body = ctx.createOscillator() body.frequency.setValueAtTime(110, time) @@ -87,11 +103,14 @@ function kick(ctx: BaseAudioContext, out: AudioNode, time: number, length: numbe bp.type = 'bandpass' bp.frequency.value = 2500 click.connect(bp).connect(envelope(ctx, time, 0.25, 0.006)).connect(dirt) + choke.sources = [body, snap, click] + return choke.sources } /** Two low pitched shells under a thick noise snap, driven for a crunchy hit. */ -function snare(ctx: BaseAudioContext, out: AudioNode, time: number, length: number): void { +function snare(ctx: BaseAudioContext, out: AudioNode, time: number, length: number): Source[] { const dirt = drive(ctx, out, 2.2, 0.75) + const sources: Source[] = [] for (const [start, end, level] of [ [200, 160, 0.35], @@ -104,6 +123,7 @@ function snare(ctx: BaseAudioContext, out: AudioNode, time: number, length: numb shell.connect(envelope(ctx, time, level, 0.15 * length)).connect(dirt) shell.start(time) shell.stop(time + 0.15 * length + 0.1) + sources.push(shell) } const snap = noiseSource(ctx, time, 0.28 * length + 0.07) @@ -115,6 +135,8 @@ function snare(ctx: BaseAudioContext, out: AudioNode, time: number, length: numb peak.frequency.value = 3500 peak.gain.value = 5 snap.connect(hp).connect(peak).connect(envelope(ctx, time, 0.45, 0.28 * length)).connect(dirt) + sources.push(snap) + return sources } /** @@ -124,9 +146,10 @@ function snare(ctx: BaseAudioContext, out: AudioNode, time: number, length: numb */ const HAT_RATIOS = [2, 3, 4.16, 5.43, 6.79, 8.21] -function hihat(ctx: BaseAudioContext, out: AudioNode, time: number, length: number): void { +function hihat(ctx: BaseAudioContext, out: AudioNode, time: number, length: number): Source[] { const mix = ctx.createGain() mix.gain.value = 0.45 + const sources: Source[] = [] for (const ratio of HAT_RATIOS) { const osc = ctx.createOscillator() osc.type = 'square' @@ -134,8 +157,10 @@ function hihat(ctx: BaseAudioContext, out: AudioNode, time: number, length: numb osc.connect(mix) osc.start(time) osc.stop(time + 0.6 * length + 0.1) + sources.push(osc) } const air = noiseSource(ctx, time, 0.6 * length + 0.1) + sources.push(air) const airLevel = ctx.createGain() airLevel.gain.value = 0.7 air.connect(airLevel).connect(mix) @@ -148,16 +173,20 @@ function hihat(ctx: BaseAudioContext, out: AudioNode, time: number, length: numb hp.type = 'highpass' hp.frequency.value = 7000 // Left clean: driving the hat's highs only adds grain. - mix.connect(bp).connect(hp).connect(envelope(ctx, time, 0.75, 0.6 * length, 0.002)).connect(choked(ctx, out, time, lastHat)) + const choke = choked(ctx, out, time, lastHat) + mix.connect(bp).connect(hp).connect(envelope(ctx, time, 0.75, 0.6 * length, 0.002)).connect(choke.gain) + choke.sources = sources + return sources } -type Voice = (ctx: BaseAudioContext, out: AudioNode, time: number, length: number) => void +type Voice = (ctx: BaseAudioContext, out: AudioNode, time: number, length: number) => Source[] const VOICES: Record = { kick, snare, hihat } /** DECAY 0–1 to a tail multiplier: ¼× (tight, a closed hat) through 1× at center to 4× (long, a washy open hat). */ export const decayLength = (decay: number): number => 2 ** (4 * decay - 2) -export function playDrum(voice: DrumVoice, ctx: BaseAudioContext, out: AudioNode, time: number, decay = 0.5): void { - VOICES[voice](ctx, out, time, decayLength(decay)) +/** Schedules one hit and returns its sources, so a Stop can cancel the ones that have not started yet. */ +export function playDrum(voice: DrumVoice, ctx: BaseAudioContext, out: AudioNode, time: number, decay = 0.5): Source[] { + return VOICES[voice](ctx, out, time, decayLength(decay)) } diff --git a/src/engine/engine.ts b/src/engine/engine.ts index 5898062..b6d645b 100644 --- a/src/engine/engine.ts +++ b/src/engine/engine.ts @@ -31,6 +31,10 @@ export class Engine { private session: Session | null = null private playhead: Playhead = STOPPED private pending: Array<{ time: number } & Playhead> = [] + /** Drum hits scheduled inside the lookahead, so Stop can cancel the ones that have not started. */ + private scheduled: Array<{ time: number; sources: AudioScheduledSourceNode[] }> = [] + /** The start in progress while the context resumes; Stop clears it, which cancels the start. */ + private starting: Promise | null = null private frame = 0 private arpCount = 0 private lastVoice: Voice | null = null @@ -74,21 +78,47 @@ export class Engine { return () => this.listeners.delete(listener) } - async start(): Promise { + /** Starts playback. Calls made while the context is still resuming share one start. */ + start(): Promise { const g = this.graph - if (!g || g.clock.running) return - if (g.ctx.state === 'suspended') await g.ctx.resume() + if (!g || g.clock.running) return Promise.resolve() + if (this.starting) return this.starting + const attempt: Promise = this.begin(g, () => this.starting === attempt) + this.starting = attempt + return attempt + } + + /** Resumes the context, then runs the clock unless a Stop (or a failed resume) got there first. */ + private async begin(g: Graph, isCurrent: () => boolean): Promise { + let resumed = true + try { + await g.ctx.resume() + } catch { + // The browser refused (autoplay policy, closed context): stay stopped. + resumed = false + } + if (!isCurrent()) return + this.starting = null + if (!resumed) return this.arpCount = 0 g.clock.start() + cancelAnimationFrame(this.frame) this.frame = requestAnimationFrame(this.publish) this.emit() } stop(): void { + this.starting = null const g = this.graph if (!g) return g.clock.stop() g.synth.releaseAll() + // Hits already queued in the lookahead never sound; the ones sounding fade naturally. + const now = g.ctx.currentTime + for (const hit of this.scheduled) { + if (hit.time > now) hit.sources.forEach((src) => src.stop(now)) + } + this.scheduled = [] this.lastVoice = null this.pending = [] cancelAnimationFrame(this.frame) @@ -112,18 +142,22 @@ export class Engine { const s = this.session if (!g || !s) return + // Only hits still ahead of the audio clock matter to Stop. + const now = g.ctx.currentTime + this.scheduled = this.scheduled.filter((hit) => hit.time > now) for (const drum of this.drums) { const strip = g.channels.get(drum.id) const track = s.drums[drum.id] if (!track?.steps[step] || !strip) continue - playDrum(drum.voice, g.ctx, strip.input, time, track.decay) + const sources = playDrum(drum.voice, g.ctx, strip.input, time, track.decay) + this.scheduled.push({ time, sources }) if (drum.voice === 'kick') g.pump.duck(time) } let note: string | null = null const { synth } = s - // With every key let go, the last note ends on this step instead of droning on. - if (synth.notes.length === 0 && this.lastVoice) { + // With every key let go, or no gate step left that can fire, the last note ends on this step instead of droning on. + if (this.lastVoice && (synth.notes.length === 0 || !synth.steps.some((on, i) => arpPlaysOnStep(i, on, synth.rate)))) { g.synth.release(this.lastVoice, time) this.lastVoice = null } @@ -136,6 +170,10 @@ export class Engine { this.arpCount++ } } + // A hidden tab stops publishing, so drop entries already past except the latest, the one publish would show. + let past = 0 + while (past < this.pending.length && this.pending[past].time <= now) past++ + if (past > 1) this.pending.splice(0, past - 1) this.pending.push({ time, step, note }) } diff --git a/src/engine/mixer.ts b/src/engine/mixer.ts index a5c13a6..c8a6e8e 100644 --- a/src/engine/mixer.ts +++ b/src/engine/mixer.ts @@ -21,7 +21,7 @@ export interface Mixer { * synth ────────────────────────────┼→ pump ──┴→ mix bus → limiter → out * └ reverb send → reverb ─────────┘ * - * Shared by the live engine and offline renders, so both hear the same thing. + * The engine builds it once per audio context. */ export function buildMixer(ctx: BaseAudioContext, drums: readonly DrumDefinition[]): Mixer { const limiter = ctx.createDynamicsCompressor() diff --git a/src/engine/synth.test.ts b/src/engine/synth.test.ts new file mode 100644 index 0000000..5305f16 --- /dev/null +++ b/src/engine/synth.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' +import { envelopeAt } from './synth' + +describe('envelopeAt', () => { + it('follows the attack and decay segments noteOn schedules', () => { + expect(envelopeAt(-0.1)).toBe(0) + expect(envelopeAt(0)).toBe(0) + expect(envelopeAt(0.004)).toBeCloseTo(0.5) // halfway up the 8 ms attack + expect(envelopeAt(0.008)).toBeCloseTo(1) + expect(envelopeAt(0.008 + 0.09)).toBeCloseTo(0.8) // halfway down the 180 ms decay to 0.6 + expect(envelopeAt(0.008 + 0.18)).toBeCloseTo(0.6) + }) + + it('holds the sustain level after the decay', () => { + expect(envelopeAt(0.5)).toBe(0.6) + expect(envelopeAt(60)).toBe(0.6) + }) + + it('only falls after the peak, so a pinned release never jumps back up', () => { + let last = 1 + for (let t = 0.009; t < 1; t += 0.001) { + const v = envelopeAt(t) + expect(v).toBeLessThanOrEqual(last + 1e-9) + last = v + } + }) +}) diff --git a/src/engine/synth.ts b/src/engine/synth.ts index db4b963..d7f21fa 100644 --- a/src/engine/synth.ts +++ b/src/engine/synth.ts @@ -29,12 +29,29 @@ const DRIVE = { amount: 1.8, level: 0.8 } /** A steep low-pass after the drive shaves off the fizz the saturation adds on top, above the 2–6 kHz presence range. */ const HIGH_CUT = 7000 +/** + * The envelope gain `elapsed` seconds after a note starts, from the same + * attack and decay segments `noteOn` schedules. Browsers without + * `cancelAndHoldAtTime` use it to pin the gain where the envelope has reached. + */ +export function envelopeAt(elapsed: number): number { + const { attack, decay, sustain } = ENVELOPE + if (elapsed <= 0) return 0 + if (elapsed < attack) return elapsed / attack + if (elapsed < attack + decay) return 1 - ((1 - sustain) * (elapsed - attack)) / decay + return sustain +} + export interface Voice { /** The unison oscillators, which follow the waveform. */ unison: OscillatorNode[] oscs: OscillatorNode[] filter: BiquadFilterNode env: GainNode + /** When the note starts, on the audio clock. */ + start: number + /** Set once the voice has a release scheduled, so a second release does not restart it. */ + releasedAt?: number } /** Arp synth voice: stereo unison + sub → plucked low-pass → ADSR → drive → high cut. Voices start and release at scheduled times. */ @@ -122,7 +139,7 @@ export class Synth { unison.forEach((o) => o.start(time + Math.random() / frequency)) sub.start(time) - const voice = { unison, oscs, filter, env } + const voice: Voice = { unison, oscs, filter, env, start: time } this.voices.add(voice) oscs[0].onended = () => { this.voices.delete(voice) @@ -133,15 +150,30 @@ export class Synth { /** Releases at `time`, the moment the next note starts, not when it was scheduled. */ release(voice: Voice, time: number): void { + // An earlier release already covers this one: the decay continues on the same curve. + if (voice.releasedAt !== undefined && voice.releasedAt <= time) return const gain = voice.env.gain if (typeof gain.cancelAndHoldAtTime === 'function') gain.cancelAndHoldAtTime(time) - else gain.cancelScheduledValues(time) + else { + // Without cancelAndHoldAtTime, cancelling drops the in-progress decay ramp and the gain snaps back to 1, so pin it where the envelope has reached. + gain.cancelScheduledValues(time) + gain.setValueAtTime(envelopeAt(time - voice.start), time) + } gain.setTargetAtTime(0, time, ENVELOPE.release / 4) + voice.releasedAt = time voice.oscs.forEach((o) => o.stop(time + ENVELOPE.release + 0.05)) } + /** Lets sounding notes fade; notes that have not started yet are cancelled outright. */ releaseAll(): void { const now = this.ctx.currentTime - for (const v of this.voices) this.release(v, now) + for (const v of this.voices) { + if (v.start > now) { + // Cancelling a gain whose events all lie ahead holds its default of 1, a blip, so stop the voice before it starts instead. + v.oscs.forEach((o) => o.stop(now)) + v.env.disconnect() + this.voices.delete(v) + } else this.release(v, now) + } } } diff --git a/src/session/commands.test.ts b/src/session/commands.test.ts index fc26c1e..d0ac683 100644 --- a/src/session/commands.test.ts +++ b/src/session/commands.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from 'vitest' +import { arpPlaysOnStep } from '../engine/arpeggiator' import { applyCommand } from './commands' import { DEFAULT_GATE } from './library' -import { defaultSession } from './schema' +import { ARP_RATES, defaultSession } from './schema' describe('session commands', () => { it('reset clears steps but keeps held notes, knobs, and tempo', () => { @@ -47,6 +48,11 @@ describe('session commands', () => { expect(s.synth.steps[0]).toBe(!DEFAULT_GATE[0]) }) + it.each(ARP_RATES)('fills in a rhythm that plays at speed %s', (rate) => { + const s = applyCommand(applyCommand(defaultSession(), { type: 'setArpRate', rate }), { type: 'toggleNote', note: 'C4' }) + expect(s.synth.steps.some((on, i) => arpPlaysOnStep(i, on, rate))).toBe(true) + }) + it('leaves your synth steps alone when you already set some', () => { let s = applyCommand(defaultSession(), { type: 'toggleSynthStep', step: 3 }) s = applyCommand(s, { type: 'setNotes', notes: ['G4', 'E4'] }) diff --git a/src/session/commands.ts b/src/session/commands.ts index 93974db..b085c40 100644 --- a/src/session/commands.ts +++ b/src/session/commands.ts @@ -10,7 +10,7 @@ import { type Session, type Waveform, } from './schema' -import { DEFAULT_GATE } from './library' +import { DEFAULT_GATES } from './library' export type DrumParam = 'volume' | 'delay' | 'decay' @@ -36,11 +36,11 @@ const toggleAt = (steps: boolean[], i: number): boolean[] => steps.map((on, j) = /** * New held notes. Holding the first key while no synth steps are set also - * fills in a default rhythm, so the keyboard always makes a sound. + * fills in a default rhythm that plays at the current SPEED, so the keyboard always makes a sound. */ function withNotes(session: Session, notes: Iterable): Session { const synth = { ...session.synth, notes: normalizeNotes(notes) } - if (session.synth.notes.length === 0 && synth.notes.length > 0 && !synth.steps.some(Boolean)) synth.steps = [...DEFAULT_GATE] + if (session.synth.notes.length === 0 && synth.notes.length > 0 && !synth.steps.some(Boolean)) synth.steps = [...DEFAULT_GATES[synth.rate]] return { ...session, synth } } diff --git a/src/session/library.ts b/src/session/library.ts index f680b5c..c69ae24 100644 --- a/src/session/library.ts +++ b/src/session/library.ts @@ -1,3 +1,5 @@ +import type { ArpRate } from './schema' + /** * Starting material for exploring: ready-made loops and chords that work * together. Everything here sits in house music (four-on-the-floor, claps on @@ -34,6 +36,16 @@ const toSteps = (on: readonly number[]): boolean[] => Array.from({ length: 16 }, /** The synth's steps when you first hold a key with none set: off-beat stabs, so the keyboard always answers. */ export const DEFAULT_GATE: readonly boolean[] = toSteps([3, 7, 11, 15]) +/** At 1/4 the arp only sounds on the beat, where off-beat stabs never land, so it gets the four beats instead. */ +const QUARTER_GATE: readonly boolean[] = toSteps([1, 5, 9, 13]) + +/** The gate to fill in at each SPEED, so the first held key sounds at any rate. */ +export const DEFAULT_GATES: Readonly> = { + '1/4': QUARTER_GATE, + '1/8': DEFAULT_GATE, + '1/16': DEFAULT_GATE, +} + const sameNotes = (a: readonly string[], b: readonly string[]) => a.length === b.length && a.every((n, i) => n === b[i]) /** The chord exactly matching the held notes, if any. */ diff --git a/src/session/store.test.ts b/src/session/store.test.ts index cf209d5..c278e90 100644 --- a/src/session/store.test.ts +++ b/src/session/store.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from 'vitest' +import { applyCommand } from './commands' +import { CHORDS, PRESETS } from './library' import { defaultSession } from './schema' import { createSessionStore } from './store' +import { decodeSession } from './url' describe('session store undo', () => { it('steps back one change at a time', () => { @@ -29,4 +32,22 @@ describe('session store undo', () => { store.undo() expect(store.getState().drums.snare.volume).toBe(defaultSession().drums.snare.volume) }) + + it('adds no undo step for commands that change nothing', () => { + const chord = [...CHORDS[0].notes] + const held = applyCommand(defaultSession(), { type: 'setNotes', notes: chord }) + const store = createSessionStore(applyCommand(held, { type: 'reset' })) + let changes = 0 + store.subscribe(() => changes++) + store.dispatch({ type: 'setWaveform', waveform: store.getState().synth.waveform }) + store.dispatch({ type: 'setNotes', notes: chord }) + store.dispatch({ type: 'reset' }) + store.dispatch({ type: 'setBpm', bpm: store.getState().bpm }) + expect(store.canUndo()).toBe(false) + expect(changes).toBe(0) + + const preset = createSessionStore(decodeSession(PRESETS[0].link)) + preset.dispatch({ type: 'loadSession', session: decodeSession(PRESETS[0].link) }) + expect(preset.canUndo()).toBe(false) + }) }) diff --git a/src/session/store.ts b/src/session/store.ts index 96ef350..8926278 100644 --- a/src/session/store.ts +++ b/src/session/store.ts @@ -1,6 +1,7 @@ import { useSyncExternalStore } from 'react' import { applyCommand, type Command } from './commands' import type { Session } from './schema' +import { encodeSession } from './url' export interface SessionStore { getState(): Session @@ -41,7 +42,8 @@ export function createSessionStore(initial: Session, now: () => number = Date.no getState: () => state, dispatch(command) { const next = applyCommand(state, command) - if (next === state) return + // The link carries the whole session, so an unchanged link means nothing changed: no undo step, no rewrite. + if (next === state || encodeSession(next) === encodeSession(state)) return const key = continuousKey(command) const at = now() const continuing = key !== null && last?.key === key && at - last.at < COALESCE_MS diff --git a/src/session/url.test.ts b/src/session/url.test.ts index 481a282..bb11ce6 100644 --- a/src/session/url.test.ts +++ b/src/session/url.test.ts @@ -1,7 +1,7 @@ -import { describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { applyCommand } from './commands' import { defaultSession } from './schema' -import { decodeSession, encodeSession } from './url' +import { decodeSession, encodeSession, writeSessionToLocation } from './url' describe('share link', () => { it('round-trips drums, tempo, and the full synth', () => { @@ -86,4 +86,27 @@ describe('share link', () => { expect(s.synth.notes).toEqual([]) expect(s.synth.mode).toBe('up') }) + + it('keeps the default tempo when the tempo field is empty or not plain digits', () => { + expect(decodeSession('2~~').bpm).toBe(120) + expect(decodeSession('2~0x7f').bpm).toBe(120) + expect(decodeSession('2~1e2').bpm).toBe(120) + expect(decodeSession('2~098').bpm).toBe(98) + }) +}) + +describe('writing the link', () => { + afterEach(() => vi.restoreAllMocks()) + + it('swallows the error browsers throw when address rewrites are rate-limited', () => { + vi.spyOn(window.history, 'replaceState').mockImplementation(() => { + throw new DOMException('Too many calls to history APIs', 'SecurityError') + }) + expect(() => writeSessionToLocation(defaultSession())).not.toThrow() + }) + + it('puts the session in the address bar', () => { + writeSessionToLocation(defaultSession()) + expect(new URLSearchParams(window.location.search).get('s')).toBe(encodeSession(defaultSession())) + }) }) diff --git a/src/session/url.ts b/src/session/url.ts index fea7c69..e1a8276 100644 --- a/src/session/url.ts +++ b/src/session/url.ts @@ -109,7 +109,8 @@ export function decodeSession(payload: string | null): Session { const [version, bpm, ...parts] = payload.split('~') if (version !== LINK_VERSION) return session - session.bpm = clampBpm(Number(bpm)) + // Plain digits only: Number('') is 0 and Number('0x7f') is 127, neither a tempo anyone typed. + if (/^\d{1,3}$/.test(bpm)) session.bpm = clampBpm(Number(bpm)) for (const part of parts) { const [id, ...fields] = part.split('.') if (id === 'syn') { @@ -141,6 +142,11 @@ export function decodeSession(payload: string | null): Session { export const readSessionFromLocation = (): Session => decodeSession(new URLSearchParams(window.location.search).get('s') ?? PRESETS[0].link) +/** Rewrites the address bar. Browsers throttle replaceState with a SecurityError; a missed rewrite is caught up by the next one. */ export function writeSessionToLocation(session: Session): void { - window.history.replaceState(null, '', `${window.location.pathname}?s=${encodeSession(session)}`) + try { + window.history.replaceState(null, '', `${window.location.pathname}?s=${encodeSession(session)}`) + } catch { + // Rate-limited: the session itself is fine, only the link lags. + } } diff --git a/src/ui/Keyboard.tsx b/src/ui/Keyboard.tsx index 25fe9c3..39aa67c 100644 --- a/src/ui/Keyboard.tsx +++ b/src/ui/Keyboard.tsx @@ -6,7 +6,7 @@ interface KeyboardProps { held: readonly string[] /** The note the synth fired on the current step, if any. */ playing: string | null - /** The synth's octave shift; the C labels follow it. */ + /** The synth's octave shift; the C labels and key names follow it, while held notes stay unshifted. */ octave: number onToggle: (note: string) => void } @@ -36,7 +36,7 @@ export const Keyboard = memo(function Keyboard({ held, playing, octave, onToggle key={n.note} type="button" className={cls(styles.white, n.note)} - aria-label={n.note} + aria-label={shifted(n.note, octave)} aria-pressed={heldSet.has(n.note)} onClick={() => onToggle(n.note)} > @@ -51,7 +51,7 @@ export const Keyboard = memo(function Keyboard({ held, playing, octave, onToggle type="button" className={cls(styles.black, n.note)} style={{ '--x': n.x } as CSSProperties} - aria-label={n.note} + aria-label={shifted(n.note, octave)} aria-pressed={heldSet.has(n.note)} onClick={() => onToggle(n.note)} > diff --git a/src/ui/LoopMachine.tsx b/src/ui/LoopMachine.tsx index 7e6c4d9..7f1ad0d 100644 --- a/src/ui/LoopMachine.tsx +++ b/src/ui/LoopMachine.tsx @@ -205,7 +205,8 @@ export function LoopMachine({ store, playhead, isPlaying, onStartStop, onReset }
{isPlaying ? 'STOP' : 'START'} -
diff --git a/src/ui/TempoControl.tsx b/src/ui/TempoControl.tsx index 9424ca4..06e37aa 100644 --- a/src/ui/TempoControl.tsx +++ b/src/ui/TempoControl.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useRef, useState } from 'react' import { ArrowKey } from './controls/LedButton' import styles from './Transport.module.css' @@ -11,9 +11,13 @@ interface TempoControlProps { export function TempoControl({ bpm, onChange }: TempoControlProps) { const [draft, setDraft] = useState(null) const shown = draft ?? String(bpm).padStart(3, '0') + // Escape blurs the field, and blur commits; this tells that commit to drop the draft instead. + const cancelled = useRef(false) const commit = () => { - if (draft !== null) { + const cancel = cancelled.current + cancelled.current = false + if (draft !== null && !cancel) { const parsed = parseInt(draft, 10) if (!Number.isNaN(parsed)) onChange(parsed) } @@ -38,7 +42,7 @@ export function TempoControl({ bpm, onChange }: TempoControlProps) { onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur() if (e.key === 'Escape') { - setDraft(null) + cancelled.current = true e.currentTarget.blur() } }} diff --git a/src/ui/Transport.module.css b/src/ui/Transport.module.css index b0dd029..265c5a0 100644 --- a/src/ui/Transport.module.css +++ b/src/ui/Transport.module.css @@ -97,6 +97,6 @@ outline-offset: 2px; } -.big[aria-pressed='true'] { +.big[data-playing='true'] { background: linear-gradient(#f2a587, #d9744f); } diff --git a/src/ui/controls/Fader.module.css b/src/ui/controls/Fader.module.css index e3bfa12..4cf191c 100644 --- a/src/ui/controls/Fader.module.css +++ b/src/ui/controls/Fader.module.css @@ -8,6 +8,9 @@ cursor: ns-resize; outline: none; touch-action: none; + /* A mouse drag starts here; without this it selects the labels it sweeps over. */ + -webkit-user-select: none; + user-select: none; } .fader::before { diff --git a/src/ui/controls/Knob.module.css b/src/ui/controls/Knob.module.css index 17fd28e..b2da6b2 100644 --- a/src/ui/controls/Knob.module.css +++ b/src/ui/controls/Knob.module.css @@ -11,6 +11,9 @@ cursor: pointer; outline: none; touch-action: none; + /* A mouse drag starts here; without this it selects the labels it sweeps over. */ + -webkit-user-select: none; + user-select: none; } .knob::before { diff --git a/src/ui/controls/LedButton.module.css b/src/ui/controls/LedButton.module.css index e895cae..fbd8391 100644 --- a/src/ui/controls/LedButton.module.css +++ b/src/ui/controls/LedButton.module.css @@ -55,11 +55,11 @@ outline: none; } -.button:disabled { +.button[aria-disabled='true'] { cursor: default; } -.button:disabled .face { +.button[aria-disabled='true'] .face { opacity: 0.45; } diff --git a/src/ui/controls/LedButton.tsx b/src/ui/controls/LedButton.tsx index 00d6149..2dbc452 100644 --- a/src/ui/controls/LedButton.tsx +++ b/src/ui/controls/LedButton.tsx @@ -1,4 +1,4 @@ -import { useId, type ReactNode } from 'react' +import { useId, useRef, type KeyboardEvent, type ReactNode } from 'react' import styles from './LedButton.module.css' interface LedChoiceProps { @@ -8,25 +8,63 @@ interface LedChoiceProps { onChange: (value: T) => void } -/** A row of small keys with an LED over the selected one, like the 909's mode buttons. */ +/** Where each key moves the selection in a group of `count`, wrapping at the ends like native radios; null for other keys. */ +function radioTarget(key: string, index: number, count: number): number | null { + switch (key) { + case 'ArrowLeft': + case 'ArrowUp': + return (index - 1 + count) % count + case 'ArrowRight': + case 'ArrowDown': + return (index + 1) % count + case 'Home': + return 0 + case 'End': + return count - 1 + default: + return null + } +} + +/** + * A row of small keys with an LED over the selected one, like the 909's mode buttons. + * One Tab stop per group (the lit key, or the first when none is lit); arrows, Home, and End pick. + */ export function LedChoice({ label, options, value, onChange }: LedChoiceProps) { const labelId = useId() + const keys = useRef<(HTMLButtonElement | null)[]>([]) + const selected = options.findIndex((o) => o.value === value) + const tabStop = selected === -1 ? 0 : selected + + const onKeyDown = (e: KeyboardEvent, index: number) => { + const to = radioTarget(e.key, index, options.length) + if (to === null) return + e.preventDefault() + keys.current[to]?.focus() + onChange(options[to].value) + } + return (
{label}
- {options.map((o) => ( + {options.map((o, i) => ( ) diff --git a/src/ui/controls/useNotchedControl.ts b/src/ui/controls/useNotchedControl.ts index fe07090..e0995b8 100644 --- a/src/ui/controls/useNotchedControl.ts +++ b/src/ui/controls/useNotchedControl.ts @@ -1,9 +1,17 @@ import { useEffect, useRef, type KeyboardEvent, type RefObject } from 'react' import { snapToNotch, valueForKey } from './notch' +/** + * Scroll distance per notch. A wheel click (about 100 px, or 3 lines in Firefox) moves one notch; + * a trackpad's many small deltas add up to a few notches per swipe. + */ +const WHEEL_NOTCH_PX = 40 +/** Pixels per line for wheels that report in lines. */ +const WHEEL_LINE_PX = 16 + /** * Shared behavior for knobs and faders: the value snaps to notches, arrow keys - * move one notch, and the mouse wheel moves one notch per tick. + * move one notch, and scrolling moves one notch per wheel click. */ export function useNotchedControl( ref: RefObject, @@ -25,10 +33,23 @@ export function useNotchedControl( useEffect(() => { const el = ref.current if (!el) return + // Scroll gathered toward the next notch, in pixels; negative is up. + let travel = 0 const onWheel = (e: WheelEvent) => { + // Sideways swipes leave the value alone, and keep the page's own horizontal gestures. + if (e.deltaY === 0) return e.preventDefault() + const unit = + e.deltaMode === WheelEvent.DOM_DELTA_LINE ? WHEEL_LINE_PX : e.deltaMode === WheelEvent.DOM_DELTA_PAGE ? el.clientHeight || 400 : 1 + const dy = e.deltaY * unit + if (Math.sign(dy) !== Math.sign(travel)) travel = 0 + travel += dy + if (Math.abs(travel) < WHEEL_NOTCH_PX) return + // At most one notch per event, so a single wheel click never skips one; the remainder carries on. + const direction = Math.sign(travel) + travel %= WHEEL_NOTCH_PX const { value: v, onChange: change } = latest.current - const next = snapToNotch(v + (e.deltaY < 0 ? 1 : -1) / notches, notches) + const next = snapToNotch(v - direction / notches, notches) if (next !== v) change(next) } el.addEventListener('wheel', onWheel, { passive: false })