Skip to content

Conversation

@disconcision
Copy link
Member

This wraps some Strudel functions in order to create audio in Hazel. Right now, it wraps only the Note function, which takes a string corresponding to a Strudel/TidalCycles cycle. This is an eDSL string which can take a space-separated list of notes (a-g), and a variety of other notations. Everything in the string is played over a second, so the more notes the faster it is.

Should be functional now on the build server; see docs/sounds:

Screenshot 2024-09-16 at 10 55 29 PM

The plan is to wrap a few more functions, defunctionalizing to create a simple ADT corresponding to Strudel code. This could be used as a basis for a future more elaborate eDSL using projectors/livelits.

Right now, audio plays whenever the program evaluates to a Note. It can be stopped via a button, and the playing audio restarts every time a valid Strudel program is returned, which is different than the last valid Strudel program for which playing began

let exampleUse: unit => unit =
() => {
initStrudel();
playNote("<c a f e>(3,8)");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@disconcision
Copy link
Member Author

@7h3kk1d did you look into the below style of binding js fns when doing the ninjakeys thing? claude suggested it but i couldn't get it to work properly. interestingly, different errors on dev and release. in dev, it was trying to access the fns on an object called runtime, eg erroring on calling runtime.play. in release, it had a more specific error about the method not being implemented

/* First, we need to declare the external JavaScript functions */
[@bs.val] external initStrudel: unit => unit = "initStrudel";
[@bs.val] external hush: unit => unit = "hush";

/* For the `note` function, we'll need to create a wrapper */
[@bs.val] external noteRaw: string => 'a = "note";

/* For `jux` and `rev`, we'll create bindings */
[@bs.send] external jux: ('a, 'a => 'a) => 'a = "jux";
[@bs.val] external rev: 'a => 'a = "rev";

/* For `play`, we'll create a binding */
[@bs.send] external play: 'a => unit = "play";

@7h3kk1d
Copy link
Contributor

7h3kk1d commented Sep 18, 2024

@7h3kk1d did you look into the below style of binding js fns when doing the ninjakeys thing? claude suggested it but i couldn't get it to work properly. interestingly, different errors on dev and release. in dev, it was trying to access the fns on an object called runtime, eg erroring on calling runtime.play. in release, it had a more specific error about the method not being implemented

/* First, we need to declare the external JavaScript functions */
[@bs.val] external initStrudel: unit => unit = "initStrudel";
[@bs.val] external hush: unit => unit = "hush";

/* For the `note` function, we'll need to create a wrapper */
[@bs.val] external noteRaw: string => 'a = "note";

/* For `jux` and `rev`, we'll create bindings */
[@bs.send] external jux: ('a, 'a => 'a) => 'a = "jux";
[@bs.val] external rev: 'a => 'a = "rev";

/* For `play`, we'll create a binding */
[@bs.send] external play: 'a => unit = "play";

I don't think I tried the bucklescript annotation. There was another syntax that I couldn't use because my function was called open which is a keyword in ocaml. Let me find it.

Comment on lines 4 to 67
let initStrudel: unit => unit =
() => {
let initStrudelFn = Js.Unsafe.js_expr("window.initStrudel");
Js.Unsafe.fun_call(initStrudelFn, [||]);
};

let hush: unit => unit =
() => {
let hushFn = Js.Unsafe.js_expr("window.hush");
Js.Unsafe.fun_call(hushFn, [||]);
};

let note: string => Js.Unsafe.any =
pattern => {
let noteFn = Js.Unsafe.js_expr("window.note");
Js.Unsafe.fun_call(noteFn, [|Js.Unsafe.inject(Js.string(pattern))|]);
};

let rev: Js.Unsafe.any => Js.Unsafe.any =
pattern => {
let revFn = Js.Unsafe.js_expr("window.rev");
Js.Unsafe.fun_call(revFn, [|Js.Unsafe.inject(pattern)|]);
};

let jux: (Js.Unsafe.any, Js.Unsafe.any => Js.Unsafe.any) => Js.Unsafe.any =
(pattern, f) => {
Js.Unsafe.meth_call(pattern, "jux", [|Js.Unsafe.inject(f)|]);
};

let play: Js.Unsafe.any => unit =
pattern => {
Js.Unsafe.meth_call(pattern, "play", [||]);
};

/* Wrapper function to chain methods */
let playNote: string => unit =
pattern => {
let n = note(pattern);
let j = jux(n, rev);
play(j);
};

/* Example usage function */
let exampleUse: unit => unit =
() => {
initStrudel();
playNote("<c a f e>(3,8)");
};

/* Function to stop the music */
let stopMusic: unit => unit = () => hush();

/* Function to initialize Strudel when the DOM is loaded */
let initOnLoad: unit => unit =
() => {
let addEventListenerFn = Js.Unsafe.js_expr("window.addEventListener");
Js.Unsafe.fun_call(
addEventListenerFn,
[|
Js.Unsafe.inject(Js.string("DOMContentLoaded")),
Js.Unsafe.inject(Js.wrap_callback(_ => initStrudel())),
|],
);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let initStrudel: unit => unit =
() => {
let initStrudelFn = Js.Unsafe.js_expr("window.initStrudel");
Js.Unsafe.fun_call(initStrudelFn, [||]);
};
let hush: unit => unit =
() => {
let hushFn = Js.Unsafe.js_expr("window.hush");
Js.Unsafe.fun_call(hushFn, [||]);
};
let note: string => Js.Unsafe.any =
pattern => {
let noteFn = Js.Unsafe.js_expr("window.note");
Js.Unsafe.fun_call(noteFn, [|Js.Unsafe.inject(Js.string(pattern))|]);
};
let rev: Js.Unsafe.any => Js.Unsafe.any =
pattern => {
let revFn = Js.Unsafe.js_expr("window.rev");
Js.Unsafe.fun_call(revFn, [|Js.Unsafe.inject(pattern)|]);
};
let jux: (Js.Unsafe.any, Js.Unsafe.any => Js.Unsafe.any) => Js.Unsafe.any =
(pattern, f) => {
Js.Unsafe.meth_call(pattern, "jux", [|Js.Unsafe.inject(f)|]);
};
let play: Js.Unsafe.any => unit =
pattern => {
Js.Unsafe.meth_call(pattern, "play", [||]);
};
/* Wrapper function to chain methods */
let playNote: string => unit =
pattern => {
let n = note(pattern);
let j = jux(n, rev);
play(j);
};
/* Example usage function */
let exampleUse: unit => unit =
() => {
initStrudel();
playNote("<c a f e>(3,8)");
};
/* Function to stop the music */
let stopMusic: unit => unit = () => hush();
/* Function to initialize Strudel when the DOM is loaded */
let initOnLoad: unit => unit =
() => {
let addEventListenerFn = Js.Unsafe.js_expr("window.addEventListener");
Js.Unsafe.fun_call(
addEventListenerFn,
[|
Js.Unsafe.inject(Js.string("DOMContentLoaded")),
Js.Unsafe.inject(Js.wrap_callback(_ => initStrudel())),
|],
);
};
let initStrudel: unit => unit =
() => Js.Unsafe.coerce(Dom_html.window)##initStrudel();
let hush: unit => unit =
() => {
Js.Unsafe.coerce(Dom_html.window)##hush();
};
let note: string => Js.Unsafe.any =
pattern => {
Js.Unsafe.coerce(Dom_html.window)##note(Js.string(pattern));
};
let rev: Js.Unsafe.any => Js.Unsafe.any =
pattern => {
Js.Unsafe.coerce(Dom_html.window)##rev(pattern);
};
let jux: (Js.Unsafe.any, Js.Unsafe.any => Js.Unsafe.any) => Js.Unsafe.any =
(pattern, f) => {
Js.Unsafe.coerce(pattern)##jux(f);
};
let play: Js.Unsafe.any => unit =
pattern => {
Js.Unsafe.coerce(pattern)##play();
};
/* Wrapper function to chain methods */
let playNote: string => unit =
pattern => {
let n = note(pattern);
let j = jux(n, rev);
play(j);
};
/* Example usage function */
let exampleUse: unit => unit =
() => {
initStrudel();
playNote("<c a f e>(3,8)");
};
/* Function to stop the music */
let stopMusic: unit => unit = () => hush();
/* Function to initialize Strudel when the DOM is loaded */
let initOnLoad: unit => Dom_events.listener =
() => {
Dom_events.listen(
Dom_html.window,
Dom_events.Typ.domContentLoaded,
(_: Js.t(Dom_html.window), _: Js.t(Dom_html.event)) => {
initStrudel();
false; // I don't think this does anything.
},
);
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The false being returned on the Dom_events.listen looks like it would be used for stop propagation but I looked through the js_of_ocaml implementation and I don't see where this is happening.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take it back https://github.com/ocsigen/js_of_ocaml/blob/ea51f5e6e1c54a13eb917d081c51175814b8b58b/lib/js_of_ocaml/dom_events.ml#L32 the newest version calls full_handler which does prevent_default so probably set that to true

disconcision and others added 13 commits January 16, 2026 22:09
Add basic Strudel audio integration to Hazel:
- Sound type with Note(String) constructor in BuiltinsADT
- Defensive JS bindings in Strudel.re that check function availability
- audio_view in EvalResult.re showing play/stop controls for Sound values
- CSS styling matching Hazel UI aesthetic

When a program evaluates to Note("pattern"), play/stop buttons appear
in the result area and clicking play triggers Strudel to play the
mini-notation pattern.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Extend Sound ADT with:
- Rev(Sound) - reverse pattern
- Fast(Float, Sound) - speed up pattern
- Slow(Float, Sound) - slow down pattern
- Seq(List(Sound)) - sequence patterns
- Stack(List(Sound)) - stack patterns (play simultaneously)

Add corresponding Strudel.re functions (rev, fast, slow, seq, stack)
to call the appropriate Strudel JS methods.

Update EvalResult.re to recursively interpret the Sound structure
and build Strudel patterns, with a human-readable description in
the audio controls.

Example usage:
  Fast(2.0, Note("c4 e4 g4"))
  Stack([Note("c4"), Note("e4"), Note("g4")])

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add a simple piano-roll style projector for editing mini-notation
string patterns. The projector shows a single-octave keyboard where
clicking notes toggles them in/out of the pattern.

Features:
- Works on any String expression
- White keys for natural notes (C, D, E, F, G, A, B)
- Black keys for sharps (C#, D#, F#, G#, A#)
- Visual feedback for active notes
- Pattern text display below keyboard

Register NotePicker in ProjectorCore.Kind and ProjectorInit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add a step sequencer style projector for editing drum patterns.
The projector shows a grid where:
- Rows are drum sounds (BD, SD, HH, OH)
- Columns are steps (configurable 4-16)
- Clicking cells toggles beats on/off

Features:
- Adjustable step count with +/- buttons
- Visual beat markers every 4 steps
- Generates mini-notation format (e.g., "bd ~ sd ~ bd ~ sd ~")
- Works on any String expression

Register RhythmGrid in ProjectorCore.Kind and ProjectorInit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Move Note/Rhythm projectors to match on Note constructor (type safety)
- Block placeholders: piano 4 rows, rhythm 6 rows
- Fix black keys escaping piano bounding box (overflow:hidden)
- Play/stop buttons with ▶/■ symbols in button panel
- Context menu shows all applicable projectors (not just first)
- Add Phase 5 modular synth vision to plan

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add Knob projector: rotary dial for Float values with click-to-set
- Add XY Pad projector: 2D control surface for (Float, Float) tuples
- Add Sample(String) constructor to Sound type for drums/samples
- Load Dirt-Samples via prebake callback in initStrudel()
- Update NotePicker/RhythmGrid to match both Note and Sample constructors
- Fix XY Pad to handle both Tuple and Parens(Tuple) syntax
- CSS styling with modular synth aesthetic (dark, glowing indicators)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Replace dark modular synth theme with light Hazel palette
- Use --T1/--T2 (tan) backgrounds, --BR2/--BR3 (brown) borders
- Use --G0/--G1 (green) for active indicators and cursors
- Add hover states with border darkening and glow effects
- Use --code-font and --STONE for value text display
- Change audio stop button from ■ to ⏸

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add hover states on containers (border darkens to --BR3)
- Add transition timing for smooth interactions
- Active keys/cells get subtle green glow effects
- Black piano keys use --BR4 instead of hardcoded #333
- Pattern text gets subtle border
- Step buttons get proper borders and active states
- Drum labels use --code-font with uppercase styling
- Inactive cells hover to white with darker border

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Design for a refractor that plays Sound values from any expression:
- Renders play/pause controls in offside area (like Probe/Statics)
- Uses dynamics to access evaluated Sound value
- Global state for mutual exclusion (only one plays at a time)
- Type-restricted to Sound expressions via statics check
- Migration path to remove results panel audio controls

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add a new Player refractor that allows playing Sound expressions from
anywhere in the program, not just in the results panel.

Key changes:
- Create SoundUtil.re: Extract sound interpretation logic from EvalResult
- Create PlayerProj.re: Player refractor with global play state for
  mutual exclusion (only one Player can play at a time)
- Add Player to ProjectorCore.re, ProjectorInit.re as a refractor
- Integrate with context menu via ProbePerform.re (TogglePlayer action)
- Add type predicate: Player only available on expressions with type Sound
- Add keyboard shortcut: Alt+P (Option+P on Mac)
- Create proj-player.css with styling matching other Strudel controls
- Remove audio_view from EvalResult.re (replaced by Player refractor)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Simplify plan from detailed implementation guide to concise reference
document showing what was actually built.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@codecov
Copy link

codecov bot commented Jan 17, 2026

Codecov Report

❌ Patch coverage is 11.46366% with 865 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.94%. Comparing base (cadbb74) to head (c9847e4).
⚠️ Report is 1 commits behind head on dev.

Files with missing lines Patch % Lines
src/haz3lcore/SoundUtil.re 0.00% 211 Missing ⚠️
...c/haz3lcore/projectors/implementations/KnobProj.re 4.54% 105 Missing ⚠️
.../haz3lcore/projectors/implementations/XYPadProj.re 4.76% 100 Missing ⚠️
src/util/Strudel.re 0.00% 89 Missing ⚠️
...lcore/projectors/implementations/RhythmGridProj.re 9.89% 82 Missing ⚠️
...lcore/projectors/implementations/NotePickerProj.re 24.00% 76 Missing ⚠️
...haz3lcore/projectors/implementations/PlayerProj.re 3.22% 60 Missing ⚠️
src/haz3lcore/ProbePerform.re 0.00% 40 Missing ⚠️
...core/projectors/implementations/ScalePickerProj.re 27.27% 32 Missing ⚠️
...ore/projectors/implementations/SamplePickerProj.re 35.71% 27 Missing ⚠️
... and 8 more
Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #1395      +/-   ##
==========================================
- Coverage   50.37%   48.94%   -1.43%     
==========================================
  Files         230      239       +9     
  Lines       25365    26329     +964     
==========================================
+ Hits        12777    12888     +111     
- Misses      12588    13441     +853     
Files with missing lines Coverage Δ
src/haz3lcore/projectors/ProjectorInit.re 79.31% <100.00%> (+6.58%) ⬆️
src/language/builtins/BuiltinsADT.re 98.59% <100.00%> (+0.28%) ⬆️
src/web/app/common/RefractorView.re 0.00% <ø> (ø)
src/web/init/Init.re 57.14% <ø> (ø)
src/haz3lcore/zipper/action/Action.re 0.00% <0.00%> (ø)
src/language/ProjectorKind.re 92.98% <95.45%> (+4.09%) ⬆️
src/web/app/editors/result/EvalResult.re 1.27% <0.00%> (ø)
src/haz3lcore/projectors/ProjectorBase.re 5.88% <0.00%> (-0.12%) ⬇️
src/web/Keyboard.re 0.00% <0.00%> (ø)
src/util/ProjectorShape.re 21.05% <0.00%> (-5.62%) ⬇️
... and 12 more

... and 11 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

disconcision and others added 9 commits January 17, 2026 13:27
Major additions:
- Strudel language background (core design, method chaining, mini-notation)
- Hazel vs Strudel divergences analysis
- Copy-pasteable curried stdlib functions for pipeline-style composition
- Phase 6: Strudel Parity Improvements:
  - 6.1 Live coding behavior (continuous scheduler, no restart on edit)
  - 6.2 Graceful sample degradation
  - 6.3 Additional constructors (Gain, Pan, Bank, JuxRev)
  - 6.4 Curried standard library
  - 6.5 Sample exploration (MVP and full browser)
  - 6.6 Projector visualization (Phase A: no source tracking, Phase B: with)
- Updated implementation priorities
- Resolved questions and expanded references

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Refactor PlayState to support seamless pattern updates without audio gaps:
- Add last_desc tracking to detect actual pattern changes
- Only call hush() when switching between different players, not when
  updating the same player's pattern
- Let scheduler.setPattern() handle updates (called internally by play())
- Add auto-update logic in view to re-play pattern when dynamics change

This enables editing Sound expressions while they're playing, with the
Strudel scheduler continuously running and smoothly transitioning to
the new pattern.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Section 6.1 now documents the actual implementation
- Includes code snippet from PlayerProj.re showing the solution
- Added testing tips (Stack for layers, Slow factor, TOO LATE messages)
- References the implementation commit

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
NotePicker:
- Validate pattern contains only valid notes at same octave
- Detect octave from pattern instead of hardcoding 4
- Show "Oct N" label in UI
- Reject mixed octaves, repetition, grouping, rests

RhythmGrid:
- Validate pattern contains only valid drum tokens (bd/sd/hh/oh/~)
- Reject repetition, grouping, sample variations, etc.
- Left-justify +/- controls so they don't move when grid expands

Add Phase 7 to Strudel plan documenting "strict at gate, expressive
within" principle for special-purpose projectors, with guidelines
for future implementations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Replace Y-position click model with intuitive point-at-center rotation
- Add drag support with pointer capture (works outside element bounds)
- Add tick marks at min/max positions to indicate dead zone
- Add debug line from center to indicator while dragging
- Change cursor to crosshair to match interaction model

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
XYPadProj:
- Fix jump bug: update position on pointerdown, not just mousemove
- Add pointer capture for drag outside bounds
- Use getBoundingClientRect for accurate positioning

KnobProj:
- Use getBoundingClientRect instead of hardcoded size estimate
- Add tick highlighting when value is at min/max limits

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…ization

- Add JuxRev(Sound) constructor for opt-in stereo widening effect
- Remove automatic juxRev from playPattern - sounds now play as authored
- Move PlayState to Strudel.re for shared access between PlayerProj and ProbePerform
- Stop playback when Player refractor is removed (fixes audio continuing after removal)
- Add graceful sample degradation with try/catch on dirt-samples loading
- Normalize flats to sharps in NotePicker for consistent display/toggle behavior
- Remove unused loadSamples function
- Add comments about pointer events in KnobProj/XYPadProj

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Mark JuxRev, graceful sample degradation, playback cleanup as complete
- Add "Critical Gaps for Usability" section with clear priorities
- Define Phase A (Music Composition): Gain, Pan, Bank constructors
- Define Phase B (Live Coding): BPM control, global transport
- Define Phase C (Polish): Sample picker, scale picker, curried stdlib
- Include test cases for each phase
- Update implementation priority list with status indicators

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Phase A complete - essential constructors for music composition:
- Gain(Float, Sound): volume control (0.0-1.0)
- Pan(Float, Sound): stereo panning (-1.0 left to 1.0 right)
- Bank(String, Sound): sample bank selection (e.g., "RolandTR909")
- Cpm(Float, Sound): tempo in cycles per minute (≈ BPM)

All constructors work with existing projectors - use Knob on Float
arguments for visual control.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
disconcision and others added 19 commits January 17, 2026 16:13
The Bank constructor (e.g., Bank(("RolandTR909", Sample("bd sd"))))
requires the tidal-drum-machines sample pack, not just dirt-samples.

Updated initStrudel to load both:
- github:tidalcycles/dirt-samples (basic bd, sd, hh, etc.)
- github:ritchse/tidal-drum-machines (RolandTR808, RolandTR909, etc.)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
The samples() function needs JSON manifest files, not raw GitHub paths.
Now loads from felixroos/dough-samples which provides proper manifests:
- Dirt-Samples.json (basic bd, sd, hh, piano, etc.)
- tidal-drum-machines.json (RolandTR808, RolandTR909, etc.)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Remove verbose sound structure description from Player display
- Show speaker icon (🔊) instead - cleaner, more obvious
- Add pulsing animation when playing (CSS keyframes)
- Both play button and speaker icon pulse while active

Future: waveform visualization via Web Audio API (added to plan)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Fix inconsistent numbering in Implementation Phases section
  (each phase now restarts at 1)
- Update Semantic Mapping table with implemented constructors
  (Gain, Pan, Bank, Cpm, JuxRev)
- Update Current State section with all Sound constructors
- Remove duplicate References section

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add dedicated Alt+P keyboard shortcut for Player projector
  (previously fell through to generic livelit shortcut)
- Add SamplePicker projector for Sample(String) with dropdown
  showing drum/melodic/fx categories
- Add ScalePicker projector for Note(String) with preset scales
  (Major, Minor, Dorian, Pentatonic, Blues, etc.)
- Add CSS styling for both new projectors
- Update plan to mark Phase C items 1-2 as complete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add async callback for sample loading, show loading state in Player
- Remove broken drum-machines URL, keep working dirt-samples
- SamplePicker: remove non-working samples (oh, rim, tom, piano, etc),
  redesign as compact 5x2 grid without redundant header
- NotePicker: remove octave label to reduce height
- RhythmGrid: replace oh with cp (clap)
- Minor placeholder size adjustments

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Strudel JS library is now loaded dynamically from unpkg only when a
Player projector is first rendered, rather than on every page load.
This reduces network dependency for users not using audio features.

- Remove blocking script tag from index.html
- Add load state machine to Strudel.re (NotLoaded|Loading|Ready|Failed)
- PlayerProj triggers loading and shows appropriate UI states
- Graceful degradation: shows "Audio unavailable" if loading fails

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants