diff --git a/.gitattributes b/.gitattributes index 9ddacc6..d92d407 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,7 @@ *.py text eol=lf *.md text eol=lf *.json text eol=lf + +# Preserve the approved binary stream and literal per-note audit bytes. +examples/doebel/DOEBEL.VGM binary +examples/doebel/NOTES.csv binary diff --git a/README.md b/README.md index 5892c11..75ea42d 100644 --- a/README.md +++ b/README.md @@ -139,3 +139,9 @@ non-owning speaker callback contract. `examples/dos_sfx_demo.c` builds as finite harpoon, silent gap, conch and all-channel stop sequence from foreground retrace polling only. +[`examples/doebel/`](examples/doebel/README.md) adds an optional three-tone +"Doebelinschmirtz Incorporated" jingle example using `lt_score`. +`DOEBEL.EXE` plays it once in foreground DOS; the checked-in score header and +PSG VGM need no Python preparation. The recognizable tune is a reduction of +the referenced *Evil Jingle*, with separate musical attribution in the example. + diff --git a/examples/doebel/APPROVED.H b/examples/doebel/APPROVED.H new file mode 100644 index 0000000..08128e8 --- /dev/null +++ b/examples/doebel/APPROVED.H @@ -0,0 +1,183 @@ +/* Frozen register fixture from the approved host audition. + * Musical attribution: README.md. Do not regenerate from candidate notes. + */ +#ifndef DOEBEL_APPROVED_H +#define DOEBEL_APPROVED_H +typedef struct { unsigned frame; lt_u8 byte; } doebel_approved_write; +#define DOEBEL_APPROVED_WRITES 173U +static const doebel_approved_write doebel_approved[] = { + { 0U, 0x9fU }, + { 0U, 0xbfU }, + { 0U, 0xdfU }, + { 0U, 0xffU }, + { 0U, 0x9fU }, + { 0U, 0xbfU }, + { 0U, 0xdfU }, + { 0U, 0xffU }, + { 0U, 0xe4U }, + { 0U, 0x80U }, + { 0U, 0x14U }, + { 0U, 0x94U }, + { 0U, 0xacU }, + { 0U, 0x1aU }, + { 0U, 0xbbU }, + { 0U, 0xcaU }, + { 0U, 0x2fU }, + { 0U, 0xd9U }, + { 4U, 0x92U }, + { 4U, 0xb9U }, + { 4U, 0xd7U }, + { 48U, 0x95U }, + { 48U, 0xbcU }, + { 55U, 0xbfU }, + { 56U, 0x9fU }, + { 57U, 0x8dU }, + { 57U, 0x17U }, + { 57U, 0x94U }, + { 61U, 0x92U }, + { 75U, 0xdaU }, + { 77U, 0x95U }, + { 82U, 0xdfU }, + { 84U, 0x9fU }, + { 86U, 0x80U }, + { 86U, 0x14U }, + { 86U, 0x94U }, + { 86U, 0xacU }, + { 86U, 0x1aU }, + { 86U, 0xbbU }, + { 86U, 0xcaU }, + { 86U, 0x2fU }, + { 86U, 0xd9U }, + { 90U, 0x92U }, + { 90U, 0xb9U }, + { 90U, 0xd7U }, + { 160U, 0xdaU }, + { 161U, 0xbcU }, + { 162U, 0x95U }, + { 167U, 0xdfU }, + { 168U, 0xbfU }, + { 169U, 0x9fU }, + { 171U, 0x8dU }, + { 171U, 0x10U }, + { 171U, 0x94U }, + { 171U, 0xa3U }, + { 171U, 0x15U }, + { 171U, 0xbbU }, + { 171U, 0xc7U }, + { 171U, 0x2aU }, + { 171U, 0xd9U }, + { 175U, 0x92U }, + { 175U, 0xb9U }, + { 175U, 0xd7U }, + { 218U, 0xbcU }, + { 219U, 0x95U }, + { 225U, 0xbfU }, + { 226U, 0x9fU }, + { 228U, 0x83U }, + { 228U, 0x15U }, + { 228U, 0x94U }, + { 232U, 0x92U }, + { 245U, 0xdaU }, + { 247U, 0x95U }, + { 252U, 0xdfU }, + { 254U, 0x9fU }, + { 256U, 0x8dU }, + { 256U, 0x10U }, + { 256U, 0x94U }, + { 256U, 0xa3U }, + { 256U, 0x15U }, + { 256U, 0xbbU }, + { 256U, 0xc7U }, + { 256U, 0x2aU }, + { 256U, 0xd9U }, + { 260U, 0x92U }, + { 260U, 0xb9U }, + { 260U, 0xd7U }, + { 331U, 0xbcU }, + { 331U, 0xdaU }, + { 332U, 0x95U }, + { 338U, 0xdfU }, + { 339U, 0x9fU }, + { 339U, 0xbfU }, + { 341U, 0x82U }, + { 341U, 0x0eU }, + { 341U, 0x94U }, + { 341U, 0xadU }, + { 341U, 0x11U }, + { 341U, 0xbbU }, + { 341U, 0xc7U }, + { 341U, 0x35U }, + { 341U, 0xd9U }, + { 345U, 0x92U }, + { 345U, 0xb9U }, + { 345U, 0xd7U }, + { 416U, 0xdaU }, + { 417U, 0xbcU }, + { 418U, 0x95U }, + { 423U, 0xdfU }, + { 424U, 0xbfU }, + { 425U, 0x9fU }, + { 426U, 0x82U }, + { 426U, 0x0eU }, + { 426U, 0x94U }, + { 426U, 0xadU }, + { 426U, 0x11U }, + { 426U, 0xbbU }, + { 426U, 0xc7U }, + { 426U, 0x35U }, + { 426U, 0xd9U }, + { 430U, 0x92U }, + { 430U, 0xb9U }, + { 430U, 0xd7U }, + { 473U, 0xbcU }, + { 473U, 0xdaU }, + { 474U, 0x95U }, + { 480U, 0xdfU }, + { 481U, 0x9fU }, + { 481U, 0xbfU }, + { 483U, 0x82U }, + { 483U, 0x0eU }, + { 483U, 0x94U }, + { 483U, 0xadU }, + { 483U, 0x11U }, + { 483U, 0xbbU }, + { 483U, 0xc7U }, + { 483U, 0x35U }, + { 483U, 0xd9U }, + { 487U, 0x92U }, + { 487U, 0xb9U }, + { 487U, 0xd7U }, + { 558U, 0xdaU }, + { 559U, 0x95U }, + { 559U, 0xbcU }, + { 565U, 0xdfU }, + { 566U, 0xbfU }, + { 567U, 0x9fU }, + { 568U, 0x82U }, + { 568U, 0x0eU }, + { 568U, 0x94U }, + { 568U, 0xadU }, + { 568U, 0x11U }, + { 568U, 0xbbU }, + { 568U, 0xc7U }, + { 568U, 0x35U }, + { 568U, 0xd9U }, + { 572U, 0x92U }, + { 572U, 0xb9U }, + { 572U, 0xd7U }, + { 927U, 0xdaU }, + { 928U, 0xbcU }, + { 929U, 0x95U }, + { 934U, 0xdfU }, + { 935U, 0xbfU }, + { 936U, 0x9fU }, + { 1023U, 0x9fU }, + { 1023U, 0xbfU }, + { 1023U, 0xdfU }, + { 1023U, 0xffU }, + { 1023U, 0x9fU }, + { 1023U, 0xbfU }, + { 1023U, 0xdfU }, + { 1023U, 0xffU }, +}; +#endif diff --git a/examples/doebel/APPROVED.json b/examples/doebel/APPROVED.json new file mode 100644 index 0000000..8a6437a --- /dev/null +++ b/examples/doebel/APPROVED.json @@ -0,0 +1,24 @@ +{ + "listening_approval": "Chad heard and approved DOEBEL-HOST.WAV on 2026-10-25; requested optional libtandy example", + "native_status": "passed normal completion, Escape cleanup and timer restoration on emulated Tandy HX; see NATIVE.json", + "publication": "approved by Chad on 2026-10-25", + "reference_wav_sha256": "9bd189015904bda25c5e75dff501696a7641934bb3ffeec87e3278f719d00b0d", + "reference_trace_sha256": "7d36113c4de4a210990410ddc2804a450a25fa419677f3dbb9c21213124a080c", + "per_note_audit_sha256": "d2de6d6634df6d537b78c4143e8c098f9dd46a3667b1e13f9fa8152876c3e6ae", + "score_json_sha256": "f1d913b8a3ef83cb692dbadd20341470469295632e4f1f7bb2204f89fcf47661", + "vgm_sha256": "2e495869bf4712937bfc296dc7be192147527cbc43a5f284f0867859779ebff3", + "frames": 1023, + "service_hz": 240, + "notes": [ + 10, + 8, + 8 + ], + "gain": 0.18, + "native_attenuation": [ + 2, + 9, + 7 + ], + "psg_writes": 173 +} diff --git a/examples/doebel/DOEBEL.H b/examples/doebel/DOEBEL.H new file mode 100644 index 0000000..5232afd --- /dev/null +++ b/examples/doebel/DOEBEL.H @@ -0,0 +1,50 @@ +/* Optional PSG reduction of the referenced Evil Jingle opening. + * Musical attribution and usage: README.md. + * Generated from editable DOEBEL.json; developer-only generator. + */ +#ifndef DOEBEL_SCORE_H +#define DOEBEL_SCORE_H +#include "lt_score.h" +#define DOEBEL_SERVICE_HZ 240U +#define DOEBEL_END_TICK 2304U +static const lt_score_note doebel_0[] = { + { 0U, 128U, 320U }, /* M01 F4 */ + { 128U, 192U, 381U }, /* M02 D4 */ + { 192U, 384U, 320U }, /* M03 F4 */ + { 384U, 512U, 269U }, /* M04 G#4 */ + { 512U, 576U, 339U }, /* M05 E4 */ + { 576U, 768U, 269U }, /* M06 G#4 */ + { 768U, 960U, 226U }, /* M07 B4 */ + { 960U, 1088U, 226U }, /* M08 B4 */ + { 1088U, 1280U, 226U }, /* M09 B4 */ + { 1280U, 2112U, 226U }, /* M10 B4 */ +}; +static const lt_score_note doebel_1[] = { + { 0U, 128U, 428U }, /* H01 C4 */ + { 192U, 384U, 428U }, /* H02 C4 */ + { 384U, 512U, 339U }, /* H03 E4 */ + { 576U, 768U, 339U }, /* H04 E4 */ + { 768U, 960U, 285U }, /* H05 G4 */ + { 960U, 1088U, 285U }, /* H06 G4 */ + { 1088U, 1280U, 285U }, /* H07 G4 */ + { 1280U, 2112U, 285U }, /* H08 G4 */ +}; +static const lt_score_note doebel_2[] = { + { 0U, 192U, 762U }, /* B01 D3 */ + { 192U, 384U, 762U }, /* B02 D3 */ + { 384U, 576U, 679U }, /* B03 E3 */ + { 576U, 768U, 679U }, /* B04 E3 */ + { 768U, 960U, 855U }, /* B05 C3 */ + { 960U, 1088U, 855U }, /* B06 C3 */ + { 1088U, 1280U, 855U }, /* B07 C3 */ + { 1280U, 2112U, 855U }, /* B08 C3 */ +}; +static const lt_score doebel_score = { + LTSCORE_VERSION, 4U, 192U, 2112U, 2304U, 355030UL, + { doebel_0, doebel_1, doebel_2 }, + { 10U, 8U, 8U }, + { 2U, 9U, 7U }, + { 4U, 6U, 8U }, + 0, 0U, 0, 0U +}; +#endif diff --git a/examples/doebel/DOEBEL.VGM b/examples/doebel/DOEBEL.VGM new file mode 100644 index 0000000..fd128a2 Binary files /dev/null and b/examples/doebel/DOEBEL.VGM differ diff --git a/examples/doebel/DOEBEL.json b/examples/doebel/DOEBEL.json new file mode 100644 index 0000000..b856539 --- /dev/null +++ b/examples/doebel/DOEBEL.json @@ -0,0 +1,75 @@ +{ + "title": "Doebelinschmirtz Incorporated — private PSG sketch", + "status": "LOCAL DEVELOPER AUDITION; native testing and explicit publication approval pending", + "reference": { + "title": "Evil Jingle (from Phineas and Ferb), opening measures 1–3", + "publisher_preview": "https://www.musicnotes.com/sheetmusic/sheet-music-boss/evil-jingle/MN0202127", + "preview_image": "https://www.musicnotes.com/product/image/MN0202127/large", + "arranger_page": "https://sheetmusicboss.com/2019/10/21/evil-jingle-from-phineas-and-ferb-piano-tutorial/", + "credit": "Words/music credited on preview to Danny Jacob, Jeff Marsh, Jon Barry, Zac Moncrief, Dan Povenmire and J.G. Orrantia; piano arrangement Andrew Wrangell; edited Samuel Dickenson.", + "scope": "Melody transcribed from the visible licensed opening preview. Three-tone reduction, approximate 2:1 swing and PSG articulation are arrangement decisions, not an exact recording recreation. No reference recording or score image is distributed." + }, + "ppq": 192, + "tempo_us": 355030, + "tempo_bpm_reference": 169, + "psg_clock_hz": 3579545, + "service_hz": 240, + "one_shot_end_tick": 2304, + "silent_loop_start_tick": 2112, + "noise": "muted", + "pc_speaker": "unused", + "master_linear_gain": 0.18, + "normalization": "none; all stems and mix use identical gain", + "voices": [ + { + "channel": 0, + "role": "reference melody", + "attenuation": 2, + "release_gap_ticks": 4, + "notes": [ + {"id":"M01","start":0,"end":128,"pitch":"F4","reference":"m1 beat1, swung eighth"}, + {"id":"M02","start":128,"end":192,"pitch":"D4","reference":"m1 offbeat1, swung eighth"}, + {"id":"M03","start":192,"end":384,"pitch":"F4","reference":"m1 beat2, quarter"}, + {"id":"M04","start":384,"end":512,"pitch":"G#4","reference":"m1 beat3, swung eighth, written sharp"}, + {"id":"M05","start":512,"end":576,"pitch":"E4","reference":"m1 offbeat3, swung eighth"}, + {"id":"M06","start":576,"end":768,"pitch":"G#4","reference":"m1 beat4, quarter"}, + {"id":"M07","start":768,"end":960,"pitch":"B4","reference":"m2 beat1, quarter"}, + {"id":"M08","start":960,"end":1088,"pitch":"B4","reference":"m2 beat2, swung eighth"}, + {"id":"M09","start":1088,"end":1280,"pitch":"B4","reference":"m2 offbeat2 tied through beat3, combined as one event"}, + {"id":"M10","start":1280,"end":2112,"pitch":"B4","reference":"m2 offbeat3 tied into m3 dotted half; one event, final beat rests"} + ] + }, + { + "channel": 1, + "role": "reduced inner harmony; omissions intentional", + "attenuation": 9, + "release_gap_ticks": 6, + "notes": [ + {"id":"H01","start":0,"end":128,"pitch":"C4"}, + {"id":"H02","start":192,"end":384,"pitch":"C4"}, + {"id":"H03","start":384,"end":512,"pitch":"E4"}, + {"id":"H04","start":576,"end":768,"pitch":"E4"}, + {"id":"H05","start":768,"end":960,"pitch":"G4"}, + {"id":"H06","start":960,"end":1088,"pitch":"G4"}, + {"id":"H07","start":1088,"end":1280,"pitch":"G4"}, + {"id":"H08","start":1280,"end":2112,"pitch":"G4"} + ] + }, + { + "channel": 2, + "role": "reference bass D2/E2/C2 raised one octave for PSG range", + "attenuation": 7, + "release_gap_ticks": 8, + "notes": [ + {"id":"B01","start":0,"end":192,"pitch":"D3"}, + {"id":"B02","start":192,"end":384,"pitch":"D3"}, + {"id":"B03","start":384,"end":576,"pitch":"E3"}, + {"id":"B04","start":576,"end":768,"pitch":"E3"}, + {"id":"B05","start":768,"end":960,"pitch":"C3"}, + {"id":"B06","start":960,"end":1088,"pitch":"C3"}, + {"id":"B07","start":1088,"end":1280,"pitch":"C3"}, + {"id":"B08","start":1280,"end":2112,"pitch":"C3"} + ] + } + ] +} diff --git a/examples/doebel/HOSTCHECK.json b/examples/doebel/HOSTCHECK.json new file mode 100644 index 0000000..d273932 --- /dev/null +++ b/examples/doebel/HOSTCHECK.json @@ -0,0 +1,56 @@ +{ + "status": "PASS: host checks only; no native/hardware/audible qualification", + "reference": { + "title": "Evil Jingle (from Phineas and Ferb), opening measures 1\u20133", + "publisher_preview": "https://www.musicnotes.com/sheetmusic/sheet-music-boss/evil-jingle/MN0202127", + "preview_image": "https://www.musicnotes.com/product/image/MN0202127/large", + "arranger_page": "https://sheetmusicboss.com/2019/10/21/evil-jingle-from-phineas-and-ferb-piano-tutorial/", + "credit": "Words/music credited on preview to Danny Jacob, Jeff Marsh, Jon Barry, Zac Moncrief, Dan Povenmire and J.G. Orrantia; piano arrangement Andrew Wrangell; edited Samuel Dickenson.", + "scope": "Melody transcribed from the visible licensed opening preview. Three-tone reduction, approximate 2:1 swing and PSG articulation are arrangement decisions, not an exact recording recreation. No reference recording or score image is distributed." + }, + "libtandy_commit": "5d7036b9cdd23be9a0a3be7e9bc53c81680b055d", + "core_files": { + "lt_chip.c": "277505ef3f6c0eaa6e0aa51c1f7577f6420bb25c0b33726431c7413fd9998148", + "lt_audio.c": "c0869860a031f8ced684725eb9464c2fd987f61ac9952a826fdbe41e3394c688", + "lt_score.c": "e05088bde51a3b9ac5227993c424a40b6844035136068a04c23e1d85f729e737" + }, + "sample_rate": 44100, + "duration_seconds": 4.512517006802721, + "service_hz": 240, + "score_ticks_per_second": 541, + "scheduler_phase_step": 147729, + "host_model": "8x oversampled ideal square waves, 2 dB PSG attenuation steps, mono sum; no analog filtering, speaker or noise", + "master_linear_gain": 0.18, + "normalization": "none", + "mix_peak": 0.17214770108816962, + "mix_peak_dbfs": -15.281975452376834, + "mix_rms": 0.10714142758052447, + "per_voice_peak": [ + 0.11357232200643479, + 0.02266065741229501, + 0.035914721669439834 + ], + "per_voice_rms": [ + 0.099524136289553, + 0.01936245481735165, + 0.03172907993812419 + ], + "note_count": [ + 10, + 8, + 8 + ], + "checked_notes": 26, + "all_notes_passed": true, + "clipped_samples": 0, + "final_attenuation": [ + 15, + 15, + 15, + 15 + ], + "silent_tail_seconds": 0.25, + "vgm_stream_matches_trace": true, + "vgm_loop": false, + "publication": "NOT APPROVED; native test and explicit publication approval required" +} diff --git a/examples/doebel/NATIVE.json b/examples/doebel/NATIVE.json new file mode 100644 index 0000000..23f938e --- /dev/null +++ b/examples/doebel/NATIVE.json @@ -0,0 +1,54 @@ +{ + "complete": true, + "date": "2026-10-25", + "scope": [ + "normal completion", + "Escape cleanup", + "IRQ0 and timer restoration" + ], + "machine": { + "emulator": "86Box, local fixed build", + "emulator_sha256": "047477bb6f5d8681e08d6d5587aaab4285800c2f4efbb41549ccf75285974b1c", + "model": "Tandy 1000 HX", + "cpu": "8088", + "cpu_hz": 7159092, + "memory_kib": 640, + "dos": "MS-DOS 6.22" + }, + "executable": { + "name": "DOEBEL.EXE", + "bytes": 14826, + "sha256": "a3857b85df56b2ff7d031916852f58ec9f64ac3e06bd1296d06b7f8d139026a9" + }, + "before_irq0_vector_bytes": "a5fe00f0", + "normal": { + "exit_status": 0, + "active_irq0_vector_bytes": "e021d00e", + "bios_ticks_including_load": 95, + "restored": { + "irq0_vector": "a5fe00f0", + "matches_before": true, + "pit_counter": 54256, + "bios_ticks_over_approx_0_31_seconds": 6 + }, + "output": "Doebelinschmirtz Incorporated - optional PSG example\r\nOne jingle; Escape cancels. Use exclusive foreground Tandy DOS.\r\nDOEBEL: stopped, PSG muted\r\n" + }, + "escape": { + "exit_status": 0, + "active_irq0_vector_bytes": "e021d00e", + "bios_ticks_from_active_playback": 8, + "restored": { + "irq0_vector": "a5fe00f0", + "matches_before": true, + "pit_counter": 28320, + "bios_ticks_over_approx_0_31_seconds": 6 + }, + "output": "Doebelinschmirtz Incorporated - optional PSG example\r\nOne jingle; Escape cancels. Use exclusive foreground Tandy DOS.\r\nDOEBEL: stopped, PSG muted\r\n" + }, + "shared_lease_acquired": true, + "owned_child_stopped": true, + "shared_lease_released": true, + "physical_hardware_test": false, + "native_audio_capture": false, + "publication": "approved by Chad on 2026-10-25" +} diff --git a/examples/doebel/NOTES.csv b/examples/doebel/NOTES.csv new file mode 100644 index 0000000..16b690c --- /dev/null +++ b/examples/doebel/NOTES.csv @@ -0,0 +1,27 @@ +id,channel,pitch,start_tick,end_tick,frame,seconds,period,actual_hz,cents_error,attack_attenuation,attack_rms,transitions_35ms,passed +M01,0,F4,0,128,0,0.0,320,349.5649,1.668,4,0.09542386,28,True +M02,0,D4,128,192,57,0.2375,381,293.5979,-0.395,4,0.09552045,22,True +M03,0,F4,192,384,86,0.358333,320,349.5649,1.668,4,0.09540569,28,True +M04,0,G#4,384,512,171,0.7125,269,415.8393,2.227,4,0.09520551,34,True +M05,0,E4,512,576,228,0.95,339,329.9728,1.812,4,0.0954681,25,True +M06,0,G#4,576,768,256,1.066667,269,415.8393,2.227,4,0.0955051,33,True +M07,0,B4,768,960,341,1.420833,226,494.9592,3.767,4,0.09521158,39,True +M08,0,B4,960,1088,426,1.775,226,494.9592,3.767,4,0.0952905,39,True +M09,0,B4,1088,1280,483,2.0125,226,494.9592,3.767,4,0.09521091,40,True +M10,0,B4,1280,2112,568,2.366667,226,494.9592,3.767,4,0.09520987,38,True +H01,1,C4,0,128,0,0.0,428,261.357,-1.778,11,0.01906232,20,True +H02,1,C4,192,384,86,0.358333,428,261.357,-1.778,11,0.01906209,20,True +H03,1,E4,384,512,171,0.7125,339,329.9728,1.812,11,0.01904457,25,True +H04,1,E4,576,768,256,1.066667,339,329.9728,1.812,11,0.01904828,26,True +H05,1,G4,768,960,341,1.420833,285,392.494,2.2,11,0.01902777,31,True +H06,1,G4,960,1088,426,1.775,285,392.494,2.2,11,0.01903598,31,True +H07,1,G4,1088,1280,483,2.0125,285,392.494,2.2,11,0.01902612,30,True +H08,1,G4,1280,2112,568,2.366667,285,392.494,2.2,11,0.01902885,30,True +B01,2,D3,0,192,0,0.0,762,146.7989,-0.395,9,0.03026659,12,True +B02,2,D3,192,384,86,0.358333,762,146.7989,-0.395,9,0.03026432,12,True +B03,2,E3,384,576,171,0.7125,679,164.7434,-0.739,9,0.03026333,13,True +B04,2,E3,576,768,256,1.066667,679,164.7434,-0.739,9,0.03025316,14,True +B05,2,C3,768,960,341,1.420833,855,130.8313,0.245,9,0.03027795,10,True +B06,2,C3,960,1088,426,1.775,855,130.8313,0.245,9,0.03028626,10,True +B07,2,C3,1088,1280,483,2.0125,855,130.8313,0.245,9,0.03028005,10,True +B08,2,C3,1280,2112,568,2.366667,855,130.8313,0.245,9,0.03028005,10,True diff --git a/examples/doebel/README.md b/examples/doebel/README.md new file mode 100644 index 0000000..8f2f272 --- /dev/null +++ b/examples/doebel/README.md @@ -0,0 +1,78 @@ +# Doebelinschmirtz Incorporated + +An optional three-tone PSG rendition of the recognizable *Evil Jingle* from +*Phineas and Ferb*, for Chad's “Doebelinschmirtz Incorporated” joke. It uses +the existing `lt_score` API. Noise and PC speaker are unused. + +The melody follows the opening of the [licensed piano score preview](https://www.musicnotes.com/sheetmusic/sheet-music-boss/evil-jingle/MN0202127), +arranged by Andrew Wrangell / Sheet Music Boss, edited by Samuel Dickenson. +The preview credits Danny Jacob, Jeff Marsh, Jon Barry, Zac Moncrief, +Dan Povenmire and J.G. Orrantia. This is a reduction of their recognizable +tune, not an original composition. Musical rights remain with their owners; +the library and playback code keep their existing MIT licenses. + +## Play or use the score + +Build from the repository root with the normal developer toolchain: + +```powershell +python tools/build.py --target dos --watcom C:/WATCOM +``` + +Run `build/dos/DOEBEL.EXE` explicitly in exclusive foreground Tandy DOS. +It plays once, mutes/releases the PSG and restores the BIOS timer. Escape +cancels. It uses the existing `lt_tick_dos` adapter at nominal 240 Hz +(PIT divisor 4,972, about 239.9803 Hz). No startup installation or speaker +access is involved. Normal completion and Escape/timer cleanup passed on an +emulated Tandy HX; see [the native receipt](NATIVE.json). + +`DOEBEL.H` is the ready-to-compile static score; `DOEBEL.VGM` is a ready +one-shot PSG stream for an existing compatible VGM player. Neither needs a +generator or Python on the playback machine. In an application that already +owns the audio handle and update cadence: + +```c +#include "DOEBEL.H" + +lt_score_init(&music, &audio); +lt_score_start(&music, &doebel_score, DOEBEL_SERVICE_HZ, 0, 0); +/* Once per existing 240-Hz update, before the silent loop boundary: */ +if (music.phase + music.step >= ((lta_u32)DOEBEL_END_TICK << 16)) + lt_score_stop(&music); +else + lt_score_tick(&music); +``` + +The caller owns all four PSG channels, serialization and timing. See +[`../doebel_demo.c`](../doebel_demo.c) for setup, errors and cleanup. Do not +install another timer in an app that already has one. + +## Edit and verify + +`DOEBEL.json` is the unchanged approved editable score: 10 melody, 8 harmony +and 8 bass events. It retains 192 PPQ, 355,030 microseconds/quarter, 2:1 swing, +the tied ending and attenuation levels 2/9/7. The bass is raised an octave to +fit PSG range. The checked-in header preserves those notes and envelope gaps. + +`generate.py --write` rebuilds only the header after deliberate edits; +`generate.py --check` checks it without writing. This is a developer tool. +The frozen `APPROVED.H` register fixture and `NOTES.csv` per-note audit are +from the host audition Chad heard and approved on 2026-10-25. The host suite +compares every actual PSG byte and service frame against that fixture, +including repeated attacks and the ending mute/release. `APPROVED.json` +retains the reference WAV and audit hashes; `HOSTCHECK.json` is the original +numerical receipt. + +The approved WAV is a 4.512517-second host approximation, with fixed master +gain 0.18, identical-gain stems, no normalization and a peak of -15.28 dBFS. +The master gain is a host-rendering factor; native PSG attenuation remains +2/9/7 with the existing score envelopes. The approximation uses ideal square +waves and does not model analog filtering or speaker response. No gain, +notes, timing or balance changes are introduced by this example. + +Listening approval covers the host sound. The short native normal-completion, +Escape and timer-restoration checks passed using the shared emulator lease. +Physical hardware and native audio capture are not qualified. Chad approved +publication on 2026-10-25. Status text in the unchanged +score JSON is historical audition metadata; this README and the validation +receipts describe current acceptance. diff --git a/examples/doebel/VALIDATION.md b/examples/doebel/VALIDATION.md new file mode 100644 index 0000000..fd6ffb6 --- /dev/null +++ b/examples/doebel/VALIDATION.md @@ -0,0 +1,47 @@ +# Local validation, 2026-10-25 + +Base: libtandy main `5d7036b9cdd23be9a0a3be7e9bc53c81680b055d`. +Chad listened to and approved the host sound, then requested this optional +example. Chad approved publication of the tested example on 2026-10-25. + +- The editable score, VGM and 26-row per-note audit are byte-identical to + the approved assets. Header changes are comments only; all events, + periods, tempo, levels and release gaps are unchanged. +- Re-rendering with this checkout's unchanged `lt_chip`, `lt_audio` and + `lt_score` produces the identical WAV, register trace, audit and VGM. + Approved WAV SHA-256: + `9bd189015904bda25c5e75dff501696a7641934bb3ffeec87e3278f719d00b0d`. +- The normal host suite passes all 12 test programs, including the new + regression comparing 173 actual PSG writes and their service frames to + the frozen approved trace. The generator's read-only header check passes. +- The normal DOS build completed all 40 steps with Open Watcom's 8086 + small-model flags and warnings treated as errors. Native acceptance then + exposed an output-name collision: the regression overwrote `DOEBEL.EXE`. + The regression now builds as `DBSCORE.EXE`; its old 18,488-byte binary was + preserved. Only the player was rebuilt with the same normal compiler + recipe, without repeating host suites. The actual `DOEBEL.EXE` is 14,826 + bytes, SHA-256 + `a3857b85df56b2ff7d031916852f58ec9f64ac3e06bd1296d06b7f8d139026a9`. +- Both requested short native checks passed on an emulated Tandy 1000 HX, + 8088 at 7.159092 MHz, 640 KiB, MS-DOS 6.22. Normal completion returned + successfully; Escape stopped active playback within eight BIOS ticks. + Both paths reported PSG mute and restored the original IRQ0 vector + `F000:FEA5`. Post-return PIT counters were in BIOS range, and the BIOS + clock advanced six ticks in each approximate 0.31-second observation. + [NATIVE.json](NATIVE.json) records the results and executable identity. +- The shared lease was acquired after the preceding owner finished. The + private child was stopped before the lease was released. Physical hardware + and native audio capture remain outside this acceptance check. + +Commands run from the repository root: + +```powershell +python examples/doebel/generate.py --check +python tools/build.py --target host --gcc-launcher C:/Users/chad/Documents/Codex/toolchains/gcc-ucrt64.py --runtime-bin C:/msys64/ucrt64/bin +python tools/build.py --target dos --watcom C:/WATCOM +git diff --check +``` + +The generated header is 1,739 bytes of source text; its 26 `lt_score_note` +events occupy 156 bytes of note data. The ready one-shot VGM is 612 bytes. +Build outputs and full compiler logs stay in ignored `build/` directories. diff --git a/examples/doebel/generate.py b/examples/doebel/generate.py new file mode 100644 index 0000000..3dacca3 --- /dev/null +++ b/examples/doebel/generate.py @@ -0,0 +1,71 @@ +"""Developer-only score-header regeneration; playback never needs Python. + +python examples/doebel/generate.py --check +python examples/doebel/generate.py --write # after intentional score edits +The approved trace/audit is a frozen listening reference, not regenerated. +""" +import argparse +import json +import math +from pathlib import Path +import re + +HERE = Path(__file__).resolve().parent + + +def render(): + score = json.loads((HERE / 'DOEBEL.json').read_text(encoding='utf-8')) + result = ['/* Optional PSG reduction of the referenced Evil Jingle opening.', + ' * Musical attribution and usage: README.md.', + ' * Generated from editable DOEBEL.json; developer-only generator.', + ' */', '#ifndef DOEBEL_SCORE_H', '#define DOEBEL_SCORE_H', + '#include "lt_score.h"', + '#define DOEBEL_SERVICE_HZ %uU' % score['service_hz'], + '#define DOEBEL_END_TICK %uU' % score['one_shot_end_tick']] + for voice in score['voices']: + result.append('static const lt_score_note doebel_%u[] = {' % voice['channel']) + previous = 0 + for note in voice['notes']: + match = re.fullmatch(r'([A-G])(#?)([0-8])', note['pitch']) + if not match: + raise ValueError(note['pitch']) + midi = (int(match[3])+1)*12 + dict(C=0, D=2, E=4, F=5, + G=7, A=9, B=11)[match[1]] + bool(match[2]) + hz = 440 * 2 ** ((midi-69)/12) + period = math.floor(score['psg_clock_hz']/(32*hz)+.5) + if not (1 <= period <= 1023 and previous <= note['start'] < + note['end'] <= score['silent_loop_start_tick']): + raise ValueError(note) + previous = note['end'] + result.append(' { %uU, %uU, %uU }, /* %s %s */' % ( + note['start'], note['end'], period, note['id'], note['pitch'])) + result.append('};') + result += ['static const lt_score doebel_score = {', + ' LTSCORE_VERSION, 4U, %uU, %uU, %uU, %uUL,' % ( + score['ppq'], score['silent_loop_start_tick'], + score['one_shot_end_tick'], score['tempo_us']), + ' { doebel_0, doebel_1, doebel_2 },'] + for field in ('notes', 'attenuation', 'release_gap_ticks'): + values = [(len(v[field]) if field == 'notes' else v[field]) for v in score['voices']] + result.append(' { ' + ', '.join('%uU' % v for v in values) + ' },') + result += [' 0, 0U, 0, 0U', '};', '#endif', ''] + return '\n'.join(result) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument('--check', action='store_true') + mode.add_argument('--write', action='store_true') + args = parser.parse_args() + expected = render() + path = HERE / 'DOEBEL.H' + if args.write: + path.write_bytes(expected.replace('\n', '\r\n').encode('ascii')) + elif path.read_text(encoding='ascii') != expected: + raise SystemExit('FAIL DOEBEL.H does not match editable score') + print('PASS editable Doebel score and checked-in header') + + +if __name__ == '__main__': + main() diff --git a/examples/doebel_demo.c b/examples/doebel_demo.c new file mode 100644 index 0000000..d38b68f --- /dev/null +++ b/examples/doebel_demo.c @@ -0,0 +1,86 @@ +/* SPDX-License-Identifier: MIT */ +/* Optional DOS example; the referenced musical data has its own notice. + * Run DOEBEL.EXE explicitly in foreground Tandy DOS. Escape cancels. + * No video switch, startup hook, bank changes or PC-speaker access. + */ +#include +#include +#include "lt_audio.h" +#include "lt_score.h" +#include "lt_tick_dos.h" +#include "doebel/DOEBEL.H" + +extern void lt_dos_emit(void *context, lt_u8 byte); +static lt_u16 psg_port = 0x00C0U; +static lt_chip chip; +static lta_handle audio; +static lt_score_player music; +static volatile unsigned finished, playback_error; + +/* Existing timer adapter, nominal 240 Hz (1,193,182 / 4,972 Hz). + * Keep the approved score's 240-Hz service contract and tempo unchanged. */ +#define DOEBEL_PIT_DIVISOR 4972U + +static void wait_for_interrupt(void); +#pragma aux wait_for_interrupt = "hlt" modify exact [] + +static void update(void *context) +{ + lta_status status; + (void)context; + if (finished) return; + if (music.phase + music.step >= (lta_u32)DOEBEL_END_TICK << 16) { + status = lt_score_stop(&music); + finished = 1U; + } else { + status = lt_score_tick(&music); + } + if (status != LTA_OK) { + playback_error = (unsigned)status; + finished = 1U; + } +} + +int main(void) +{ + lta_request request; + int timer_open = 0; + int failed = 0; + puts("Doebelinschmirtz Incorporated - optional PSG example"); + puts("One jingle; Escape cancels. Use exclusive foreground Tandy DOS."); + lt_init(&chip, lt_dos_emit, &psg_port); + if (lta_init(&audio, &chip, 1U) != LTA_OK) return 1; + lta_request_init(&request); + request.op = LTA_OP_ACQUIRE; + request.mask = 15U; + if (lta_command(&audio, &request) != LTA_OK) return 1; + if (lt_score_init(&music, &audio) != LTA_OK) { + failed = 1; + } else if (lttk_open(DOEBEL_PIT_DIVISOR, update, 0) != LTTK_OK) { + puts("DOEBEL: timer busy"); + failed = 1; + } else { + timer_open = 1; + if (lt_score_start(&music, &doebel_score, + DOEBEL_SERVICE_HZ, 0, 0) != LTA_OK) { + failed = 1; + } else { + lttk_run(1); + while (!finished) { + if (kbhit() && getch() == 27) break; + wait_for_interrupt(); + } + lttk_run(0); + } + if (lt_score_stop(&music) != LTA_OK) failed = 1; + } + /* Work is stopped before foreground audio cleanup. Release mutes all + * owned channels; close restores the BIOS timer using the same adapter. */ + lta_request_init(&request); + request.op = LTA_OP_RELEASE; + if (lta_command(&audio, &request) != LTA_OK) failed = 1; + if (timer_open && lttk_close() != LTTK_OK) failed = 1; + if (playback_error) failed = 1; + puts(failed ? "DOEBEL: playback/cleanup failed" : "DOEBEL: stopped, PSG muted"); + return failed; +} diff --git a/tests/doebel_score_core.c b/tests/doebel_score_core.c new file mode 100644 index 0000000..6a4f52b --- /dev/null +++ b/tests/doebel_score_core.c @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: MIT */ +/* Preserve the actual PSG writes/timing that generated Chad's approved WAV. + * The literal fixture is frozen from that audition, not regenerated from + * the candidate score. Musical data attribution: examples/doebel/README.md. + */ +#include +#include "lt_audio.h" +#include "lt_score.h" +#include "../examples/doebel/DOEBEL.H" +#include "../examples/doebel/APPROVED.H" + +static lt_chip chip; +static lta_handle audio; +static lt_score_player music; +static unsigned frame, index, failures; + +static void emit(void *context, lt_u8 byte) +{ + (void)context; + if (index >= DOEBEL_APPROVED_WRITES || + doebel_approved[index].frame != frame || + doebel_approved[index].byte != byte) { + ++failures; + } + ++index; +} + +int main(void) +{ + lta_request request; + unsigned voice; + lt_init(&chip, emit, 0); + if (lta_init(&audio, &chip, 1U) != LTA_OK) return 1; + lta_request_init(&request); + request.op = LTA_OP_ACQUIRE; + request.mask = 15U; + if (lta_command(&audio, &request) != LTA_OK) return 2; + if (lt_score_init(&music, &audio) != LTA_OK) return 3; + if (lt_score_start(&music, &doebel_score, + DOEBEL_SERVICE_HZ, 0, 0) != LTA_OK) return 4; + while (music.phase + music.step < (lta_u32)DOEBEL_END_TICK << 16) { + ++frame; + if (lt_score_tick(&music) != LTA_OK) return 5; + } + ++frame; + if (lt_score_stop(&music) != LTA_OK) return 6; + lta_request_init(&request); + request.op = LTA_OP_RELEASE; + if (lta_command(&audio, &request) != LTA_OK) return 7; + if (frame != 1023U || index != DOEBEL_APPROVED_WRITES) ++failures; + for (voice = 0U; voice < 4U; ++voice) + if (chip.attenuation[voice] != 15U || chip.owner[voice]) ++failures; + if (failures) { + printf("FAIL Doebel approved trace: %u differences\n", failures); + return 8; + } + puts("PASS Doebel approved PSG bytes, timing, levels and ending mute"); + return 0; +} diff --git a/tools/build.py b/tools/build.py index 6c820df..cb7182a 100644 --- a/tools/build.py +++ b/tools/build.py @@ -12,6 +12,7 @@ 'audio': ('audio_core', ['lt_chip', 'lt_audio']), 'sfx': ('sfx_core', ['lt_chip', 'lt_audio', 'lt_sfx']), 'score': ('score_core', ['lt_chip', 'lt_audio', 'lt_sfx', 'lt_score']), + 'dbscore': ('doebel_score_core', ['lt_chip', 'lt_audio', 'lt_score']), 'graphics': ('gfx_host', ['lt_gfx', 'lt_gfx_rows']), 'surface': ('surface_core', ['lt_surface']), 'time': ('time_core', ['lt_time']), @@ -21,6 +22,9 @@ } # DOS-only demos of the platform services: name -> sources. DOS_DEMOS = { + 'DOEBEL': ['examples/doebel_demo.c', 'src/lt_chip.c', + 'src/lt_audio.c', 'src/lt_score.c', 'adapters/lt_dos.c', + 'adapters/lt_tick_dos.c'], 'PAGEDEMO': ['examples/page_demo.c', 'src/lt_chip.c', 'src/lt_gfx.c', 'src/lt_event.c', 'adapters/lt_dos.c', 'adapters/lt_input_dos.c', 'adapters/lt_dos_app.c', @@ -83,6 +87,9 @@ def run(argv, label): raise RuntimeError(label + ': ' + result.stdout + result.stderr) return result + run([sys.executable, ROOT / 'examples/doebel/generate.py', '--check'], + 'doebel score data') + if args.target == 'dos': watcom = args.watcom.resolve() if os.name == 'nt':