Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

macos: perc-kit waltz patterns + sustained notes + bachbeat demo

Wire the real artery-tui WALTZ_PATTERNS (classic/dark/baroque/phonk/
viennese/drill/dreamy/minimal) through notepat's percussion kit, plus
a bachbeat mode that plays the first N notes of Bach's Cello Suite
No. 1 Prelude over a steady 4/4 groove.

- scripts/waltz-seq.py: emits AC_INJECT_SEQUENCE from a chosen WALTZ
style + BPM + bars. Maps kick/snare/hat/hatOpen to the correct
perc-kit keys (left grid: c/d/g/a; right grid: h/i/l/m — via
NOTE_TO_KEY for +c/+d/+g/+a). With --melody on, interleaves a
4-bar minor-pentatonic groove on the OTHER grid with sustained
bass anchors (6 steps ≈ 750 ms at 120 bpm) and shorter arp holds.

- scripts/bachbeat-seq.py: parses MIDI_EVENTS from
artery/test-notepat-bach-prelude.mjs, converts note labels through
the same LABEL_TO_KEY map notepat uses, and lays them over a
simple kick/snare/hat beat on the other grid.

- main.c sequencer: AC_INJECT_SEQUENCE now accepts an optional third
field per entry (key,delay_ms,hold_ms). Non-zero hold queues the
keyup for later; tap-style events keep the existing two-field
form. Also added SDL mappings for pageup/pagedown/home/end/shift
so the kit-toggle + arrow-key volume mix works headlessly.

- demo.sh: new MODE=waltz|bachbeat|melody switch; KIT_MIXDOWN ticks
down perc-kit volume via arrowright+arrowdown×N after the kit
flip (default 3 = ~70%, stops drums overwhelming the melody);
auto-computes AC_HEADLESS_MS from the selected body length.

