-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathc64cast.schema.json
More file actions
2594 lines (2594 loc) · 110 KB
/
Copy pathc64cast.schema.json
File metadata and controls
2594 lines (2594 loc) · 110 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
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "c64cast configuration",
"description": "Generated by `c64cast --print-schema`. Do not edit by hand \u2014 regenerate with `make schema`.",
"type": "object",
"additionalProperties": false,
"properties": {
"hardware": {
"type": "object",
"description": "Hardware backend selection.",
"additionalProperties": false,
"properties": {
"backend": {
"type": "string",
"description": "Hardware backend family driving the C64.",
"enum": [
"ultimate",
"teensyrom"
],
"default": "ultimate"
}
}
},
"teensyrom": {
"type": "object",
"description": "TeensyROM+ backend connection.",
"additionalProperties": false,
"properties": {
"transport": {
"type": "string",
"description": "TR control link: USB serial or raw TCP (port 2112).",
"enum": [
"serial",
"tcp"
],
"default": "serial"
},
"serial_port": {
"type": [
"string",
"null"
],
"description": "Serial device for transport=serial over a plain USB data cable (e.g. /dev/cu.usbmodem* or COM3; NOT an FTDI null-modem cable). On macOS, leave unset to auto-detect the TeensyROM by its USB serial number; required (no auto-detect yet) on other platforms."
},
"baud": {
"type": "integer",
"description": "Serial baud rate (TR uses full USB bandwidth; 2 Mbaud 8N1).",
"default": 2000000
},
"host": {
"type": [
"string",
"null"
],
"description": "TR IP address for transport=tcp (find via CCGMS \"ATC\" or RTC sync). Required for tcp."
},
"tcp_port": {
"type": "integer",
"description": "TR TCP listener port (firmware default 2112).",
"default": 2112
},
"storage": {
"type": "string",
"description": "Where helper PRGs are uploaded + launched from.",
"enum": [
"sd",
"usb"
],
"default": "sd"
}
}
},
"ultimate64": {
"type": "object",
"description": "Ultimate 64 target + transport.",
"additionalProperties": false,
"properties": {
"url": {
"type": "string",
"description": "Base URL of the Ultimate 64 (REST + DMA host).",
"default": "http://192.168.2.64"
},
"system": {
"type": "string",
"description": "Target video system timing (affects frame rate + SID PLAY rate).",
"enum": [
"NTSC",
"PAL"
],
"default": "NTSC"
},
"dma_port": {
"type": "integer",
"description": "TCP port of the U64 Ultimate DMA Service (firmware default 64).",
"default": 64
},
"dma_password": {
"type": [
"string",
"null"
],
"description": "U64 network password, if set. Prefer the C64CAST_DMA_PASSWORD env var over committing it here."
},
"auto_reu": {
"type": "boolean",
"description": "Auto-enable + size the U64 REU (live, volatile, restored at teardown) for runs that hard-require it ([audio].use_reu_pump or explicit [video].use_reu_staged = true). Removes the manual F2 enable step. false = manage the REU yourself. No effect on no-REU backends or under --skip-probe.",
"default": true
},
"sid_model": {
"type": "string",
"description": "Auto-configure the SID chip model (6581/8580) to match what a .sid file's PSID header requests, remapping to a matching physical socket or an UltiSID core if needed. 'off' disables. An explicit '6581'/'8580' forces that model for every chip, ignoring the header.",
"enum": [
"auto",
"6581",
"8580",
"off"
],
"default": "auto"
},
"sid_panning": {
"type": "array",
"description": "Stereo pan per SID audio source, U64 only. Max 4 entries \u2014 the U64 has one pan control per source (2 SID sockets + 2 UltiSID cores), and entry N pans the Nth source the tune uses. Each entry is an int -5..5 (negative = left, 0 = center) or a label ('Left 3', 'Center', 'Right 2'). Empty = auto spread: 1 source centered, 2 [-3, 3], 3 [0, -3, 3], 4 [-2, 2, -5, 5] \u2014 ordered so the primary chip stays nearest center. Fewer positions exist without socketed SIDs: with none, only the 2 UltiSID cores are pannable, so chips beyond the 2nd share a pan.",
"default": []
},
"sid_volume": {
"type": "array",
"description": "Mixer level per SID audio source, U64 only. Max 4 entries \u2014 the U64 has one volume control per source (2 SID sockets + 2 UltiSID cores), and entry N sets the Nth source the tune uses, same indexing as sid_panning. Each entry is a dB int (0, -6, 3) or a label ('0 dB', '-6 dB', 'off'). Empty = auto: a source the tune plays on is raised to 0 dB when it would otherwise be OFF (silent), a source already at a deliberate level is left alone, and every source the tune does not use is muted. The ladder is sparse below -18 dB: -42, -36, -30, -27, -24, then every dB from -18 to +6.",
"default": []
}
}
},
"video": {
"type": "object",
"description": "Webcam input + experimental video paths.",
"additionalProperties": false,
"properties": {
"device": {
"type": [
"integer",
"string"
],
"description": "Webcam device: an integer cv2 index (-1 = system default camera, cv2 index 0), or a string matched to a camera by name substring (e.g. \"Cam Link\") or USB VID:PID (e.g. \"0fd9:0066\"). String selection needs the 'camera' extra; run --list-devices to see names + VID:PID.",
"default": -1
},
"use_reu_staged": {
"type": [
"boolean",
"string"
],
"description": "REU bank-swap double-buffer for video push. \"auto\" (default) stages bitmap modes (hires/mhires) when the startup probe finds the U64's REU enabled, leaving char modes on the cheaper host-DMA path; true forces it on for every mode, false off. auto silently falls back to host-DMA when REU isn't confirmed.",
"default": "auto"
},
"double_buffer": {
"type": [
"boolean",
"string"
],
"description": "Host-DMA double-buffer (page flip) for tear-free bitmap video where REU staging can't help. \"auto\" (default) enables it for bitmap modes (hires/mhires) when REU staging is off and either the backend has no REU (e.g. TeensyROM) or the scene has a text overlay (whose presence turns the REU path off to dodge bank-swap shimmer, otherwise leaving single-buffer host-DMA that tears on cuts). true forces it on for bitmap modes, false off; gated off when the REU mic pump is active (shared $0314). Independent of [video].use_reu_staged (the REU path).",
"default": "auto"
}
}
},
"audio": {
"type": "object",
"description": "SID audio streaming.",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"description": "Master switch for SID audio streaming (the 4-bit $D418 DAC). On by default; mute with the --no-audio CLI flag.",
"default": true
},
"device": {
"type": [
"integer",
"string"
],
"description": "Audio input device: an integer index (-1 = system default microphone), or a string matched to an input device by name substring (e.g. \"Cam Link\"). Run --list-devices to see names + indices.",
"default": -1
},
"sample_rate": {
"type": "integer",
"description": "Audio sample rate in Hz fed to the SID DAC. Default 12000 lifts the Nyquist to ~6.0 kHz so fricatives/sibilants survive (8000 lost them). HW-verified clean on a real NTSC U64-II via a pitch A/B sweep (no NMI handler overrun) in both char and bitmap modes, and safe on PAL. Note the REAL streaming ceiling sits BELOW the isolated-handler ceiling (max_safe_sample_rate ~13.6 kHz NTSC): the host-DMA audio ring writes themselves halt the 6510 and steal cycles from the NMI handler, so the overrun onset under the live pipeline was measured at ~12500 Hz (identical in char and bitmap \u2014 the audio feed, not the video, is the driver). 12000 keeps margin below that. Rates past the isolated-handler ceiling are rejected at load (see c64.nmi_rate_safety). Sampler-backend playback uses [audio].sampler_sample_rate instead.",
"default": 12000
},
"backend": {
"type": "string",
"description": "Video-audio backend: 'auto' (sampler on a capable U64, else DAC), 'dac' (4-bit $D418 NMI DAC, all backends, lo-fi), or 'sampler' (U64 'Ultimate Audio' FPGA PCM, high fidelity, off the C64 bus).",
"enum": [
"auto",
"dac",
"sampler"
],
"default": "auto"
},
"sampler_sample_rate": {
"type": "integer",
"description": "Sample rate (Hz) for the Ultimate Audio sampler backend. 1000..48000; default 44100 (CD quality). The FPGA plays at the nearest divider of its 6.25 MHz reference (a <0.5% constant pitch offset, drift-free).",
"default": 44100
},
"sampler_bits": {
"type": "integer",
"description": "PCM bit depth for the Ultimate Audio sampler backend: 8 (signed) or 16 (signed little-endian). Default 16.",
"default": 16
},
"sampler_clock_hz": {
"type": "integer",
"description": "Ultimate Audio sampler reference clock (Hz), used to derive the rate divider AND the resample target so they stay matched (heard speed = real_clock / this). Default is the MEASURED effective clock of the shipping U64 firmware (~6160000 Hz): the FPGA runs ~1.44% slow vs the 6250000 Hz design nominal, so nominal made sampler audio drift against video. This is a firmware property (same across U64 units), not per-unit \u2014 so it ships baked in. If a firmware update fixes the clock (or on hardware that clocks it correctly), set 6250000. Re-measure with scripts/diags/sampler_av_align_calib.py (prints the value). Only affects the sampler backend.",
"default": 6160000
},
"mic_sensitivity": {
"type": "number",
"description": "Microphone input gain multiplier.",
"default": 1.5
},
"noise_gate": {
"type": "number",
"description": "Mic level below which input is squelched to silence.",
"default": 0.05
},
"dither": {
"type": "boolean",
"description": "TPDF dither on the 4-bit quantization step. Default off; flip on for smoother hiss on already-noisy sources.",
"default": false
},
"digi_boost": {
"type": "boolean",
"description": "EXPERIMENTAL: lock SID voices to a DC pulse so the ADSR D/As bias the master mixer, raising $D418 playback level.",
"default": false
},
"dac_curve": {
"type": "string",
"description": "SID $D418 DAC companding curve. 'auto' (default) = per-system calibrated table if present, else 'mahoney_ultisid' on the Ultimate, else 'linear'. 'linear' = classic 4-bit volume nibble. 'mahoney_ultisid' = Mahoney 8-bit technique (full $D418 byte, ~6-7 effective bits) with the baked emulated-UltiSID table. 'calibrated' = this system's per-unit table from --calibrate-dac (errors if none). Non-linear curves require the Mahoney SID env (auto-installed) and are mutually exclusive with digi_boost.",
"enum": [
"auto",
"linear",
"mahoney_ultisid",
"calibrated"
],
"default": "auto"
},
"dac_calibration_profile": {
"type": [
"string",
"null"
],
"description": "Override the auto-derived calibration file key (device unique_id / TR USB serial) with this name \u2014 calibration/dac/profile-<name>.json. Use when a TeensyROM+ moves between physical C64s: name each host's calibration once at --calibrate-dac time, then pass the same name on every playback run against that host."
},
"sid_filter_cutoff": {
"type": "integer",
"description": "SID low-pass cutoff for the PWM carrier voice (0 = disabled). Attenuates the carrier above the audio band.",
"default": 0
},
"use_reu_pump": {
"type": "boolean",
"description": "EXPERIMENTAL: stream video/mic audio from a REU ring (bus-clean) instead of per-write host DMA. Requires REU enabled.",
"default": false
},
"reu_pump_governor": {
"type": "boolean",
"description": "C64-side rate governor for the REU audio pump: the pump IRQ skips a chunk when its write head outruns the reader, stopping drift/echo with no host writes. Only active with use_reu_pump.",
"default": true
},
"host_dma_servo": {
"type": "boolean",
"description": "Closed-loop pacing for the host-DMA audio worker (mic / videos): reads the C64 NMI read pointer and adjusts the producer's software pace so the ring write head holds a fixed gap behind the reader, stopping the ~26s drift/echo. Pure host-side timing, no C64 writes. Not the REU pump path.",
"default": true
},
"nmi_rate_adaptive": {
"type": "boolean",
"description": "Adaptive NMI-rate compensation: closed-loop on the measured C64 consumer rate, raises the NMI rate to cancel a video slowdown from bus-halt-stolen NMI ticks. DEFAULT OFF \u2014 modern fps caps + REU-staged double-buffer drove that loss to ~0, so this only adds pitch error now. Supersedes pitch_mult_* when on. Host-DMA path only.",
"default": false
},
"source_alignment_marker": {
"type": "boolean",
"description": "DEBUG/CAPTURE ONLY: prepend a 100 ms chirp to REU audio as a capture-alignment anchor. Turn OFF for production listening.",
"default": false
},
"pitch_mult_petscii": {
"type": "number",
"description": "Host-DMA servo playback-rate multiplier for PETSCII mode (light char-mode load). 1.0 = none (default; U64-II NTSC is dead-on). Quantized: the NMI period is an integer cycle count, so a request rounds onto the latch grid (~1.2% steps at 12 kHz) \u2014 1.005 is a no-op, 1.015 lands on +1.19%.",
"default": 1.0
},
"pitch_mult_hires": {
"type": "number",
"description": "Host-DMA servo playback-rate multiplier for Hires / Hires-edges modes. 1.0 = none (default; modern fps caps + REU staging leave ~0 loss on U64-II NTSC). Re-tune only if a platform (PAL/TR+) drifts. Quantized: the NMI period is an integer cycle count, so a request rounds onto the latch grid (~1.2% steps at 12 kHz) \u2014 1.005 is a no-op, 1.015 lands on +1.19%.",
"default": 1.0
},
"pitch_mult_mhires": {
"type": "number",
"description": "Host-DMA servo playback-rate multiplier for MultiHires mode. 1.0 = none (default; modern fps caps + REU staging leave ~0 loss on U64-II NTSC). Re-tune only if a platform (PAL/TR+) drifts. Quantized: the NMI period is an integer cycle count, so a request rounds onto the latch grid (~1.2% steps at 12 kHz) \u2014 1.005 is a no-op, 1.015 lands on +1.19%.",
"default": 1.0
},
"pitch_mult_mcm": {
"type": "number",
"description": "Host-DMA servo playback-rate multiplier for MCM mode (char-based, light load; U64-II NTSC: good at 1.0). Quantized: the NMI period is an integer cycle count, so a request rounds onto the latch grid (~1.2% steps at 12 kHz) \u2014 1.005 is a no-op, 1.015 lands on +1.19%.",
"default": 1.0
},
"pitch_mult_blank": {
"type": "number",
"description": "Host-DMA servo playback-rate multiplier for Blank mode (no video input; 1.0 = none). Quantized: the NMI period is an integer cycle count, so a request rounds onto the latch grid (~1.2% steps at 12 kHz) \u2014 1.005 is a no-op, 1.015 lands on +1.19%.",
"default": 1.0
},
"dac_bitmap_tempo_hires": {
"type": "number",
"description": "Observed $D418-DAC playback-speed fraction on Hires / Hires-edges bitmap modes (measure via clock/wall). Content is time-compressed by 1/value (pitch-preserving) so bitmap+DAC video plays at real time. 1.0 = off. Host-DMA DAC path only \u2014 no effect on the Ultimate Audio sampler or the REU pump. Default 0.89 = U64-II NTSC (Hires drains slightly faster than MHires); re-measure per platform (PAL / TR+).",
"default": 0.89
},
"dac_bitmap_tempo_mhires": {
"type": "number",
"description": "Observed $D418-DAC playback-speed fraction on MultiHires bitmap mode (measure via clock/wall). Content is time-compressed by 1/value (pitch-preserving) so bitmap+DAC video plays at real time. 1.0 = off. Host-DMA DAC path only \u2014 no effect on the Ultimate Audio sampler or the REU pump. Default 0.88 = U64-II NTSC; re-measure per platform (PAL / TR+).",
"default": 0.88
}
}
},
"vision": {
"type": "object",
"description": "Webcam hand-gesture control (extra).",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable webcam hand-gesture control (pinch=pause/resume, swipe=skip, open-hand=cycle). Needs the 'vision' extra.",
"default": false
},
"model_path": {
"type": "string",
"description": "Path to the MediaPipe HandLandmarker .task model bundle (download separately; see assets/models/README.md).",
"default": "assets/models/hand_landmarker.task"
},
"num_hands": {
"type": "integer",
"description": "Max hands the tracker detects per frame.",
"default": 1
},
"min_detection_confidence": {
"type": "number",
"description": "Minimum confidence to detect a hand (0..1). Raise it if your torso/face occasionally register as a phantom hand.",
"default": 0.7
},
"min_tracking_confidence": {
"type": "number",
"description": "Minimum confidence to keep tracking a hand across frames (0..1).",
"default": 0.5
},
"poll_interval_s": {
"type": "number",
"description": "Seconds between gesture-recognition ticks (~0.066 = 15 Hz).",
"default": 0.066
},
"pinch_threshold": {
"type": "number",
"description": "Thumb-index normalized distance below which a pinch registers.",
"default": 0.05
},
"swipe_velocity": {
"type": "number",
"description": "Wrist horizontal speed (frame-widths/sec) that triggers a skip. HW-tuned: deliberate swipes peak ~0.5-1.1, drift stays < ~0.2.",
"default": 0.4
},
"gesture_cooldown_s": {
"type": "number",
"description": "Minimum seconds between fired gesture events (debounce).",
"default": 1.0
},
"gesture_dwell_s": {
"type": "number",
"description": "Seconds a pose (pinch / open hand) must be held STILL before it fires (0 = first frame). With the stillness gate this rejects busy/moving hands and poses passing through on the way to a swipe. Swipe (motion) ignores it.",
"default": 0.4
},
"hold_threshold_s": {
"type": "number",
"description": "Seconds a pinch must be held while paused to resume.",
"default": 3.0
},
"mirror": {
"type": "boolean",
"description": "Mirror the frame before tracking so swipe direction matches the mirrored webcam view.",
"default": true
},
"performance": {
"type": "boolean",
"description": "Live DJ/VJ Phase 6: remap the RUNNING-state gestures to clip-launch performance actions instead of transport \u2014 swipe = launch the next [[performance.clips]] slot, pinch-hold = bypass effect layer 0, open-hand-hold = bypass effect layer 1. Off (default) keeps the transport mapping (swipe=skip, pinch=pause, open-hand=cycle style). Pinch-hold-to-resume while paused is unchanged either way. Needs a [[performance.clips]] grid for the clip-advance gesture to do anything.",
"default": false
}
}
},
"interstitial": {
"type": "object",
"description": "The 'UP NEXT' card shown between scenes.",
"additionalProperties": false,
"properties": {
"duration_s": {
"type": "number",
"description": "How long the 'UP NEXT' interstitial shows between scenes.",
"default": 4.0
},
"text_color": {
"type": "string",
"description": "Interstitial text color: a C64 color name, 'rainbow', or 'random'.",
"default": "rainbow"
},
"background": {
"type": "string",
"description": "Animated parallax background style behind the interstitial text.",
"enum": [
"starfield",
"petscii_bars",
"raster_bars",
"checker",
"nature",
"city",
"none",
"random"
],
"default": "random"
}
}
},
"playlist": {
"type": "object",
"description": "Playlist behavior + video interleaving.",
"additionalProperties": false,
"properties": {
"videos_dir": {
"type": "string",
"description": "Directory of videos to interleave between scenes.",
"default": "assets/videos"
},
"interleave_videos": {
"type": "boolean",
"description": "Insert a video from videos_dir after each scene (multi-scene playlists only; ignored in single-scene mode).",
"default": false
},
"songlengths_file": {
"type": [
"string",
"null"
],
"description": "Path to an HVSC Songlengths.md5 file; gives waveform scenes their true duration when duration_s is unset. Left unset (the default), an unpacked HVSC under assets/sids/ (either the whole C64Music/ tree or just its contents) is auto-detected. Set to an empty string to disable auto-detection."
},
"loop": {
"type": "boolean",
"description": "Loop the playlist after the last scene (--no-loop exits after one pass; useful for 'play one video and quit').",
"default": true
},
"fade_duration_s": {
"type": "number",
"description": "Fade-in/out duration (seconds) at scene setup/teardown: non-black pixels rise from black on entry and sink to black on a normal scene end, across every compose-based display mode. 0 disables (hard cuts). A CTRL skip aborts an in-progress fade immediately.",
"default": 0.4
}
}
},
"debug": {
"type": "object",
"description": "Logging, heartbeat, profiling.",
"additionalProperties": false,
"properties": {
"verbose": {
"type": "integer",
"description": "Log verbosity (0 = INFO; 1+ = DEBUG). CLI: -v / -vv.",
"default": 0
},
"heartbeat": {
"type": "number",
"description": "Seconds between health heartbeat log lines (0 disables).",
"default": 10.0
},
"skip_probe": {
"type": "boolean",
"description": "Skip the startup U64 reachability probe.",
"default": false
},
"log_file": {
"type": [
"string",
"null"
],
"description": "Also mirror log output to this file (useful for headless runs)."
},
"profile": {
"type": "boolean",
"description": "Emit per-scene frame-timing summaries (render/compose/push/wait).",
"default": false
},
"profile_interval": {
"type": "number",
"description": "Seconds between profiler summary lines.",
"default": 10.0
},
"frame_numbers": {
"type": "boolean",
"description": "Overlay the playback timecode + source frame number on video/slideshow/webcam frames (debug aid for locating flashing/flickering frames).",
"default": false
}
}
},
"preview": {
"type": "object",
"description": "Local mirror window of the C64 display.",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"description": "Open a local window mirroring the U64 display (needs a desktop session; no extra required).",
"default": false
},
"fps": {
"type": "integer",
"description": "Preview window refresh rate.",
"default": 30
},
"scale": {
"type": "integer",
"description": "Integer pixel scale factor for the preview window.",
"default": 3
},
"charset_path": {
"type": [
"string",
"null"
],
"description": "C64 character ROM used to render char modes in the preview.",
"default": "assets/roms/characters.901225-01.bin"
}
}
},
"recording": {
"type": "object",
"description": "Record the rendered display to a file.",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"description": "Record the rendered display to a video file (cv2.VideoWriter).",
"default": false
},
"path": {
"type": "string",
"description": "Output video file path.",
"default": "recording.mp4"
},
"fps": {
"type": "integer",
"description": "Recording frame rate.",
"default": 30
},
"scale": {
"type": "integer",
"description": "Integer pixel scale factor for the recording.",
"default": 2
},
"fourcc": {
"type": "string",
"description": "FourCC codec code passed to cv2.VideoWriter.",
"default": "mp4v"
}
}
},
"color": {
"type": "object",
"description": "Global pre-quantize color shaping for mcm/mhires/petscii: static channel boost + hue corrections, plus per-source adaptive auto_fit (video/slideshow).",
"additionalProperties": false,
"properties": {
"channel_boost": {
"type": "array",
"description": "Per-channel pre-quantize gain [blue, green, red] (OpenCV BGR order). Empty = built-in default [1.3, 1.2, 1.0] (blue/green lift toward C64-friendly hues; red left neutral).",
"default": []
},
"hue_corrections": {
"type": "array",
"description": "List of [[color.hue_corrections]] bands applied before quantize (keys: hue_lo_deg, hue_hi_deg, sat_thresh, val_thresh, sat_mult, val_mult, hue_target_deg, name). Empty = built-in purple rescue only.",
"default": []
},
"hue_corrections_replace_defaults": {
"type": "boolean",
"description": "If true, user hue_corrections REPLACE the built-in defaults instead of extending them.",
"default": false
},
"auto_fit": {
"type": "boolean",
"description": "Per-source adaptive color fit for video + slideshow scenes: pre-scan the source and stretch its contrast + saturation to fill the C64 gamut (faithful \u2014 hue preserved). Ignored by webcam scenes (can't pre-scan).",
"default": true
},
"auto_fit_strength": {
"type": "number",
"description": "Strength of the auto_fit transform, 0..1 (1 = full, 0 = off). Lerps the derived stretch toward identity.",
"default": 1.0
},
"force_palette": {
"type": "boolean",
"description": "EXTREME forced-palette remap (mcm/mhires): k-means the source into N clusters and map each to a DISTINCT C64 color so all N colors are used. Pre-scanned for video + slideshow; adapts live (rolling, warm-start + hysteresis) for webcam/wled/generative. Deliberate false-color (NOT faithful) \u2014 off by default; also reachable via the SHIFT cycle's 'percell+forced' stop once enabled. Tip: `--suggest-palette FILE` ranks a good force_palette_colors set.",
"default": false
},
"force_palette_colors": {
"type": [
"integer",
"array"
],
"description": "How force_palette allocates C64 colors: either an int count of distinct colors to spread the source across (2..16), OR an explicit list of colors to whitelist \u2014 each a color name (fuzzy + case-insensitive, e.g. \"light blue\", \"lgrn\", \"blk\") or an index 0..15. A list's length sets the color count.",
"default": 16
},
"dither": {
"type": "string",
"description": "Spatial dither applied before nearest-palette quantization on mhires/mcm/hires. 'auto' picks the best method that's actually useful for the scene: floyd-steinberg (highest quality) for static scenes (slideshow), blue_noise (vectorized, temporally stable \u2014 no added shimmer, and no Bayer grid structure) for motion scenes (video/webcam/generative). Any value can be forced on any scene; floyd-steinberg/atkinson are a Python-level per-pixel loop and can shimmer frame-to-frame on motion; 'ordered' (Bayer) is the older motion default and still available if the cross-hatch pattern is wanted (see docs/caveats.md).",
"enum": [
"auto",
"none",
"ordered",
"blue_noise",
"floyd-steinberg",
"atkinson"
],
"default": "auto"
},
"dither_strength": {
"type": "number",
"description": "Dither strength, roughly 0..2.0. For 'ordered'/'blue_noise' it scales the threshold spread (same scale for both, so switching between them doesn't need a strength retune); for floyd-steinberg/atkinson it scales how much of each pixel's quantization error is diffused to its neighbors (1.0 = the textbook kernel weights).",
"default": 0.5
},
"color_match": {
"type": "string",
"description": "Color space for the nearest-palette match on the quantizing modes (mcm/mhires/hires/petscii). 'perceptual' measures nearest-color in CIE-Lab (perceptually uniform \u2014 picks the color the eye calls closest, e.g. a warm gray \u2192 orange/brown, not muddy gray). 'rgb' is the classic brightness-weighted BGR metric. Both keep the channel_boost + gray-penalty shaping; only the distance space differs. 'auto' (default) picks perceptual on every quantizing mode (a no-op on hires edges / blank, which pick no colors).",
"enum": [
"auto",
"rgb",
"perceptual"
],
"default": "auto"
},
"cell_strategy": {
"type": "string",
"description": "How mhires percell mode fills each 4\u00d78 cell's 3 per-cell color slots from the colors present in that cell. 'frequency' = the 3 most-common (temporally stable). 'luminance' = darkest/median/brightest (preserves a cell's full tonal span). 'contrast' = the two luma extremes plus the color farthest from both. 'error-min' = the trio minimizing the cell's reconstruction error (best quality, costlier). 'auto' (default) uses error-min for static scenes (slideshow \u2014 composed once) and frequency for motion scenes (video/webcam/generative, where frequency's stability avoids per-frame slot churn). Only affects mhires with palette_mode=percell.",
"enum": [
"auto",
"frequency",
"luminance",
"contrast",
"error-min"
],
"default": "auto"
},
"motion_smoothing": {
"type": "number",
"description": "Temporal smoothing for mhires percell mode, 0..1. The percell path smooths its per-cell color choices over time (an EMA over color counts plus per-pixel/per-cell decision hysteresis) to suppress frame-to-frame flicker on noisy video. That smoothing trades motion-tracking for stability, so on a hard shot cut an outline from the previous shot lingers as an after-image for a moment. 1.0 (full smoothing) is the most stable but ghostiest; 0.0 tracks the source exactly (no after-image) but can flicker on grainy content. The default 0.25 was picked by hardware A/B as the best ghost/flicker balance. Lower it if after-images still bother you, raise it if motion shimmers. No effect on other modes or palette_modes.",
"default": 0.25
}
}
},
"dsp": {
"type": "object",
"description": "Host-side audio DSP before the 4-bit DAC: compressor/limiter, expander (replaces the hard gate), pre-emphasis, and mic AGC.",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"description": "Master switch for the host-side audio DSP chain (ON by default \u2014 the 4-bit DAC needs it). Set false for the legacy linear encode + hard mic gate.",
"default": true
},
"pre_emphasis": {
"type": [
"number",
"null"
],
"description": "High-frequency boost amount; y[n]=x+amt*(x-x[-1]). Brightens speech for intelligibility. Unset = source-aware default (mic 0.7 / line 0.6); a number forces that amount for all sources; 0 disables. Per-scene [[scenes]].pre_emphasis overrides this."
},
"expander": {
"type": "boolean",
"description": "Downward expander with hysteresis (replaces the hard noise gate when DSP is enabled). Attenuates below the threshold.",
"default": true
},
"expander_threshold_db": {
"type": "number",
"description": "Level below which the expander attenuates (dBFS).",
"default": -45.0
},
"expander_ratio": {
"type": "number",
"description": "Expansion ratio (>1; larger = more attenuation below thresh).",
"default": 2.0
},
"expander_hysteresis_db": {
"type": "number",
"description": "Gap (dB) below the open threshold before the gate closes \u2014 prevents chatter on signal hovering at the threshold.",
"default": 6.0
},
"expander_floor_db": {
"type": "number",
"description": "Maximum attenuation the expander applies (dB).",
"default": -60.0
},
"expander_attack_ms": {
"type": "number",
"description": "Expander gain open (attack) time constant in ms.",
"default": 5.0
},
"expander_release_ms": {
"type": "number",
"description": "Expander gain close (release) time constant in ms.",
"default": 80.0
},
"compress": {
"type": "boolean",
"description": "Soft-knee feed-forward compressor + makeup gain \u2014 the main win for fitting program dynamics into 4 bits.",
"default": true
},
"comp_threshold_db": {
"type": "number",
"description": "Compression threshold (dBFS); above this, gain reduces.",
"default": -18.0
},
"comp_ratio": {
"type": "number",
"description": "Compression ratio (>=1; e.g. 3 = 3:1 above threshold).",
"default": 3.0
},
"comp_knee_db": {
"type": "number",
"description": "Soft-knee width in dB around the threshold (0 = hard knee).",
"default": 6.0
},
"comp_attack_ms": {
"type": "number",
"description": "Compressor attack time constant in ms.",
"default": 5.0
},
"comp_release_ms": {
"type": "number",
"description": "Compressor release time constant in ms.",
"default": 120.0
},
"comp_makeup_auto": {
"type": "boolean",
"description": "Auto-compute makeup gain so threshold-level signal exits near unity. Set false to use comp_makeup_db explicitly.",
"default": true
},
"comp_makeup_db": {
"type": "number",
"description": "Explicit makeup gain (dB) when comp_makeup_auto is false.",
"default": 0.0
},
"limiter": {
"type": "boolean",
"description": "Fast peak limiter / brickwall ceiling \u2014 final safety stage.",
"default": true
},
"limiter_ceiling": {
"type": "number",
"description": "Limiter output ceiling, linear 0..1 (just under full scale).",
"default": 0.95
},
"limiter_release_ms": {
"type": "number",
"description": "Limiter gain recovery (release) time constant in ms.",
"default": 50.0
},
"agc": {
"type": "boolean",
"description": "Automatic gain control for the MIC path only (line/video audio is already peak-normalized). Slow gain toward a target. EXPERIMENTAL: being level-based it can boost a sustained noise floor during long pauses \u2014 best on clean mics, or pair with the expander / raise agc_noise_floor_db above the floor.",
"default": false
},
"agc_target_db": {
"type": "number",
"description": "AGC target RMS level (dBFS).",
"default": -18.0
},
"agc_max_gain_db": {
"type": "number",
"description": "Maximum AGC gain/attenuation magnitude (dB).",
"default": 24.0
},
"agc_time_ms": {
"type": "number",
"description": "AGC adaptation time constant in ms (larger = slower/steadier).",
"default": 300.0
},
"agc_noise_floor_db": {
"type": "number",
"description": "Below this input RMS, AGC holds gain instead of amplifying the noise floor.",
"default": -60.0
}
}
},
"audio_features": {
"type": "object",
"description": "Analyzer that turns live audio input into reactive-visual features (level / bands / transients / tempo) for a generative scene with audio_source = 'mic' and reactive = true.",
"additionalProperties": false,
"properties": {
"bands": {
"type": "integer",
"description": "Number of log-spaced frequency bands the analyzer reports (low\u2192high). Generators fold these into bass/mid/treble thirds, so multiples of 3 are not required; 8 matches the spectrum_petscii overlay's bands. More bands = finer spectral detail, no meaningful cost.",
"default": 8
},
"onset_sensitivity": {
"type": "number",
"description": "Transient-detection sensitivity. The spectral-flux threshold is divided by this, so >1 fires onsets more readily (sparse/soft material, a quiet feed) and <1 fires less (dense or heavily compressed material where everything reads as a transient). 1.0 is the tuned default.",
"default": 1.0
},
"poll_hz": {
"type": "number",
"description": "Analysis rate in Hz. 60 matches a full-rate display, so every rendered frame sees fresh features. Lower it only to save host CPU; below ~30 transients start to smear.",
"default": 60.0
},
"fft_size": {
"type": "integer",
"description": "Analysis window in samples. Larger = finer frequency resolution but blurrier transient timing; 1024 is the balance point at the DAC's sample rates.",
"default": 1024
},
"listen_sample_rate": {
"type": "integer",
"description": "Capture rate in Hz for audio_source = 'listen' (the listen-only path, which never feeds the DAC and so isn't bound to its ~12 kHz rate). 44100 gives the analyzer full-bandwidth audio \u2014 real hi-hat energy above the DAC's 6 kHz Nyquist and cleaner transients. Ignored by audio_source = 'mic' (that path analyzes at the DAC rate, matching what the C64 actually plays).",
"default": 44100
}
}
},
"control": {
"type": "object",
"description": "HTTP control plane (extra).",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"description": "Run the HTTP control plane (pause/resume/skip/reload); requires the 'control' extra.",
"default": false
},
"host": {
"type": "string",
"description": "Bind address for the control-plane HTTP server.",
"default": "127.0.0.1"
},
"port": {
"type": "integer",
"description": "Bind port for the control-plane HTTP server.",
"default": 8765
}
}
},
"midi_control": {
"type": "object",
"description": "MIDI CC control surface for live performance: scene jumps, style cycling, transport, live effect params (extra).",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"description": "Run the MIDI control listener; requires the 'midi' extra.",
"default": false
},
"port": {
"type": [
"string",
"null"
],
"description": "MIDI input port name (substring match, case-insensitive). None = first available port."
},
"broadcast_channel": {
"type": "integer",
"description": "1-based MIDI channel that targets every system at once in ensemble mode. Other channels 1..N target the Nth system in ensemble order. Ignored in single-system mode (the one playlist is always the target).",
"default": 16
},
"jump_transition": {
"type": "string",
"description": "How a 'jump' action changes scenes: 'cut' (instant, no interstitial \u2014 the live-performance default) or 'interstitial' (routes through the normal UP-NEXT card).",
"enum": [
"cut",
"interstitial"
],
"default": "cut"
},
"osd": {
"type": "string",
"description": "On-screen display for live-tune feedback: a brief 'param value' message appears when you sweep a knob or change a mode via MIDI/WLED, then fades. 'top' or 'bottom' picks the corner; 'off' disables it. Rendered pre-quantization so it shows on every display mode (like --frame-numbers).",
"enum": [
"bottom",
"top",
"off"
],
"default": "bottom"
},
"loop_audio": {
"type": "string",
"description": "What happens to a video's audio once a transport.* action touches the scene: 'on' (default) keeps audio playing and re-syncs it across every seek/pause/loop splice; 'mute' restores the Phase-2 escape valve (audio mutes for the rest of that scene's run). Falls back to mute behavior automatically when the scene has no audio. The REU-pump audio path is always forced off under transport regardless.",
"enum": [
"on",
"mute"
],
"default": "on"
},
"cc_map": {
"type": "array",
"description": "MIDI-message -> action mappings ([[midi_control.cc_map]] tables); see --describe section:midi_control. Set to [] to disable the shipped defaults, or override/extend individual entries. Each entry: type ('cc'|'note'|'pc'|'mmc'), number (0-127 for cc/note/pc; an MMC command byte \u2014 0x01 stop, 0x02 play, 0x04 FF, 0x05 RW, 0x06 record, 0x09 pause \u2014 for mmc), action ('pause'|'resume'|'toggle_pause'|'skip'|'cycle_style'|'jump'|'param'|'transport.play_pause'|'transport.stop'|'transport.loop_toggle'|'transport.rw'|'transport.ff'|'transport.jog'|'transport.record'|'loop_slot'); 'jump' also needs an int scene; 'param' also needs a string target ('effect.<name>', 'source.<name>', 'scene.<name>' for scope scenes, or 'mode.<name>' for the display mode's live color knobs \u2014 dither_strength/method, motion_smoothing, auto_fit_strength, cell_strategy, color_match, palette_mode). A knob (cc) sweeps a scalar or bucket-selects a choice; a note/pad cycles a choice. The transport.* actions give DJ-style control of a playing video scene (pause-in-place, seek, RW/FF with acceleration while a note is held, an A/B loop, and a rotary jog \u2014 'mode' 'abs'|'rel', default 'rel'); once touched, that scene's audio follows every seek/pause/loop by default (see loop_audio, and docs/architecture.md's transport note). A 'mmc' entry also matches an MMC transport SysEx from a DAW/controller. 'transport.record' arms a loop (Record -> Stop workflow, red border while armed); 'loop_slot' also needs an int 'slot' >= 1 (a pad number) and recalls that per-video saved loop on a plain press, saves the current loop into it while Stop is held, or clears it while Record is held (note mappings only \u2014 an mmc record/stop can't reliably hold for the chord, since MMC has no release event). 'look_save'/'look_recall' (Phase 6) each need an int 'slot' >= 1 \u2014 a look captures the active clip + effect-chain state on save and re-fires it on recall.",
"default": [
{
"type": "note",
"number": 36,
"action": "skip"
},
{
"type": "note",
"number": 37,
"action": "cycle_style"
},
{
"type": "note",
"number": 38,
"action": "toggle_pause"
},
{
"type": "note",
"number": 39,
"action": "jump",
"scene": 0
},
{
"type": "note",
"number": 40,
"action": "jump",
"scene": 0
},
{
"type": "note",
"number": 41,
"action": "jump",
"scene": 1
},
{
"type": "note",
"number": 42,
"action": "jump",
"scene": 2
},
{
"type": "note",
"number": 43,
"action": "jump",
"scene": 3
},
{
"type": "note",
"number": 44,
"action": "jump",
"scene": 4
},
{
"type": "note",
"number": 45,
"action": "jump",
"scene": 5
},
{
"type": "note",
"number": 46,
"action": "jump",
"scene": 6
},
{
"type": "note",
"number": 47,
"action": "jump",
"scene": 7
},
{
"type": "note",
"number": 48,
"action": "jump",
"scene": 8
},
{
"type": "note",
"number": 49,
"action": "jump",
"scene": 9
},
{
"type": "note",
"number": 50,
"action": "jump",
"scene": 10
},
{
"type": "note",
"number": 51,
"action": "jump",
"scene": 11
},
{
"type": "note",
"number": 52,
"action": "jump",
"scene": 12
},
{
"type": "note",
"number": 53,
"action": "jump",
"scene": 13
},
{
"type": "note",
"number": 54,
"action": "jump",