Add buildable C engine, shaders and music source

This commit is contained in:
CRTkafa 2026-09-05 11:16:54 +07:00
parent 1d805b8896
commit 18721711c4
44 changed files with 23427 additions and 3 deletions

163
tools/test_music_form.py Normal file
View file

@ -0,0 +1,163 @@
"""Score/API regression checks; no build, rendering, or audio playback.
Run: python -m unittest discover -s tools -p test_music_form.py -v
"""
import re
import unittest
import tempfile
from unittest.mock import patch
from pathlib import Path
import arrange_v11 as A
import motif as M
import export_song as E
class MusicFormTests(unittest.TestCase):
def test_exporter_preserves_current_score_and_check_is_read_only(self):
with tempfile.TemporaryDirectory(prefix="crtk-score-test-") as tmp:
output = Path(tmp) / "score.h"
self.assertEqual(E.main(["--output", str(output)]), 0)
self.assertEqual(output.read_text(encoding="utf-8"), A.header())
with patch.object(Path, "write_text", side_effect=AssertionError("check wrote a file")):
self.assertEqual(E.main(["--check", "--output", str(output)]), 0)
self.assertEqual(E.main(["--check", "--output", str(Path(tmp)/"missing.h")]), 1)
output.write_text("obsolete v10 score", encoding="utf-8")
with patch.object(Path, "write_text", side_effect=AssertionError("check wrote a file")):
self.assertEqual(E.main(["--check", "--output", str(output)]), 1)
def test_exact_form_and_clock(self):
self.assertEqual([(n,a,b) for n,a,b,_ in A.SECTIONS], [
("INTRO",0,1), ("SYNTHWAVE",2,7), ("METAL VERSE",8,11),
("CHORUS",12,15), ("SOLO",16,39), ("ARRIVAL",40,45), ("OUTRO",46,47)])
self.assertEqual([o for _,a,b,_ in A.SECTIONS for o in range(a,b+1)], list(range(48)))
source = (A.HERE.parent / "synth.h").read_text()
for name,value in (("SPR",5000),("SRATE",44100),("TPR",6),("ROWS",32),("ORDERS",48)):
self.assertRegex(source, rf"#define\s+{name}\s+{value}\b")
self.assertEqual(sum((t+1)*5000//6 - t*5000//6 for t in range(6)),5000)
self.assertEqual(A.ORDERS*A.ROWS*A.SPR,7680000)
self.assertAlmostEqual(A.ORDERS*A.ROWS*A.SPR/A.SRATE,174.149659864,places=8)
self.assertEqual(A.sect[:2], [0,0])
self.assertEqual(A.sect[46:], [10,10])
self.assertTrue(all(s not in (0,7,10) for s in A.sect[2:46]))
def test_no_duplicate_musical_orders(self):
# Exclude drums, metadata, gain and risers: percussion or a new label
# must not disguise a repeated arrangement. Include voiced harmony.
signatures = {}
for o in range(48):
sig = tuple(tuple(lane[o]) for lane in
(A.lead,A.solo,A.harm,A.bass,A.gtr,A.hold,A.root,A.ctyp))
sig += (A.arpo[o], A.orgm[o], A.syn[o])
self.assertNotIn(sig,signatures, f"duplicate orders {signatures.get(sig)} and {o}")
signatures[sig] = o
for lane,orders in ((A.solo,range(16,40)),(A.solo,range(40,46)),(A.lead,range(12,16))):
phrases = [tuple(lane[o]) for o in orders]
self.assertEqual(len(phrases),len(set(phrases)))
def test_dense_runs_and_lyrical_contrast(self):
counts = [sum(n>=0 for n in A.solo[o]) for o in range(16,40)]
self.assertGreaterEqual(sum(n>=30 for n in counts),9)
self.assertGreaterEqual(len(set(counts)),10)
rhythms = {tuple(r for r,n in enumerate(A.solo[o]) if n>=0) for o in range(16,40)}
self.assertGreaterEqual(len(rhythms),14)
for o in (*range(25,31),36,37,38):
longest = current = 0
for n in A.solo[o]:
current = current+1 if n>=0 else 0
longest = max(longest,current)
self.assertGreaterEqual(longest,28)
self.assertLess(max(counts[:4]),min(counts[4:9]))
# Lyrical figures explicitly refresh selected notes; rests aren't ties.
for o in range(20,25):
pitches = [n for n in A.solo[o] if n>=0]
self.assertTrue(any(a==b for a,b in zip(pitches,pitches[1:])))
self.assertEqual(A.solo[39][16:28],[-1]*12)
self.assertTrue(all(n>=0 for n in A.solo[39][28:32]))
def test_v8_contour_preserved_without_register_clamp(self):
expected = [57,59,60,62,64,65,67,69,67,65,64,62,60,59,57,55,
57,60,64,69,72,69,64,60,57,59,60,64,69,67,64,57]
self.assertEqual(A.solo[25],expected)
self.assertEqual(max(A.solo[38]),81)
self.assertEqual(A.solo[40][0],83)
self.assertEqual(M.deg(0),57) # A4 is MIDI 69 minus 12.
def test_pitch_scale_and_bass_riff_consonance(self):
for o in range(48):
key = 9 + (2 if o>=40 else 0)
scale = {(key+n)%12 for n in M.MIN_IV}
for lane in (A.lead,A.solo,A.harm,A.bass,A.gtr):
self.assertEqual(len(lane[o]),32)
for n in lane[o]:
self.assertTrue(n==-1 or 0<=n<=127)
if n>=0: self.assertIn(n%12,scale)
for ci,rt in enumerate(A.root[o]):
chord = {(rt+n)%12 for n in (0,4 if A.ctyp[o][ci] else 3,7)}
self.assertTrue(chord <= scale)
for r in range(32):
rt = A.root[o][r//8]
if A.bass[o][r]>=0: self.assertEqual(A.bass[o][r]%12,rt)
if A.gtr[o][r]>=0:
self.assertIn((A.gtr[o][r]-rt)%12,(0,7))
self.assertIn(A.gtr[o][r]%12,scale)
self.assertIn((A.gtr[o][r]+7)%12,scale)
self.assertGreaterEqual(A.gtr[o][r],21)
for o,r in ((8,26),(9,27),(10,28),(11,16),(11,20)):
self.assertEqual(A.gtr[o][r]%12,4) # E power chord, not B + F#
def test_drum_codes_preserve_kicks(self):
kick_codes, snare_codes, crash_codes = {1,6,7}, {2,7}, {5,6}
crashes = []
for o in range(48):
self.assertEqual(len(A.drum[o]),32)
for r,code in enumerate(A.drum[o]):
self.assertIn(code,range(8))
if code in crash_codes:
crashes.append((o,r))
self.assertIn(code,kick_codes)
if code==7:
self.assertIn(o,range(2,8))
self.assertIn(r,(4,12,20,28))
if 2<=o<=7:
for r in range(0,32,4):
self.assertIn(A.drum[o][r],kick_codes)
for r in (4,12,20,28):
self.assertIn(A.drum[o][r],snare_codes)
self.assertEqual(crashes,[(o,0) for o in (2,8,12,20,25,36,40)])
def test_one_foreground_owner(self):
for o in range(48):
active = [any(n>=0 for n in lane[o]) for lane in (A.lead,A.solo,A.gtr)]
self.assertLessEqual(sum(active),1)
self.assertEqual(A.arpo[o],-1) # no extra chip arp over synth arp
if A.orgm[o]==2 and active[1]:
self.assertIn(o,range(31,36))
self.assertGreaterEqual(min(n for n in A.solo[o] if n>=0),69)
self.assertFalse(active[0] or active[2])
if A.syn[o]:
self.assertIn(o,range(2,8))
self.assertFalse(any(active))
self.assertEqual(A.orgm[o],0)
def test_generated_header_matches_runtime_api(self):
text = (A.HERE.parent / "song_data.h").read_text(encoding="utf-8")
self.assertEqual(text,A.header())
parsed = {}
for typ,name,body in re.findall(r"static const (signed char|unsigned char|short) (g_sq\w+)\[\] = \{(.*?)\};",text,re.S):
parsed[name] = (typ,list(map(int,re.findall(r"-?\d+",body))))
expected_names = {"g_sqLead","g_sqSolo","g_sqHarm","g_sqBass","g_sqGtr","g_sqHold",
"g_sqDrum","g_sqRoot","g_sqType","g_sqArpOct","g_sqOrgMode",
"g_sqRiser","g_sqGain","g_sqSyn","g_sqSect"}
self.assertEqual(set(parsed),expected_names)
source = (A.HERE.parent / "synth.h").read_text()
self.assertEqual(set(re.findall(r"\bg_sq\w+",source)),expected_names)
for name,(typ,values) in parsed.items():
self.assertEqual((typ,values),(A.TABLES[name][0],A.flatten(A.TABLES[name][1])))
expected_size = 1536 if name in {"g_sqLead","g_sqSolo","g_sqHarm","g_sqBass","g_sqGtr","g_sqHold","g_sqDrum"} else 192 if name in {"g_sqRoot","g_sqType"} else 48
self.assertEqual(len(values),expected_size)
lo,hi = (-128,127) if typ=="signed char" else (0,255) if typ=="unsigned char" else (-32768,32767)
self.assertTrue(all(lo<=v<=hi for v in values))
if __name__ == "__main__":
unittest.main()