Demo recordings at ~/Desktop/ac-boot-shots/demo-<style>.mkv include
boot animation → prompt typing 'notepat' → jump → drum-kit switch
on right grid → volume mixdown → waltz or bachbeat over the pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+450 -36
+56 -16
fedac/native/macos/main.c
··· 106 106 case SDLK_ESCAPE: s = "escape"; break; 107 107 case SDLK_BACKSPACE: s = "backspace"; break; 108 108 case SDLK_TAB: s = "tab"; break; 109 + case SDLK_PAGEUP: s = "pageup"; break; 110 + case SDLK_PAGEDOWN: s = "pagedown"; break; 111 + case SDLK_HOME: s = "home"; break; 112 + case SDLK_END: s = "end"; break; 113 + case SDLK_LSHIFT: 114 + case SDLK_RSHIFT: s = "shift"; break; 109 115 default: break; 110 116 } 111 117 if (s) { snprintf(out, n, "%s", s); return; } ··· 845 851 const char *inject_key = getenv("AC_INJECT_KEY"); 846 852 int injected = 0; 847 853 848 - // AC_INJECT_SEQUENCE="<key>,<ms>|<key>,<ms>|…" — scripted typing 849 - // timeline. The first key fires at <ms> from start; each subsequent 850 - // key fires <ms> after the prior event. Supports multi-char names 851 - // (e.g. "enter", "space", "backspace") for the prompt. Used by the 852 - // demo recorder to type "notepat<enter>" without real input. 854 + // AC_INJECT_SEQUENCE="<key>,<delay_ms>[,<hold_ms>]|…" — scripted 855 + // typing timeline. <delay_ms> is cumulative from the prior event. 856 + // Optional <hold_ms> delays the paired keyup; omitted (or 0) means 857 + // fire keyup immediately (classic tap). Useful for sustained notes 858 + // and chords under drums. Multi-char key names supported 859 + // (e.g. "enter", "space", "pageup", "arrowup"). 853 860 const char *seq_env = getenv("AC_INJECT_SEQUENCE"); 854 - typedef struct { char key[32]; int at_ms; } SeqEvent; 861 + typedef struct { char key[32]; int at_ms; int hold_ms; } SeqEvent; 862 + typedef struct { char key[32]; int at_ms; int fired; } PendingUp; 855 863 SeqEvent *seq = NULL; 856 864 int seq_len = 0, seq_cur = 0; 865 + PendingUp *ups = NULL; 866 + int ups_cap = 0, ups_len = 0; 857 867 if (seq_env && seq_env[0]) { 858 868 // First pass: count segments to allocate. 859 869 int count = 1; 860 870 for (const char *p = seq_env; *p; p++) if (*p == '|') count++; 861 871 seq = calloc(count, sizeof *seq); 872 + ups = calloc(count, sizeof *ups); 873 + ups_cap = count; 862 874 int cumul = 0; 863 875 const char *p = seq_env; 864 876 while (*p && seq_len < count) { ··· 870 882 if (klen >= (int)sizeof(seq[0].key)) klen = sizeof(seq[0].key) - 1; 871 883 memcpy(seq[seq_len].key, p, klen); 872 884 seq[seq_len].key[klen] = 0; 885 + // Optional second comma = hold_ms. 886 + const char *comma2 = memchr(comma + 1, ',', (size_t)(pipe - (comma + 1))); 873 887 int dly = atoi(comma + 1); 888 + int hold = (comma2 && comma2 < pipe) ? atoi(comma2 + 1) : 0; 889 + if (hold < 0) hold = 0; 874 890 cumul += dly; 875 - seq[seq_len].at_ms = cumul; 891 + seq[seq_len].at_ms = cumul; 892 + seq[seq_len].hold_ms = hold; 876 893 seq_len++; 877 894 if (!*pipe) break; 878 895 p = pipe + 1; ··· 915 932 break; 916 933 } 917 934 // Scripted input timeline — dispatch the next pending key when 918 - // its cumulative delay has elapsed. Each dispatch fires a paired 919 - // down+up event so the piece's keyup handlers (notepat releases 920 - // notes on keyup) run naturally. 935 + // its cumulative delay has elapsed. If hold_ms is 0 we fire 936 + // down+up back-to-back (tap); otherwise the keyup goes onto a 937 + // pending queue drained below. Lets notepat sustain notes so 938 + // the melody grooves rather than clicking. 921 939 while (seq && seq_cur < seq_len && 922 940 (int)(SDL_GetTicks() - start_tick) >= seq[seq_cur].at_ms) { 923 - PieceEvent pd = {0}, pu = {0}; 941 + PieceEvent pd = {0}; 924 942 snprintf(pd.key, sizeof pd.key, "%s", seq[seq_cur].key); 925 943 snprintf(pd.type, sizeof pd.type, "keyboard:down:%s", seq[seq_cur].key); 926 - snprintf(pu.key, sizeof pu.key, "%s", seq[seq_cur].key); 927 - snprintf(pu.type, sizeof pu.type, "keyboard:up:%s", seq[seq_cur].key); 928 944 piece_act(pc, &pd); 929 - piece_act(pc, &pu); 930 - fprintf(stderr, "[sequence] fired %s @ %dms\n", 931 - seq[seq_cur].key, seq[seq_cur].at_ms); 945 + if (seq[seq_cur].hold_ms <= 0) { 946 + PieceEvent pu = {0}; 947 + snprintf(pu.key, sizeof pu.key, "%s", seq[seq_cur].key); 948 + snprintf(pu.type, sizeof pu.type, "keyboard:up:%s", seq[seq_cur].key); 949 + piece_act(pc, &pu); 950 + } else if (ups && ups_len < ups_cap) { 951 + snprintf(ups[ups_len].key, sizeof ups[ups_len].key, 952 + "%s", seq[seq_cur].key); 953 + ups[ups_len].at_ms = seq[seq_cur].at_ms + seq[seq_cur].hold_ms; 954 + ups[ups_len].fired = 0; 955 + ups_len++; 956 + } 957 + fprintf(stderr, "[sequence] fired %s @ %dms (hold %d)\n", 958 + seq[seq_cur].key, seq[seq_cur].at_ms, 959 + seq[seq_cur].hold_ms); 932 960 seq_cur++; 961 + } 962 + // Drain pending keyups whose hold window has elapsed. 963 + int now_rel = (int)(SDL_GetTicks() - start_tick); 964 + for (int i = 0; i < ups_len; i++) { 965 + if (ups[i].fired) continue; 966 + if (now_rel < ups[i].at_ms) continue; 967 + PieceEvent pu = {0}; 968 + snprintf(pu.key, sizeof pu.key, "%s", ups[i].key); 969 + snprintf(pu.type, sizeof pu.type, "keyboard:up:%s", ups[i].key); 970 + piece_act(pc, &pu); 971 + ups[i].fired = 1; 933 972 } 934 973 if (inject_key && !injected && (SDL_GetTicks() - start_tick) >= 300) { 935 974 PieceEvent pe = {0}; ··· 1102 1141 if (au) audio_wav_stop(au); 1103 1142 } 1104 1143 free(seq); 1144 + free(ups); 1105 1145 piece_destroy(pc); 1106 1146 free(fb.pixels); 1107 1147 SDL_DestroyTexture(tex);
+126
fedac/native/macos/scripts/bachbeat-seq.py
··· 1 + #!/usr/bin/env python3 2 + # bachbeat-seq.py — emit an AC_INJECT_SEQUENCE playing the opening of 3 + # Bach's Cello Suite No. 1 Prelude (BWV 1007) on one grid + a simple 4 + # 4/4 drum groove on the other. 5 + # 6 + # Source: artery/test-notepat-bach-prelude.mjs (MIDI_EVENTS array, 7 + # transposed to notepat range). We take the first N events so the demo 8 + # stays under ~20 s. Notes are held for ~90% of their duration so the 9 + # phrasing comes through; gaps are ~10% so consecutive same-pitch 10 + # repeats still register as separate hits. 11 + # 12 + # Drums: steady kick on beat 1, hat every eighth note, snare on beat 3. 13 + # Simple rock/lofi beat that underlays the baroque line. 14 + 15 + import re, sys 16 + from pathlib import Path 17 + 18 + # ---- Note-label → notepat keyboard key (matches fedac/native/pieces/notepat.mjs) ---- 19 + LABEL_TO_KEY = { 20 + "-a": "control", "-a#": "z", "-b": "x", 21 + "c": "c", "c#": "v", "d": "d", "d#": "s", "e": "e", "f": "f", 22 + "f#": "w", "g": "g", "g#": "r", "a": "a", "a#": "q", "b": "b", 23 + "+c": "h", "+c#": "t", "+d": "i", "+d#": "y", "+e": "j", "+f": "k", 24 + "+f#": "u", "+g": "l", "+g#": "o", "+a": "m", "+a#": "p", "+b": "n", 25 + "++c": ";", "++c#": "'", "++d": "]", 26 + } 27 + 28 + # Drum mapping for the RIGHT grid (matches waltz-seq.py). 29 + DRUM_KEY = {"kick": "h", "snare": "i", "hat": "l", "hatOpen": "m"} 30 + 31 + MIDI_TICKS_PER_BEAT = 480 32 + 33 + def load_events(max_events: int): 34 + """Parse MIDI_EVENTS from the artery source — regex beats pulling in 35 + Node deps. Extracts {key, ticks} tuples in order.""" 36 + # scripts/ → macos/ → native/ → fedac/ → REPO/ → artery/ 37 + src = (Path(__file__).resolve().parent.parent.parent.parent.parent 38 + / "artery/test-notepat-bach-prelude.mjs").resolve() 39 + text = src.read_text() 40 + pat = re.compile(r'\{\s*key:\s*"([^"]+)"\s*,\s*ticks:\s*(\d+)') 41 + out = [] 42 + for m in pat.finditer(text): 43 + label, ticks = m.group(1), int(m.group(2)) 44 + key = LABEL_TO_KEY.get(label) 45 + if not key: 46 + continue 47 + out.append((key, ticks)) 48 + if len(out) >= max_events: break 49 + return out 50 + 51 + def build(bpm: int, events_count: int, start_offset_ms: int, 52 + melody_grid: str = "left") -> str: 53 + """Build the combined bach + drums sequence. melody_grid picks which 54 + grid holds the notes; the other grid plays drums.""" 55 + if melody_grid not in ("left", "right"): 56 + raise SystemExit("melody_grid must be 'left' or 'right'") 57 + # Drum grid is the other one — load the right drum-key table. 58 + # For this script we only handle drums on the RIGHT (the typical 59 + # live-piano layout); if melody_grid=="right" we fall back to 60 + # LEFT-grid drum keys (c/d/g/a). 61 + drum_keys = DRUM_KEY if melody_grid == "left" else { 62 + "kick": "c", "snare": "d", "hat": "g", "hatOpen": "a", 63 + } 64 + 65 + ms_per_beat = 60000 / bpm 66 + ms_per_tick = ms_per_beat / MIDI_TICKS_PER_BEAT 67 + 68 + # ---- Melody notes ---- 69 + events = load_events(events_count) 70 + melody_hits = [] # (abs_ms, key, hold_ms) 71 + t = 0 72 + for key, ticks in events: 73 + dur_ms = max(60, int(round(ticks * ms_per_tick))) 74 + hold = max(40, int(dur_ms * 0.88)) 75 + melody_hits.append((t, key, hold)) 76 + t += dur_ms 77 + 78 + total_ms = t 79 + 80 + # ---- Drum beat (4/4) underneath ---- 81 + # Steady eighth-note hat, kick on 1, snare on 3. Simple & spaced so 82 + # it doesn't mask the baroque line. 83 + drum_hits = [] 84 + eighth_ms = ms_per_beat / 2 85 + beat_ms = ms_per_beat 86 + n_beats = int(total_ms / beat_ms) + 1 87 + for beat in range(n_beats): 88 + b_start = beat * beat_ms 89 + beat_in_bar = beat % 4 90 + # Kick on beat 1 of every bar. 91 + if beat_in_bar == 0: 92 + drum_hits.append((int(b_start), drum_keys["kick"], 0)) 93 + # Snare on beat 3. 94 + if beat_in_bar == 2: 95 + drum_hits.append((int(b_start), drum_keys["snare"], 0)) 96 + # Hat on every eighth (on + off). 97 + drum_hits.append((int(b_start), drum_keys["hat"], 0)) 98 + drum_hits.append((int(b_start + eighth_ms), drum_keys["hat"], 0)) 99 + # Occasional open hat for flavor on the "and of 4". 100 + if beat_in_bar == 3: 101 + drum_hits.append((int(b_start + eighth_ms), drum_keys["hatOpen"], 0)) 102 + 103 + # Merge melody + drums, sort by time (then key for stable order). 104 + all_hits = [(t, k, h) for (t, k, h) in melody_hits] \ 105 + + [(t, k, h) for (t, k, h) in drum_hits if t < total_ms] 106 + all_hits.sort(key=lambda h: (h[0], h[1])) 107 + 108 + # Emit deltas from start_offset_ms anchor. Optional per-event hold. 109 + out, prev = [], -start_offset_ms 110 + for t, k, h in all_hits: 111 + delta = t - prev 112 + if delta < 0: delta = 0 113 + if h > 0: out.append(f"{k},{delta},{h}") 114 + else: out.append(f"{k},{delta}") 115 + prev = t 116 + return "|".join(out) 117 + 118 + def main(): 119 + bpm = int(sys.argv[1]) if len(sys.argv) > 1 else 100 120 + evts = int(sys.argv[2]) if len(sys.argv) > 2 else 64 121 + start = int(sys.argv[3]) if len(sys.argv) > 3 else 0 122 + grid = sys.argv[4] if len(sys.argv) > 4 else "left" 123 + print(build(bpm, evts, start, grid)) 124 + 125 + if __name__ == "__main__": 126 + main()
+87 -20
fedac/native/macos/scripts/demo.sh
··· 24 24 HANDLE="${HANDLE:-jeffrey}" 25 25 CITY="${CITY:-Los Angeles}" 26 26 HOUR="${HOUR:-13}" 27 - OUT="${OUT:-$HOME/Desktop/ac-boot-shots/demo.mkv}" 27 + # WALTZ picks a drum pattern from scripts/waltz-seq.py (classic, dark, 28 + # dreamy, baroque, minimal, phonk, viennese, drill). BPM + BARS control 29 + # tempo and length. When WALTZ=melody the legacy oom-pah-pah melody 30 + # sequence plays instead — useful for quick A/B vs the drum grids. 31 + MODE="${MODE:-waltz}" # waltz | bachbeat 32 + WALTZ="${WALTZ:-classic}" # classic, dark, dreamy, baroque, minimal, phonk, viennese, drill 33 + BPM="${BPM:-120}" # waltz default 120; bachbeat default 100 34 + BARS="${BARS:-4}" # waltz bar count 35 + BACH_EVENTS="${BACH_EVENTS:-48}" # bachbeat: how many bach notes to play 36 + # GRID chooses which hand plays drums. Default "right" = right-hand kit 37 + # + left-hand melody (classic live-piano setup). "left" reverses it. 38 + GRID="${GRID:-right}" 39 + # MELODY=on interleaves an oom-pah-pah bass+chord line on the OTHER 40 + # grid so notes and drums play simultaneously (waltz mode only). 41 + MELODY="${MELODY:-on}" 42 + # KIT_VOL down-ticks for the perc kit after it's enabled — each tick 43 + # drops ~10% volume (notepat arrowdown). 3 = kit ~70%, 5 = ~50%. 44 + KIT_MIXDOWN="${KIT_MIXDOWN:-3}" 45 + case "$MODE" in 46 + bachbeat) OUT_DEFAULT="$HOME/Desktop/ac-boot-shots/demo-bachbeat.mkv" ;; 47 + *) OUT_DEFAULT="$HOME/Desktop/ac-boot-shots/demo-$WALTZ.mkv" ;; 48 + esac 49 + OUT="${OUT:-$OUT_DEFAULT}" 28 50 STAGE="${STAGE:-/tmp/ac-demo-final}" 29 51 30 52 REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd) ··· 41 63 # Boot animation runs for ~2s before this, so the earliest keystroke is 42 64 # effectively ~2.5s into the final video. 43 65 # 44 - # Layout: 45 - # prompt typing: n o t e p a t <enter> (quick, ~1s) 46 - # ~1.4s pause while notepat loads + settles 47 - # waltz: 4 bars × 3 beats at 500ms/beat = 6s 48 - # z c e (Cm bar) 49 - # z f a (Fm bar) 50 - # z g b (Gm bar) 51 - # z c e (Cm bar back home) 52 - SEQ=( 53 - "n,500" "o,120" "t,120" "e,120" "p,120" "a,120" "t,120" "enter,400" 54 - "z,1380" "c,500" "e,500" 55 - "z,500" "f,500" "a,500" 56 - "z,500" "g,500" "b,500" 57 - "z,500" "c,500" "e,500" 58 - ) 59 - # Join with '|' separators (AC_INJECT_SEQUENCE syntax). 60 - IFS='|' SEQ_STR="${SEQ[*]}"; unset IFS 66 + # 0.5s prompt typing: n o t e p a t <enter> (quick, ~1s) 67 + # 1.4s pause while notepat loads + settles 68 + # + pageup/pagedown flips LEFT or RIGHT grid into the perc kit 69 + # + 0.4s breath before the groove kicks in 70 + # + generated drum sequence (+ optional interleaved melody) 71 + 72 + # GRID=right → pagedown (right grid → perc, left grid stays melodic) 73 + # GRID=left → pageup (left grid → perc, right grid stays melodic) 74 + if [[ "$GRID" == "left" ]]; then 75 + KIT_KEY="pageup" 76 + SELECT_KEY="arrowleft" # so arrow-down mixes LEFT grid 77 + PREVIEW_KEY="c" # left-grid kick preview 78 + else 79 + KIT_KEY="pagedown" 80 + SELECT_KEY="arrowright" # so arrow-down mixes RIGHT grid 81 + PREVIEW_KEY="h" # right-grid kick preview (+c → h) 82 + fi 83 + 84 + # Build the kit-mixdown prefix: select the kit side, then fire arrowdown 85 + # $KIT_MIXDOWN times to reduce the perc volume before the beat kicks in. 86 + MIX_SEQ="$KIT_KEY,1400|$SELECT_KEY,200" 87 + for ((i=0; i<KIT_MIXDOWN; i++)); do 88 + MIX_SEQ+="|arrowdown,100" 89 + done 90 + MIX_SEQ+="|$PREVIEW_KEY,400" 91 + 92 + PROMPT_SEQ="n,500|o,120|t,120|e,120|p,120|a,120|t,120|enter,400|$MIX_SEQ" 93 + 94 + case "$MODE" in 95 + bachbeat) 96 + # Default BPM 100 for bachbeat feels right with the baroque meter. 97 + BB_BPM=${BPM:-100} 98 + DRUM_SEQ="$("$(dirname "${BASH_SOURCE[0]}")"/bachbeat-seq.py \ 99 + "$BB_BPM" "$BACH_EVENTS" 300 \ 100 + "$([[ "$GRID" == "left" ]] && echo right || echo left)")" 101 + ;; 102 + melody) 103 + DRUM_SEQ="z,500|c,500|e,500|z,500|f,500|a,500|z,500|g,500|b,500|z,500|c,500|e,500" 104 + ;; 105 + *) 106 + # start_offset_ms = 300 gives a clean breath after the preview kick. 107 + DRUM_SEQ="$("$(dirname "${BASH_SOURCE[0]}")"/waltz-seq.py \ 108 + "$WALTZ" "$BPM" "$BARS" 300 "$GRID" "$MELODY")" 109 + ;; 110 + esac 111 + SEQ_STR="$PROMPT_SEQ|$DRUM_SEQ" 61 112 62 113 # Generate the TTS greeting. -r 165 slightly slower than default so it 63 114 # reads less urgent; -v Samantha is the long-time macOS US voice. ··· 65 116 -o "$STAGE/tts.aiff" \ 66 117 "good afternoon $HANDLE. enjoy $CITY." 67 118 68 - echo "→ running ac-native for ~12 s …" 69 - AC_HEADLESS_MS=12000 \ 119 + # Estimate runtime so AC_HEADLESS_MS doesn't cut the last hits. Prompt 120 + # typing is ~1.6s; kit-switch + mixdown + preview is ~(1.6 + 0.1×mix + 0.4)s. 121 + mix_ms=$(( 1400 + 200 + 100 * KIT_MIXDOWN + 400 )) 122 + case "$MODE" in 123 + bachbeat) 124 + # Bachbeat note count × per-note duration. Per-note ~= 60000/bpm * (ticks/480). 125 + # MIDI_EVENTS are all 120 ticks ≈ 0.25 beat, so per-note ~= 15000/bpm. 126 + body_ms=$(( BACH_EVENTS * 15000 / BPM )) 127 + label="bachbeat events=$BACH_EVENTS bpm=$BPM" 128 + ;; 129 + *) 130 + body_ms=$(( BARS * 3 * 60000 / BPM )) 131 + label="waltz=$WALTZ bpm=$BPM bars=$BARS" 132 + ;; 133 + esac 134 + run_ms=$(( 2000 + 1620 + mix_ms + body_ms + 1800 )) 135 + echo "→ running ac-native for ~$(( run_ms / 1000 )) s ($label grid=$GRID mixdown=$KIT_MIXDOWN) …" 136 + AC_HEADLESS_MS=$run_ms \ 70 137 AC_BOOT_ANIM=1 \ 71 138 AC_WIN_W=1280 AC_WIN_H=800 \ 72 139 AC_SHOT_HANDLE="$HANDLE" \
+181
fedac/native/macos/scripts/waltz-seq.py
··· 1 + #!/usr/bin/env python3 2 + # waltz-seq.py — emit an AC_INJECT_SEQUENCE string for a given waltz style. 3 + # 4 + # Patterns are lifted straight from artery/test-notepat.mjs (WALTZ_PATTERNS). 5 + # Each 3/4 bar has 12 sixteenth-note steps (4 per beat × 3 beats). 6 + # 7 + # notepat has two grids (left + right) that can independently switch 8 + # between "notes" and "perc" kit via PageUp / PageDown. This script emits 9 + # drum hits for whichever grid is holding the kit: 10 + # --grid=left → letters c/d/g/a (keys: c/d/g/a) 11 + # --grid=right → letters +c/+d/+g/+a (keys: h/i/l/m) 12 + # 13 + # With --melody on, the script also interleaves a simple oom-pah-pah 14 + # bass line on the OTHER grid (left-hand notes when grid=right, and 15 + # vice versa), so drums and notes play simultaneously. The melody uses 16 + # I–IV–V–I over the BARS count. 17 + # 18 + # Output format expected by main.c's AC_INJECT_SEQUENCE parser: 19 + # <key>,<delta_ms>|<key>,<delta_ms>|... 20 + # deltas are cumulative from the prior event (not absolute). 21 + # 22 + # Usage: 23 + # waltz-seq.py [style [bpm [bars [start_offset_ms [grid [melody]]]]]] 24 + # Defaults: classic 120 4 0 right off 25 + 26 + import sys 27 + 28 + WALTZ_PATTERNS = { 29 + "classic": { 30 + "kick": [1,0,0,0, 0,0,0,0, 0,0,1,0], 31 + "snare": [0,0,0,0, 1,0,0,0, 0,0,0,0], 32 + "hat": [1,1,1,1, 1,1,1,1, 1,1,1,1], 33 + "hatOpen": [0,0,0,0, 0,0,0,1, 0,0,0,0], 34 + }, 35 + "dark": { 36 + "kick": [1,0,0,0, 0,0,1,0, 0,0,0,0], 37 + "snare": [0,0,0,0, 1,0,0,0, 1,0,0,0], 38 + "hat": [1,1,1,1, 1,1,1,1, 1,1,1,1], 39 + "hatOpen": [0,0,0,1, 0,0,0,1, 0,0,0,1], 40 + }, 41 + "dreamy": { 42 + "kick": [1,0,0,0, 0,0,0,0, 0,0,0,0], 43 + "snare": [0,0,0,0, 1,0,0,0, 0,0,0,0], 44 + "hat": [1,0,1,0, 1,0,1,0, 1,0,1,0], 45 + "hatOpen": [0,0,0,0, 0,0,0,0, 0,0,0,1], 46 + }, 47 + "baroque": { 48 + "kick": [1,0,0,1, 0,0,0,0, 1,0,0,0], 49 + "snare": [0,0,0,0, 1,0,0,0, 0,0,1,0], 50 + "hat": [1,1,1,1, 1,1,1,1, 1,1,1,1], 51 + "hatOpen": [0,0,0,0, 0,0,1,0, 0,0,0,1], 52 + }, 53 + "minimal": { 54 + "kick": [1,0,0,0, 0,0,0,0, 0,0,0,0], 55 + "snare": [0,0,0,0, 0,0,0,0, 1,0,0,0], 56 + "hat": [0,0,0,0, 1,0,0,0, 0,0,0,0], 57 + }, 58 + "phonk": { 59 + "kick": [1,0,0,0, 0,0,1,0, 0,1,0,0], 60 + "snare": [0,0,0,0, 1,0,0,0, 0,0,0,1], 61 + "hat": [1,0,1,1, 1,0,1,1, 1,0,1,1], 62 + "hatOpen": [0,0,0,0, 0,0,0,1, 0,0,0,1], 63 + }, 64 + "viennese": { 65 + "kick": [1,0,0,0, 0,0,0,0, 0,0,0,0], 66 + "snare": [0,0,0,0, 1,0,1,0, 1,0,0,0], 67 + "hat": [1,1,1,1, 1,1,1,1, 1,1,1,1], 68 + }, 69 + "drill": { 70 + "kick": [1,0,0,0, 0,0,0,0, 0,0,1,0], 71 + "snare": [0,0,0,0, 0,0,0,0, 1,0,0,0], 72 + "hat": [1,1,1,1, 1,1,1,1, 1,1,1,1], 73 + "hatOpen": [0,0,0,1, 0,0,0,1, 0,0,0,1], 74 + }, 75 + } 76 + 77 + # Drum name → notepat perc-kit keyboard key, per grid. 78 + # Left grid: c(c4) d(d4) g(g4) a(a4) 79 + # Right grid: h(+c5) i(+d5) l(+g5) m(+a5) [via NOTE_TO_KEY in notepat.mjs] 80 + DRUM_KEY_BY_GRID = { 81 + "left": {"kick": "c", "snare": "d", "hat": "g", "hatOpen": "a"}, 82 + "right": {"kick": "h", "snare": "i", "hat": "l", "hatOpen": "m"}, 83 + } 84 + 85 + # Melody grooves — list of (key, step_index) pairs per bar. Step indices 86 + # are 0..11 over the 12 sixteenth-note steps of a 3/4 bar. 87 + # 88 + # Left-hand grooves run on the LEFT grid in C minor pentatonic 89 + # (c, d#, f, g, a#). Notepat left-grid keys: 90 + # c → c4 d#→ s f → f g → g a#→ q 91 + # z → -a# (sub-octave bass) x → -b (sub-octave bass alt) 92 + # 93 + # Four-bar phrase with varied patterns so it doesn't feel mechanical: 94 + # bar 0: bass anchor + ascending arp 95 + # bar 1: syncopated with call-response 96 + # bar 2: 8th-note run up the pentatonic 97 + # bar 3: bar 0 again (return home) 98 + MELODY_BY_GRID = { 99 + "left": [ 100 + # bar 0 — long bass + rising arp resolving on held 5th 101 + [("z", 0, 6), ("c", 4, 2), ("s", 5, 2), ("f", 6, 2), 102 + ("g", 8, 3), ("q", 9, 2), ("g", 10, 3)], 103 + # bar 1 — syncopated: bass + upper-neighbor, held 7th 104 + [("z", 0, 4), ("f", 2, 3), ("g", 3, 2), ("q", 4, 4), 105 + ("c", 6, 3), ("s", 8, 2), ("g", 10, 3)], 106 + # bar 2 — 8th-note run up the pentatonic 107 + [("z", 0, 3), ("c", 2, 2), ("s", 3, 2), ("f", 4, 2), 108 + ("g", 5, 2), ("q", 6, 2), ("c", 8, 3), ("s", 10, 3)], 109 + # bar 3 — return to bar 0 shape 110 + [("z", 0, 6), ("c", 4, 2), ("s", 5, 2), ("f", 6, 2), 111 + ("g", 8, 3), ("q", 9, 2), ("g", 10, 3)], 112 + ], 113 + "right": [ 114 + [("h", 0, 6), ("h", 4, 2), ("y", 5, 2), ("k", 6, 2), 115 + ("l", 8, 3), ("p", 9, 2), ("l", 10, 3)], 116 + [("h", 0, 4), ("k", 2, 3), ("l", 3, 2), ("p", 4, 4), 117 + ("h", 6, 3), ("y", 8, 2), ("l", 10, 3)], 118 + [("h", 0, 3), ("h", 2, 2), ("y", 3, 2), ("k", 4, 2), 119 + ("l", 5, 2), ("p", 6, 2), ("h", 8, 3), ("y", 10, 3)], 120 + [("h", 0, 6), ("h", 4, 2), ("y", 5, 2), ("k", 6, 2), 121 + ("l", 8, 3), ("p", 9, 2), ("l", 10, 3)], 122 + ], 123 + } 124 + 125 + def build(style: str, bpm: int, bars: int, start_ms: int, 126 + grid: str = "right", melody: bool = False) -> str: 127 + pat = WALTZ_PATTERNS.get(style) 128 + if not pat: 129 + raise SystemExit(f"unknown style '{style}'. pick from: {', '.join(WALTZ_PATTERNS)}") 130 + if grid not in DRUM_KEY_BY_GRID: 131 + raise SystemExit(f"unknown grid '{grid}'. pick from: left, right") 132 + drum_keys = DRUM_KEY_BY_GRID[grid] 133 + # 3/4 has 3 beats per bar; 4 sixteenth-steps per beat → 12 steps/bar. 134 + step_ms = int(round(60000 / bpm / 4)) 135 + steps_per_bar = 12 136 + 137 + # Collect every hit as (abs_ms, key, hold_ms). Drums default hold=0 138 + # (taps); melody notes carry hold durations so they sustain. 139 + hits = [] 140 + for bar in range(bars): 141 + bar_start = bar * steps_per_bar * step_ms 142 + for step in range(steps_per_bar): 143 + t = bar_start + step * step_ms 144 + for drum, arr in pat.items(): 145 + if arr[step]: 146 + hits.append((t, drum_keys[drum], 0)) 147 + 148 + # Optional minor-pentatonic groove on the OTHER grid. hold_steps 149 + # is in 16th-step units — multiplied by step_ms for absolute ms. 150 + if melody: 151 + melody_grid = "left" if grid == "right" else "right" 152 + groove = MELODY_BY_GRID[melody_grid] 153 + bar_groove = groove[bar % len(groove)] 154 + for key, step, hold_steps in bar_groove: 155 + hits.append((bar_start + step * step_ms, key, 156 + hold_steps * step_ms)) 157 + 158 + hits.sort(key=lambda h: (h[0], h[1])) 159 + 160 + # Emit with deltas. Events at same ms share delta=0. Notes with 161 + # hold > 0 emit three fields (key,delay,hold); drums stay two-field. 162 + out, prev = [], -start_ms 163 + for t, k, h in hits: 164 + delta = t - prev 165 + if delta < 0: delta = 0 166 + if h > 0: out.append(f"{k},{delta},{h}") 167 + else: out.append(f"{k},{delta}") 168 + prev = t 169 + return "|".join(out) 170 + 171 + def main(): 172 + style = sys.argv[1] if len(sys.argv) > 1 else "classic" 173 + bpm = int(sys.argv[2]) if len(sys.argv) > 2 else 120 174 + bars = int(sys.argv[3]) if len(sys.argv) > 3 else 4 175 + start = int(sys.argv[4]) if len(sys.argv) > 4 else 0 176 + grid = sys.argv[5] if len(sys.argv) > 5 else "right" 177 + melody = (len(sys.argv) > 6 and sys.argv[6].lower() in ("on", "1", "true", "yes")) 178 + print(build(style, bpm, bars, start, grid, melody)) 179 + 180 + if __name__ == "__main__": 181 + main()