From 206e1c62d9533db79d455a651ecad327d38f3536 Mon Sep 17 00:00:00 2001 From: Chad Doebelin Date: Sun, 25 Oct 2026 17:20:07 -0500 Subject: [PATCH] examples: add approved Doebel PSG score and optional DOS player --- .gitattributes | 4 + README.md | 6 ++ examples/doebel/APPROVED.H | 183 +++++++++++++++++++++++++++++++++ examples/doebel/APPROVED.json | 24 +++++ examples/doebel/DOEBEL.H | 50 +++++++++ examples/doebel/DOEBEL.VGM | Bin 0 -> 612 bytes examples/doebel/DOEBEL.json | 75 ++++++++++++++ examples/doebel/HOSTCHECK.json | 56 ++++++++++ examples/doebel/NATIVE.json | 54 ++++++++++ examples/doebel/NOTES.csv | 27 +++++ examples/doebel/README.md | 78 ++++++++++++++ examples/doebel/VALIDATION.md | 47 +++++++++ examples/doebel/generate.py | 71 +++++++++++++ examples/doebel_demo.c | 86 ++++++++++++++++ tests/doebel_score_core.c | 59 +++++++++++ tools/build.py | 7 ++ 16 files changed, 827 insertions(+) create mode 100644 examples/doebel/APPROVED.H create mode 100644 examples/doebel/APPROVED.json create mode 100644 examples/doebel/DOEBEL.H create mode 100644 examples/doebel/DOEBEL.VGM create mode 100644 examples/doebel/DOEBEL.json create mode 100644 examples/doebel/HOSTCHECK.json create mode 100644 examples/doebel/NATIVE.json create mode 100644 examples/doebel/NOTES.csv create mode 100644 examples/doebel/README.md create mode 100644 examples/doebel/VALIDATION.md create mode 100644 examples/doebel/generate.py create mode 100644 examples/doebel_demo.c create mode 100644 tests/doebel_score_core.c 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 0000000000000000000000000000000000000000..fd128a29b2fa3771233757346369f94c1b8b5196 GIT binary patch literal 612 zcmWGb&s9iZVqgehWMG&%&x`>EqBxmhOfb#KAOPp{z*#_j^8@w=+zGXm-W8nOpOOii2y(gtKfO$X}j4G;i2W^sTh z&@smYw2&QhPY$SUJ4l;93zCjzpjxPVLOFq^0}VlT572pd%=~5)um|EIu&MV!<^w|r z$%ufP4Y7(T@e= ((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':