-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPROGRESS.txt
More file actions
683 lines (613 loc) · 77.1 KB
/
PROGRESS.txt
File metadata and controls
683 lines (613 loc) · 77.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
Cross-platform roadmap pointer recorded on 2026-04-02
- The durable roadmap for frontend and host unification plus upcoming warp sequencing now lives in `PLATFORM_UNIFICATION_AND_WARP_SPRINT_PLAN.md`.
- Use that file for the cross-platform order of operations:
- merge `codex/UI-refactor`
- add the shared browser-side host contract and `resourceClient`
- migrate iPhone to the React and Vite frontend
- then move desktop to the repo-owned native host
- then do waveform warp work
Stock CHOC safe-area probe results recorded on 2026-04-01
- I built a separate iPhone simulator app at `experiments/choc_webview_probe/`.
- That app uses upstream CHOC `WebView` headers from commit `9cfd6054f8d863aa39029c6f3daf71b56746b4d7`, not the patched Cosimo copy.
- The page itself only uses web-side tools:
- `<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">`
- zero page margins
- CSS `env(safe-area-inset-*)`
- Baseline stock CHOC result in a valid full-screen app:
- native iPhone safe area was `top 62 / bottom 34`
- the page saw the same `62px / 34px`
- but the actual page viewport was only `402 x 778` on a `402 x 874` screen
- so the page still lost exactly the safe-area height
- Direct native knob experiments:
- baseline: viewport stayed `402 x 778`
- `setMinimumViewportInset(UIEdgeInsetsZero, UIEdgeInsetsZero)` only: still `402 x 778`
- `scrollView.contentInsetAdjustmentBehavior = .never` only: viewport became `402 x 874`
- both together: same `402 x 874`
- Critical stock-CHOC lifecycle result:
- in the `fetchResource` startup path, `webviewIsReady` fired before the first `fetchResource` call and before the first successful JavaScript layout measurement
- setting `scrollView.contentInsetAdjustmentBehavior = .never` inside `webviewIsReady` was early enough to make the first measured viewport `402 x 874`
- Current best conclusion:
- the critical native knob is `WKWebView.scrollView.contentInsetAdjustmentBehavior = .never`
- `setMinimumViewportInset` did not fix this probe by itself
- stock CHOC does have an early-enough hook (`webviewIsReady`) to set the critical native option before first layout in this probe
Real Cosimo app stock-CHOC experiment branch recorded on 2026-04-01
- Branch: `codex/ios-stock-choc-webview-test`
- Goal:
- test the real Cosimo iPhone app with the iPhone-specific CHOC safe-area patch removed
- keep stock CHOC
- move the one critical fix into repo-owned host code by setting `WKWebView.scrollView.contentInsetAdjustmentBehavior = .never` inside `webviewIsReady`
- Changes on this branch:
- removed the iPhone-specific safe-area override from `ios_auv3/Vendor/cmajor/include/choc/choc/gui/choc_WebView.h`
- removed the support struct added for that override from `ios_auv3/Vendor/cmajor/include/choc/choc/platform/choc_ObjectiveCHelpers.h`
- added the external stock-CHOC fix in `ios_auv3/Source/CosimoCmajorPlugin.h` by grabbing the native `WKWebView*` in `webviewIsReady`, setting `contentInsetAdjustmentBehavior = .never`, and disabling automatic scroll-indicator inset adjustment if that selector exists
- Current run status:
- simulator build succeeded for `CosimoSynth_Standalone`
- simulator app installed and launched successfully
- device build succeeded for `CosimoSynth_Standalone`
- device app installed successfully on the paired iPhone
- device launch attempt failed only because the phone was locked
- Current artifacts:
- simulator screenshot captured at `/tmp/cosimo-stock-choc-sim.png`
- Next step:
- unlock the paired iPhone and relaunch `dev.cosimo.wavetable-synth`
- then compare the real app behavior on phone against the simulator result and decide whether the old CHOC safe-area patch is still needed
CHOC vendor cleanup result recorded on 2026-04-01
- The real iPhone app was rebuilt and launched with:
- stock `choc_WebView.h`
- stock `choc_ObjectiveCHelpers.h`
- stock `choc_javascript_QuickJS.h`
- the native `WKWebView` scroll-view fix applied in repo code inside `ios_auv3/Source/CosimoCmajorPlugin.h`
- The page-level iPhone top gutter was also corrected:
- `patch_gui/index.js` now uses `--cosimo-ios-top-inset: 0px`
- the old black bar at the top was our own HTML/CSS, not a native header
- Result:
- the real iPhone app works without any local CHOC patch
- the vendored CHOC files now match upstream commit `9cfd6054f8d863aa39029c6f3daf71b56746b4d7`
- safe-area behavior is now owned in repo code plus normal page CSS, not in a CHOC fork
Pinned Cmajor runtime fetch cleanup recorded on 2026-04-01
- The repo no longer needs a checked-in `ios_auv3/Vendor/cmajor/` tree.
- The shipping iPhone build no longer patches generated Cmajor output after `cmaj generate --target=cpp`.
- One pinned fetch helper now owns the Cmajor runtime source:
- `scripts/ensure_cmajor_runtime.py`
- fetches `https://github.com/cmajor-lang/cmajor.git`
- pins tag `1.0.3066`
- verifies commit `172db53232337154d5a1c0f9a448318129dfacd9`
- initializes the `include/choc` submodule because CHOC is not stored inline in the main Cmajor git tree
- stores the runtime under `build/deps/cmajor-1.0.3066/`
- `ios_auv3/CMakeLists.txt` now asks that helper for the runtime path instead of reading `ios_auv3/Vendor/cmajor/`.
- `ios_auv3/vite.config.mjs` now asks that helper for the same runtime path so `/cmaj_api` in the iPhone web dev server stays in sync with the real build.
- The remaining old generated-output patching only exists in historical code on `master` and in tests that deliberately rewrite temporary copied fixtures.
Architecture clarification recorded on 2026-04-01
- This repo has had two separate initiatives that are easy to blur together:
- the iPhone native-shell refactor
- the desktop React UI refactor
- They did not land as one clean stack where desktop first moved to a repo-owned native host and then React was built on top of it.
- The branch history after `master` only has three commits:
- `ed2e629` `Checkpoint desktop React patch view`
- `53cc2ab` `Checkpoint remaining iOS native shell work`
- `e3874a9` `Refactor desktop patch UI and fix glide control`
- So the work was interleaved on this branch even though the planning docs described the initiatives separately.
Short chronology
- 2026-03-26:
- There was an older rollback episode that returned the iPhone standalone app to the stock JUCE wrapper in branch `codex/stock-juce-standalone-auv3`, commit `dcf77e8` `Return iOS standalone to stock JUCE wrapper`.
- That older rollback was merged into the main history at `4a4a121`.
- 2026-03-30:
- The current iPhone native-shell refactor landed.
- `scripts/generate_ios_auv3_plugin.sh` switched to raw `cmaj generate --target=cpp`.
- The iPhone target moved to `ios_auv3/Source/CosimoPluginMain.cpp` plus `ios_auv3/Source/CosimoCmajorPlugin.h`.
- The repo started pinning the runtime snapshot under `ios_auv3/Vendor/cmajor/`.
- The repo-owned iPhone host kept the full `PatchWebView` bridge protocol but implemented it in repo code.
- 2026-04-01:
- The desktop React and Vite refactor landed for the patch UI.
- The desktop patch manifest now points at `patch_gui/desktop/index.js`.
- The worker also moved onto a Vite build output.
- The old checked-in `ios_auv3/Vendor/cmajor/` snapshot was replaced with a pinned runtime fetch into `build/deps/cmajor-1.0.3066/`.
- The real iPhone app was validated on stock CHOC plus a repo-owned `WKWebView` scroll-view fix, and the extra page-level top gutter was removed.
- But the desktop native host was not rewritten in the same pass.
What is true right now
- iPhone:
- The iPhone app and AUv3 now use a repo-owned shell around raw generated performer output.
- The repo-owned bridge host is `PatchWebViewHost` in `ios_auv3/Source/CosimoCmajorPlugin.h`.
- But that host still creates `choc::ui::WebView`.
- So the iPhone runtime is still on the CHOC WebView path even though the surrounding shell is repo-owned.
- The CHOC runtime is now fetched into `build/deps/cmajor-1.0.3066/`, not checked into source control.
- The safe-area behavior is no longer implemented by patching CHOC. It is implemented by repo code that sets the native `WKWebView` scroll-view inset policy in `webviewIsReady`, plus the normal page CSS.
- Desktop:
- The desktop UI is now the React and Vite build from `patch_gui/desktop/index.js`.
- But the standalone desktop host still uses Cmajor's stock host stack:
- `tools/desktop_native/Source/cmaj_PatchLoaderPlugin.cpp` derives from `cmaj::plugin::JUCEPluginBase`
- `cmaj_JUCEPlugin.h` still creates `cmaj::PatchWebView`
- `cmaj_PatchWebView.h` still creates `choc::ui::WebView`
- The desktop host changes on this branch were dev-server wiring and patch-manifest wiring, not a repo-owned native host rewrite.
Current conclusion about the CHOC WebView patch
- The local CHOC safe-area patch is no longer needed.
- The real iPhone app now works on stock CHOC because the real fix lives in repo code:
- the host grabs the native `WKWebView`
- sets `scrollView.contentInsetAdjustmentBehavior = .never`
- disables automatic scroll-indicator inset adjustment
- The desktop React refactor did not remove the CHOC dependency itself because desktop still uses the stock Cmajor `JUCEPluginBase -> PatchWebView -> choc::ui::WebView` path.
- So the right answer is:
- the CHOC WebView patch is not a permanent architectural requirement
- it is not a current implementation requirement anymore either
- but CHOC itself is still a current runtime dependency until the remaining native WebView layer is in-housed too
What "finish the shell refactor properly" means if the goal is to remove CHOC entirely
- iPhone:
- Replace `choc::ui::WebView` inside `PatchWebViewHost` with a repo-owned `WKWebView` wrapper.
- Keep the existing repo-owned bridge protocol, resource resolver, and bundle/dev-server loader.
- The safe-area behavior already lives in repo code, so this remaining step is about removing the CHOC wrapper layer itself, not about restoring the old patch.
- Desktop:
- Replace the current `JUCEPluginBase` and `cmaj::PatchWebView` host path with a repo-owned JUCE editor and repo-owned WebView host.
- The current desktop React UI can sit on top of that later, but it does not do that yet.
Practical rule for future sessions
- Do not say "the native-shell refactor is done everywhere" or "desktop is already on the repo-owned host".
- The accurate statement is:
- iPhone shell refactor: mostly done, uses stock CHOC WebView, and no longer needs a CHOC fork
- desktop React UI refactor: done for the web UI layer, not for the native host layer
Native iOS shell cleanup follow-up landed on 2026-03-31
- This was a cleanup pass after the repo-owned iOS AUv3 shell refactor, focused on removing flaky test contracts and tightening the remaining footguns instead of adding new features.
- The Debug dev-server and editor inspection path is now less racy:
- `ios_auv3/Source/CosimoAUv3HostHarness.mm` now retries editor-state capture until the host-page inspection data is actually ready instead of returning the first partial patch-view snapshot.
- `ios_auv3/Source/CosimoHostViewController.mm` now refreshes editor state before writing layout smoke output.
- `scripts/run_ios_auv3_host_smoke.py` now refuses to treat incomplete patch-view metrics as a successful inspect result.
- The repo-owned runtime no longer silently swallows several resource and audio-data failures:
- `ios_auv3/Source/CosimoCmajorPlugin.h` now logs resource-read and modification-time failures instead of hiding them completely.
- The raw-performer generator contract is tighter without breaking the real Xcode build path:
- `scripts/generate_ios_auv3_plugin.sh` still rejects the repo root and unrelated non-empty directories
- outside this repo, it now only accepts output paths that end with `generated/cmajor`, which matches the actual CMake/Xcode build layout and avoids deleting arbitrary directories
- The iOS test contract is cleaner:
- added a real standalone missing-library behavior test that deletes one installed wavetable source file and proves the app stays on the installer screen
- reduced several structure-freeze tests so they assert the external contract instead of large swaths of implementation text
- removed a bad host-smoke assertion that was treating a stale runtime endpoint snapshot as proof of saved-state correctness; the host smoke now focuses on the real AU state blob path, while runtime endpoint delivery is covered by its own test
- Temporary cleanup tracker:
- `TMP_IOS_AUV3_CLEANUP_TASKS.md`
- What was verified in this cleanup pass:
- `uv run pytest -q tests/test_ios_auv3_build.py -k 'generator_'`
- `uv run pytest -q tests/test_ios_auv3_build.py -k 'test_ios_host_smoke_discovers_the_extension_and_restores_state_across_relaunch or test_ios_host_smoke_freezes_parameter_and_state_shape or test_ios_host_smoke_keeps_the_editor_inside_phone_and_tablet_viewports'`
- `uv run pytest -q tests/test_ios_auv3_build.py`
- Final result: `28 passed in 411.09s (0:06:51)`
Native iOS shell refactor follow-up landed on 2026-03-30
- Plan: `IOS_AUV3_NATIVE_SHELL_REFACTOR_PLAN.md`.
- Kept the repo-owned AUv3 shell around raw `cmaj generate --target=cpp` performer output and, at that time, a pinned runtime snapshot under `ios_auv3/Vendor/cmajor/`.
- Removed the simulator-only repo-checkout resource fallback that was masking packaging bugs. Debug now tries the local Vite server if it is reachable and otherwise loads the bundled UI and patch assets. Release always loads bundled files.
- Added a debug and test only editor inspection hook, gated by `COSIMO_ENABLE_EDITOR_INSPECTION`, so the simulator host tests can capture the real extension-side host page, DOM layout, and catalog snapshot without leaving always-on telemetry in shipping builds.
- Added the ATS exceptions needed for the local web-development server and threaded `COSIMO_ENABLE_EDITOR_INSPECTION` through `scripts/generate_ios_auv3_xcode_project.sh`.
- Expanded `tests/test_ios_auv3_build.py` so it now proves:
- the built standalone app and AUv3 extension bundles contain the shipped UI, `cmaj_api`, assets, and patch manifest files
- the shipped bundles can serve the UI and patch resources from the real bundle layout
- Debug loads modified dev-server HTML and JavaScript without rebuilding and falls back to bundled files when the server is absent
- Release ignores the dev-server URL and still uses bundled files
- the AUv3 shows the missing-library gate screen when the shared wavetable library is absent
- the editor reads a shared-library catalog override from the App Group install location
- host smoke still discovers the AUv3, opens the editor, restores state, and fits phone and tablet viewports
- What was verified:
- `uv run pytest -q tests/test_ios_auv3_build.py`
- Result: `21 passed`
- Deliberate non-goal kept for follow-up:
- the large queue-size hack and the underlying wavetable mip transport design were not changed in this refactor
Host smoke and iPhone wavetable resource follow-up landed on 2026-03-31
- The AUv3 host smoke no longer claims state restore success by replaying raw parameter values. `ios_auv3/Source/CosimoAUv3HostHarness.mm` now saves the real AU `fullState` blob when available, writes the expected observable parameter values next to that blob, restores through the real AU state property, and polls the parameter tree until the blob has actually taken effect.
- The frozen host-smoke snapshot in `ios_auv3/expected_host_smoke.json` now matches the real JUCE AU state envelope keys:
- `data`
- `jucePluginState`
- `manufacturer`
- `subtype`
- `type`
- `version`
- The AUv3 audio smoke now sends a second note after startup because Simulator occasionally drops the first note while the out-of-process extension is still warming up. The smoke still fails if the instance never produces audio.
- The shared-library readiness gate still uses full `validateInstalledLibrary()` validation, so a partial install can no longer report as ready just because the catalog JSON exists.
- Fixed the real iPhone wavetable regression:
- the worker and display loaders now prefer the resolved resource URL for source WAV files and only fall back to the audio-data bridge when no fetchable URL exists
- the custom `cosimo://bundle/...` URL handler now serves raw bytes for binary resources instead of corrupting `.wav` files through `loadFileAsString()`
- `BS2 - Acid.wav` now loads correctly from the installed factory library on the physical iPhone
- DO NOT BREAK THIS AGAIN: `BS2 - Acid.wav` is the canary for the iPhone source-WAV path. The failure is not "the table is too big." The failure happens when someone routes iPhone source WAV files through `readResourceAsAudioData()` first instead of using the resolved `getResourceAddress(...)` URL first. The required rule is simple and non-negotiable: use the native bridge for catalog JSON text, but use URL-first loading for source WAV files and only fall back to the audio-data bridge when no fetchable URL exists. Any adapter or refactor that turns `prefersResourceReadBridge` into "force all WAVs through the bridge" is reintroducing this exact regression.
- Cleanup after the iPhone regression hunt:
- removed the one-off `COSIMO_EDITOR_TEST_AUDIO_SNAPSHOT_PATH` runtime hook and its `resourceAudio` inspection payload
- the shared-library regression test now uses the normal catalog snapshot path by overriding the first installed table to `assets/factory_sources/imported/BS2 - Acid.wav`
- `ios_auv3/Source/CosimoPluginMain.cpp` now only includes the QuickJS implementation header when the QuickJS worker path is actually enabled
- What was verified:
- `node --test tests/test_wavetable_worker.mjs`
- `node --test --test-name-pattern='bank loading resolves the selected source wavetable from the runtime catalog|bank loading prefers the resolved resource URL for factory wavetable source paths when both loader paths are available' tests/test_wavetable_display.mjs`
- rebuilt and reinstalled `build/ios_device_run/CosimoSynth_artefacts/Debug/Standalone/Cosimo Synth.app`
- confirmed on the physical iPhone that `BS2 - ACID` now loads successfully
Main-view control strip and MSEG launcher simplification landed on 2026-03-30
- Removed the visible `Voice Routing` heading and `Play Mode` field label from the shipping patch UI.
- Kept the voice mode selector and glide control on one shared row in both desktop and iPhone layouts.
- Moved the glide value readout onto the same row as the glide slider instead of stacking it under a labeled field.
- Removed the `Open Editor` button above the main-view MSEG preview.
- Reworked the main-view MSEG preview footer so it now shows:
- the current length in seconds as a bare value
- the same loop icon toggle pattern used in the expanded MSEG editor, with active/inactive color state
- Left the expanded MSEG editor structure intact apart from reusing the same loop-icon visual language in the launcher.
- What was verified for this UI simplification:
- `node --test tests/test_patch_view_layout.mjs`
- `cmaj generate --target=javascript --output=build/generated_js_test WavetableSynth.cmajorpatch`
- `uv run python scripts/run_ios_auv3_host_smoke.py --build-dir build/ios_sim_host_smoke --output build/ios_sim_host_smoke/result.json`
Mono / legato surface reduction landed on 2026-03-30
- Removed the `Note Priority` control from the shipping patch UI and from `cmajor/WavetableSynth.cmajor`. Mono note selection is now hardcoded to newest-held-note behavior.
- Reduced the exposed play modes to `Poly`, `Mono`, and `Legato`.
- Defined the two mono modes concretely:
- `Mono` now means one voice, newest held note takes over, overlap glide stays active, and the envelope retriggers on note changes.
- `Legato` now means one voice, newest held note takes over, overlap glide stays active, and the envelope does not retrigger on note changes.
- Updated `tests/test_note_dispatcher_probe.py` so it now proves the reduced behavior directly:
- `Mono` prefers the newest held note.
- both `Mono` and `Legato` glide on overlap and when returning to an older still-held note.
- `Mono` retriggers while `Legato` does not.
- Updated `tests/test_patch_view_layout.mjs` so it now proves the webview only exposes `Poly`, `Mono`, and `Legato`, and no longer renders `Note Priority`.
- What was verified for this reduction:
- `cmaj generate --target=javascript --output=build/generated_js_test WavetableSynth.cmajorpatch`
- `uv run pytest -q tests/test_note_dispatcher_probe.py -q`
- `node --test tests/test_patch_view_layout.mjs`
Mono / legato / MPE pass landed on 2026-03-29
- Active plan note: `TRANSIENT_MONO_MPE_IMPLEMENTATION_PLAN.md`.
- Follow-up fix on the same day:
- The first `Mono` retrigger envelope implementation was wrong. A second overlapping note could reset the envelope to zero but fail to re-enter attack while the key was still down, which made `Mono` go silent on a second note.
- Fixed `wt::RetriggerableFixedASR` in `cmajor/FixedFrameOscillator.cmajor` so a new `NoteOn` during sustain or release restarts the attack stage instead of staying in the sustain loop at zero.
- Tightened `tests/test_note_dispatcher_probe.py` so it now proves both parts of the expected behavior: `Mono ST` keeps the envelope running, and plain `Mono` recovers audibly to the new note and new pitch after its retrigger attack.
- What was re-verified for the follow-up fix:
- `cmaj generate --target=javascript --output=build/generated_js_test WavetableSynth.cmajorpatch`
- `cmaj generate --target=juce --output=/tmp/cosimo_juce_probe WavetableSynth.cmajorpatch`
- `uv run pytest -q tests/test_note_dispatcher_probe.py -q`
- `uv run pytest -q tests/test_shared_voice_engine_probe.py tests/test_runtime_wavetable_mip_probe.py -q`
- `node --test tests/test_patch_view_layout.mjs`
- Current phone build status after the follow-up fix:
- Rebuilt, reinstalled, and relaunched the standalone iPhone app on 2026-03-29.
- Current installed app bundle path on device is `/private/var/containers/Bundle/Application/1F5478BB-022E-4712-9D69-42D7460CC5A5/Cosimo Synth.app/`.
- What changed:
- Replaced the top-level poly-only allocator path with a repo-owned `wt::NoteDispatcher` in `cmajor/WavetableSynth.cmajor`. The shipping patch now does `midiIn -> std::midi::MPEConverter -> wt::NoteDispatcher(16) -> wt::SharedVoiceEngine(16)`.
- Added exposed patch parameters for `playMode`, `notePriority`, and `glideTime`.
- Added mono play modes: `Poly`, `Mono`, `Mono ST`, `Mono FP`, and `Mono ST + FP`.
- Added mono note-priority policies: `Last`, `High`, and `Low`.
- Added a new shared-engine event type, `wt::VoiceRetune`, so mono note changes can retune the active lane without forcing an envelope retrigger.
- Extended `wt::SharedVoiceEngine` in `cmajor/FixedFrameOscillator.cmajor` with per-voice channel, pitch, bend, pressure, slide, and glide state. Pitch is now recomputed continuously so mip selection follows bend and glide automatically.
- Implemented actual audible mono retrigger behavior by replacing the shared-engine envelope node with a repo-owned `wt::RetriggerableFixedASR`. The stock Cmajor `std::envelopes::FixedASR` does not retrigger on overlapping note-ons, so mono retrig and mono single-trigger would otherwise sound identical.
- Kept the current runtime wavetable-loading architecture intact: source WAV files still ship separately in `assets/factory_sources/`, the JavaScript worker still builds mip frames at runtime, and the shared DSP bank still receives those frames once for all voices.
- Added webview controls in `patch_gui/index.js` for play mode, note priority, and glide time on both desktop and iPhone layouts.
- What was verified for the mono/MPE pass:
- `uv run pytest -q tests/test_note_dispatcher_probe.py -q`
- `uv run pytest -q tests/test_shared_voice_engine_probe.py tests/test_runtime_state_coordinator.py tests/test_runtime_wavetable_mip_probe.py tests/test_mseg_cmajor_probe.py -q`
- `node --test tests/test_patch_view_layout.mjs tests/test_wavetable_worker.mjs tests/test_wavetable_display.mjs tests/test_mseg_renderer.mjs tests/test_mseg_editor.mjs`
- `uv run pytest -q tests/test_ios_auv3_build.py -q -k 'not ios_host_smoke'`
- `cmaj generate --target=javascript --output=build/generated_js_test WavetableSynth.cmajorpatch`
- The new probe file `tests/test_note_dispatcher_probe.py` proves:
- mono priority chooses the correct held note for `Last`, `High`, and `Low`
- fingered portamento modes emit the correct `glide` / `retrigger` retune policy
- `Mono ST` and `Mono` are audibly different because the shared-engine envelope now really retriggers
- pitch bend changes the rendered audio pitch after note-on
- Current phone build status after the mono/MPE pass:
- Rebuilt with `xcodebuild -project build/ios_device_run/CosimoSynthAUv3.xcodeproj -scheme CosimoSynth_Standalone -configuration Debug -destination id=00008120-000139383644C01E DEVELOPMENT_TEAM=JUFVT28775 CODE_SIGN_STYLE=Automatic CODE_SIGN_IDENTITY='Apple Development' -allowProvisioningUpdates build`
- Confirmed the rebuilt standalone app bundle still contains `assets/factory-bank-catalog.json` and `assets/factory_sources/`
- Installed with `xcrun devicectl device install app --device 00C7F433-8B6A-5CAC-856F-56D7385E12F9 'build/ios_device_run/CosimoSynth_artefacts/Debug/Standalone/Cosimo Synth.app'`
- Launched with `xcrun devicectl device process launch --device 00C7F433-8B6A-5CAC-856F-56D7385E12F9 dev.cosimo.wavetable-synth`
Path 1 polyphony refactor started on 2026-03-29
- Active task: replace the current voice-owned wavetable bank with one shared-bank owning processor while keeping the current JavaScript patch view, JavaScript wavetable worker, and graph-level Cmajor voice allocation.
- Working plan: `TRANSIENT_PATH1_POLYPHONY_IMPLEMENTATION_PLAN.md`.
- First implementation step: add focused proof tests for shared-bank polyphony and note-release independence before rewiring the shipping patch.
- Landed in this pass:
- Added `wt::SharedVoiceEngine` to `cmajor/FixedFrameOscillator.cmajor`. It owns one mutable wavetable bank, one runtime wavetable service state machine, and per-voice phasor, envelope, MSEG, generation, and display-selection state.
- Rewired `cmajor/WavetableSynth.cmajor` so the shipping patch now does `midiIn -> std::midi::MPEConverter -> std::voices::VoiceAllocator(16) -> wt::SharedVoiceEngine(16)` and then trims/stereos the shared-owner mono output.
- Kept the existing runtime worker endpoint names and payload shapes, so `patch_gui/wavetable-worker.mjs`, `patch_gui/index.js`, and the iPhone generated-plugin checks stayed stable.
- Added `tests/test_shared_voice_engine_probe.py` with two proof tests: one shared-bank render equals the sum of two independent voices, and releasing one voice does not cut off the other.
- What was verified for the Path 1 landing:
- `cmaj generate --target=javascript --output=/tmp/cosimo_poly_runtime.cjs WavetableSynth.cmajorpatch`
- `uv run pytest -q tests/test_runtime_state_coordinator.py -q`
- `uv run pytest -q tests/test_shared_voice_engine_probe.py -q`
- `node --test tests/test_wavetable_worker.mjs tests/test_wavetable_display.mjs tests/test_patch_view_layout.mjs`
- `uv run pytest -q tests/test_runtime_wavetable_mip_probe.py tests/test_mseg_cmajor_probe.py -q`
- `uv run pytest -q tests/test_wavetable_worker_runtime.py tests/test_fixed_frame_probe.py tests/test_runtime_state_coordinator.py tests/test_shared_voice_engine_probe.py -q`
- `uv run pytest -q tests/test_ios_auv3_build.py -q -k 'not ios_host_smoke'`
Current wavetable-loading source of truth
- The reviewed plan for restoring mip-selected playback while keeping the source WAVs as separate bundled files lives in `SERUM_WAVETABLE_BUNDLING_PLAN.md`.
What is true right now
- The app still ships wavetable source files separately in `assets/factory_sources/` and uses `assets/factory-bank-catalog.json` as the runtime catalog.
- The active synth path has a patch worker, `patch_gui/wavetable-worker.mjs`, which reads the selected source WAV, compiles mip frames in JavaScript, and streams them into the patch. It now classifies source-read failures separately from mip-build failures, aborts dead committed loading generations with a watchdog timeout, and does not immediately throw away a reconstructed active table on worker restart.
- `cmajor/FixedFrameOscillator.cmajor` is mip-aware again. It accepts `wavetableLoadBegin`, `wavetableMipFrame`, and `serviceLoadAbort`, emits `wavetableMipRequest`, `wavetableUploadAck`, and `runtimeServiceState`, rejects stale older generations, raises mip-request urgency to `1` when it is currently playing a darker fallback mip, and now uses the real Cmajor `processor.session` value instead of hardcoded session `1`.
- `cmajor/WavetableSynth.cmajor` now owns the runtime wavetable state model. It publishes `runtimeState`, tracks the desired table separately from the active/loading DSP table, fences stale worker failures both by desired-intent serial and by the currently active/loading generation, and exposes `runtimeSyncRequest` plus `retryDesiredTableRequest`.
- The patch GUI in `patch_gui/index.js` no longer treats `wavetableSelect` as audible truth. It asks for `runtimeState`, keeps the selector bound to the requested table, keeps the displayed/audible wavetable view bound to the active or loading DSP table, and now has an explicit retry action for a failed same-table load instead of relying on an impossible native `<select>` re-select path.
- The iPhone JUCE generator script, `scripts/generate_ios_auv3_plugin.sh`, no longer injects the old native raw-table uploader. It keeps the existing bundle-aware manifest loader and now relies on the patch worker instead.
What was verified
- `node --test tests/test_wavetable_worker.mjs tests/test_wavetable_display.mjs tests/test_patch_view_layout.mjs`
- `uv run pytest tests/test_wavetable_worker_runtime.py tests/test_runtime_wavetable_mip_probe.py tests/test_runtime_state_coordinator.py tests/test_fixed_frame_probe.py -q`
- `uv run pytest tests/test_ios_auv3_build.py -q -k 'not ios_host_smoke'`
- `uv run pytest tests/test_ios_auv3_build.py -q -k generated_ios_plugin_externalises_the_bank_but_keeps_patch_ui_resources`
- `uv run pytest tests/test_ios_auv3_build.py -q -k ios_host_smoke`
What is fixed in the latest wavetable-loader pass
- The worker no longer keeps chasing an obsolete loading generation when the user changes to a different wavetable mid-load. It now aborts that stale load and follows the new desired table.
- The worker now auto-retries one timed-out load for the currently desired wavetable instead of leaving that table dead until the user manually retries it.
- The UI now surfaces the actual wavetable failure state. A timed-out wavetable transfer shows as `Wavetable load timed out.` and failed loads expose the retry affordance consistently.
- The iPhone patch view now actually has a mobile `display-status` target and an explicit `table-error-banner`, so runtime wavetable failures are no longer silently dropped on iOS.
- The patch view now latches the last wavetable failure until the requested table actually becomes active again, which prevents immediate worker retries from erasing the only visible failure message.
- The worker now emits detailed console logs for runtime-state updates, source reads, committed loads, mip requests, transfer progress, and watchdog timeouts. The patch view also logs runtime-state transitions and explicit wavetable failures.
- The worker watchdog timeout was raised from `8000 ms` to `20000 ms` to reduce false transfer timeouts while debugging large imported Serum tables.
Current iPhone build status
- The current installable app bundle in this worktree is `build/ios_device_run/CosimoSynth_artefacts/Debug/Standalone/Cosimo Synth.app`.
- The current app bundle contains the expected runtime assets:
- `Cosimo Synth.app/assets/factory-bank-catalog.json`
- `Cosimo Synth.app/assets/factory_sources/`
- A fresh signed build was rebuilt from `build/ios_device_run/CosimoSynthAUv3.xcodeproj`, installed onto the physical iPhone, and launched successfully on 2026-03-29.
- The generated Xcode project in this worktree did not set `DEVELOPMENT_TEAM`. The device build only succeeded after adding the command-line overrides `DEVELOPMENT_TEAM=JUFVT28775 CODE_SIGN_STYLE=Automatic CODE_SIGN_IDENTITY='Apple Development' -allowProvisioningUpdates`.
- The exact successful device build command was:
- `xcodebuild -project build/ios_device_run/CosimoSynthAUv3.xcodeproj -scheme CosimoSynth_Standalone -configuration Debug -destination id=00008120-000139383644C01E DEVELOPMENT_TEAM=JUFVT28775 CODE_SIGN_STYLE=Automatic CODE_SIGN_IDENTITY='Apple Development' -allowProvisioningUpdates build`
- The paired phone currently uses two different identifiers in Apple tooling:
- `xcodebuild` destination id: `00008120-000139383644C01E`
- `devicectl` device id: `00C7F433-8B6A-5CAC-856F-56D7385E12F9`
- The exact successful install and launch commands were:
- `xcrun devicectl device install app --device 00C7F433-8B6A-5CAC-856F-56D7385E12F9 'build/ios_device_run/CosimoSynth_artefacts/Debug/Standalone/Cosimo Synth.app'`
- `xcrun devicectl device process launch --device 00C7F433-8B6A-5CAC-856F-56D7385E12F9 dev.cosimo.wavetable-synth`
- The current installed app bundle path on device is `/private/var/containers/Bundle/Application/E0A3AFCB-497B-4DF4-AC85-7713ABFBB39B/Cosimo Synth.app/`.
What happens next
- Exercise wavetable switching on the physical iPhone again and inspect the new explicit failure banner plus the new `[wavetable-worker]` and `[wavetable-view]` console logs if failures still occur.
Detour completed on 2026-03-26
- Read `phase-6-mseg-plan.md`, `full-proposal.md`, `cmajor/FixedFrameOscillator.cmajor`, `cmajor/WavetableSynth.cmajor`, and `patch_gui/index.js`.
- Confirmed that the current synth has wavetable scanning but no modulation plumbing, no MSEG endpoints, and no GUI stored-state path yet.
- Confirmed from the official Cmajor patch docs that `PatchConnection.sendEventOrValue()` can coerce JavaScript arrays into complex endpoint types and that stored-state APIs exist for persisting editor data.
- Expanded `phase-6-mseg-plan.md` with the narrow first-cut plan: one-shot `MSEG 1`, seconds-based timing, one routed destination (`Wavetable Position`), stored GUI curve state, shared DSP slot buffers, and Python-versus-Cmajor verification.
Detour completed on 2026-03-27
- Read 30 real Vital `.vitallfo` files from `/Users/winterfell/Downloads/Sonicspore FREE BUNDLE VITAL/Sonicspore Free LFO Shpaes VITAL` and confirmed they are plain JSON shape files.
- Cloned Vital to `/Users/winterfell/src/vital` and traced the shape serializer in `src/common/line_generator.cpp`, the runtime LFO engine in `src/synthesis/modulators/synth_lfo.cpp`, and the import-export UI in `src/interface/editor_sections/lfo_section.cpp`.
- Confirmed that Vital stores only shape data in `.vitallfo`, keeps playback behavior in separate synth parameters, inverts file `y` into output-space during render, uses duplicate `x` values for step edges, and plays back from a pre-rendered 2048-sample cubic-interpolated buffer.
- Confirmed that the checked-in Vital source does not show a dedicated Serum LFO importer, but it does document shared wavetable WAV metadata parsing through the RIFF `clm ` chunk.
- Wrote the full technical brief to `VITAL_AND_SERUM_LFO_RESEARCH.md` so the next implementation step can define this repo's own LFO shape spec, playback spec, and import rules separately.
- Compared the old `full-proposal.md` MSEG section against Vital and decided that the final spec should keep the buffer-playback architecture but replace the proposed bezier-native saved format with a Vital-style point-plus-curve shape format, separate playback objects, separate route objects, and a 2048-sample rendered shape buffer with cubic pad samples.
- Drafted `MSEG_VITAL_FOUNDATION_DRAFT.md` as the reduced direction: Vital-style normalized shape data, Lumos-style sustain-loop playback, mono-only v1, no seconds mode, no stereo phase, and no attempt to copy Vital's full playback feature set.
- Revised `MSEG_VITAL_FOUNDATION_DRAFT.md` after review: removed point IDs from the shape model, moved loop markers into playback or preset state as normalized positions independent of points, restored a seconds-based rate mode plus tempo straight/dotted/triplet rates, and clarified that `tail_loop` is the useful part of Vital's `LoopPoint` while exact `LoopHold` remains deferred.
- Simplified the playback model again after further discussion: the current source of truth is now one shape, one rate, one optional loop window, and one note-off policy, with use-case mappings for one-shot, Lumos sustain-loop behavior, Vital-style tail looping, and immediate loop exit on note-off; `phase-6-mseg-plan.md` was updated to match that model and the 2048-plus-padding runtime buffer choice.
- Wrote `phase-6a-mseg-fixed-route-plan.md` as the narrowed implementation target for the first real MSEG slice.
- Locked the active 6A subset to one editable MSEG, add/move/delete point editing, one-shot seconds-based playback, one fixed route to wavetable frame position, and one depth control.
- Kept the future-facing shape-versus-playback split in the 6A doc so later loop windows, note-off policies, tempo sync, and more destinations stay additive instead of forcing a schema rewrite.
- Broke the work into four concrete tasks: JS model plus renderer, GUI editor plus persistence plus upload, DSP reader plus fixed route, and reference-driven verification.
Phase 6B landed on 2026-03-29
- Wrote `phase-6b-mseg-playback-controls-plan.md` as the next implementation slice after 6A: one visible seconds-rate control, full-shape looping while the note is held, `finish_loop` note-off behavior, and the existing fixed route from `MSEG 1` to wavetable position.
- Changed the default MSEG playback object from one-shot to full-shape looped playback, kept the playback schema future-facing, and clamped seconds rates to the current supported range of `0.05` to `8.0`.
- Fixed the patch-view rate control path so a DOM slider string like `"0.375"` now becomes a real playback seconds value instead of silently falling back to `1.0`.
- Extended `wt::MsegReader` in `cmajor/Mseg.cmajor` to use real loop metadata, stop future wraps after note-off for the active `finish_loop` behavior, and keep the existing rendered-buffer plus cubic-interpolation contract.
- Wired note-off from `cmajor/WavetableSynth.cmajor` into the MSEG reader so loop exit is driven by the actual note lifecycle instead of only by note-on retrigger.
- Updated the Python reference path in `bench.py` so the JavaScript model, Python oracle, and Cmajor probe all agree about exact full-loop seam behavior, note-off exit, and disabled-loop transport payloads.
- Tightened the tests so the playback truth now lives in literal objects and literal sample arrays instead of being re-derived from the same helper functions under test.
What was verified for phase 6B
- `node --test tests/test_mseg_renderer.mjs tests/test_mseg_editor.mjs tests/test_patch_view_layout.mjs tests/test_wavetable_display.mjs`
- `uv run pytest -q tests/test_mseg_reference.py`
- `uv run pytest -q tests/test_mseg_cmajor_probe.py -m cmajor`
Detour started on 2026-04-02
- Paused the `DesktopPatchView.tsx` extraction itself to build a real test safety net first.
- Wrote `TRANSIENT_DESKTOP_PATCH_VIEW_EXTRACTION_TEST_PLAN.md` as the transient source of truth for the extraction test plan.
- The immediate goal is to replace brittle source-text regex checks with one real browser harness suite that catches blank-screen regressions and covers the desktop behaviors that must survive the extraction.
Detour progress on 2026-04-02
- Dropped the first `jsdom` test-harness attempt because `nexusui` and the desktop custom-element path do not initialize cleanly there, which would have made the tests easy to fake and hard to trust.
- Kept the real browser harness suite as the safety net instead: `tests/test_desktop_patch_view_browser.mjs` exercises the actual desktop harness in Chromium and verifies that catalog failures, frame failures, wavetable selection, retry, stage drag, keyboard routing, runtime pending-selection presentation, and MSEG editor wiring all work without the page going blank.
- Instrumented the desktop harness-only mock connection in `ui/shared/patch-connection-mock.ts` and `ui/desktop/harness-main.tsx` so browser tests can inspect sent events, runtime state, keyboard activity, MIDI routing, and stored MSEG state without touching production code paths.
- Removed the weakest desktop regex tests from `tests/test_patch_view_layout.mjs` now that the same behavior is covered by real browser interactions.
What was verified for the DesktopPatchView extraction safety net
- `node --test tests/test_patch_view_layout.mjs tests/test_wavetable_display.mjs tests/test_wavetable_worker.mjs tests/test_desktop_patch_view_browser.mjs`
Detour completed on 2026-04-02
- Added a real browser-level desktop React safety net in `tests/test_desktop_patch_view_browser.mjs` instead of continuing to rely on desktop source-text regex checks.
- The new desktop browser suite now proves the real desktop harness page can boot without a blank screen, request `runtimeSyncRequest` on mount, surface catalog and frame-load failures visibly, commit wavetable selection, send retry requests, preserve wavetable stage drag semantics, preserve keyboard routing, and wire the MSEG modal editor into real stored-state updates.
- Instrumented the desktop harness-only mock connection in `ui/shared/patch-connection-mock.ts` and `ui/desktop/harness-main.tsx` so browser tests can inspect sent events, runtime state, keyboard activity, and stored MSEG state without touching production code paths.
- Removed the brittle desktop-specific source-text tests from `tests/test_patch_view_layout.mjs` that only grepped `DesktopPatchView.tsx` for strings instead of proving behavior.
- Deleted the abandoned `jsdom` draft harness files and removed the unused `@testing-library/react`, `jsdom`, and `tsx` dev dependencies so the branch only carries the browser-based safety net we actually trust.
- Rebuilt the generated UI assets with `npm run ui:build` after the testing changes.
Detour completed on 2026-04-02
- Finished the missing direct-test layers from `TRANSIENT_DESKTOP_PATCH_VIEW_EXTRACTION_TEST_PLAN.md` instead of stopping at the page-level browser suite.
- Extracted the desktop-only adapters into `ui/desktop/desktop-keyboard-adapter.tsx` and `ui/desktop/desktop-nexus-number-field.tsx`, and extracted the shared desktop hooks into `ui/desktop/desktop-patch-hooks.ts` so they can be tested directly before the larger `DesktopPatchView.tsx` split.
- Added direct browser-mounted adapter tests in `tests/test_desktop_widget_adapters_browser.mjs` for `ensureKeyboardElement`, `KeyboardDock`, and `NexusNumberField`, using `tests/helpers/desktop_patch_modules_browser.tsx` to mount the real modules under Chromium.
- Added direct browser-mounted hook tests in `tests/test_shared_synth_hooks_browser.mjs` for `useFactoryBankCatalog`, `useFactoryTableFrames`, `useObservedDisplayPosition`, `useMsegState`, `useStagePositionDrag`, and `useMsegEditorInteractions`, again through the real extracted modules instead of source-text checks.
- The direct adapter tests caught two real regressions in the extracted code: `KeyboardDock` was rebuilding the keyboard element whenever `rootNote` or `noteCount` changed, and `NexusNumberField` was ending text entry twice after a blur plus unmount. Both are now fixed in the extracted adapters.
- The direct hook tests also caught a real harness mismatch in the MSEG editor interaction coverage: the old helper was feeding local SVG coordinates into pointer events even though the hook reads real client coordinates from `getBoundingClientRect()`. The helper now uses actual client coordinates so the delete and drag tests are exercising the real hit-testing path.
- Added `patch_gui/wavetable-display.d.ts` and updated `tsconfig.json` so `npx tsc --noEmit` is clean while `ui/shared/wavetable-display.ts` re-exports the legacy iPhone renderer.
What was verified for the direct adapter and hook layers
- `npx tsc --noEmit`
- `node --test tests/test_desktop_widget_adapters_browser.mjs`
- `node --test tests/test_shared_synth_hooks_browser.mjs`
- `node --test tests/test_desktop_patch_view_browser.mjs tests/test_desktop_widget_adapters_browser.mjs tests/test_shared_synth_hooks_browser.mjs tests/test_patch_view_layout.mjs tests/test_wavetable_display.mjs tests/test_wavetable_worker.mjs`
DesktopPatchView extraction completed on 2026-04-02
- Finished the structural extraction pass described by `DESKTOP_PATCH_VIEW_ARCHITECTURE_REVIEW.md` instead of stopping at the test harnesses.
- Moved the reusable synth panels and controls out of `ui/desktop/DesktopPatchView.tsx` into `ui/shared/synth-components.tsx`, including the wavetable stage, MSEG overview, MSEG editor surface, voice-mode control, range field, and the shared canvas wavetable mount.
- Left the desktop-only shell in `ui/desktop/DesktopPatchView.tsx`: the brand header, runtime-sync and patch-binding orchestration, the desktop keyboard row, the Nexus number widget usage, and the desktop modal wrapper still live there, which is the intended boundary for the future iPhone React shell.
- Added direct browser-mounted shared-component tests in `tests/test_shared_synth_components_browser.mjs` so the extracted shared module is proven to work with plain props and callbacks instead of only through the desktop page.
- The extraction cut `ui/desktop/DesktopPatchView.tsx` from the old 1201-line all-in-one file down to a much thinner desktop composition shell while keeping the real browser safety net green.
What was verified for the DesktopPatchView extraction
- `npx tsc --noEmit`
- `npm run ui:build`
- `node --test --test-concurrency=1 tests/test_shared_synth_components_browser.mjs tests/test_desktop_patch_view_browser.mjs tests/test_desktop_widget_adapters_browser.mjs tests/test_shared_synth_hooks_browser.mjs tests/test_patch_view_layout.mjs tests/test_wavetable_display.mjs tests/test_wavetable_worker.mjs`
Detour started on 2026-04-03
- Paused the actual iPhone React migration implementation long enough to lock down the current iPhone frontend behavior with characterization tests first.
- Wrote `TRANSIENT_IOS_REACT_MIGRATION_PLAN.md` as the transient execution plan for replacing the old iPhone `patch_gui/index.js` UI path with the new React and Vite frontend.
- The immediate goal is to strengthen `tests/test_wavetable_display.mjs` so the migration has to preserve the current iPhone safe-area shell, wavetable stage shell, play-mode and glide controls, MSEG modal behavior, footer keyboard behavior, octave controls, and the known `BS2 - Acid.wav` resource-loading rule.
Detour progress on 2026-04-03
- Expanded `tests/test_wavetable_display.mjs` to characterize the old iPhone UI more directly: the stage shell now has explicit tests for the overlay picker, retry button, `Swipe + Drag` hint, dual-canvas stack, play-mode select, glide slider, footer keyboard split, octave clamping, stage swipe-versus-drag behavior, and the iPhone-specific factory-library recovery message.
- Added a real mounted iPhone browser harness in `tests/helpers/ios_harness_browser.mjs` and a new browser suite in `tests/test_ios_patch_view_browser.mjs` that boots the real `patch_gui/index.ios.js` wrapper in Chromium against real repo assets instead of only reading HTML strings.
- The mounted iPhone browser suite now proves the real wrapper sends `runtimeSyncRequest` on boot, reads the catalog through the bridge-backed path, fetches source WAVs through URL resolution, still loads `BS2 - Acid` without falling back to the audio bridge, keeps the audible table visible while a new desired table is pending, exposes retry for retryable failures, preserves the play-mode and glide controls, preserves the MSEG modal shell in portrait and landscape, and preserves the footer keyboard octave controls.
What was verified for the iPhone migration characterization safety net
- `node --test tests/test_ios_patch_view_browser.mjs`
- `node --test tests/test_wavetable_display.mjs`
- `node --test --test-concurrency=1 tests/test_ios_patch_view_browser.mjs tests/test_wavetable_display.mjs tests/test_wavetable_worker.mjs tests/test_patch_view_layout.mjs`
Detour hardening completed on 2026-04-03
- Replaced the synthetic `page.setContent(...)` iPhone browser harness with the real shipped host page at `patch_gui/index.ios.html`, using a fake native bridge that speaks the real embedded patch message protocol instead of mounting `patch_gui/index.ios.js` into an invented document.
- Extended the repo static test server so `/cmaj_api/*` resolves to the pinned Cmajor browser runtime under `build/deps/.../javascript/cmaj_api`, which lets the mounted iPhone host page import the same browser helpers it uses in the real app.
- Strengthened `tests/test_ios_patch_view_browser.mjs` so it now proves the actual host-page boundary, the viewport `viewport-fit=cover` metadata, the docked footer keyboard geometry in portrait and landscape, the mounted stage gestures (picker taps ignored, horizontal wavetable swipe, vertical scan drag), and a mounted MSEG shape edit that persists `mseg1.shape` and uploads `mseg1Buffer`.
- Renamed the HTML-string tests in `tests/test_wavetable_display.mjs` so they are explicitly described as template-characterization coverage instead of overclaiming mounted runtime behavior.
- Replaced the flaky octave-boundary loop in the mounted browser suite with deterministic step-by-step clicks that wait for each root-note update.
What was verified after hardening the iPhone migration safety net
- `npx tsc --noEmit`
- `node --test tests/test_ios_patch_view_browser.mjs`
- `node --test tests/test_wavetable_display.mjs`
- `node --test --test-concurrency=1 tests/test_ios_patch_view_browser.mjs tests/test_wavetable_display.mjs tests/test_wavetable_worker.mjs tests/test_patch_view_layout.mjs`
Final iPhone regression hardening on 2026-04-03
- Replaced the mounted iPhone browser suite's last synthetic form-control edits with real Playwright interactions for the wavetable picker, the voice-mode select, and the glide slider. The suite no longer passes by mutating `.value` inside `page.evaluate(...)`.
- Removed the unused synthetic shadow-pointer helper from `tests/helpers/ios_harness_browser.mjs` so the harness no longer advertises a weaker bypass path than the mounted suite actually uses.
- The mounted retry flow exposed a real iPhone UI bug in `patch_gui/index.js`: the retry button sat inside `.stage-copy`, which has `pointer-events: none`, but the button never opted back into pointer events. Added `pointer-events: auto` to `.table-retry-button` so the stage retry control is actually clickable again.
- Tightened the mounted retry-state assertion so it waits for the stable user-facing timeout text in the bank readout instead of overconstraining a transient `displayStatus` value while the currently audible table is still visible.
- Tightened the mounted iPhone browser harness again so it now records host-page ready and fallback callback counts, can deliberately fail specific bundled resources, and exposes the mounted MSEG depth control state. That lets the safety net prove one ready notification on normal boot, zero bundled-fallback requests on normal boot, and the visible `Display unavailable` recovery path when a source WAV is missing.
- Tightened `tests/test_ios_patch_view_browser.mjs` so it now covers the mounted retry button through shadow-root hit-testing instead of synthetic `element.click()`, covers left and right safe-area inset overrides in the live shell, and covers the mounted MSEG depth control. Updated the static HTML characterization in `tests/test_wavetable_display.mjs` to match the new left/right safe-area CSS variables (`--cosimo-ios-safe-right` and `--cosimo-ios-safe-left`).
What was verified after the final iPhone regression hardening
- `node --test tests/test_ios_patch_view_browser.mjs`
- `node --test --test-concurrency=1 tests/test_ios_patch_view_browser.mjs tests/test_wavetable_display.mjs tests/test_wavetable_worker.mjs tests/test_patch_view_layout.mjs`
Last iPhone safety-net tightening on 2026-04-03
- Switched the mounted iPhone browser harness to a mobile browser context with touch enabled, and changed the mounted stage and MSEG edit tests to use real touch events instead of mouse-style input. The browser suite now exercises the same interaction class the phone uses for the stage and MSEG surface.
- Tightened the pending-table assertion in `tests/test_ios_patch_view_browser.mjs` so it requires the exact live loading label (`Loading <table>…`) instead of also accepting the steady-state `shapes` text too early.
- Tightened the mounted footer assertion in `tests/helpers/ios_harness_browser.mjs` so `footerVisible` now means the footer is actually rendered with visible geometry, not merely present in the DOM.
- Renamed the mounted safe-area test to say exactly what it proves: host inset overrides applied by the iPhone host page. The real `env(safe-area-inset-*)` contract is still locked down separately by the HTML-template characterization coverage in `tests/test_wavetable_display.mjs`.
iPhone React frontend migration completed on 2026-04-03
- Replaced the old browser-side iPhone UI path behind `patch_gui/index.ios.js` with a generated React and Vite bundle from `ui/ios/*`, while keeping the existing iPhone host bootstrap in `patch_gui/index.ios-host.js` unchanged. This was a frontend migration, not a native host rewrite.
- Added the new iPhone React shell in `ui/ios/IOSPatchView.tsx`, the iPhone keyboard adapter in `ui/ios/ios-keyboard-adapter.tsx`, the shadow-root entrypoint in `ui/ios/patch-view-entry.tsx`, the iPhone stylesheet in `ui/ios/styles.css`, and the iPhone Vite config in `ios_auv3/vite.config.mjs`. `ui/build.mjs` now emits the real shipped bundle at `patch_gui/index.ios.js`.
- Preserved the iPhone-specific browser behavior that the strict mounted suite characterizes: the safe-area shell, footer keyboard docking, wavetable stage swipe-and-drag behavior, retry flow, MSEG modal flow, play-mode and glide controls, and the `BS2 - Acid.wav` URL-first source loading rule.
- The real iPhone wrapper build initially failed because Xcode reused stale compile response files that still pointed at the removed `ios_auv3/Vendor/cmajor/include` path. A clean device rebuild regenerated those arguments from the current generated project, which already points at the pinned runtime under `build/deps/cmajor-1.0.3066/include`.
- Built the real `CosimoSynth_Standalone` app from `build/ios_device_run/CosimoSynthAUv3.xcodeproj`, verified that the app bundle contains `assets/factory-bank-catalog.json`, `assets/factory_sources`, and the new React `patch_gui/index.ios.js` bundle, then installed and launched `dev.cosimo.wavetable-synth` on the paired iPhone.
What was verified for the iPhone React frontend migration
- `npx tsc --noEmit`
- `npm run ui:build`
- `node --test --test-concurrency=1 tests/test_desktop_patch_view_browser.mjs tests/test_desktop_widget_adapters_browser.mjs tests/test_shared_synth_components_browser.mjs tests/test_shared_synth_hooks_browser.mjs tests/test_patch_view_layout.mjs tests/test_ios_patch_view_browser.mjs tests/test_wavetable_display.mjs tests/test_wavetable_worker.mjs`
- `xcodebuild -project build/ios_device_run/CosimoSynthAUv3.xcodeproj -scheme CosimoSynth_Standalone -configuration Debug -destination id=00008120-000139383644C01E DEVELOPMENT_TEAM=JUFVT28775 CODE_SIGN_STYLE=Automatic CODE_SIGN_IDENTITY='Apple Development' -allowProvisioningUpdates clean build`
- `xcrun devicectl device install app --device 00C7F433-8B6A-5CAC-856F-56D7385E12F9 'build/ios_device_run/CosimoSynth_artefacts/Debug/Standalone/Cosimo Synth.app'`
- `xcrun devicectl device process launch --device 00C7F433-8B6A-5CAC-856F-56D7385E12F9 dev.cosimo.wavetable-synth`
iPhone React MSEG portrait fix on 2026-04-03
- A user-found regression in the new iPhone React frontend showed the MSEG preview and MSEG modal editor laid out in the wrong axis when the phone was upright. The graph still used the desktop-style left-to-right time axis instead of the iPhone portrait top-to-bottom time axis, and the main-panel preview had also grown taller than the old compact iPhone shell.
- Fixed the browser-side shared MSEG renderer in `ui/shared/synth-components.tsx` and the browser-side shared MSEG pointer mapping in `ui/shared/synth-hooks.ts` so both the preview and the editor can render and edit in a vertical orientation. This stays entirely in the browser/frontend layer; it does not change the iPhone native host in `patch_gui/index.ios-host.js`.
- The iPhone React shell in `ui/ios/IOSPatchView.tsx` now selects the vertical MSEG orientation only when the phone is upright and keeps the horizontal layout in landscape. The iPhone preview shell in `ui/ios/styles.css` is back to the old compact portrait height of `128px`.
- Tightened the shared browser tests and the mounted iPhone browser tests so they now prove the rotated SVG path direction, the rotated editor coordinate mapping, and the compact portrait preview height against the real built `patch_gui/index.ios.js` bundle.
What was verified for the iPhone React MSEG portrait fix
- `npx tsc --noEmit`
- `npm run ui:build`
- `node --test --test-concurrency=1 tests/test_shared_synth_components_browser.mjs tests/test_shared_synth_hooks_browser.mjs tests/test_ios_patch_view_browser.mjs tests/test_wavetable_display.mjs tests/test_wavetable_worker.mjs tests/test_patch_view_layout.mjs`
iPhone React MSEG portrait direction correction on 2026-04-03
- The first portrait MSEG fix still allowed the time axis to run upward. The real iPhone rule is stricter: when the phone is upright, both the small MSEG preview and the full MSEG editor must run time from the top of the phone to the bottom.
- Fixed the shared browser-side MSEG coordinate mapping in `ui/shared/mseg.ts` so the vertical orientation is a clockwise rotation of the normal graph, not a counterclockwise one. Higher time values now land lower on the screen in portrait, and the vertical fill path in both `ui/shared/synth-components.tsx` and `patch_gui/index.js` now closes against the left edge in the right order for that clockwise rotation.
- Tightened the browser tests so they fail if portrait MSEG time runs upward again. `tests/test_shared_synth_hooks_browser.mjs` now uses an independent vertical coordinate calculation that expects time to move downward, `tests/test_shared_synth_components_browser.mjs` now checks that later editor points are lower on the screen, and `tests/test_ios_patch_view_browser.mjs` now checks that the real mounted preview and modal curves in the shipped `patch_gui/index.ios.js` bundle move downward in portrait while still keeping the old compact preview height.
- Rebuilt the real `CosimoSynth_Standalone` wrapper app from this tree, verified that the bundle contains `assets/factory-bank-catalog.json`, `assets/factory_sources`, and `patch_gui/index.ios.js`, and reinstalled it on the paired phone. The remote launch step failed only because the device was locked, not because the app or bundle failed.
iPhone React build-boundary fix on 2026-04-03
- The strict simulator-bundle test exposed a real packaging regression after the iPhone React migration: `scripts/generate_ios_auv3_plugin.sh` started running `npm run ui:build`, but Xcode invokes that generator from a temporary build directory, not from the repo root. That made `npm` look for `package.json` under the temp Xcode build folder and fail before the app bundle could be rebuilt.
- Fixed the generator script so both `npm run ui:build` and `uv run python build_assets.py` execute from the repo root explicitly. This does not change the iPhone native host; it only fixes the browser-bundle build step.
- Tightened `ios_auv3/CMakeLists.txt` and `tests/test_ios_auv3_build.py` so the simulator and device bundle path now tracks `ui/ios/*`, `ui/shared/*`, `ui/build.mjs`, `ios_auv3/vite.config.mjs`, and `package.json`, and the built-bundle test now fails unless the packaged `patch_gui/index.ios.js` contains the real React iPhone entry (`createIOSPatchView` and `CosimoIOSReactViewElement`) rather than the old wrapper path.
- Added a focused generator test in `tests/test_ios_auv3_build.py` that runs `generate_ios_auv3_plugin.sh` from the wrong working directory with fake `npm`, `uv`, and `cmaj` binaries, then proves the script still runs the frontend build from the repo root. This locks down the exact Xcode failure that surfaced here.
What was verified for the iPhone React build-boundary fix
- `node --test --test-concurrency=1 tests/test_ios_patch_view_browser.mjs tests/test_wavetable_display.mjs tests/test_wavetable_worker.mjs tests/test_patch_view_layout.mjs`
- `uv run pytest -q tests/test_ios_auv3_build.py -k 'test_ios_auv3_generator_runs_npm_and_build_assets_from_the_repo_root or test_ios_auv3_cmake_declares_the_repo_owned_shell_and_bundle_copy_contract or test_actual_built_bundle_roots_load_the_runtime_patch_and_ui_files'`
iPhone React portrait MSEG preview/editor split fix on 2026-04-03
- A user-reported regression in the new iPhone React frontend showed that upright portrait mode was applying the same MSEG orientation to both surfaces. That was wrong. The real iPhone rule is split: the small main-panel MSEG preview stays horizontal with time running left-to-right, while the full portrait MSEG editor rotates so time runs top-to-bottom.
- Fixed the browser-side React iPhone shell in `ui/ios/IOSPatchView.tsx` by separating the two orientations. The preview now always renders with horizontal time, while the modal editor still uses the portrait-only vertical orientation. This stayed entirely in the browser/frontend layer and did not change the iPhone native host in `patch_gui/index.ios-host.js`.
- Tightened the mounted browser regression test in `tests/test_ios_patch_view_browser.mjs` so it now injects a known asymmetric four-point MSEG shape, waits until that exact shape is visibly rendered and stable, and then proves the main-panel preview stays horizontal in portrait while the full editor rotates vertical. Three adversarial reviewer agents approved that red test before the implementation change.
- Rebuilt the shipped `patch_gui/index.ios.js` bundle, rebuilt the real `CosimoSynth_Standalone` wrapper app, verified that the app bundle still contains `assets/factory-bank-catalog.json`, `assets/factory_sources`, and the rebuilt `patch_gui/index.ios.js`, then reinstalled and launched `dev.cosimo.wavetable-synth` on the paired iPhone.
What was verified for the iPhone React portrait MSEG preview/editor split fix
- `npx tsc --noEmit`
- `npm run ui:build`
- `node --test --test-concurrency=1 tests/test_ios_patch_view_browser.mjs tests/test_wavetable_display.mjs tests/test_shared_synth_components_browser.mjs`
- `xcodebuild -project build/ios_device_run/CosimoSynthAUv3.xcodeproj -scheme CosimoSynth_Standalone -configuration Debug -destination id=00008120-000139383644C01E DEVELOPMENT_TEAM=JUFVT28775 CODE_SIGN_STYLE=Automatic CODE_SIGN_IDENTITY='Apple Development' -allowProvisioningUpdates build`
- `xcrun devicectl device install app --device 00C7F433-8B6A-5CAC-856F-56D7385E12F9 'build/ios_device_run/CosimoSynth_artefacts/Debug/Standalone/Cosimo Synth.app'`
- `xcrun devicectl device process launch --device 00C7F433-8B6A-5CAC-856F-56D7385E12F9 dev.cosimo.wavetable-synth`
Legacy monolithic browser entrypoint removal on 2026-04-03
- Deleted `patch_gui/index.js`. The repo no longer keeps the old monolithic iPhone browser UI file around after the React migration.
- Added `ui/shared/display-gesture.ts` and `ui/shared/keyboard-geometry.ts` so the gesture math and keyboard sizing that used to be duplicated around the old path now live in shared frontend modules.
- Added `tests/test_shared_frontend_logic.mjs` so the runtime-table, display-gesture, and keyboard-geometry behavior is now tested directly against the current shared frontend code instead of against the deleted legacy class.
- Updated `tools/patch_gui_harnesses/dev-harness.mjs` to boot the real desktop React bundle directly, and updated the active plan files so they no longer describe iPhone as still running the deleted `patch_gui/index.js` UI path.
What was verified for the legacy browser entrypoint removal
- `npx tsc --noEmit`
- `npm run ui:build`
- `node --test --test-concurrency=1 tests/test_shared_frontend_logic.mjs tests/test_patch_view_layout.mjs tests/test_wavetable_display.mjs tests/test_wavetable_worker.mjs tests/test_ios_patch_view_browser.mjs tests/test_desktop_widget_adapters_browser.mjs tests/test_desktop_patch_view_browser.mjs tests/test_shared_synth_components_browser.mjs tests/test_shared_synth_hooks_browser.mjs`
Desktop native HMR launcher hardening on 2026-04-08
- The old desktop dev path was not real Vite HMR. The standalone app in `dev-server` mode was importing `http://127.0.0.1:5174/patch_gui/desktop/index.js` directly, which meant the desktop app could keep talking to a stale in-memory transform graph if the long-lived Vite server missed a file invalidation.
- Fixed the desktop dev server in `ui/vite.desktop.config.mjs` so the live desktop entry it serves now imports `@vite/client`, exposes a repo-local status endpoint at `http://127.0.0.1:5174/__cosimo-dev-status`, and uses explicit polling with `awaitWriteFinish` for the desktop source tree.
- Added `scripts/run_desktop_native_dev.sh` and changed `npm run desktop:native:dev` to use it. That launcher now starts a fresh desktop Vite server for this repo, rebuilds the native wrapper in `dev-server` mode, and launches `CosimoDesktopNative.app` from the rebuilt wrapper path instead of assuming some old process on port `5174` is still valid.
- Split the desktop runtime loader generation from the compiled desktop bundle build. In `dev-server` mode, `scripts/build_desktop_native.sh` now regenerates the stable `patch_gui/desktop/index.js` loader with `node ui/build.mjs --desktop-runtime` and does not depend on rebuilding the compiled desktop `patch_gui/desktop/app.js` bundle.
- The desktop loader contract did not change: `WavetableSynth.cmajorpatch` still points at `patch_gui/desktop/index.js`, and the compiled desktop app still loads `./app.js` by default outside `dev-server` mode.
What was verified for the desktop native HMR launcher hardening
- `bun x tsc --noEmit`
- `node --test tests/test_desktop_standalone_loader.mjs tests/test_patch_view_layout.mjs`
- `uv run pytest -q tests/test_ios_auv3_build.py -k test_ios_ui_dev_server_configuration_exists`
- `node ui/build.mjs --desktop-runtime`
- `npm run ui:build`
- `npm run desktop:native:dev`
- After `npm run desktop:native:dev`, the live desktop Vite status endpoint replied with `kind = "cosimo-desktop-vite"`, `usesViteClient = true`, and `watchMode = "polling"`, the server stayed listening on `127.0.0.1:5174`, and the standalone `CosimoDesktopNative` app process was running from the rebuilt wrapper.
- Verified the live watch path directly by adding and then removing a harmless probe token in `ui/desktop/patch-view-entry.tsx` and confirming the running Vite server updated the transformed module at `http://127.0.0.1:5174/ui/desktop/patch-view-entry.tsx` immediately in both directions while the dev session stayed up.
Patch manifest generation removal on 2026-04-08
- `build_assets.py` no longer rewrites `WavetableSynth.cmajorpatch` or `WavetableSynth.iOS.cmajorpatch`. Those two patch manifests are now treated as checked-in source files that must be edited directly when the synth source list, worker path, or view entry changes.
- `build_assets.py` now has one build responsibility: regenerate the derived runtime wavetable catalog `assets/factory-bank-catalog.json` from `assets/factory-table-catalog.json` and the referenced source wavetable files.
- Updated the durable repo guidance in `AGENTS.md` so the desktop/build instructions no longer imply that Python rewrites the manifests during normal builds.
- Tightened the build-system regression tests so they no longer describe manifest regeneration as intentional and now prove that `uv run python build_assets.py` leaves both patch manifests untouched.
What was verified for the patch manifest generation removal
- `uv run python build_assets.py`
- `node --test tests/test_patch_view_layout.mjs`
- `uv run pytest -q tests/test_ios_auv3_build.py -k 'test_ios_ui_dev_server_configuration_exists or test_ios_auv3_generator_runs_npm_and_build_assets_from_the_repo_root'`
- `npm run desktop:native:dev`
OTT lab Ableton plugin crash fix on 2026-04-12
- Ableton crashed when it loaded the official Cmajor generic AU named `CmajPlugin.component`, with `CmajPlugin.json` pointing at `fx/ott_lab/OttLab.cmajorpatch`. The crash report showed the value-change host notification path: `JuceAU::audioProcessorParameterChanged -> sendValueChangedMessageToListeners -> PatchParameter::setValue`.
- The same OTT patch did not crash when loaded through the official generic VST3 named `CmajPlugin.vst3`. The practical conclusion is: use official generic VST3 for fast Ableton lab patch iteration, and do not use official generic AU for WebView knob testing.
- Removed the generated-wrapper patching path from the default OTT/Chorus lab workflow. `npm run ott:jit:install` and `npm run chorus:jit:install` now install only `CmajPlugin.vst3` plus the VST3 `CmajPlugin.json`. If a repo-pointed generic AU loader exists, the install script moves it out of Ableton's scan path. The earlier generated `OTTLab.component` and `OTTLab.vst3` bundles were also removed from Ableton's plugin folders so the only active OTT lab loader is `CmajPlugin.vst3`.
What was verified for the OTT lab Ableton plugin crash fix
- User tested official generic `CmajPlugin.vst3` in Ableton with the OTT lab patch and reported no crash when turning the knob.
- `npm run ott:jit:install`
- `npm run ott:dry-run`
- `uv run pytest -q tests/test_ott_lab_probe.py`
OTT lab transient clamp fix on 2026-04-12
- The default OTT lab patch was creating a loud onset spike because the band input/output gain boosts were active immediately, while the compressor gain reduction started from silence and took too long to catch up. With a 0.75 peak 1 kHz loud step, the old default wet output peaked around 1.84 in the first 5 ms.
- Changed `fx/ott_lab/OttLab.cmajor` so each band now has separate upper and lower detector envelopes. The upper detector is clamped to the lower edge of the above-threshold soft knee for downward compression, and the lower detector is clamped to the upper edge of the below-threshold soft knee for upward compression. This matches the actual above-threshold/below-threshold compressor structure more closely than using one shared envelope for both jobs.
- Kept the band attack/release controls on the detector envelopes and reduced the final gain smoother to `0.04 ms`. That final smoother is only anti-zipper protection now; it is no longer a second slow attack stage after the detector.
- Added a fixed internal `3 ms` wet-path lookahead. The detector reads the current crossover bands, while the audio being gain-controlled is delayed by the lookahead buffer. The internal dry path used for the wet/dry mix is delayed by the same band buffers, while bypass and `Mix = 0%` still output the original dry input.
- Added a regression test that renders the real generated Cmajor OTT lab patch at the default settings, finds the first loud processed-output window, and fails if that onset is still a dry-level spike. The same test also proves the sustained section is still compressed, so it cannot pass by bypassing the effect.
What was verified for the OTT lab transient clamp fix
- `npm run ott:dry-run`
- `uv run pytest -q tests/test_ott_lab_probe.py`
OTT lab static per-band drive on 2026-04-12
- Added a global `Band Drive` parameter to `fx/ott_lab/OttLab.cmajor`. It drives all three bands into their own soft saturator after that band's OTT compression and band output gain, before the bands are summed back together.
- This is deliberately static gain staging. There is no envelope follower, no dynamic output trim, and no automatic loudness compensation. At `Band Drive = 0%`, the saturator returns the compressed band sample unchanged.
- The soft-clip ceiling for each band is derived from that band's downward-compression threshold plus that band's output gain plus a fixed `6 dB` peak margin. The drive range is `0 dB` to `18 dB`; the saturator applies a fixed half-drive output trim so higher drive clamps peaks without collapsing the output to silence.
- Measured with the default OTT settings, a 1 kHz sine at 0.75 input amplitude, and the fixed 3 ms lookahead still active:
- `Band Drive 0%`: peak `0.129856`, RMS `0.091752`
- `Band Drive 25%`: peak `0.144693`, RMS `0.107772`
- `Band Drive 50%`: peak `0.131583`, RMS `0.107900`
- `Band Drive 75%`: peak `0.105390`, RMS `0.093464`
- `Band Drive 100%`: peak `0.082876`, RMS `0.076225`
- Added Cmajor probe tests that prove the new knob changes the processed OTT path, does not silence the output, and does not clip the unprocessed path when OTT `Amount = 0%`.
What was verified for the OTT lab static per-band drive
- `npm run ott:dry-run`
- `uv run pytest -q tests/test_ott_lab_probe.py`
OTT lab broadband envelope match on 2026-04-12
- Added broadband envelope matching to `fx/ott_lab/OttLab.cmajor`. It compares the delayed dry reference envelope against the summed processed wet envelope, applies a bounded correction to the wet signal, then applies the normal `Output Gain` and `Mix`.
- This is not per-band matching. The low, mid, and high bands are already recombined before the matcher sees the signal.
- Added development-facing controls:
- `Envelope Match`: `0%` to `100%`, default `0%`
- `Env Boost Clamp`: max upward correction in dB, default `6 dB`
- `Env Cut Clamp`: max downward correction in dB, default `6 dB`
- `Env Attack`: detector attack speed, default `5 ms`
- `Env Release`: detector release speed, default `120 ms`
- The detector keeps tracking even when `Envelope Match = 0%`, so turning the knob up later does not start from stale zero envelope state.
- Measured with default OTT settings and a 1 kHz sine at 0.75 input amplitude:
- dry reference: peak `0.750000`, RMS `0.530330`
- unmatched processed wet: peak `0.129856`, RMS `0.091752`
- `Envelope Match 100%`, `6 dB` clamps: peak `0.259096`, RMS `0.183070`
- `Envelope Match 100%`, `12 dB` clamps: peak `0.516965`, RMS `0.365272`
- Added Cmajor probe tests that prove broadband matching pulls the summed wet level toward the dry reference, boost clamp controls upward correction, cut clamp controls downward correction, and attack speed changes how quickly matching reacts to a loud step.
What was verified for the OTT lab broadband envelope match
- `npm run ott:dry-run`
- `uv run pytest -q tests/test_ott_lab_probe.py`
- `npm run ott:jit:install`
- `git diff --check`
OTT lab detector timing retune on 2026-04-13
- Retuned the default OTT lab detector attack/release values to match the `vitOTT`/Vital-style timings the user selected:
- Low: `2.8 ms` attack, `40 ms` release
- Mid: `1.4 ms` attack, `28 ms` release
- High: `0.7 ms` attack, `15 ms` release
- `Time = 100%` now uses those values directly. `Time = 10%` still scales them down to one tenth, and `Time = 1000%` still scales them up by ten.
- Updated the Cmajor probe wrapper and the timing-sensitive probe assertions so the tests describe the new faster default behavior instead of the older slow Ableton-style millisecond values. The previous 2026-04-12 measurement bullets remain historical measurements from before this retune.
What was verified for the OTT lab detector timing retune
- `npm run ott:dry-run`
- `uv run pytest -q tests/test_ott_lab_probe.py`
- `git diff --check`
OTT lab local A-G snapshots on 2026-04-13
- Added a compact local snapshot strip to `fx/ott_lab/view/index.js` for slots `A` through `G`.
- Each slot is a single small focusable text input. There are no per-slot `SAVE`, `COPY`, or `PASTE` buttons.
- Clicking an empty slot starts that slot from the current visible parameter values. Clicking a filled slot recalls it and makes it active.
- After a slot is active, parameter changes automatically update that slot in `localStorage`; there is no separate save action.
- The text field next to the `A` through `G` slots edits the active slot's free-text `label`. Switching active slots updates that text field, and typing in the field persists to the active slot immediately.
- When a slot input is focused, `Cmd+C` copies that slot's JSON and `Cmd+V` pastes JSON into that slot. The paste target can be an empty slot or an existing slot.
- The slot controls are editable text inputs that self-reset to their slot letter, so the browser/WebView has a native text-focus target for clipboard shortcuts without letting accidental typing rename the slot buttons. The view handles native `copy` and `paste` events, with delayed keyboard fallback paths if those native events do not arrive.
- Copy, paste, recall, and validation failures now show a small toast inside the OTT lab card instead of relying on a large inline message.
- Snapshots are browser-side only. They store visible Cmajor parameter endpoint values in `localStorage` under `cosimo.ottLab.snapshotSlots.v1`; hidden/internal parameters are not saved.
- `COPY` exports a JSON object with `kind`, `schema`, `patchID`, `slot`, `label`, and `values`, so the text can be pasted back into any slot with the sound label intact.
- `PASTE` first tries the browser clipboard API. If WebKit blocks clipboard reads, the view opens a small manual paste box where the same JSON can be pasted and applied.
- Pasted snapshots validate the OTT lab `patchID`, schema, known endpoint IDs, numeric finiteness, and parameter ranges before anything is sent to Cmajor. Invalid JSON does not partially change the sound.
- Added `npm run test:ott:view` for the browser-level snapshot test.
What was verified for the OTT lab local A-G snapshots
- `npm run test:ott:view`
- `npm run ott:dry-run`
- `uv run pytest -q tests/test_ott_lab_probe.py`
- `npm run ott:jit:install`
- `git diff --check`