Compare commits

438 Commits

Author SHA1 Message Date
minjaesong
d058f11329 adding missing mon2taud 2026-05-06 05:54:36 +09:00
minjaesong
60b07a325a xm2taud (wip), separate sustain and loop def 2026-05-06 05:31:55 +09:00
minjaesong
1e482e32a8 attempting to fix VM reboot bug(2) 2026-05-04 16:00:39 +09:00
minjaesong
4ff48bba1c attempting to fix VM reboot bug 2026-05-04 15:44:59 +09:00
minjaesong
2dcdff83c8 long overdue README update 2026-05-04 15:09:57 +09:00
minjaesong
89d3c5d776 taut.js: patern view simulator update 2026-05-04 02:52:01 +09:00
minjaesong
517d0ad9a7 taut.js: fxNames update 2026-05-04 02:14:19 +09:00
minjaesong
9524bf36e0 eff 8 (bitcrusher) and 9 (overdrive); *2taud.py rescales eff O on sample resampling 2026-05-04 02:04:29 +09:00
minjaesong
8e17256224 minor bugfix 2026-05-04 01:49:29 +09:00
minjaesong
ac409bf961 reverting changes :/ 2026-05-03 16:54:42 +09:00
minjaesong
94e3ce55ce zfm: larger scroll peek window 2026-05-03 16:50:49 +09:00
minjaesong
9a9893b9a3 bios: using js version again 2026-05-03 16:50:35 +09:00
minjaesong
789c78f1e7 taud: more fixes 2026-05-03 16:50:24 +09:00
minjaesong
c7e7ee650d fix: no-param note handling divergence 2026-05-03 15:10:36 +09:00
minjaesong
5d968fecf5 fix: ImpulseTracker style instrument filters 2026-05-03 00:44:31 +09:00
minjaesong
aaf3cc28b2 Offset added to Taud instrument format doc 2026-05-03 00:30:57 +09:00
minjaesong
24375727db taud amiga period bug fix (multi-tick Exx/Fxx) 2026-05-02 23:52:58 +09:00
minjaesong
6a7ef670d9 fix: tracker mixer flags not setup properly on fresh boot 2026-05-02 23:43:07 +09:00
minjaesong
1bbf0de381 instrument volume fadeout 2026-05-02 21:13:00 +09:00
minjaesong
5e6ac17146 song global volume and mixer volume 2026-05-02 19:28:11 +09:00
minjaesong
d2b1e792b9 taud: cue and pattern compression 2026-05-02 19:00:07 +09:00
minjaesong
219ca1e475 IT SusLoop 2026-05-02 14:26:57 +09:00
minjaesong
902ab00132 impl S6x and Wxx cmd 2026-05-02 14:15:34 +09:00
minjaesong
5dc87a80be fix: S Bx00 not working as indtended 2026-05-02 13:51:43 +09:00
minjaesong
2b91251d6e fix: random pitch changes; NNA note cut (not off!) for MOD and S3M 2026-05-02 03:25:57 +09:00
minjaesong
f84d317f95 NNA impl 2026-05-02 03:17:07 +09:00
minjaesong
f295223f15 IT instrument shenanigans 2026-05-02 02:48:24 +09:00
minjaesong
6de9476c4f fix: unmatched brackets :( 2026-05-02 02:23:38 +09:00
minjaesong
e317d79a21 S3M eff X; PT funk repeat 2026-05-02 02:22:20 +09:00
minjaesong
fe59df18f7 IT filters 2026-05-01 23:19:49 +09:00
minjaesong
a4adc428d0 taud: spec elaboration on filter cutoff and resonance 2026-05-01 18:07:13 +09:00
minjaesong
31e46b78ce notefx support for amiga freq mode 2026-05-01 17:54:17 +09:00
minjaesong
ac94a52329 it2taud to use new Taud instrument fields 2026-05-01 12:42:27 +09:00
minjaesong
01ff4b1d47 reflecting spec changes 2026-05-01 12:25:47 +09:00
minjaesong
50802186ce taud inst spec change 2026-05-01 07:42:08 +09:00
minjaesong
7184392521 2taud converters refactoring 2026-05-01 06:47:35 +09:00
minjaesong
018b9f5eb3 mod2taud.py 2026-05-01 06:34:03 +09:00
minjaesong
bb0810798d taut font update 2026-05-01 01:54:29 +09:00
minjaesong
909f970d60 it2taud: 12 vol/pan envelope nodes; experimental 'filter bake in'
Implemented in it2taud.py:
- _parse_it_pf_envelope_raw() (it2taud.py:677) — parses IT's third envelope at IMPI+0x1D4, keeping all 25 nodes (no decimation), distinguishing pitch vs filter mode via flag bit 7.
- _env_value_at() — tick-time linear interpolation honouring env-loop wrap.
- _clone_sample(), _plan_baked_length() — sample copy and entry + N×loop_len length planner (N up to 16).
- _bake_pitch_envelope() — time-varying linear-interpolated resampling, rate = 2^(env_v/12).
- _bake_filter_envelope() — RBJ 2-pole resonant LP biquad with time-varying coefficients; cutoff mapped 110 Hz (env_v=−32) → ~28 kHz (env_v=+32), Q from inst.ifr ∈ [0.5, 6.0].
- ITInstrument extended with pf_nodes, pf_flags, ifc, ifr. parse_instruments() reads IFC/IFR at IMPI+0x39/0x3A and pf envelope at IMPI+0x1D4.
- assemble_taud() use_instruments branch now substitutes baked copies in the proxy[] list (originals in samples[] stay intact).
- --no-pf-envelope CLI flag for A/B testing; module docstring updated.
2026-05-01 01:48:40 +09:00
minjaesong
80c26c6b35 taud: 12 envelope nodes; taut proj tab 2026-05-01 01:36:04 +09:00
minjaesong
515e0268e6 taut inst: global volume 2026-04-30 21:54:11 +09:00
minjaesong
606fa736af some graphics changes 2026-04-30 17:08:58 +09:00
minjaesong
89effb5b24 beat indicator, secondary row emph 2026-04-30 15:10:32 +09:00
minjaesong
376c3c4766 it2taud.py 2026-04-30 14:25:03 +09:00
minjaesong
0a247897e4 taud font better numbers 2026-04-29 21:30:57 +09:00
minjaesong
b838b35525 taud: amiga mode pitchbend 2026-04-29 20:07:25 +09:00
minjaesong
1148454fb3 graphics: colour 0 is default to half-transparent black 2026-04-29 19:20:15 +09:00
minjaesong
cfb7b97bf0 taut: popup back col 2026-04-29 15:57:41 +09:00
minjaesong
176aa824fe taut: comp for pat and cue 2026-04-29 15:50:52 +09:00
minjaesong
d33484c3c8 perkele 2026-04-29 15:49:57 +09:00
minjaesong
737b1cebe7 did it work? 2026-04-29 15:41:47 +09:00
minjaesong
176766b793 did it fix it? 2026-04-29 14:29:18 +09:00
minjaesong
191c733913 taut: better top bar(2) 2026-04-29 14:09:37 +09:00
minjaesong
895f1b27ef taut: better top bar 2026-04-29 13:19:22 +09:00
minjaesong
538d718568 taut: top bar 2026-04-29 12:41:20 +09:00
minjaesong
b3c5719e3a it2taud sample signedness fix 2026-04-29 12:27:52 +09:00
minjaesong
27e4bc1ae5 2taud update 2026-04-29 11:27:29 +09:00
minjaesong
2282e0c10b it2taud, instrument format changes 2026-04-29 09:21:28 +09:00
minjaesong
e7287fae37 taut: graphical buttons 2026-04-29 03:53:40 +09:00
minjaesong
65d89db9c6 minor colour change(2) 2026-04-28 09:16:48 +09:00
minjaesong
53173a359c minor colour change 2026-04-28 09:14:02 +09:00
minjaesong
8d7d534bc8 taud logo; command.js supporting .alias file 2026-04-28 01:20:48 +09:00
minjaesong
dc3c73252e taut: colour scheme change 2026-04-27 23:44:51 +09:00
minjaesong
2053526dfa taut: minor bugfixes 2026-04-27 18:19:58 +09:00
minjaesong
6bc49e3f0b fix: taut voice mute status persisting on other tabs 2026-04-27 13:29:19 +09:00
minjaesong
02c4f9590c taut: popups 2026-04-27 07:31:47 +09:00
minjaesong
34afa95137 more simulateRowState fix 2026-04-27 03:06:53 +09:00
minjaesong
284108f07f more ui changes 2026-04-27 02:58:12 +09:00
minjaesong
76011d4fa9 taut: better channel sim; s3m converter S8x->PanEff 2026-04-27 02:25:23 +09:00
minjaesong
b44d9c6b68 taut: fancier UI 2026-04-26 23:36:29 +09:00
minjaesong
e47e9e1259 taut: transport control 2026-04-26 22:36:39 +09:00
minjaesong
c5789ec28b taud: panning law toggle 2026-04-26 20:08:02 +09:00
minjaesong
93f7f436a3 taud note eff more PT conv table 2026-04-26 19:24:01 +09:00
minjaesong
3ca31e57a1 taut: fully column navigatable 2026-04-26 14:14:40 +09:00
minjaesong
1f630aee62 taut: more ui improvements 2026-04-26 13:31:54 +09:00
minjaesong
3f3644d165 taut: minor ui change 2026-04-26 02:19:24 +09:00
minjaesong
e29f9c3032 taut: patterns tab 2026-04-26 02:05:10 +09:00
minjaesong
85b8586a3a taut: panelised view 2026-04-25 15:29:02 +09:00
minjaesong
92b9984ef8 taut: fx colour change 2026-04-25 15:03:56 +09:00
minjaesong
3f98d25828 taut control and font changes 2026-04-25 11:33:43 +09:00
minjaesong
d4ea9b2d29 more correct pitch slide conversion rule 2026-04-24 20:17:20 +09:00
minjaesong
a1b62f3155 taud-related changes (docs, converter supports Y eff) 2026-04-24 18:35:24 +09:00
minjaesong
4802e10dfc better tracker font, IT eff for Taud 2026-04-24 12:18:34 +09:00
minjaesong
6d70960e5c Panbrello is 'Y' not 'W', oops 2026-04-24 09:21:10 +09:00
minjaesong
3d99568359 taud: implemented eff W (panbrello) 2026-04-24 09:15:24 +09:00
minjaesong
1fe966ca09 more taut font change 2026-04-24 02:42:09 +09:00
minjaesong
037b2c1a16 taut font change 2026-04-24 02:33:59 +09:00
minjaesong
ea09065802 more taut gui 2026-04-24 02:17:41 +09:00
minjaesong
8d28bde119 taud base note def changed (A3@440Hz) 2026-04-23 23:11:09 +09:00
minjaesong
755afb7df4 reatable fx names; horz scroll fix 2026-04-23 22:23:15 +09:00
minjaesong
539df453ec taut: fix voleff 'v1' rendering as 'v.1' 2026-04-23 21:51:47 +09:00
minjaesong
5e3ffea6d3 taut: better scrolling behav(2) 2026-04-23 21:18:30 +09:00
minjaesong
4bda55d511 taut: better scrolling behav 2026-04-23 21:12:11 +09:00
minjaesong
852c0e6e80 taut: displaying note symbol 2026-04-23 21:03:20 +09:00
minjaesong
25309cf5b6 format revision and tracker GUI 2026-04-23 20:48:40 +09:00
minjaesong
44f11120d8 tightening formats 2026-04-23 14:47:53 +09:00
minjaesong
bc16ffabb4 minor colour change 2026-04-23 14:04:49 +09:00
minjaesong
ad5e5b62bc more tracker gui 2026-04-23 13:59:48 +09:00
minjaesong
887c2fbfba s3m to taud fix (not emitting volcmd on note retrigger) 2026-04-23 13:35:38 +09:00
minjaesong
e58eb2c12b more tracker stuff 2026-04-23 12:43:56 +09:00
minjaesong
3a91edb379 playback ctrl 2026-04-23 09:59:48 +09:00
minjaesong
74d94b350c taut: always scroll to centre 2026-04-23 01:32:44 +09:00
minjaesong
4559e4f3f6 better tauty 2026-04-23 00:16:32 +09:00
minjaesong
c5bc0d6526 tauty 2026-04-22 23:03:33 +09:00
minjaesong
a92727862e taut: detailed custom notation def 2026-04-22 17:38:47 +09:00
minjaesong
31d25d940b taut: note symbols(2) 2026-04-22 17:23:28 +09:00
minjaesong
460046c4ed taut: note symbols 2026-04-22 16:01:57 +09:00
minjaesong
6eeccb4baa manually entering syms 2026-04-21 23:18:21 +09:00
minjaesong
a7c44bd05f taut font update 2026-04-21 13:28:13 +09:00
minjaesong
4e647f9fe1 tracker wip 2026-04-20 17:38:02 +09:00
minjaesong
b2faab9377 s3m converter: not emitting skip order (0xFE) 2026-04-20 03:20:55 +09:00
minjaesong
e833d75b2c tracker engine bugfixes 2026-04-20 03:10:08 +09:00
minjaesong
f84ea5e68a tracker effects definition 2026-04-20 01:35:23 +09:00
minjaesong
5374ca43c3 proper tracker effects documentation 2026-04-19 19:18:52 +09:00
minjaesong
bef85f6e2f tracker impl, s3m converter, larger tracker sample bin 2026-04-19 02:52:12 +09:00
minjaesong
f02ad1de79 minor spec change for Taud 2026-04-17 17:28:03 +09:00
minjaesong
7f0ff3e653 Taud tracker: explicit nop and key-off behaviour 2026-04-17 16:42:17 +09:00
minjaesong
8702104bfe tracker impl 2026-04-17 12:03:43 +09:00
minjaesong
7d899936e2 audio changes 2026-04-16 21:58:06 +09:00
minjaesong
6aa2542bb8 audio device changes 2026-04-16 15:04:44 +09:00
minjaesong
2ac084acd7 psg.mjs 2026-04-16 02:05:21 +09:00
minjaesong
1208690c4f graal update with graal compiler 2026-04-13 22:20:50 +09:00
minjaesong
ca977b074d vm update 2026-04-10 20:36:55 +09:00
minjaesong
102801d8b0 tav: vendor string update 2026-01-21 22:00:47 +09:00
minjaesong
10351bafb1 tav fix: fractional framerate breaking audio encoding 2026-01-21 21:41:03 +09:00
minjaesong
9310885260 tav fix: webm being recognised as still image 2026-01-21 21:21:11 +09:00
minjaesong
b10d5d3a34 tav: vendor string update 2025-12-30 09:36:19 +09:00
minjaesong
86b44565e0 tav: revived adaptive gop alloc 2025-12-30 09:27:14 +09:00
minjaesong
54b61fb436 tav: support for fractional framerate 2025-12-25 18:26:56 +09:00
minjaesong
4afe3816c7 tav: extended header XFPS 2025-12-25 13:26:38 +09:00
minjaesong
f09dd66185 playtav: fixed using wrong flag 2025-12-25 11:20:21 +09:00
minjaesong
b590415231 playtav: still picture playback 2025-12-25 11:13:34 +09:00
minjaesong
237d3d6fd2 playtav: playing next file must not work with still images 2025-12-25 03:21:07 +09:00
minjaesong
3421d71012 TAV: still picture impl 2025-12-23 04:00:53 +09:00
minjaesong
5d5576c077 fix: mmio-based readKey() having delayed event because of race condition 2025-12-20 11:17:50 +09:00
minjaesong
64026be133 disabling broken code until fixed 2025-12-19 23:18:16 +09:00
minjaesong
96d697e158 iPF progressive mode decoder 2025-12-19 21:41:51 +09:00
minjaesong
1680137b7d external iPF encoder 2025-12-19 21:36:48 +09:00
minjaesong
c71920b95d TAV fix 13/7 wavelet not decoding correctly 2025-12-18 20:09:06 +09:00
minjaesong
629ed5fb12 tsvm "compiler" update 2025-12-18 10:29:46 +09:00
minjaesong
4f6efbe000 Update terranmon.txt 2025-12-18 10:28:56 +09:00
minjaesong
4362610c70 rm readme 2025-12-18 10:07:08 +09:00
minjaesong
ed63af903b TAV-DT: belief propagation LDPC decoder 2025-12-17 21:25:27 +09:00
minjaesong
c742bc354a TAV-DT: slight format change to allow detailed failure mode inspection 2025-12-17 10:02:17 +09:00
minjaesong
e893ca2df5 TAV-DT: more format revision 2025-12-17 03:49:07 +09:00
minjaesong
ca037c8e74 TAV: encoder vendor string update 2025-12-17 02:09:31 +09:00
minjaesong
1144338059 TAV: different filter for FPS up/downconversion 2025-12-17 02:06:36 +09:00
minjaesong
71dff4b0e0 TAV: size option working same as old encoder 2025-12-17 01:49:07 +09:00
minjaesong
9e1191c0c2 TAV: dead zone defaults 2025-12-16 19:16:47 +09:00
minjaesong
3a19b6aea8 TAV: output FPS conversion 2025-12-16 18:05:12 +09:00
minjaesong
67413f2749 TAV-DT: no Zstd 2025-12-16 10:54:03 +09:00
minjaesong
4929d84cec TAV-DT format revision and soft sync recovery 2025-12-15 17:42:08 +09:00
minjaesong
3d76006ad9 TAV-DT LDPC test 2025-12-15 17:14:52 +09:00
minjaesong
506fcbe79d TAV-DT syncing on damaged stream wip 2025-12-15 02:00:01 +09:00
minjaesong
42341b4e10 TAV-DT multithreaded decoding 2025-12-12 16:26:30 +09:00
minjaesong
b9d9d221dd TAV-DT multithreaded encoding 2025-12-12 16:01:22 +09:00
minjaesong
01a89f3b36 Working TAV-DT encoder/decoder 2025-12-12 08:39:40 +09:00
minjaesong
300f88a44c tav-dt format change 2025-12-11 18:25:25 +09:00
minjaesong
2d6167393a fixing issue where graphics mode 5 failing to display anything 2025-12-09 22:34:56 +09:00
minjaesong
f1d1e36164 tavenc: fixed malformed intra frame packets 2025-12-09 17:36:09 +09:00
minjaesong
50092aef60 tavdec: multithreaded audio decode 2025-12-09 10:44:32 +09:00
minjaesong
017aef26ab tavenc/dec: interlaced mode 2025-12-09 08:40:42 +09:00
minjaesong
efdb915208 tavenc: header support for extended dimensions (max(width,height) > 65535) 2025-12-09 04:59:40 +09:00
minjaesong
5d99191b5e tavenc: proper sync packet after i-frames 2025-12-09 04:44:40 +09:00
minjaesong
621c312922 tavenc: extended header, automatic multithreading, tad autoselect fix 2025-12-09 03:44:35 +09:00
minjaesong
c6c50c2ebe tavenc: multithreaded decoding 2025-12-08 16:07:20 +09:00
minjaesong
34a1f0e3db tavenc: multithreaded encoding 2025-12-08 11:06:03 +09:00
minjaesong
9b72a62cdb tavenc: shelving the tiling mode due to poor performance 2025-12-07 23:42:20 +09:00
minjaesong
9e2c9e6efd tavenc: tiling on uniform 2025-12-07 23:27:07 +09:00
minjaesong
0907e22f53 tavenc: tiling 2025-12-07 15:27:32 +09:00
minjaesong
3828bd7fbc reference tav decoder 2025-12-07 04:58:36 +09:00
minjaesong
189646a8dc libtavenc: fixed gop size 2025-12-07 04:36:19 +09:00
minjaesong
7f951366da tavlib: quant fix 2 2025-12-07 02:51:17 +09:00
minjaesong
a2233aedaf tavlib: quant fix 2025-12-06 08:58:21 +09:00
minjaesong
dad1da741f tav: librarying 2025-12-05 09:21:47 +09:00
minjaesong
94ae24e9e4 tav: librarying 2025-12-05 03:39:32 +09:00
minjaesong
d3cc05789f tav: more dt decoder fix 2025-12-02 20:20:59 +09:00
minjaesong
bc5779d4f5 tav: temporal CDF 5/3 is causing coeff overflow on -q 5, replacing with Haar+sports mode 2025-12-02 16:39:30 +09:00
minjaesong
046fa98025 working TAV-DT decoder 2025-12-02 14:37:36 +09:00
minjaesong
196b9a0c01 TAV-DT encoding/scene detection const sweak 2025-12-02 04:49:21 +09:00
minjaesong
7848357cf2 TAV-DT doc 2025-12-02 00:36:46 +09:00
minjaesong
a3f8be9773 additional noise on post shader 2025-12-01 21:23:56 +09:00
minjaesong
8adf44e5ae more TAV doc 2025-12-01 18:43:37 +09:00
minjaesong
602b42c6ff JS impl tav_inspector 2025-12-01 14:58:32 +09:00
minjaesong
79eb81e8d0 tav: D1 res is 720x480 2025-12-01 01:52:39 +09:00
minjaesong
c522244574 tav: fix: incorrect brightness jumping on MT mode 2025-11-30 23:46:59 +09:00
minjaesong
28b391cfd4 tav: multithread mode progress indicator 2025-11-30 18:19:06 +09:00
minjaesong
26d216ca13 tav: lean memory usage 2025-11-30 14:23:00 +09:00
minjaesong
f598daec1e tav: mt by default 2025-11-30 13:59:39 +09:00
minjaesong
3f8cf6a38c tav: lean memory usage 2025-11-30 13:41:41 +09:00
minjaesong
902d971ae7 tav: first working multithreaded encoder 2025-11-30 13:24:24 +09:00
minjaesong
5ecf2dcadd tav: multithread wip 2025-11-30 09:58:21 +09:00
minjaesong
9edeca929d tav: encoder ENDT fix 2025-11-29 01:58:54 +09:00
minjaesong
3b401139e9 more experiments for avx512 2025-11-26 02:36:49 +09:00
minjaesong
acaade1062 better CRT/Composite shader 2025-11-26 01:03:40 +09:00
minjaesong
e3099195e4 gpu: 5bpp mode 2025-11-25 09:53:53 +09:00
minjaesong
08bb33bf27 TAV: preset implementation 2025-11-24 20:01:08 +09:00
minjaesong
6132012e74 TAV: code cleanup, better preset selection 2025-11-24 11:16:19 +09:00
minjaesong
dbbb471a11 TAV: decomp level 4 for default res is optimal (fast enc/dec, minimal filesize change) 2025-11-24 03:34:08 +09:00
minjaesong
53b87ff735 TAV: avx512 intrinsic 2025-11-24 03:14:29 +09:00
minjaesong
d49ec39b73 TAV: interlaced 3d dwt decoding 2025-11-24 01:04:58 +09:00
minjaesong
dd60b2c569 TAV update: D1 and D1PAL as an acceptable resolution keyword 2025-11-23 23:22:14 +09:00
minjaesong
1c7ab17b1c TAV update: CDF 5/3 for motion coder 2025-11-23 18:16:12 +09:00
minjaesong
e928d2d3ec TAV fix: '--intra-only --interlaced' would cause crash due to buffer size mismatch 2025-11-22 21:11:54 +09:00
minjaesong
5129e354bb TAV: new resolution keyword: original" 2025-11-22 02:25:17 +09:00
minjaesong
be1b92f188 TAV: fixed memory bugs on intra-only coding 2025-11-21 21:58:07 +09:00
minjaesong
2533b2dc19 TAV: videotex mode; TSVM documentation fix 2025-11-21 16:53:35 +09:00
minjaesong
a61a21d28b TAV: lax scene detection algo 2025-11-20 02:42:11 +09:00
minjaesong
92274a8e19 TAV: letterbox detection encoding complete 2025-11-20 02:36:35 +09:00
minjaesong
44cc54264a TAV: letterbox detection 2 2025-11-17 11:47:36 +09:00
minjaesong
8199cbc955 TAV: letterbox detection 2025-11-17 04:26:52 +09:00
minjaesong
aa7e20695d fix: wrong timecode calculation on NTSC framerates 2025-11-16 02:49:03 +09:00
minjaesong
5c87325366 fix: wrong subtitle timecode on certain SRT files 2025-11-15 19:54:53 +09:00
minjaesong
64e100e532 fix: audio and subtitles don't sync up
note: it seems encoder outputs malformed subtitle on Tom Scott video
2025-11-15 14:16:12 +09:00
minjaesong
233f1e7dcd more authentic CRT shader 2025-11-14 17:54:20 +09:00
minjaesong
19f813eb7d TAV: dead code removal 2025-11-13 11:10:39 +09:00
minjaesong
a45a919c84 zfm: size string for gigabytes 2025-11-13 10:35:45 +09:00
minjaesong
9247471bf2 more TAV/TAD documentation update 2025-11-11 21:15:27 +09:00
minjaesong
6add391d07 TAV encoder: vendor string update 2025-11-11 14:54:28 +09:00
minjaesong
b19afdae3a TAV decoder: no need for dequant rounding now 2025-11-11 14:08:35 +09:00
minjaesong
bd530f803f fix: TAV C decoder outputting wrong brightness 2025-11-11 13:28:11 +09:00
minjaesong
901f6b52b4 fix: TAV decomposition level errenously calculating on monoblock when it shouldn't 2025-11-11 10:00:29 +09:00
minjaesong
bff5021a7a fix: EZBC for TAV producing dark bloches on white background due to coeff clipping 2025-11-11 03:22:10 +09:00
minjaesong
9425c58e53 TAV/TAD fix wip 2025-11-11 00:17:51 +09:00
minjaesong
c1d6a959f5 TAV/TAD doc update 2025-11-10 17:01:44 +09:00
minjaesong
edb951fb1a removing obsolete makefile directives 2025-11-10 10:52:25 +09:00
minjaesong
0f5875d45b fix: TAD for TSVM producing distorted audio 2025-11-10 10:43:49 +09:00
minjaesong
0e6f2162c8 TAV and TAD now shares same code for encoding and decoding 2025-11-10 10:35:17 +09:00
minjaesong
28e9a88f8d TAD: EZBC with fixes 2025-11-10 01:45:33 +09:00
minjaesong
3f97f1a59e TAD: embedded zero tree coding (basically 1D EZBC) 2025-11-09 13:34:28 +09:00
minjaesong
c0d1d54bed TAD: more tuning 2025-11-08 02:04:11 +09:00
minjaesong
aa9ecee7ca TAD: pre/de-emphasis 2025-11-07 23:13:08 +09:00
minjaesong
8878d37e5b TAD: pre/de-emphasis 2025-11-07 15:16:35 +09:00
minjaesong
e743fbf3c0 TAD: more tuning 2025-11-07 10:51:01 +09:00
minjaesong
d9d395c62c TAD: psychoacoustically optimised quantisation 2025-11-07 03:40:56 +09:00
minjaesong
00c882aa8d TEV/TAV: SSF-TC impl 2025-11-06 01:18:19 +09:00
minjaesong
af3679921d TAV decoder: now with working audio 2025-11-04 18:22:41 +09:00
minjaesong
332e8760ad TAV decoder: GOP decoding, GOP grain synthesis 2025-11-04 12:00:50 +09:00
minjaesong
c85b007ba9 TAV decoder fix: limited RGB range 2025-11-04 02:10:32 +09:00
minjaesong
61b0bdaed7 doc update 2025-11-04 00:43:14 +09:00
minjaesong
9d98cc1a21 TAV decoder: rewrote to output to file, currently only does I-frames which is NOT a regression from the old code 🤷 2025-11-03 22:49:44 +09:00
minjaesong
76c42f20b3 TAV fix: odd number base quantiser causing luminance flicker on every first GOP frames 2025-11-03 03:11:12 +09:00
minjaesong
e871264ae5 TAV: wip 2025-11-03 02:36:12 +09:00
minjaesong
f3b68e1164 TAV: fixed video luminance errors on -q 4 and 5 2025-10-31 04:41:48 +09:00
minjaesong
755d4deb95 TAV: more various fixes and confirming temporal level 3 is unsuitable 2025-10-31 01:40:02 +09:00
minjaesong
46ad919407 TAD documentation update 2025-10-30 22:13:29 +09:00
minjaesong
c61bf7750f TAV: fix - TAD audio incorrectly decoding due to incorrect step size reconstruction 2025-10-30 22:02:57 +09:00
minjaesong
991d035bcc TAD: working kotlin decoder 2025-10-30 17:08:42 +09:00
minjaesong
480d2d8538 TAV: TAD integration wip 2025-10-30 00:34:15 +09:00
minjaesong
4a6edeca09 TAD: imma just finalise it here 2025-10-29 23:59:08 +09:00
minjaesong
692defdbb8 TAD: more slight dithering 2025-10-29 03:51:49 +09:00
minjaesong
ee2ddef1c1 TAD: coefficient dithering on decoder 2025-10-29 03:19:36 +09:00
minjaesong
999e1deda0 TAD: coefficient dithering 2025-10-29 02:52:09 +09:00
minjaesong
a67d8b5f08 TAD: auto filename selection 2025-10-29 02:11:04 +09:00
minjaesong
f06f339d99 TAD: bringing coeff weight back 2025-10-29 01:47:14 +09:00
minjaesong
86864c4b7a TAD: somehow removing entropy coding yields better compression? 2025-10-28 04:19:11 +09:00
minjaesong
86de627734 TAD: back to twobitmap 2025-10-28 04:04:41 +09:00
minjaesong
c6de68291d TAD: quantised value stats 2025-10-28 03:15:15 +09:00
minjaesong
b3a91bf6cb makefile: zstd probing for macOS 2025-10-27 09:42:06 +09:00
minjaesong
1d0f369827 TAD: arbitrary steps with bitplanes 2025-10-27 09:14:52 +09:00
minjaesong
9c27d114fc TAD: even the slight companding vastly improves low-volume samples but also increases encoded size by a lot 2025-10-27 01:22:36 +09:00
minjaesong
67f7c091eb TAD: better compression using bitmap and delta prediction 2025-10-26 20:30:28 +09:00
minjaesong
370d511f44 TAD: better bit allocation using statistics 2025-10-26 18:16:28 +09:00
minjaesong
9fcb7fc95c TAD: more wip 2025-10-26 02:49:39 +09:00
minjaesong
52f25f7d04 TAV: two-pass GOP slicer 2025-10-25 00:01:37 +09:00
minjaesong
69583e5f1e TAV: frame statistics for 3D-DWT mode 2025-10-24 18:38:50 +09:00
minjaesong
56a1bac19a TAV: video with TAD audio playback 2025-10-24 18:10:53 +09:00
minjaesong
3adc50365b TAV: TAD encoding 2025-10-24 17:05:16 +09:00
minjaesong
cd88885fbf TAD: kotlin side update 2025-10-24 09:19:55 +09:00
minjaesong
9dc71095a0 TAD: now processing entirely in float 2025-10-24 09:12:28 +09:00
minjaesong
a9319fd812 TAD: Terrarum Advanced Audio to use with video compression 2025-10-24 02:06:52 +09:00
minjaesong
6f669f4fd9 TAV: hopefully more steady playback 2025-10-23 16:01:23 +09:00
minjaesong
e147072d03 removing obsolete files 2025-10-23 10:09:11 +09:00
minjaesong
53da0bfcee TAV: fix: iframes not decoding 2025-10-23 09:26:58 +09:00
minjaesong
34427d61d7 TAV: minimal size for GOP 2025-10-23 01:05:46 +09:00
minjaesong
7f7222fe54 TAV: fix initial GOP skipping 2025-10-22 19:31:10 +09:00
minjaesong
4265891093 TAV: pcm8 audio 2025-10-22 10:43:47 +09:00
minjaesong
758b134abd TAV: experimental separate audio format mode 2025-10-22 09:33:15 +09:00
minjaesong
4eec98cdca TAV: half-fixed 3d dwt playback 2025-10-22 01:32:19 +09:00
minjaesong
9ac0424be3 TAV: double buffered playback 2025-10-21 16:17:00 +09:00
minjaesong
f0ad0ef034 TAV: EZBC on the header 2025-10-20 18:53:40 +09:00
minjaesong
9553b281af TAV: EZBC entropy coding 2025-10-20 18:36:20 +09:00
minjaesong
019f0aaed5 TAV: trying mpeg-style mocomp 2025-10-19 17:56:06 +09:00
minjaesong
120058be6d TAV: some more mocomp shit 2025-10-18 05:47:17 +09:00
minjaesong
3b9e02b17f TAV: will replace frame aligning with something else, or maybe with nothing 2025-10-17 06:48:21 +09:00
minjaesong
93622fc8ca TAV: 3D DWT makes coherent picture at least 2025-10-17 02:01:08 +09:00
minjaesong
0cf1173dd6 TAV: iS tHiS aN iMpRoVeMeNt¿ 2025-10-16 09:37:20 +09:00
minjaesong
cc2f3e4d57 tav_inspector update 2025-10-16 04:39:47 +09:00
minjaesong
e179a15f33 TAV: more experiments 2025-10-16 02:35:53 +09:00
minjaesong
e19af854dc TAV: Haar delta decoding 2025-10-16 01:39:51 +09:00
minjaesong
ea72dec996 TAV: still bugfixing 2025-10-16 00:03:58 +09:00
minjaesong
7e248bc83d TAV: experimental 3D DWT encoder 2025-10-15 16:04:27 +09:00
minjaesong
b40b2ff0a1 TAV: much better delta coding 2025-10-15 00:20:46 +09:00
minjaesong
5dcf2177d5 TAV: no delta coding by default (but allows skip coding) 2025-10-14 14:16:05 +09:00
minjaesong
871d7bcdfe TAV: packet inspector 2025-10-14 01:25:16 +09:00
minjaesong
4c48d761b9 TAV: extended header spec 2025-10-14 00:36:39 +09:00
minjaesong
94749a3ad6 TAV: timecode packets 2025-10-14 00:27:26 +09:00
minjaesong
e705d274de TAV decoding minor changes 2025-10-13 01:42:44 +09:00
minjaesong
222b9866a8 fix: RGB upload function broken for non-native size 2025-10-10 00:11:51 +09:00
minjaesong
0b7b8cdd35 fix: incorrect MMIO addressing of FB3/4 2025-10-09 20:55:14 +09:00
minjaesong
31457974be more refactoring 2025-10-09 16:50:49 +09:00
minjaesong
0284b37678 graphics mode 5, faster RGB upload 2025-10-09 16:44:41 +09:00
minjaesong
912e35a122 graphics mode 5 2025-10-09 16:01:26 +09:00
minjaesong
78a7cdc08f minor fix 2025-10-09 01:51:03 +09:00
minjaesong
1a072f6a0c tav: grain synthesis on the spec 2025-10-09 00:01:53 +09:00
minjaesong
17b5063ef0 tav player: seek and pause 2025-10-08 21:25:33 +09:00
minjaesong
9826efd98a tav: int maths on still frame detection 2025-10-08 16:41:42 +09:00
minjaesong
f980f23efe tav: just more stat numbers 2025-10-08 16:17:14 +09:00
minjaesong
67445b040c tav: skip frame coding 2025-10-08 11:09:06 +09:00
minjaesong
d08511a39d film grain effect to alleviate 3d scene with low-res texture look 2025-10-08 02:07:29 +09:00
minjaesong
f918cd429c REWIND command for serial device 2025-10-07 23:43:24 +09:00
minjaesong
769b6481da HSDPA supporting file larger than 2GB 2025-10-07 23:33:29 +09:00
minjaesong
00e390d879 playtav: cue navigation using arrow keys 2025-10-07 21:41:37 +09:00
minjaesong
7474a9d472 proper UCF writer 2025-10-07 21:07:10 +09:00
minjaesong
abce002cdd testdiskdrive now uses input stream which enables I/O on files larger than 2GB 2025-10-07 19:54:43 +09:00
minjaesong
e36d4041ce TAV: subtitle font handling 2025-10-07 18:14:07 +09:00
minjaesong
da084c0074 TAV: interlaced mode 2025-10-07 17:51:47 +09:00
minjaesong
3c9441e67f TAV: less aggressive deadzonning 2025-10-07 15:27:55 +09:00
minjaesong
581b270c31 TAV: interlacing for backwards compat 2025-10-07 14:42:34 +09:00
minjaesong
fd62df99a4 TAV: yet another preset change 2025-10-07 04:36:57 +09:00
minjaesong
ad232d1c84 TAV: twobitmap for better compression 2025-10-07 03:55:56 +09:00
minjaesong
0b066a693e visualiser update 2025-10-07 02:55:53 +09:00
minjaesong
cdec0fe020 TAV: even more preset changes 2025-10-07 02:27:15 +09:00
minjaesong
21e3fe4c1e TAV: default Zstd level to 9 for faster encoding 2025-10-06 20:47:53 +09:00
minjaesong
364e33eede better hangul print 2025-10-06 20:43:45 +09:00
minjaesong
5d4e775ad0 TAV: default quality and zstd level change 2025-10-06 20:43:35 +09:00
minjaesong
f7d98e74e3 quantisation deadzonning (massive compression gain) 2025-10-05 23:35:09 +09:00
minjaesong
d6019019dc filter preset change 2025-10-05 19:17:20 +09:00
minjaesong
6c83c925cb terranmon.txt update 2025-10-05 18:53:31 +09:00
minjaesong
5c3a3a112c new: unicode.visualStrlen 2025-10-04 22:39:17 +09:00
minjaesong
71102f1d70 fixing hangul print functions 2025-10-04 01:10:14 +09:00
minjaesong
6222e9d8bd revived unicode print function 2025-10-03 23:28:23 +09:00
minjaesong
60e0ff9e61 fix: font rom not uploading 2025-10-03 22:42:42 +09:00
minjaesong
7d61074a13 Faster ICtCp colour conversion using extensive LUT 2025-10-03 14:50:52 +09:00
minjaesong
5f14169e6b encoder preset change (more quality) 2025-10-03 14:41:23 +09:00
minjaesong
29b3da1dbc ICtCp float math 2025-10-03 02:00:14 +09:00
minjaesong
27ad3361ea UCF reading and writing 2025-10-02 23:49:57 +09:00
minjaesong
d4fae0071b tav: ictcp decoding fix 2025-10-02 20:20:11 +09:00
minjaesong
0666734c9d tav: --dump-frame 2025-10-01 22:54:12 +09:00
minjaesong
f1cad6d9fa tav: coefficient dump visualiser 2025-10-01 22:48:57 +09:00
minjaesong
027d3289ca media player gui wip 2025-10-01 20:25:10 +09:00
minjaesong
b9f38dfa08 TAV: revised psychovisual model 2025-10-01 12:21:04 +09:00
minjaesong
3e40b048a7 TAV: allowing multi-title if video is larger than default size 2025-10-01 09:32:34 +09:00
minjaesong
70dfc7bf13 TAV: better initial q-index prediction for target bitrate mode 2025-10-01 01:53:25 +09:00
minjaesong
8c7550e581 TAV: removed quantiser halving for non-CDF9/7 filters 2025-10-01 01:31:54 +09:00
minjaesong
ff6821eb55 better preservation of high frequency diagonals 2025-10-01 01:07:05 +09:00
minjaesong
4e219d1a71 much better bitrate control 2025-09-30 17:53:46 +09:00
minjaesong
8688fc3742 better bitrate control 2025-09-30 17:05:15 +09:00
minjaesong
0f784eb741 bitrate mode wip 2025-09-30 16:06:58 +09:00
minjaesong
7f22ec8cc7 quality preset change 2025-09-30 03:19:58 +09:00
minjaesong
b457be4bbf minor fix 2025-09-30 01:45:15 +09:00
minjaesong
41a8b578b5 Apparently you can push the chroma extremely far 2025-09-30 01:05:14 +09:00
minjaesong
836e69a40b CoCg-only channel handling 2025-09-29 22:16:44 +09:00
minjaesong
8b808ca297 TAV sharpen filters 2025-09-29 21:38:23 +09:00
minjaesong
7608e7433a TAV: channel layouts 2025-09-29 16:34:08 +09:00
minjaesong
907cc37b01 TAV: more format doc updates 2025-09-29 14:52:37 +09:00
minjaesong
1d3d218238 TAV: channel-concatenated coeffs preprocessing 2025-09-29 14:42:52 +09:00
minjaesong
5012ca4085 TAV: decompression done on GPU 2025-09-29 01:35:19 +09:00
minjaesong
66909537a0 TAV: improved compression using some coefficient preprocessing 2025-09-29 01:17:53 +09:00
minjaesong
01278815c7 added CLAUDE.md 2025-09-29 00:27:32 +09:00
minjaesong
65a01f36a4 more encoder param tuning 2025-09-28 20:28:11 +09:00
minjaesong
6ff634cc12 more wavelets for experimentation 2025-09-28 08:55:15 +09:00
minjaesong
d85f8002cc perceptual model copied to TAV decoder_tav.c 2025-09-27 00:50:25 +09:00
minjaesong
c50d015515 TAV decoder for ffmpeg/ffplay 2025-09-27 00:43:20 +09:00
minjaesong
efab1c3a88 TAV: more documentation 2025-09-25 18:27:46 +09:00
minjaesong
4d9981ec23 TAV: now writes encoder q-value to the header 2025-09-25 18:15:53 +09:00
minjaesong
ca18595134 TAV: GOP length adjustment 2025-09-25 16:20:50 +09:00
minjaesong
b4e9d84f5f TAV decoder visual "hack" (increasing acutance) 2025-09-25 15:49:58 +09:00
minjaesong
e2dd0744d2 simpler method of delta coding 2025-09-25 00:48:20 +09:00
minjaesong
2b59d5dd8d Revert "predictive delta encoding wip"
This reverts commit 21fd10d2 but introduces changes from d117b15e
2025-09-25 00:43:03 +09:00
minjaesong
bb3f715ad6 spatial delta prediction 2025-09-25 00:34:39 +09:00
minjaesong
699c6394a1 multiframe-based delta prediction 2025-09-24 23:39:34 +09:00
minjaesong
d117b15e0f better NTSC framerate handling 2025-09-24 21:53:01 +09:00
minjaesong
5564fa5c9b predictive delta encoding with dithering 2025-09-24 21:37:20 +09:00
minjaesong
21fd10d2b6 predictive delta encoding wip 2025-09-24 16:55:23 +09:00
minjaesong
338a0b2e5d tev/tav spec update 2025-09-24 02:43:26 +09:00
minjaesong
0b3497b013 tav playing concatenated video streams (fixed) 2025-09-24 00:41:49 +09:00
minjaesong
a9ba57c09a tav playing concatenated video streams 2025-09-22 23:40:33 +09:00
minjaesong
05101ecd08 turns out extra sync packet at the end of video was a terrible idea 2025-09-22 22:38:32 +09:00
minjaesong
e001445095 more perceptual optimisation 2025-09-22 14:45:59 +09:00
minjaesong
4851f61c56 how far can i push the chroma subquantisation? 2025-09-22 10:37:33 +09:00
minjaesong
be43384968 resurrecting delta encoding 2025-09-22 02:47:46 +09:00
minjaesong
28624309d7 even more psychovisual model 2025-09-22 01:01:15 +09:00
minjaesong
3584520ff9 more psychovisual model 2025-09-22 00:03:45 +09:00
minjaesong
613b8a97dd still working on the psychovisual model 2025-09-21 21:13:39 +09:00
minjaesong
5d48cc62eb still working on the psychovisual model 2025-09-21 01:36:31 +09:00
minjaesong
206e43a308 TAV: first working psychovisual tuning 2025-09-20 11:15:04 +09:00
minjaesong
d3a18c081a TAV: base code for adding psychovisual model 2025-09-20 02:02:59 +09:00
minjaesong
c14b692114 --arate option to override audio bitrate 2025-09-18 01:28:02 +09:00
minjaesong
89aada888d i hate ntsc framerate 2025-09-18 00:57:49 +09:00
minjaesong
b373471629 fix: framerate conversion not working as it should for TAV 2025-09-18 00:54:23 +09:00
minjaesong
35f1a0c2f2 monoblock TAV 2025-09-18 00:37:44 +09:00
minjaesong
f4b03b55b6 monoblock TAV 2025-09-17 23:36:37 +09:00
minjaesong
8279b15b43 TAV using subtitle parsing of TEV 2025-09-17 09:37:00 +09:00
minjaesong
9652143d93 more code cleanup 2025-09-17 01:24:05 +09:00
minjaesong
89e8fc39ce final encoder code cleanup 2025-09-17 00:55:23 +09:00
minjaesong
9ca575eee4 show_usage change 2025-09-16 23:11:31 +09:00
minjaesong
953de6feb6 Merge wavelet_video branch: Add TAV encoder with audio buffer management and NTSC frame duplication
- Implemented TAV (TSVM Advanced Video) encoder with DWT-based compression
- Added sophisticated audio buffer deficit tracking system ported from TEV
- Fixed NTSC frame duplication ghosting by emitting extra sync packets
- Resolved merge conflicts in GraphicsJSR223Delegate.kt

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-16 23:05:20 +09:00
minjaesong
ae59946883 ntsc framerate handling 2025-09-16 22:59:45 +09:00
minjaesong
a639e116c5 TEV-like statistics 2025-09-16 22:40:58 +09:00
minjaesong
9e8aeeb112 audio handling 2025-09-16 22:23:31 +09:00
minjaesong
47f93194a7 p-frame for tav 2025-09-16 18:57:11 +09:00
minjaesong
be193269d8 280x224 macrotile 2025-09-16 16:18:01 +09:00
minjaesong
391adffad4 encoder optimisation 2025-09-16 15:20:28 +09:00
minjaesong
dab56ee55d decoder optimisation 2025-09-16 14:46:56 +09:00
minjaesong
3011c73168 code cleanup 2025-09-16 10:26:03 +09:00
minjaesong
a5da200507 wavelet deblocking using simulated overlapping tiles 2025-09-16 10:03:17 +09:00
minjaesong
54f335e3de working wavelet encoder 2025-09-16 02:51:00 +09:00
minjaesong
4bb234a89b wip 2025-09-15 23:47:28 +09:00
minjaesong
4c0a282de7 removing any reference on interlaced 2025-09-15 19:14:51 +09:00
minjaesong
113c01b851 112x112 blocks for TAV, which greatly improves the encoding speed 2025-09-15 19:08:53 +09:00
minjaesong
1343dd10cf TAV with ICtCp colour space 2025-09-15 16:35:44 +09:00
minjaesong
b497570a3b using "correct" colourimetry
(cherry picked from commit ded609e65e)
2025-09-15 13:00:58 +09:00
minjaesong
9f901681a6 first working version 2025-09-15 12:56:42 +09:00
minjaesong
ded609e65e using "correct" colourimetry 2025-09-15 10:14:44 +09:00
minjaesong
34cf5cb591 ICtCp colour space impl 2025-09-15 09:52:23 +09:00
minjaesong
9c2aa96b73 ICtCp colour space wip 2025-09-15 02:12:16 +09:00
minjaesong
d446a4e2f5 wip6 2025-09-14 22:26:02 +09:00
minjaesong
db57516a46 wip5 2025-09-13 23:06:31 +09:00
minjaesong
712506c91c wip4 2025-09-13 22:02:56 +09:00
minjaesong
722e8e893f wip3 2025-09-13 15:24:32 +09:00
minjaesong
dca09cf4a3 wip2 2025-09-13 13:32:14 +09:00
minjaesong
62d6ee94cf tav wip 2025-09-13 13:28:01 +09:00
minjaesong
198e951102 various encoder bug fixes 2025-09-13 00:39:12 +09:00
minjaesong
1f5f72733a dead code pruning 2025-09-12 19:13:12 +09:00
minjaesong
957522a460 Knusperli-esque post deblocking filter 2025-09-12 14:32:12 +09:00
minjaesong
433e3ea3ae Impl Knusperli deblocking decoding
https://github.com/google/knusperli
2025-09-12 13:50:37 +09:00
minjaesong
dc223fe00b encoder example text update 2025-09-12 09:24:41 +09:00
minjaesong
190cb130bf two pass coding for Knusperli deblocking 2025-09-11 00:23:45 +09:00
minjaesong
29907ec357 TEV doc update 2025-09-10 02:08:10 +09:00
minjaesong
8601f614b4 optimised tevDecode 2025-09-10 01:49:32 +09:00
minjaesong
3f9747ebf0 optimised deinterlacing 2025-09-10 01:33:31 +09:00
minjaesong
c498526a90 uploadRGBToFramebuffer optimisation with bulk mem access 2025-09-10 01:21:16 +09:00
minjaesong
5a5ac8ef74 complexity calculation now considers chroma 2025-09-09 19:02:15 +09:00
minjaesong
9f7a4ef2e7 tev rate factor impl fix 2025-09-09 10:07:52 +09:00
minjaesong
3495dfca5e tev slight optimisation 2025-09-09 09:47:56 +09:00
minjaesong
cf1ee80aa1 fixing another stupid bug 2025-09-09 00:11:45 +09:00
minjaesong
b80d5b858a encoder complexity function update 2025-09-08 23:58:25 +09:00
239 changed files with 59111 additions and 5235 deletions

6
.gitignore vendored
View File

@@ -9,6 +9,8 @@ buildapp/out/TerranBASIC*
buildapp/TerranBASIC_linux.*
buildapp/TerranBASIC_macOS.*
buildapp/TerranBASIC_windows.*
*.o
*.a
# Java native errors
hs_err_pid*
@@ -64,3 +66,7 @@ assets/disk0/home/basic/*
assets/disk0/movtestimg/*.jpg
assets/disk0/*.mov
assets/diskMediabin/*
video_encoder/*
assets/disk0/tvdos/bin/tautfont.png

View File

@@ -21,36 +21,54 @@
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-reflect/1.8.21/kotlin-reflect-1.8.21.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-test/1.8.21/kotlin-test-1.8.21.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.21/kotlin-stdlib-common-1.8.21.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/graal-sdk-22.3.1.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-71.1.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-22.3.1-edit.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-22.3.1.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-22.3.1-edit.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-22.3.1.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/polyglot-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-language-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-runtime-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-compiler-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/compiler-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/compiler-management-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/word-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/collections-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/nativeimage-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jniutils-23.1.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-1.12.1.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-22.3.1-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-22.3.1-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-language-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-language-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-1.12.1-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-1.12.1-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-71.1-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-71.1-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-openal-3.3.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-opengl-3.3.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-jemalloc-3.3.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-22.3.1-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-22.3.1-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/TerranVirtualDisk-src.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jorbis-0.0.17-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jorbis-0.0.17-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/graal-sdk-22.3.1-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/graal-sdk-22.3.1-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/polyglot-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/polyglot-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-runtime-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-runtime-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/collections-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/collections-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/nativeimage-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/nativeimage-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jniutils-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jniutils-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jlayer-1.0.1-gdx-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jlayer-1.0.1-gdx-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3-javadoc.jar" path-in-jar="/" />
@@ -62,15 +80,15 @@
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-openal-3.3.3-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-opengl-3.3.3-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-opengl-3.3.3-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-22.3.1-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-22.3.1-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3-natives-windows.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-jemalloc-3.3.3-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-jemalloc-3.3.3-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-natives-linux.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-natives-macos.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-22.3.1-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-22.3.1-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-23.1.10-javadoc.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-23.1.10-sources.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3-natives-linux.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3-natives-macos.jar" path-in-jar="/" />
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-jnigen-loader-2.3.1-javadoc.jar" path-in-jar="/" />

133
.idea/cody_history.xml generated Normal file
View File

@@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChatHistory">
<accountData>
<list>
<AccountData>
<accountId value="VXNlcjo1NTUxMzY=" />
<chats>
<list>
<chat>
<internalId value="8a32c880-d8ec-4d84-b5f7-a62b2edf63c3" />
<llm>
<llm>
<model value="anthropic/claude-3-5-sonnet-20240620" />
<provider value="Anthropic" />
<title value="Claude 3.5 Sonnet" />
<usage>
<list>
<option value="chat" />
<option value="edit" />
</list>
</usage>
</llm>
</llm>
</chat>
<chat>
<internalId value="f4e7338b-57d3-44e0-b1d9-04aa2dfb4bae" />
<llm>
<llm>
<model value="anthropic/claude-3-5-sonnet-20240620" />
<provider value="Anthropic" />
<title value="Claude 3.5 Sonnet" />
<usage>
<list>
<option value="chat" />
<option value="edit" />
</list>
</usage>
</llm>
</llm>
</chat>
<chat>
<internalId value="26ac815b-5db9-488b-be0b-ab174fbc5646" />
<llm>
<llm>
<model value="anthropic/claude-3-5-sonnet-20240620" />
<provider value="Anthropic" />
<title value="Claude 3.5 Sonnet" />
<usage>
<list>
<option value="chat" />
<option value="edit" />
</list>
</usage>
</llm>
</llm>
</chat>
<chat>
<internalId value="8ab39b26-cdda-4256-878f-e0416e66bbea" />
<llm>
<llm>
<model value="anthropic/claude-3-5-sonnet-20240620" />
<provider value="Anthropic" />
<title value="Claude 3.5 Sonnet" />
<usage>
<list>
<option value="chat" />
<option value="edit" />
</list>
</usage>
</llm>
</llm>
</chat>
<chat>
<internalId value="f79a288a-adc5-4d45-a069-4bf024d90236" />
<llm>
<llm>
<model value="anthropic/claude-3-5-sonnet-20240620" />
<provider value="Anthropic" />
<title value="Claude 3.5 Sonnet" />
<usage>
<list>
<option value="chat" />
<option value="edit" />
</list>
</usage>
</llm>
</llm>
</chat>
<chat>
<internalId value="20bc02fd-c6b5-4590-a00b-b7012a630ef4" />
<llm>
<llm>
<model value="anthropic/claude-3-5-sonnet-20240620" />
<provider value="Anthropic" />
<title value="Claude 3.5 Sonnet" />
<usage>
<list>
<option value="chat" />
<option value="edit" />
</list>
</usage>
</llm>
</llm>
</chat>
</list>
</chats>
<defaultLlm>
<llm>
<model value="anthropic/claude-3-5-sonnet-20240620" />
<provider value="Anthropic" />
<tags>
<list>
<option value="gateway" />
<option value="accuracy" />
<option value="recommended" />
<option value="free" />
</list>
</tags>
<title value="Claude 3.5 Sonnet" />
<usage>
<list>
<option value="chat" />
<option value="edit" />
</list>
</usage>
</llm>
</defaultLlm>
</AccountData>
</list>
</accountData>
</component>
</project>

9
.idea/markdown.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
<option name="splitLayout" value="SHOW_EDITOR" />
</component>
</project>

2
.idea/misc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

17
.idea/runConfigurations/AppLoader.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="AppLoader" type="Application" factoryName="Application" nameIsGenerated="true">
<option name="ALTERNATIVE_JRE_PATH" value="21" />
<option name="MAIN_CLASS_NAME" value="net.torvald.tsvm.AppLoader" />
<module name="tsvm_executable" />
<option name="VM_PARAMETERS" value="-ea --upgrade-module-path=lib/compiler-23.1.10.jar:lib/compiler-management-23.1.10.jar:lib/truffle-compiler-23.1.10.jar:lib/truffle-api-23.1.10.jar:lib/truffle-runtime-23.1.10.jar:lib/polyglot-23.1.10.jar:lib/collections-23.1.10.jar:lib/word-23.1.10.jar:lib/nativeimage-23.1.10.jar:lib/jniutils-23.1.10.jar -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseZGC -XX:+UseDynamicNumberOfGCThreads --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="net.torvald.tsvm.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

16
.idea/runConfigurations/TerranBASIC.xml generated Normal file
View File

@@ -0,0 +1,16 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="TerranBASIC" type="Application" factoryName="Application" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="net.torvald.tsvm.TerranBASIC" />
<module name="TerranBASICexecutable" />
<option name="VM_PARAMETERS" value="--upgrade-module-path=lib/compiler-23.1.10.jar:lib/compiler-management-23.1.10.jar:lib/truffle-compiler-23.1.10.jar:lib/truffle-api-23.1.10.jar:lib/truffle-runtime-23.1.10.jar:lib/polyglot-23.1.10.jar:lib/collections-23.1.10.jar:lib/word-23.1.10.jar:lib/nativeimage-23.1.10.jar:lib/jniutils-23.1.10.jar -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="net.torvald.tsvm.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

16
.idea/runConfigurations/TsvmEmulator.xml generated Normal file
View File

@@ -0,0 +1,16 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="TsvmEmulator" type="Application" factoryName="Application" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="net.torvald.tsvm.TsvmEmulator" />
<module name="tsvm_executable" />
<option name="VM_PARAMETERS" value="-ea --upgrade-module-path=lib/compiler-23.1.10.jar:lib/compiler-management-23.1.10.jar:lib/truffle-compiler-23.1.10.jar:lib/truffle-api-23.1.10.jar:lib/truffle-runtime-23.1.10.jar:lib/polyglot-23.1.10.jar:lib/collections-23.1.10.jar:lib/word-23.1.10.jar:lib/nativeimage-23.1.10.jar:lib/jniutils-23.1.10.jar -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseZGC -XX:+UseDynamicNumberOfGCThreads --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="net.torvald.tsvm.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

2
.idea/vcs.xml generated
View File

@@ -2,5 +2,7 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/assets/disk0/home/tetrino" vcs="Git" />
<mapping directory="$PROJECT_DIR$/assets/disk0/home/tvnes" vcs="Git" />
</component>
</project>

8
2taud.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env fish
for f in *.mod; python3 mod2taud.py $f assets/disk0/(basename $f .mod).taud; end
for f in *.s3m; python3 s3m2taud.py $f assets/disk0/(basename $f .s3m).taud; end
for f in *.it; python3 it2taud.py $f assets/disk0/(basename $f .it).taud; end
for f in *.xm; python3 xm2taud.py $f assets/disk0/(basename $f .xm).taud; end
for f in *.mon; python3 mon2taud.py $f assets/disk0/(basename $f .mon).taud; end
for f in *.MON; python3 mon2taud.py $f assets/disk0/(basename $f .MON).taud; end

419
CLAUDE.md Normal file
View File

@@ -0,0 +1,419 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
**tsvm** is a virtual machine that mimics 8-bit era computer architecture and runs programs written in JavaScript. The project includes:
- The virtual machine core
- Reference BIOS implementation
- TVDOS (operating system)
- Videotron2K video display controller emulator
- TerranBASIC integration
- Multiple platform build system
## Documentations
Documentation for TSVM and TVDOS are available on `./doc/*.tex` as machine-readable format.
Documentatino for TSVM architecture is available on `terranmon.txt`
## Reference Materials
Third-party source-code references that inform TSVM implementations live in
`reference_materials/<topic>/`. Each topic folder has a `README.md` that
summarises the takeaway and points back into the verbatim source files.
**Consult these before reimplementing tracker / codec / DSP behaviour from
memory** — TSVM aims to match the audible behaviour of the originals.
Current topics:
- `reference_materials/tracker_filter/` — Impulse Tracker / OpenMPT / Schism
Tracker resonant low-pass filter source. Defines the cutoff formula, the
resonance damping curve, and the **IIR-only 2-pole topology** (NOT a
biquad — no feedforward x[n1] / x[n2] terms) that `AudioAdapter.kt` uses
for Taud playback.
- `reference_materials/ft2-clone` — Modernised clone for the original FastTracker 2
- `reference_materials/impulse-tracker` — The original source code for ImpulseTracker
- `reference_materials/MilkyTracker` — FastTracker 2 compatible tracker
- `reference_materials/schismtracker` — Open-source re-implementation of ImpulseTracker
When fetching new references, copy the relevant upstream files verbatim into
a topic folder, write a `README.md` summarising the relevant maths /
algorithms with file:line citations, and add an entry here.
## Architecture
### Core Components
- **tsvm_core/**: Core virtual machine implementation in Kotlin
- `VM.kt`: Main virtual machine class with memory management and peripheral slots
- `peripheral/`: Hardware peripherals (graphics adapters, disk drives, TTY, audio, etc.)
- `vdc/`: Videotron2K video display controller
- Various delegates for JavaScript integration via GraalVM
- **tsvm_executable/**: Main emulator application
- `VMGUI.kt`: LibGDX-based GUI implementation
- `TsvmEmulator.java`: Main application entry point
- Menu systems for configuration, audio, memory management
- **TerranBASICexecutable/**: TerranBASIC interpreter application
- `TerranBASIC.java`: Entry point for BASIC interpreter
- `VMGUI.kt`: GUI for BASIC environment
### Key Technologies
- **Kotlin/Java**: Primary implementation language
- **LibGDX**: Graphics and windowing framework
- **GraalVM**: JavaScript execution engine for running programs in the VM
- **LWJGL**: Native library bindings
- **IntelliJ IDEA**: Development environment (*.iml module files)
### Virtual Hardware
The VM emulates various peripherals through the `peripheral/` package:
- Graphics adapters with different capabilities
- Disk drives (including TevdDiskDrive for custom disk format)
- TTY terminals and character LCD displays
- Audio devices and MP2 audio environment
- Network modems and serial interfaces
- Memory management units
## Build and Development
### Building Applications
Use the build scripts in `buildapp/`:
- `build_app_linux_x86.sh` - Linux x86_64 AppImage
- `build_app_linux_arm.sh` - Linux ARM64 AppImage
- `build_app_mac_x86.sh` - macOS Intel
- `build_app_mac_arm.sh` - macOS Apple Silicon
- `build_app_windows_x86.sh` - Windows x86
### Prerequisites
1. Download JDK 21 runtimes to `~/Documents/openjdk/*` with specific naming:
- `jdk-21.0.1-x86` (Linux AMD64)
- `jdk-21.0.1-arm` (Linux Aarch64)
- `jdk-21.0.1-windows` (Windows AMD64)
- `jdk-21.0.1.jdk-arm` (macOS Apple Silicon)
- `jdk-21.0.1.jdk-x86` (macOS Intel)
2. Run `jlink` commands to create custom Java runtimes in `out/runtime-*` directories
### Development Commands
- **Build JAR**: Use IntelliJ IDEA build system to compile modules
- **Run Emulator**: Execute `TsvmEmulator.java` main method or use built JAR
- **Run TerranBASIC**: Execute `TerranBASIC.java` main method
- **Package Apps**: Run appropriate build script from `buildapp/` directory
### Assets and File System
- `assets/disk0/`: Virtual disk content including TVDOS system files
- `assets/bios/`: BIOS ROM files and implementations
- `My_BASIC_Programs/`: Example BASIC programs for testing
- TVDOS filesystem uses custom format with specialised drivers
## Videotron2K
The Videotron2K is a specialised video display controller with:
- Assembly-like programming language
- 6 general registers (r1-r6) and special registers (tmr, frm, px, py, c1-c6)
- Scene-based programming model
- Drawing commands (plot, fillin, goto, fillscr)
- Conditional execution with postfixes (zr, nz, gt, ls, ge, le)
Programs are structured with SCENE blocks and executed with perform commands.
## Memory Management
- VM supports up to USER_SPACE_SIZE memory
- 64-byte malloc units with reserved blocks
- Peripheral slots (1-8 configurable)
- Memory-mapped I/O for peripheral access
- JavaScript programs run in sandboxed GraalVM context
### Peripheral Memory Addressing
Peripheral memories can be accessed using `vm.peek()` and `vm.poke()` functions, which takes absolute address.
- Peripherals take up negative number of the memory space, and their addressing is in backwards (e.g. Slot 1 starts at -1048577 and ends at -2097152)
- Peripherals take up two memory regions: MMIO area and Memory Space area; MMIO is accessed by PeriBase (and its children) using `mmio_read()` and `mmio_write()`, and the Memory Space is accessed using `peek()` and `poke()`.
- Peripheral at slot *n* takes following addresses
1. MMIO area (-131072×n)-1 to -131072×(n+1)
2. Memory Space area -(1048576×n)-1 to (-1048576×(n+1))
## Testing
- Use example programs in `My_BASIC_Programs/` for BASIC testing
- JavaScript test programs available in `assets/disk0/`
- Videotron2K assembly examples in documentation
## Notes
- The 'gzip' namespace in TSVM's JS programs is a misnomer: the actual 'gzip' functions (defined in CompressorDelegate.kt) call Zstd functions.
## TVDOS
### TVDOS Movie Formats
#### Legacy iPF Format
- Format documentation on `terranmon.txt` (search for "TSVM MOV file format" and "TSVM Interchangeable Picture Format (aka iPF Type 1/2)")
- Video Encoder implementation on `assets/disk0/tvdos/bin/encodemov.js` (iPF Format 1 and 2) and `assets/disk0/tvdos/bin/encodemov2.js` (iPF Format 1-delta)
- Actual encoding/decoding code is in `GraphicsJSR223Delegate.kt`
- Audio uses standard MP2
#### TEV Format (TSVM Enhanced Video)
- **Modern video codec** optimized for TSVM hardware with 60-80% better compression than iPF
- **C Encoder**: `video_encoder/encoder_tev.c` - Hardware-accelerated encoder with motion compensation and DCT
- How to build: `make clean && make`
- **Rate Control**: Supports both quality mode (`-q 0-4`) and bitrate mode (`-b N` kbps)
- **JS Decoder**: `assets/disk0/tvdos/bin/playtev.js` - Native decoder for TEV format playback
- How to build: `must be done manually by the user; the TSVM is not machine-interactable`
- **Hardware accelerated decoding**: Extended GraphicsJSR223Delegate.kt with TEV functions:
- `tevDecode()` - The main decoding function (now accepts rate control factor)
- `tevIdct8x8()` - Fast 8×8 DCT transforms
- `tevMotionCopy8x8()` - Sub-pixel motion compensation
- **Features**:
- 16×16 DCT blocks (vs 4×4 in iPF) for better compression
- Motion compensation with ±8 pixel search range
- YCoCg-R 4:2:0 Chroma subsampling (more aggressive quantisation on Cg channel)
- Full 8-Bit RGB colour for increased visual fidelity, rendered down to TSVM-compliant 4-Bit RGB with dithering upon playback
- **Usage Examples**:
```bash
# Quality mode
./encoder_tev -i input.mp4 -o output.tev -q 3
# Playback
playtev output.tev
```
- **Format documentation**: `terranmon.txt` (search for "TSVM Enhanced Video (TEV) Format")
- **Version**: 2.1 (includes rate control factor in all video packets)
#### TAV Format (TSVM Advanced Video)
- **Successor to TEV**: DWT-based video codec using wavelet transforms instead of DCT
- **C Encoder**: `video_encoder/encoder_tav.c` - Multi-wavelet encoder with perceptual quantisation
- How to build: `make tav`
- **Wavelet Support**: Multiple wavelet types for different compression characteristics
- **JS Decoder**: `assets/disk0/tvdos/bin/playtav.js` - Native decoder for TAV format playback
- **Hardware accelerated decoding**: Extended GraphicsJSR223Delegate.kt with TAV functions
- **Packet analyser**: `video_encoder/tav_inspector.c` - Debugging tool that parses TAV packets into human-readable form
- **Features**:
- **Multiple Wavelet Types**: 5/3 reversible, 9/7 irreversible, CDF 13/7, DD-4, Haar
- **Single-tile encoding**: One large DWT tile for optimal quality (no blocking artifacts)
- **Perceptual quantisation**: HVS-optimized coefficient scaling
- **YCoCg-R colour space**: Efficient chroma representation with "simulated" subsampling using anisotropic quantisation (search for "ANISOTROPY_MULT_CHROMA" on the encoder)
- **6-level DWT decomposition**: Deep frequency analysis for better compression (deeper levels possible but 6 is the maximum for the default TSVM size)
- **Significance Map Compression**: Improved coefficient storage format exploiting sparsity for 16-18% additional compression (2025-09-29 update)
- **Concatenated Maps Layout**: Cross-channel compression optimisation for additional 1.6% improvement (2025-09-29 enhanced)
- **Usage Examples**:
```bash
# Different wavelets
./encoder_tav -i input.mp4 -w 0 -o output.tav # 5/3 reversible (lossless capable)
./encoder_tav -i input.mp4 -w 1 -o output.tav # 9/7 irreversible (default, best compression)
./encoder_tav -i input.mp4 -w 2 -o output.tav # CDF 13/7 (experimental)
./encoder_tav -i input.mp4 -w 16 -o output.tav # DD-4 (four-point interpolating)
./encoder_tav -i input.mp4 -w 255 -o output.tav # Haar (demonstration)
# Quality levels (0-5)
./encoder_tav -i input.mp4 -q 0 -o output.tav # Lowest quality, smallest file
./encoder_tav -i input.mp4 -q 5 -o output.tav # Highest quality, largest file
# Temporal 3D DWT (GOP-based encoding)
./encoder_tav -i input.mp4 --temporal-dwt -o output.tav
# Playback
playtav output.tav
```
**CRITICAL IMPLEMENTATION NOTES**:
**Wavelet Coefficient Layout**:
- TAV uses **2D Spatial Layout** in memory: `[LL, LH, HL, HH, LH, HL, HH, ...]` for each decomposition level
- **Forward transform must output**: `temp[0...half-1] = low-pass`, `temp[half...length-1] = high-pass`
- **Inverse transform must expect**: Same 2D spatial layout and exactly reverse forward operations
- **Common mistake**: Assuming linear layout leads to grid/checkerboard artifacts
**Wavelet Implementation Pattern**:
- All wavelets must follow the **exact same structure** as the working 5/3 implementation:
```c
// Forward: 1. Predict step, 2. Update step
temp[half + i] = data[odd_index] - prediction; // High-pass
temp[i] = data[even_index] + update; // Low-pass
// Inverse: Reverse order - 1. Undo update, 2. Undo predict
temp[i] -= update; // Undo low-pass update
temp[half + i] += prediction; // Undo high-pass predict
```
- **Boundary handling**: Use symmetric extension for filter taps beyond array bounds
- **Reconstruction**: Interleave even/odd samples: `data[2*i] = low[i], data[2*i+1] = high[i]`
**Debugging Grid Artifacts**:
- **Symptom**: Checkerboard or grid patterns in decoded video
- **Cause**: Mismatch between encoder/decoder coefficient layout or lifting step operations
- **Solution**: Ensure forward and inverse transforms use identical coefficient indexing and reverse operations exactly
**Supported Wavelets**:
- **0**: 5/3 reversible (lossless when unquantised, JPEG 2000 standard)
- **1**: 9/7 irreversible (best compression, CDF 9/7 variant, default choice)
- **2**: CDF 13/7 (experimental, simplified implementation)
- **16**: DD-4 (four-point interpolating Deslauriers-Dubuc, for still images)
- **255**: Haar (demonstration only, simplest possible wavelet)
- **Format documentation**: `terranmon.txt` (search for "TSVM Advanced Video (TAV) Format")
- **Version**: Current (perceptual quantisation, multi-wavelet support, EZBC compression)
#### TAV Temporal 3D DWT (GOP Unified Encoding)
Implemented on 2025-10-15 for improved temporal compression through group-of-pictures (GOP) encoding:
**Key Features**:
- **3D DWT**: Applies DWT in both spatial (2D) and temporal (1D) dimensions for optimal spacetime compression
- **Unified GOP Preprocessing**: Single EZBC tree for all frames and channels in a GOP (width×height×N_frames×3_channels)
- **GOP Size**: Typically 8 frames (configurable), with scene change detection for adaptive GOPs
- **Single-frame Fallback**: GOP size of 1 automatically uses traditional I-frame encoding
**Packet Format**:
- **0x12 (GOP_UNIFIED)**: `[gop_size][compressed_size][compressed_data]`
- **0xFC (GOP_SYNC)**: `[frame_count]` - Indicates N frames were decoded from GOP block
- **Timecode Emission**: One timecode packet per GOP (not per frame)
**Technical Implementation**:
```c
// Unified preprocessing structure (encoder_tav.c:2371-2509)
[All_Y_maps][All_Co_maps][All_Cg_maps][All_Y_values][All_Co_values][All_Cg_values]
// Where maps are grouped by channel across all GOP frames for optimal Zstd compression
```
**Usage**:
```bash
# Enable temporal 3D DWT
./encoder_tav -i input.mp4 --temporal-dwt -o output.tav
# Inspect GOP structure
./tav_inspector output.tav -v
```
**Compression Benefits**:
- **Temporal Coherence**: Exploits similarity across consecutive frames
- **Unified Compression**: Zstd compresses entire GOP as single block, finding patterns across time
- **Adaptive GOPs**: Scene change detection ensures optimal GOP boundaries
#### TAD Format (TSVM Advanced Audio)
- **Perceptual audio codec** for TSVM using CDF 9/7 biorthogonal wavelets
- **C Encoder**: `video_encoder/encoder_tad.c` - Core Encoder library; `video_encoder/encoder_tad_standalone.c` - Standalone encoder with FFmpeg integration
- How to build: `make tad`
- **Quality Levels**: 0-5 (0=lowest quality/smallest, 5=highest quality/largest; designed to be in sync with TAV encoder)
- **C Decoders**:
- `video_encoder/decoder_tad.c` - Shared decoder library with `tad32_decode_chunk()` function
- `video_encoder/decoder_tad.h` - Exports shared decoder API
- `video_encoder/decoder_tav.c` - TAV decoder that uses shared TAD decoder for audio packets
- **Shared Architecture** (Fixed 2025-11-10): Both standalone TAD and TAV decoders now use the same `tad32_decode_chunk()` implementation, eliminating code duplication and ensuring identical output
- **Kotlin Decoder**: `AudioAdapter.kt` - Hardware-accelerated TAD decoder for TSVM runtime
- **Quantisation Fix** (2025-11-10): Fixed BASE_QUANTISER_WEIGHTS to use channel-specific 2D array (Mid/Side) instead of single 1D array, resolving severe audio distortion
- **Features**:
- **32 KHz stereo**: TSVM audio hardware native format
- **Variable chunk sizes**: Any size ≥1024 samples, including non-power-of-2 (e.g., 32016 for TAV 1-second GOPs)
- **Pre-emphasis filter**: First-order IIR filter (α=0.5) shifts quantisation noise to lower frequencies
- **Gamma compression**: Dynamic range compression (γ=0.5) before quantisation
- **M/S stereo decorrelation**: Exploits stereo correlation for better compression
- **9-level CDF 9/7 DWT**: Fixed 9 decomposition levels for all chunk sizes
- **Perceptual quantisation**: Channel-specific (Mid/Side) frequency-dependent weights with lambda companding (λ=6.0)
- **EZBC encoding**: Binary tree embedded zero block coding exploits coefficient sparsity (86.9% Mid, 97.8% Side)
- **Zstd compression**: Level 7 on concatenated EZBC bitstreams for additional compression
- **Non-power-of-2 support**: Fixed 2025-10-30 to handle arbitrary chunk sizes correctly
- **Usage Examples**:
```bash
# Encode with default quality (Q3)
encoder_tad -i input.mp4 -o output.tad
# Encode with highest quality
encoder_tad -i input.mp4 -o output.tad -q 5
# Encode without Zstd compression
encoder_tad -i input.mp4 -o output.tad --no-zstd
# Verbose output with statistics
encoder_tad -i input.mp4 -o output.tad -v
# Decode back to PCM16
decoder_tad -i input.tad -o output.pcm
```
- **Format documentation**: `terranmon.txt` (search for "TSVM Advanced Audio (TAD) Format")
- **Version**: 1.1 (EZBC encoding with non-power-of-2 support, updated 2025-10-30; decoder architecture and Kotlin quantisation weights fixed 2025-11-10; documentation updated 2025-11-10 to reflect pre-emphasis and EZBC)
**TAD Encoding Pipeline**:
1. **Pre-emphasis filter** (α=0.5) - Shifts quantisation noise toward lower frequencies
2. **Gamma compression** (γ=0.5) - Dynamic range compression
3. **M/S decorrelation** - Transforms L/R to Mid/Side
4. **9-level CDF 9/7 DWT** - Wavelet decomposition (fixed 9 levels)
5. **Perceptual quantisation** - Lambda companding (λ=6.0) with channel-specific weights
6. **EZBC encoding** - Binary tree embedded zero block coding per channel
7. **Zstd compression** (level 7) - Additional compression on concatenated EZBC bitstreams
**TAD Compression Performance**:
- **Target Compression**: 2:1 against PCMu8 baseline (4:1 against PCM16LE input)
- **Achieved Compression**: 2.51:1 against PCMu8 at quality level 3
- **Audio Quality**: Preserves full 0-16 KHz bandwidth
- **Coefficient Sparsity**: 86.9% zeros in Mid channel, 97.8% in Side channel (typical)
- **EZBC Benefits**: Exploits sparsity, progressive refinement, spatial clustering
**TAD Integration with TAV**:
TAD is designed as an includable API for TAV video encoder integration. The variable chunk size
support enables synchronized audio/video encoding where audio chunks can match video GOP boundaries.
TAV embeds TAD-compressed audio using packet type 0x24 with Zstd compression.
**TAD Hardware Acceleration**:
TSVM accelerates TAD decoding with AudioAdapter.kt (backend) and AudioJSR223Delegate.kt (API):
- Backend decoder in AudioAdapter.kt with non-power-of-2 chunk size support (fixed 2025-10-30)
- API functions in AudioJSR223Delegate.kt for JavaScript access
- Supports chunk sizes from 1024 to 32768+ samples (any size ≥1024)
- Fixed 9-level CDF 9/7 inverse DWT with correct length tracking for non-power-of-2 sizes
**Critical Implementation Note (Fixed 2025-10-30)**:
Multi-level inverse DWT must pre-calculate the exact sequence of lengths from forward transform:
```kotlin
val lengths = IntArray(levels + 1)
lengths[0] = chunk_size
for (i in 1..levels) {
lengths[i] = (lengths[i - 1] + 1) / 2
}
// Apply inverse DWT using lengths[level] for each level
```
Using simple doubling (`length *= 2`) is incorrect for non-power-of-2 sizes and causes
mirrored subband artifacts.
**TAD Decoding Pipeline**:
1. **Zstd decompression** - Decompress concatenated EZBC bitstreams
2. **EZBC decoding** - Binary tree decoder reconstructs quantised int8 coefficients per channel
3. **Lambda decompanding** - Inverse Laplacian CDF mapping with channel-specific weights
4. **9-level inverse CDF 9/7 DWT** - Wavelet reconstruction with proper non-power-of-2 length tracking
5. **M/S to L/R conversion** - Transform Mid/Side back to Left/Right
6. **Gamma expansion** (γ⁻¹=2.0) - Restore dynamic range
7. **De-emphasis filter** (α=0.5) - Reverse pre-emphasis, remove frequency shaping
8. **PCM32f to PCM8** - Noise-shaped dithering for final 8-bit output
**Critical Quantisation Weights Note (Fixed 2025-11-10)**:
The TAD decoder MUST use channel-specific quantisation weights for Mid (channel 0) and Side (channel 1) channels. The Kotlin decoder (AudioAdapter.kt) originally used a single 1D weight array, which caused severe audio distortion. The correct implementation uses a 2D array:
```kotlin
// CORRECT (Fixed 2025-11-10)
private val BASE_QUANTISER_WEIGHTS = arrayOf(
floatArrayOf( // Mid channel (0)
4.0f, 2.0f, 1.8f, 1.6f, 1.4f, 1.2f, 1.0f, 1.0f, 1.3f, 2.0f
),
floatArrayOf( // Side channel (1)
6.0f, 5.0f, 2.6f, 2.4f, 1.8f, 1.3f, 1.0f, 1.0f, 1.6f, 3.2f
)
)
// During dequantisation:
val weight = BASE_QUANTISER_WEIGHTS[channel][sideband] * quantiserScale
coeffs[i] = normalisedVal * TAD32_COEFF_SCALARS[sideband] * weight
```
The different weights for Mid and Side channels reflect the perceptual importance of different frequency bands in each channel. Using incorrect weights causes:
- DC frequency underamplification (using 1.0 instead of 4.0/6.0)
- Incorrect stereo imaging and extreme side channel distortion
- Severe frequency response errors that manifest as "clipping-like" distortion

224
README.md
View File

@@ -1,8 +1,222 @@
![tsvm](tsvm_screenshot.png)
**tsvm** /tiː.ɛs.viː.ɛm/ is a virtual machine with the architecture that mimics the 8-bit era of
computers, and runs programs written in Javascript.
# tsvm
**tsvm** repository includes the virtual machine itself, the reference BIOS
implementation and a DOS; BASIC is provided by the [TerranBASIC](https://github.com/curioustorvald/TerranBASIC)
repository.
**tsvm** /tiː.ɛs.viː.ɛm/ is a fantasy computer platform: a virtual machine whose
architecture is inspired by the 8-bit and early 16-bit home computers, built
from the ground up around running JavaScript as its native machine code.
What started as "an 8-bit-flavoured VM that runs JS" has grown into a complete,
self-hosted retro computing ecosystem — with its own BIOS, operating system,
filesystem, video and audio codecs, video display coprocessor with its own
assembly language, tracker music format, and a stack of userland tools that
together come closer to a small alternate-history computer line than a
single-binary emulator.
This repository contains the virtual machine core, the reference BIOS
implementations, the **TVDOS** operating system, the **Videotron2K** video
display controller, hardware-accelerated codec backends for the **TEV / TAV /
TAD** media formats, and the multi-platform packaging scripts. The
[TerranBASIC](https://github.com/curioustorvald/TerranBASIC) repository
provides the matching BASIC dialect that ships on the system disk.
## What's actually in here
### The virtual machine
- **VM core** (`tsvm_core/`) — memory model, peripheral bus, MMIO, JS
sandboxing through GraalVM, watchdog, DMA engine, and cooperative scheduling.
Up to 8 hot-pluggable peripheral slots, each with a dedicated MMIO window
and memory-space window mapped into the VM's negative address range.
- **Multiple BIOS implementations** (`assets/bios/`) — including the reference
`tsvmbios.js`, an OpenBIOS variant, the TBM-BIOS for TerranBASIC machines,
and the Pip-Boy-style `pipboot.rom`. BIOSes are first-class swappable
components, not a fixed boot blob.
- **Reference monitor / debugger** (`mon.js`) for poking at memory and
peripherals from a running machine.
- **Multi-platform packaging** (`buildapp/`) — scripts to produce Linux x86_64
/ ARM64 AppImages, macOS Intel / Apple Silicon bundles, and Windows builds,
each with its own `jlink`-trimmed JDK 21 runtime.
### Peripherals (the "hardware")
Living under `tsvm_core/src/net/torvald/tsvm/peripheral/`:
- **Graphics adapters** — the standard `GraphicsAdapter`, plus `TexticsAdapter`
for text-mode framebuffers, `ExtDisp` for external displays, and a
`RemoteGraphicsAdapter` for networked rendering.
- **Audio devices** — `AudioAdapter` (the main programmable sound chip with
PCM channels, an Impulse Tracker-style resonant low-pass filter, and a
hardware-accelerated **TAD** decoder), `OpenALBufferedAudioDevice`, and the
`MP2Env` MPEG audio environment.
- **Disk drives** — `TevdDiskDrive` (TEVD custom filesystem),
`ClusteredDiskDrive`, `TestDiskDrive`, and a latency-simulator script for
testing slow-storage behaviour.
- **Networking and serial** — `HttpModem`, `HSDPA` / `HostFileHSDPA` for
high-speed packet I/O, `SerialStdioHost`, `BlockTransferInterface` /
`BlockTransferPort`.
- **Terminals and displays** — `TTY`, `GlassTty`, `TermSim`, and a
`CharacterLCDdisplay` for HD44780-flavoured projects.
- **Memory expansion** — `RamBank` for bank-switched memory, plus a
programmable `TestFunctionGenerator`.
### Videotron2K — the video coprocessor
Videotron2K is a programmable video display controller with its **own
assembly-like language**, six general registers (`r1``r6`), special registers
(`tmr`, `frm`, `px`, `py`, `c1``c6`), a scene-based programming model, and
conditional postfixes (`zr`, `nz`, `gt`, `ls`, `ge`, `le`). Programs declare
`SCENE` blocks and dispatch them with `perform`. Drawing primitives include
`plot`, `fillin`, `fillscr`, and `goto`. See `Videotron2K.md` and the VDC
implementation under `tsvm_core/.../vdc/`.
### TVDOS — the operating system
`assets/disk0/tvdos/` is a complete DOS-style userland:
- **Kernel and drivers** — `TVDOS.SYS`, `HSDPADRV.SYS`, `hyve.SYS`,
installable drivers under `moviedev/` and `tuidev/`.
- **Custom filesystem** — TEVD, with the on-disk format documented in
`tvdos/filesystem.md`.
- **Internationalisation** — Colemak / Dvorak / QWERTY keymaps and an `i18n/`
resource tree.
- **Userland binaries** (`tvdos/bin/`) — a shell (`command.js`), file tools
(`hexdump`, `less`, `tee`, `touch`, `printfile`, `writeto`, `defrag`,
`lfs`, `drives`), an editor (`edit.js`), a file manager (`zfm.js`), a
network fetcher (`geturl`), gzip/Zstd helpers, palette tools, and a battery
of media players (`playmp2`, `playpcm`, `playwav`, `playmv1`, `playtev`,
`playtav`, `playtad`, `playucf`).
- **Taut tracker** — a full in-VM tracker (`taut.js`,
`taut_instredit.js`, `taut_sampleedit.js`, `taut_notationedit.js`,
`taut_fileop.js`) with its own font and chrome assets.
### Codecs and media formats
tsvm ships a small but serious codec lab. Encoders are written in C and live
in `video_encoder/`; decoders are split between JavaScript players in TVDOS
and hardware-accelerated Kotlin backends in the VM core.
- **iPF (Type 1 / 2 / 1-delta)** — picture and legacy movie format. Encoders:
`encodeipf.js`, `encodemov.js`, `encodemov2.js`. Documented in
`terranmon.txt`.
- **TEV (TSVM Enhanced Video)** — modern DCT codec with motion compensation,
16×16 blocks, YCoCg-R 4:2:0, and either quality-mode or bitrate-mode rate
control. Encoder: `video_encoder/encoder_tev.c`. Decoder: `playtev.js`,
with `tevDecode` / `tevIdct8x8` / `tevMotionCopy8x8` accelerated in
`GraphicsJSR223Delegate.kt`.
- **TAV (TSVM Advanced Video)** — successor to TEV based on the Discrete
Wavelet Transform. Five wavelet types (5/3 reversible, 9/7 irreversible,
CDF 13/7, DD-4, Haar), 6-level decomposition, EZBC sparsity coding,
perceptual quantisation, and an optional **3D temporal DWT** that encodes
whole groups of pictures as one unified wavelet tree. Includes a packet
inspector (`tav_inspector.c`) and coefficient visualiser
(`tav_visualise_coefficients.c`).
- **TAD (TSVM Advanced Audio)** — perceptual audio codec at 32 kHz stereo,
using CDF 9/7 wavelets, M/S decorrelation, gamma compression, pre-emphasis,
EZBC, and Zstd. Achieves ~2.5:1 compression vs. PCMu8 at quality 3 while
preserving the full 016 kHz band. Designed to be embeddable inside TAV so
audio chunks can align with video GOP boundaries.
- **Taud** — tracker module format with conversion tools from
the major formats: `it2taud.py` (Impulse Tracker), `mod2taud.py`
(ProTracker / FastTracker), `s3m2taud.py` (Scream Tracker 3), plus
`2taud.sh` and shared helpers in `taud_common.py`. Note effects are
documented in `TAUD_NOTE_EFFECTS.md`. The `AudioAdapter` runs the same
IIR-only 2-pole resonant low-pass topology used by Impulse Tracker /
OpenMPT / Schism.
- **MP2** — reference MPEG-1 Layer II environment via `MP2Env.kt` and
`playmp2.js`.
### Languages and runtimes
- **JavaScript** is the VM's native code, executed by GraalVM in a sandboxed
context with a curated set of host bindings (graphics, audio, filesystem,
DMA, compression, networking, low-level peek/poke).
- **TerranBASIC** is provided by the
[TerranBASIC](https://github.com/curioustorvald/TerranBASIC) repository and
shipped as `tbas` on the system disk. The `TerranBASICexecutable/` subproject
packages a BASIC-only flavour of the machine.
- **Videotron2K assembly** for VDC programs.
### Documentation
- `terranmon.txt` — the architecture reference (memory map, peripheral
protocol, codec bitstreams).
- `doc/*.tex` — machine-readable LaTeX sources for the TSVM and TVDOS manuals,
built with `doc/makepdf.sh`.
- `Videotron2K.md` — VDC programming guide.
- `TAUD_NOTE_EFFECTS.md` — tracker effect reference.
- `CLAUDE.md` — a condensed map of the project for collaborators (and
language-model assistants) working in the tree.
## Building and running
### Prerequisites
JDK 21 runtimes laid out under `~/Documents/openjdk/` with platform-specific
names:
- `jdk-21.0.1-x86` — Linux AMD64
- `jdk-21.0.1-arm` — Linux Aarch64
- `jdk-21.0.1-windows` — Windows AMD64
- `jdk-21.0.1.jdk-x86` — macOS Intel
- `jdk-21.0.1.jdk-arm` — macOS Apple Silicon
`jlink` is then used to produce trimmed runtimes under `out/runtime-*`.
### Common entry points
- **Run the emulator** — `TsvmEmulator.java` (in `tsvm_executable/`).
- **Run TerranBASIC-only build** — `TerranBASIC.java` (in
`TerranBASICexecutable/`).
- **Package an installable bundle** — pick the right script in `buildapp/`:
- `build_app_linux_x86.sh`
- `build_app_linux_arm.sh`
- `build_app_mac_x86.sh`
- `build_app_mac_arm.sh`
- `build_app_windows_x86.sh`
- **Build C encoders** — in `video_encoder/`: `make` (TEV), `make tav`,
`make tad`.
### Encoding sample media
```bash
# Quality-mode TEV encode
./encoder_tev -i input.mp4 -o clip.tev -q 3
# TAV with 9/7 wavelet, quality 4
./encoder_tav -i input.mp4 -w 1 -q 4 -o clip.tav
# TAV with 3D temporal DWT (GOP-unified encoding)
./encoder_tav -i input.mp4 --temporal-dwt -o clip.tav
# TAD audio at the highest quality
./encoder_tad -i input.mp4 -o track.tad -q 5
```
Then, inside TVDOS:
```
A:\> playtev clip.tev
A:\> playtav clip.tav
A:\> playtad track.tad
```
## Repository layout
```
tsvm_core/ VM core, peripherals, VDC, JS bindings (Kotlin)
tsvm_executable/ Main emulator GUI (LibGDX)
TerranBASICexecutable/ For creatingTerranBASIC executable
assets/bios/ BIOS ROMs and source
assets/disk0/ Boot disk image, including all of TVDOS
video_encoder/ C encoders, decoder libs, inspectors (TEV / TAV / TAD)
ipf_encoder/ Reference iPF encoder
doc/ LaTeX sources for the TSVM / TVDOS manuals
buildapp/ Per-platform packaging scripts
My_BASIC_Programs/ Example BASIC programs
*.py, *.sh, *.kts Conversion tools and ad-hoc utilities
```
## Licence
See `COPYING`.

1059
TAUD_NOTE_EFFECTS.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -122,8 +122,39 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
private var rebootRequested = false
private fun reboot() {
vmRunner.close()
coroutineJob.interrupt()
// Order is critical: stop ALL execution first, then dispose peripherals
// before re-initialising. Without this, the old JS thread races the new
// one on shared VM memory / IO state and can SIGSEGV on disposed peripherals.
// 1. Stop parallel/child contexts. park() interrupts and joins them.
vm.park()
vm.poke(-90L, -128)
// 2. Interrupt the main runner thread and cancel the GraalVM context.
if (::coroutineJob.isInitialized) coroutineJob.interrupt()
try { if (::vmRunner.isInitialized) vmRunner.close() } catch (_: Throwable) {}
// 3. Wait for the main runner thread to actually finish.
if (::coroutineJob.isInitialized && coroutineJob !== Thread.currentThread()) {
try {
coroutineJob.join(2000L)
if (coroutineJob.isAlive) {
System.err.println("[VMGUI] runner ${vm.id} did not exit within 2s; proceeding anyway")
coroutineJob.interrupt()
}
}
catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
}
// 4. Now it's safe to release native resources held by peripherals.
for (i in 1 until vm.peripheralTable.size) {
try {
vm.peripheralTable[i].peripheral?.dispose()
}
catch (_: Throwable) {}
}
vm.init()
init()

Binary file not shown.

View File

@@ -1,5 +1,5 @@
con.reset_graphics();con.curs_set(0);con.clear();
graphics.resetPalette();graphics.setBackground(0,0,0);
graphics.resetPalette();graphics.setPalette(0, 0, 0, 0, 15);graphics.setBackground(0,0,0);
let logo = gzip.decomp(base64.atob("H4sICJoBTGECA3Rzdm1sb2dvLnJhdwDtneu2nCoQhPf7v6xLEMUL5lxyVk6yhxm7mmZGpfqnK7uC+gkN1TA/fhTFF+Ni8eOjwedPXsgLeSEvDPLCIC8M8sIgL+SFvJAX8kJeGOSFQV4Y5IVBXsgLeSEv5IW8MMgLow1e1i4XfH/kJR8deSEvcl48eSEvAC+RvJAXgJedvJAXOS9DR17Ii5yXSF7IC8DLTl7Ii5yX0JEX8iLnZSUv5EXOy7Nsl7yQF6h7IS/kBcheyAt5eYx+Jy/kRc7L0pEX8iLmZezIC3kR8zJ05IW8iHnxO3khL2JeDnAhL+Tlj8HoABfyQl6kqS55IS9/rrssHXkhL1Jewt6RF/Ii5GVYO4vYctouxGVLe2cXXvHg3TeN3eeu6rR9lRafl5ewGr3I6RHEOXXmMSse/PeSwTV7Vac9V2nxSXkZotmnv/ffvulYAZZ//h8HP/f+e0tC9qpK2+01WnxSXtZq372bu1oxwc/9u+mesld12lOVFp+Ul65SXtHHrl5s8HNfs+9vNdHeqrT4/rz8/kxC6mrGUJiR/hwfvIn2UKXFDfAyIhlgWSyFGenyopWo9lKlxffn5f9s122VcUHzx4casCF7VaXt9hotboCX+OsJpq56ROipj9mRczTRjlVa3AAvTmhym0QqykjHl3kqpp2qtPj+vKxY/1waoSAj/TlyDibaoUqLG+AlvG8w+h1PTUY6H+SpiPZapcX35yX18sWIN5tIDz2eP+oH5dq+Sosb4GV6z0RaY8lM2Q99MtGeq7S4AV4cOJqbm1XyjDQc5qli7X6v0uL787J8PfHv6sVobh3h2mOVFjfAi4fWIt5qIq3ZhZDVRHur0uL787J95auPTmAiPSwHOckikUx7qNLiBngZ35zsApZMzP5VNNFeqrT4/rz8zOTe3L3ILBnIOgK14aVJ3ES6Jy/z+7OX3+bwmHXUy/JUifZUpcUN8OIhJ+WtJhJmHWHaqUqL78/Lqkr+3mIi+ezI6U20Q5UWN8BL+ES2K7Nk5uzIOZtor1VafH9e/rOO0vt56RyakXp5nnqoXaXFDfAyfWLx5fe1N3lGugF5agQn6jYtboCXt1tHj664NCMdgZ7wQFvpfaS+dV6Wr8/MpgWWzJB9WYOJ9lilxQ3wMujWOt9hIi3ZwWAx0d6qtPj+vGyFz89k6UeY7TpsVdYbFUrJVS+wfxrBp2DxalIUf0gwXMytI5n2Ujp+t87LbrsQLk0TXlkye3adSG76vNAuqGqHTKT78vL6L3stL4cvZpIXSvXoPG4ytI503w55QeNoLTaJh7IJzrOSoXWkM5E4HqFxmFgO5tbRsXaZVzaQl2r57rFNswo7pkXhcq2G1pHKRLovL2Xz6T1tSwxOZQM7WaGUhwv6n2qXeh+OvNis16V5wBfeo6xQSrUqGw2tI42JdF9erPyAFB2onLdkZIVSq0b7kOBN1eK2eDH0G2eH9f5BkJHm99jvXqN9eKuDRrUxXkzrGWKPDHWr2jqKKu2jTmlRqTbGi229VArI7NVrC6W8Rlsww1eoNseLcT3mDKA4H2ZT69OruLZkBRFXbY4X63rvzYlX3x93ssv22AeNdi9xKPAWN8eLeQFvcmoTSWYd/XsV1j5EwZXZXs3wYl5ht3vpELAdZKTTi6uo9iYaalDVBnmxr/j+Zf2DJpLPLqjmr6LawlRWbXu1w0uFHUi/hiSsbEpWKLWotBdhx1FS6NUILxW2lGzS6mr3KiMdnl9FtQ/vcdSotslLjT0CMzApwayjDZrwwFO13iTjvTcvNc4jC7iJJLOORo1BBZifOturKV5qbFr777ECRo/QOurlC7ZBfoNeo9osLzU23Ue0bEp2PPOsKslCire0hV4t8VJjG5LDvmyxdfSF9xpQnwH0Re3yUuE8+BkzkWTHM6/Q0vSsKj43MJFuz0uN35tw0MxEbh3Bsx5wzmNgIt2flwq/ZxNlII7ZbDe/x/7b5ESoDW6eE6o2zov9kJSQlVXZ8cwRrD7eVGu20rXgtnmx/z2+QebcDLn1V/f19CriCg3SfwSrkpdatVOSzxuzjuTzukXVXRSbSI3wYvx7wklmyfydPz6svw7ZVdnhcPtJThtPRwSq5OXnVMLUS3LS6cmYJW18Oe2VaiumO8UmUjO8/J0zGA5KQbj80cv22E+KITT1muWUY1Xy8j8x0WpUisLl1Sk7wfWvp71C7cMO02tUA3n5Y4YwmyCzCC2ZlP3kZ9G66pH20dCymp4W0Cgv//QyIS5bKlvE25T+t3++897cWw86VUde8OgnoS+TFJhNwlWysp4wKVUjedHEa2B2XQXfUaGUZXVgVKq+znjJy7MeRvY/O/wHWQfpmkeRU/r0FMMyE+navPQf5wU6ZubZHvtnUXKEzaJWXa/MS61T6KzGI2jXrc9aR77Kjt5Br+ovzEu1U+iM8l2kgO/5Hnv74sCtQHW+MC8fOtUdeB3yk29D1joK6k5O2/OWlE2dnZflnLwsgCXzZ58UhNNeTBvyDUtMpLPzEs/JS1TUSrzaY29dhzEXqW7X5SWck5eAWDKwdQRrQylr0d77s/PizsmLw3Os/PHMS5X8bStUXS7Ly0d+tRNca5edoft6j/2z0P1q2lio+rzXOz0v8xl5mfGs9GCPvWnGe1gld6gaL8vLcEZeBjwpx6yjsoQ/Fqumy/JyxgEp4UkWaB2VJXCuXDVclpcTzqgjWoQk2WP/LPCfHlkNVNfL8nLCGZLDZ/2odVSyohAMVHd/VV7Ol/E+9gqHpdcpuxAvOoUdPvNIdO5Pr9x7fwFe3Om7F6ElA1lHehNpMlF9klpdgJezZTBRw/SIWkf678XZqI6X5aU/1RQp391LtqauAvDKPdfFSHW7LC/nMpGC1pIBrSOtieStVIfL8nKmlHdWWzJR2RFgJtJmprpcl5fzlE1takvGJ8n3W2wijWaq2f7vIry4k6QwyaktmUXdESAm0t7bqU7X5aXGKXQaI8/ZjZnyjgDRng1V04V5qXAKnQIXb1fatCOV6nJtb6kaLszLCYak5AyNHqQjkGuvpqrrlXmxP4UOTXWd5azfQ/cu1Q6mqpnh90K8fHhafdghQMuKG3bnQu3U26rGa/NifAodNBYJvlzE6Angncu0J2PVxyTrWrwYn0IHeEaSDxcwenZ0X6ZM21mrjhfnxfYUOvFQJHwPcqMnwvct0V7MVbfL82J5Cp1sJIrir1Zca7w7+K4l2oO9qr8+L19mp9AJYJmhdyCdwa2Kez7W3iqozrfg5cvmFLpXPUDalhjQbkBq9ATFDR9rjxVUv/eEl+WF8ZEgLwzywiAvDPLC509eyAt5IS8M8sIgLwzywiAv5IW8kBfyQl4Y5IVBXhjkhUFeyAt5IS/khbwwyAuDvDDIC+OWvPwFgd7gz8BmAQA="));
@@ -77,7 +77,7 @@ tmr = sys.nanoTime();
while (sys.nanoTime() - tmr < 2147483648) sys.spin();
// clear screen
graphics.clearPixels(255);con.color_pair(239,255);
con.clear();con.move(1,1);
con.clear();con.move(1,1);graphics.resetPalette();
///////////////////////////////////////////////////////////////////////////////

View File

@@ -5,4 +5,6 @@ set PATH=\tvdos\installer;\tvdos\tuidev;$PATH
set KEYBOARD=us_colemak
rem this line specifies which shell to be presented after the boot precess:
tvdos/i18n/korean
zfm
command -fancy

View File

@@ -8,31 +8,32 @@ if (!exec_args[1]) {
return 1
}
let lowfilename = exec_args[1] + "_low.chr"
let highfilename = exec_args[1] + "_high.chr"
const fullFilePath = _G.shell.resolvePathInput(exec_args[1]).full
let lowfilename = fullFilePath + "_low.chr"
let highfilename = fullFilePath + "_high.chr"
let workarea = sys.malloc(1920)
// dump low rom
sys.poke(-1299460, 16)
for (let i = 0; i < 1920; i++) {
let byte = sys.peek(-1300607 - i)
let byte = sys.peek(-133121 - i)
sys.poke(workarea + i, byte)
}
filesystem.open("A", lowfilename, "W")
dma.ramToCom(workarea, filesystem._toPorts("A")[0], 1920)
const lowfile = files.open(lowfilename)
lowfile.pwrite(workarea, 1920, 0)
println("Wrote CHR rom " + lowfilename)
// dump high rom
sys.poke(-1299460, 17)
for (let i = 0; i < 1920; i++) {
let byte = sys.peek(-1300607 - i)
let byte = sys.peek(-133121 - i)
sys.poke(workarea + i, byte)
}
filesystem.open("A", highfilename, "W")
dma.ramToCom(workarea, filesystem._toPorts("A")[0], 1920)
const highfile = files.open(highfilename)
highfile.pwrite(workarea, 1920, 0)
println("Wrote CHR rom " + highfilename)
sys.free(workarea)

View File

@@ -1,2 +1,2 @@
println("몬스터 시트라, 이 이름은 특이하게 생긴 프랑스 자동차나 스칸디나비아 보드카에서 온 것은 아닙니다. 고대부터 유래된 과일 시트론에서 영감을 받아 태어난 이 제품은 레몬과 비슷하지만 더 원초적이고 투박합니다. 마치 몬스터 에너지처럼요. 이 고대의 과일과 선조들에게서 영감을 얻은 우리는 전형적인 드링크를 새롭게 해석한 울트라 시트라를 만들었습니다. 울트라 시트라는 새콤달콤한 맛이 입안에서 잔잔하게 퍼지며 상쾌한 맛으로 마무리하죠. 저칼로리에 무설탕이지만 몬스터 에너지만의 블렌드는 변함없이 가득 담겨있답니다.")
println("멕시코에서는 매년 할로윈 이후 '죽은 자의 날'을 기념합니다. 신비한 분위기 속의 메리골드 꽃과 추억들은 떠난 이들을 축제로 이끕니다. 누구나 매혹될 이국적인 천사의 주스 블렌드, 망고 로코. 환상적인 맛과 몬스터 에너지 만의 마법으로 파티는 계속될 것입니다.")
unicode.println("몬스터 시트라, 이 이름은 특이하게 생긴 프랑스 자동차나 스칸디나비아 보드카에서 온 것은 아닙니다. 고대부터 유래된 과일 시트론에서 영감을 받아 태어난 이 제품은 레몬과 비슷하지만 더 원초적이고 투박합니다. 마치 몬스터 에너지처럼요. 이 고대의 과일과 선조들에게서 영감을 얻은 우리는 전형적인 드링크를 새롭게 해석한 울트라 시트라를 만들었습니다. 울트라 시트라는 새콤달콤한 맛이 입안에서 잔잔하게 퍼지며 상쾌한 맛으로 마무리하죠. 저칼로리에 무설탕이지만 몬스터 에너지만의 블렌드는 변함없이 가득 담겨있답니다.")
unicode.println("멕시코에서는 매년 할로윈 이후 '죽은 자의 날'을 기념합니다. 신비한 분위기 속의 메리골드 꽃과 추억들은 떠난 이들을 축제로 이끕니다. 누구나 매혹될 이국적인 천사의 주스 블렌드, 망고 로코. 환상적인 맛과 몬스터 에너지 만의 마법으로 파티는 계속될 것입니다.")

View File

@@ -1,6 +1,6 @@
if (exec_args[1] === undefined) {
println("Usage: compile -le/-lo myfile.js")
println(" The compiled and linked file will be myfile.out")
println(" The compiled and linked file will be myfile.exc")
return 1
}
@@ -14,7 +14,7 @@ if (exec_args[2]) {
_G.shell.execute(`rm ${tempFilename}.gz`)
_G.shell.execute(`link -${exec_args[1][2]} ${tempFilename}.bin`)
_G.shell.execute(`mv ${tempFilename}.out ${filenameWithoutExt}.out`)
_G.shell.execute(`mv ${tempFilename}.exc ${filenameWithoutExt}.exc`)
_G.shell.execute(`rm ${tempFilename}.bin`)
}
// with no linking

View File

@@ -1,6 +1,6 @@
if (exec_args[1] === undefined) {
println("Usage: decompile myfile.bin")
println("The compiled file will be myfile.bin.js")
println("Usage: decompile myfile.exc")
println("The compiled file will be myfile.exc.js")
return 1
}
_G.shell.execute(`enc ${exec_args[1]} ${exec_args[1]}.gz`)

View File

@@ -1,3 +1,5 @@
// a simple, symmetric obfuscator with infinite-length key
function seq(s) {
let out = ""
let cnt = 0

View File

@@ -11,7 +11,6 @@ let infile = files.open(infilePath)
if (!infile.exists) throw Error("No such file: " + infilePath)
let outfile = files.open(infilePath.substringBeforeLast(".") + ".out")
let outMode = exec_args[1].toLowerCase()
let type = {
@@ -21,6 +20,13 @@ let type = {
"-c": "\x04"
}
let ext = {
"-r": ".exc", // executable
"-e": ".exc", // executable
"-o": ".lib", // library
"-c": ".cob" // core object
}
function toI32(num) {
const buffer = new ArrayBuffer(4)
const view = new DataView(buffer)
@@ -40,6 +46,7 @@ let addr = 0
if (exec_args[3] !== undefined && exec_args[3].toLowerCase() == "-a" && exec_args[4] !== undefined)
addr = parseInt(exec_args[4], 16)
let outfile = files.open(infilePath.substringBeforeLast(".") + ext[exec_args[3].toLowerCase()])
outfile.sappend("\x20\xC0\xCC\x0A")
outfile.sappend(type[outMode] || "\x00")
outfile.bappend(toI24(addr))

View File

@@ -1,5 +1,5 @@
if (exec_args[1] === undefined) {
println("Usage: load myfile.out")
println("Usage: load myfile.exc")
println(" This will load the binary image onto the Core Memory")
return 1
}

View File

@@ -2,26 +2,18 @@ if (!exec_args[1]) {
printerrln("Usage: jpdectest image.jpg")
}
filesystem.open("A", exec_args[1], "R")
const fullFilePath = _G.shell.resolvePathInput(exec_args[1])
const file = files.open(fullFilePath.full)
const fileLen = file.size
const infile = sys.malloc(file.size); file.pread(infile, fileLen, 0)
let status = com.getStatusCode(0)
let infile = undefined
if (0 != status) return status
let fileLen = filesystem.getFileLen("A")
println(`DMA reading ${fileLen} bytes from disk...`)
infile = sys.malloc(fileLen)
dma.comToRam(0, 0, infile, fileLen)
println("decoding")
//println("decoding")
// decode
const [imgw, imgh, channels, imageData] = graphics.decodeImageResample(infile, fileLen, -1, -1)
println(`dim: ${imgw}x${imgh}`)
println(`converting to displayable format...`)
//println(`dim: ${imgw}x${imgh}`)
//println(`converting to displayable format...`)
// convert colour
graphics.setGraphicsMode(0)

View File

@@ -2,26 +2,18 @@ if (!exec_args[1]) {
printerrln("Usage: jpdectesthigh image.jpg")
}
filesystem.open("A", exec_args[1], "R")
const fullFilePath = _G.shell.resolvePathInput(exec_args[1])
const file = files.open(fullFilePath.full)
const fileLen = file.size
const infile = sys.malloc(file.size); file.pread(infile, fileLen, 0)
let status = com.getStatusCode(0)
let infile = undefined
if (0 != status) return status
let fileLen = filesystem.getFileLen("A")
println(`DMA reading ${fileLen} bytes from disk...`)
infile = sys.malloc(fileLen)
dma.comToRam(0, 0, infile, fileLen)
println("decoding")
//println("decoding")
// decode
const [imgw, imgh, channels, imageData] = graphics.decodeImageResample(infile, fileLen, -1, -1)
println(`dim: ${imgw}x${imgh}`)
println(`converting to displayable format...`)
//println(`dim: ${imgw}x${imgh}`)
//println(`converting to displayable format...`)
// convert colour
graphics.setGraphicsMode(4)

View File

@@ -4,7 +4,7 @@ music.pread(samples, 65534)
audio.setPcmMode(0)
audio.setMasterVolume(0, 255)
audio.putPcmDataByPtr(samples, 65534, 0)
audio.putPcmDataByPtr(0, samples, 65534, 0)
audio.setLoopPoint(0, 65534)
audio.play(0)*/
@@ -127,7 +127,7 @@ while (sampleSize > 0) {
let readLength = (sampleSize < BLOCK_SIZE) ? sampleSize : BLOCK_SIZE
readBytes(readLength, decodePtr)
audio.putPcmDataByPtr(decodePtr, readLength, 0)
audio.putPcmDataByPtr(0, decodePtr, readLength, 0)
audio.setSampleUploadLength(0, readLength)
audio.startSampleUpload(0)

View File

@@ -0,0 +1,21 @@
const taud = require("taud")
const fullFilePath = _G.shell.resolvePathInput(exec_args[1])
if (fullFilePath === undefined) {
println(`Usage: ${exec_args[0]} path_to.taud`)
return 1
}
const PLAYHEAD = 0
println("Playing "+fullFilePath.full)
audio.resetParams(PLAYHEAD)
audio.purgeQueue(PLAYHEAD)
audio.stop(PLAYHEAD)
taud.uploadTaudFile(fullFilePath.full, 0, PLAYHEAD)
audio.setMasterVolume(PLAYHEAD, 255)
audio.setMasterPan(PLAYHEAD, 128)
audio.setCuePosition(PLAYHEAD, 0)
audio.play(PLAYHEAD)

View File

@@ -0,0 +1,186 @@
// Tracker Mode — Bach's Prelude in C Major (BWV 846)
// Run from the TVDOS shell: js tracker_test.js
// Uploads ~92 patterns on startup (takes a moment).
// -- Note table (12-TET, 4096-TET encoding) ------------------------------------
// C3 = 0x4000; each semitone = 4096/12 ≈ 341.33 steps; each octave = 4096 steps.
// Sharp suffix: s (e.g. Cs3); flat aliases also provided (e.g. Db3 = Cs3).
// Special values: Note.OFF = key-off, Note.CUT = note cut, Note.NOP = no-op.
var Note = (function() {
var SEMITONE = 4096 / 12;
var C3 = 0x4000;
function n(oct, semi) { return Math.round(C3 + (oct - 3) * 4096 + semi * SEMITONE) & 0xFFFF; }
var t = {};
var names = ['C','Cs','D','Ds','E','F','Fs','G','Gs','A','As','B'];
var flats = ['C','Db','D','Eb','E','F','Gb','G','Ab','A','Bb','B'];
for (var oct = 0; oct <= 9; oct++) {
for (var s = 0; s < 12; s++) {
t[names[s] + oct] = n(oct, s);
if (flats[s] !== names[s]) t[flats[s] + oct] = n(oct, s);
}
}
t.OFF = 0x0000; // key-off
t.CUT = 0xFFFE; // note cut (immediate)
t.NOP = 0xFFFF; // no-op (empty row)
return t;
}());
var PLAYHEAD = 0;
// -- 1. Sample: triangle wave (256 samples @ C3) --------------------------------
var SAMPLE_LEN = 256;
var sampleBytes = new Array(SAMPLE_LEN);
for (var i = 0; i < SAMPLE_LEN; i++) {
var phase = (i / SAMPLE_LEN) * 2.0;
var val_ = phase < 1.0 ? phase : 2.0 - phase;
sampleBytes[i] = Math.round(val_ * 254) & 0xFF;
}
var memBase = audio.getMemAddr();
for (var i = 0; i < SAMPLE_LEN; i++) {
sys.poke(memBase - i, sampleBytes[i]);
}
// -- 2. Instrument 0 -----------------------------------------------------------
var instBytes = new Array(64).fill(0);
instBytes[2] = 0; instBytes[3] = 1; // sampleLength = 256
instBytes[4] = 0x00; instBytes[5] = 0x7D; // samplingRate = 32000
instBytes[10] = 0x00; instBytes[11] = 0x01; // sampleLoopEnd = 256 (whole sample)
instBytes[12] = 1; // loopMode = 1 (forward)
instBytes[16] = 255; instBytes[17] = 0; // envelope: vol=255, hold
audio.uploadInstrument(1, instBytes);
// -- 3. Piano-roll builder -----------------------------------------------------
// Source convention: C1=0, C2=12, C3=24, C4=36 (i.e. C3=24, octave every 12).
function midiToTsvm(n) {
var oct = Math.floor(n / 12) + 1;
return Math.round(0x3000 + oct * 4096 + (n % 12) * (4096 / 12)) & 0xFFFF;
}
var noteMap = {}; // absRow → TSVM note value
var rowCursor = 0;
function seq(notes, lens) {
for (var i = 0; i < notes.length; i++) {
noteMap[rowCursor] = midiToTsvm(notes[i]);
rowCursor += lens[i];
}
}
var TD = 3; // rows per note step (= source TICK_DIVISOR)
function prel(n1, n2, n3, n4, n5) {
seq([n1, n2, n3, n4, n5, n3, n4, n5, n1, n2, n3, n4, n5, n3, n4, n5],
[TD+2, TD, TD, TD-1, TD, TD, TD, TD, TD, TD, TD, TD-1, TD, TD, TD, TD]);
}
function end1(n1,n2,n3,n4,n5,n6,n7,n8,n9) {
seq([n1, n2, n3, n4, n5, n6, n5, n4, n5, n7, n8, n7, n8, n9, n8, n9],
[TD+2, TD, TD, TD-1, TD, TD, TD, TD, TD, TD, TD, TD-1, TD, TD, TD, TD]);
}
function end2(n1,n2,n3,n4,n5,n6,n7,n8,n9) {
seq([n1, n2, n3, n4, n5, n6, n5, n4, n5, n4, n3, n4, n7, n8, n9, n7],
[TD+2, TD+1, TD+1, TD+1, TD+1, TD+2, TD+2, TD+2,
TD+3, TD+3, TD+4, TD+4, TD+6, TD+8, TD+12, TD+24]);
}
function end3(ns) {
for (var i = 0; i < ns.length; i++) {
noteMap[rowCursor] = midiToTsvm(ns[i]);
rowCursor += 1;
}
for (var i = 0; i < TD*2; i++) {
noteMap[rowCursor] = Note.NOP
rowCursor += 1;
}
}
// -- 4. Build the piece --------------------------------------------------------
rowCursor = 16 * TD; // 160-row intro silence
prel(24,28,31,36,40);
prel(24,26,33,38,41);
prel(23,26,31,38,41);
prel(24,28,31,36,40);
prel(24,28,33,40,45);
prel(24,26,30,33,38);
prel(23,26,31,38,43);
prel(23,24,28,31,36);
prel(21,24,28,31,36);
prel(14,21,26,30,36);
prel(19,23,26,31,35);
prel(19,22,28,31,37);
prel(17,21,26,33,38);
prel(17,20,26,29,35);
prel(16,19,24,31,36);
prel(16,17,21,24,29);
prel(14,17,21,24,29);
prel( 7,14,19,23,29);
prel(12,16,19,24,28);
prel(12,19,22,24,28);
prel( 5,17,21,24,28);
prel( 6,12,21,24,27);
prel( 8,17,23,24,26);
prel( 7,17,19,23,26);
prel( 7,16,19,24,28);
prel( 7,14,19,24,29);
prel( 7,14,19,23,29);
prel( 7,15,21,24,30);
prel( 7,16,19,24,31);
prel( 7,14,19,24,29);
prel( 7,14,19,23,29);
prel( 0,12,19,22,28);
end1( 0,12,17,21,24,29,21,17,14);
end2( 0,11,31,35,38,41,26,29,28);
end3([0,12,28,31,36]);
noteMap[rowCursor] = Note.OFF; // key-off at start of final silence
rowCursor += 16 * TD - 5; // 155 more rows of silence
var totalRows = rowCursor; // 5836
var NUM_ROWS = 64;
var numPatterns = Math.ceil(totalRows / NUM_ROWS); // 92
// -- 5. Build and upload patterns ----------------------------------------------
for (var p = 0; p < numPatterns; p++) {
var patBytes = new Array(512).fill(0);
for (var r = 0; r < NUM_ROWS; r++) {
var absRow = p * NUM_ROWS + r;
var noteVal = (noteMap[absRow] !== undefined) ? noteMap[absRow] : Note.NOP;
var isOn = (noteVal !== Note.NOP && noteVal !== Note.OFF && noteVal !== Note.CUT);
var off = r * 8;
patBytes[off] = noteVal & 0xFF;
patBytes[off + 1] = (noteVal >> 8) & 0xFF;
patBytes[off + 2] = 1; // instrument 1
patBytes[off + 3] = 63; // volume
patBytes[off + 4] = 31; // pan (centre)
}
audio.uploadPattern(p, patBytes);
}
// -- 6. Cue sheet: one entry per pattern, last halts -------------------------
// Cue format: 32 bytes, 20 voices with 12-bit pattern numbers packed as:
// bytes 0-9: low nybbles (byte i = voice i*2 in hi-nybble, voice i*2+1 in lo-nybble)
// bytes 10-19: mid nybbles (same packing)
// bytes 20-29: high nybbles (same packing)
// byte 30: instruction (0=NOP, 1=Halt)
// Voice 0 plays pattern c; voices 1-19 are disabled (0xFFF).
for (var c = 0; c < numPatterns; c++) {
var cueBytes = new Array(32).fill(0xFF);
// voice 0 = c (12-bit), voice 1 = 0xFFF → byte0=(c&0xF)<<4|0xF
cueBytes[0] = ((c & 0xF) << 4) | 0xF; // lo nybbles v0,v1
cueBytes[10] = (((c >> 4) & 0xF) << 4) | 0xF; // mid nybbles v0,v1
cueBytes[20] = (((c >> 8) & 0xF) << 4) | 0xF; // hi nybbles v0,v1
cueBytes[30] = (c === numPatterns - 1) ? 0x01 : 0;
audio.uploadCue(c, cueBytes);
}
// -- 7. Playback ---------------------------------------------------------------
// BPM=500, tickRate=1: 1 row = 5 ms; 10 rows/step × 16 steps/bar ≈ 75 bars/min.
audio.setTrackerMode(PLAYHEAD);
audio.setBPM(PLAYHEAD, 250);
audio.setTickRate(PLAYHEAD, 6);
audio.setMasterVolume(PLAYHEAD, 255);
audio.setMasterPan(PLAYHEAD, 128);
audio.setCuePosition(PLAYHEAD, 0);
audio.play(PLAYHEAD);
println("Bach's Prelude in C Major -- " + numPatterns + " patterns loaded.");
println("Stop: audio.stop(" + PLAYHEAD + ")");

View File

@@ -1,7 +1,7 @@
///////////////////////////////////////////////////////////////////////////////
// High Speed Disk Peripheral Adapter (HSDPA) Driver for TVDOS
// This driver treats each disk from HSDPA as a single large file
// Created by Claude on 2025-08-16
// Created by CuriousTorvald and Claude on 2025-08-16
///////////////////////////////////////////////////////////////////////////////
// Add TAPE device names to reserved names
@@ -117,8 +117,9 @@ for (let tapeIndex = 0; tapeIndex < 4; tapeIndex++) {
// Get file size - for HSDPA tapes, we don't know the size ahead of time
// So we return a very large number to indicate it's available
// Using Number.MAX_SAFE_INTEGER to support files >2GB
driver.getFileLen = (fd) => {
return 0x7FFFFFFF // Return max positive 32-bit integer
return Number.MAX_SAFE_INTEGER // 2^53 - 1 (9007199254740991) - safe for JS arithmetic
}
// Sequential read from tape

View File

@@ -147,7 +147,7 @@ _TVDOS.variables = {
LANG: "EN",
KEYBOARD: "us_qwerty",
PATH: "\\tvdos\\bin;\\home",
PATHEXT: ".com;.bat;.app;.js",
PATHEXT: ".com;.bat;.app;.js;.alias",
HELPPATH: "\\tvdos\\help",
OS_NAME: "TSVM Disk Operating System",
OS_VERSION: _TVDOS.VERSION
@@ -225,8 +225,9 @@ class TVDOSFileDescriptor {
}
/** reads the file bytewise and puts it to the specified memory address
* @param count optional -- how many bytes to read
* @param offset optional -- how many bytes to skip initially
* @param ptr -- where the bytes should be dumped
* @param count -- how many bytes to read
* @param offset -- how many bytes to skip initially from the file
*/
pread(ptr, count, offset) {
this.driver.pread(this, ptr, count, offset)
@@ -241,7 +242,9 @@ class TVDOSFileDescriptor {
}
/** writes the bytes stored in the memory[ptr .. ptr+count-1] to file[offset .. offset+count-1]
* - @param offset is optional
* @param ptr -- where the bytes are
* @param count -- how many bytes to write
* @param offset -- position in the file
*/
pwrite(ptr, count, offset) {
this.driver.pwrite(this, ptr, count, offset)
@@ -420,11 +423,11 @@ _TVDOS.DRV.FS.SERIAL.sread = (fd) => {
}
_TVDOS.DRV.FS.SERIAL.bread = (fd) => {
let str = _TVDOS.DRV.FS.SERIAL.sread(fd)
let bytes = new Int8Array(str.length)
let bytes = []//new Int8Array(str.length)
for (let i = 0; i < str.length; i++) {
// let p = str.charCodeAt(i)
// bytes[i] = (p > 127) ? p - 255 : p
bytes[i] = str.charCodeAt(i)
bytes.push(str.charCodeAt(i))
}
return bytes
}
@@ -870,11 +873,21 @@ Object.freeze(_TVDOS.DRV.FS.DEVPT)
_TVDOS.DRV.FS.DEVFBIPF = {}
_TVDOS.DRV.FS.DEVFBIPF.pwrite = (fd, infilePtr, count, _2) => {
let decodefun = ([graphics.decodeIpf1, graphics.decodeIpf2])[sys.peek(infilePtr + 13)]
let flags = sys.peek(infilePtr+12)
let ipfType = sys.peek(infilePtr+13)
let isProgressive = (flags & 0x80) != 0
let hasAlpha = (flags & 0x01) != 0
// Select decode function based on type and progressive flag
let decodefun
if (isProgressive) {
decodefun = ([graphics.decodeIpf1Progressive, graphics.decodeIpf2Progressive])[ipfType]
} else {
decodefun = ([graphics.decodeIpf1, graphics.decodeIpf2])[ipfType]
}
let width = sys.peek(infilePtr+8) | (sys.peek(infilePtr+9) << 8)
let height = sys.peek(infilePtr+10) | (sys.peek(infilePtr+11) << 8)
let hasAlpha = (sys.peek(infilePtr+12) != 0)
let ipfType = sys.peek(infilePtr+13)
let imgLen = sys.peek(infilePtr+24) | (sys.peek(infilePtr+25) << 8) | (sys.peek(infilePtr+26) << 16) | (sys.peek(infilePtr+27) << 24)
let ipfbuf = sys.malloc(imgLen)
@@ -1014,136 +1027,6 @@ _TVDOS.DRV.FS.NET.exists = (fd) => {
return (0 == com.getStatusCode(port[0]))
}
///////////////////////////////////////////////////////////////////////////////
// Legacy Serial filesystem, !!pending for removal!!
/*const filesystem = {};
filesystem._toPorts = (driveLetter) => {
if (driveLetter.toUpperCase === undefined) {
throw Error("'"+driveLetter+"' (type: "+typeof driveLetter+") is not a valid drive letter");
}
var port = _TVDOS.DRIVES[driveLetter.toUpperCase()];
if (port === undefined) {
throw Error("Drive letter '" + driveLetter.toUpperCase() + "' does not exist");
}
return port
};
filesystem._close = (portNo) => {
com.sendMessage(portNo, "CLOSE")
}
filesystem._flush = (portNo) => {
com.sendMessage(portNo, "FLUSH")
}
// @return disk status code (0 for successful operation)
// throws if:
// - java.lang.NullPointerException if path is null
// - Error if operation mode is not "R", "W" nor "A"
filesystem.open = (driveLetter, path, operationMode) => {
var port = filesystem._toPorts(driveLetter);
filesystem._flush(port[0]); filesystem._close(port[0]);
var mode = operationMode.toUpperCase();
if (mode != "R" && mode != "W" && mode != "A") {
throw Error("Unknown file opening mode: " + mode);
}
com.sendMessage(port[0], "OPEN"+mode+'"'+path+'",'+port[1]);
return com.getStatusCode(port[0]);
};
filesystem.getFileLen = (driveLetter) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "GETLEN");
var response = com.getStatusCode(port[0]);
if (135 == response) {
throw Error("File not opened");
}
if (response < 0 || response >= 128) {
throw Error("Reading a file failed with "+response);
}
return Number(com.pullMessage(port[0]));
};
// @return the entire contents of the file in String
filesystem.readAll = (driveLetter) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "READ");
var response = com.getStatusCode(port[0]);
if (135 == response) {
throw Error("File not opened");
}
if (response < 0 || response >= 128) {
throw Error("Reading a file failed with "+response);
}
return com.pullMessage(port[0]);
};
filesystem.readAllBytes = (driveLetter) => {
var str = filesystem.readAll(driveLetter);
var bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes;
};
filesystem.write = (driveLetter, string) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "WRITE"+string.length);
var response = com.getStatusCode(port[0]);
if (135 == response) {
throw Error("File not opened");
}
if (response < 0 || response >= 128) {
throw Error("Writing a file failed with "+response);
}
com.sendMessage(port[0], string);
filesystem._flush(port[0]); filesystem._close(port[0]);
};
filesystem.writeBytes = (driveLetter, bytes) => {
var string = btostr(bytes); // no spreading: has length limit
filesystem.write(driveLetter, string);
};
filesystem.isDirectory = (driveLetter) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "LISTFILES");
var response = com.getStatusCode(port[0]);
return (response === 0);
};
filesystem.mkDir = (driveLetter) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "MKDIR");
var response = com.getStatusCode(port[0]);
if (response < 0 || response >= 128) {
var status = com.getDeviceStatus(port[0]);
throw Error("Creating a directory failed with ("+response+"): "+status.message+"\n");
}
return (response === 0); // possible status codes: 0 (success), 1 (fail)
};
filesystem.touch = (driveLetter) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "TOUCH");
var response = com.getStatusCode(port[0]);
return (response === 0);
};
filesystem.mkFile = (driveLetter) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "MKFILE");
var response = com.getStatusCode(port[0]);
return (response === 0);
};
filesystem.remove = (driveLetter) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "DELETE");
var response = com.getStatusCode(port[0]);
return (response === 0);
};
Object.freeze(filesystem);*/
///////////////////////////////////////////////////////////////////////////////
const files = {}
@@ -1366,12 +1249,12 @@ unicode.getUniprint = (c) => {
return unicode.uniprint[k]
}}
print = function(str) {
unicode.print = (str) => {
if ((typeof str === 'string' || str instanceof String) && str.length > 0) {
let cp = unicode.utf8toCodepoints(str)
cp.forEach(c => {
let q = unicode.getUniprint(c)
if (q == undefined || !q[0](c)) {
con.addch(4)
con.curs_right()
@@ -1381,6 +1264,34 @@ print = function(str) {
}
})
}
else {
sys.print(str)
}
}
unicode.println = (str) => {
unicode.print(str+'\n\n')
}
unicode.strlen = (str) => {
// Convert string to an array of codepoints using spread operator
// This correctly handles surrogate pairs and counts each codepoint as one
return unicode.utf8toCodepoints(str).length
}
unicode.visualStrlen = (str) => {
function isTripleWidth(c) {
return (0xAC00 <= c && c <= 0xD7FF) && [1,4,8,10,13].includes(((c - 0xAC00) / 588)|0)
}
function isDoubleWidth(c) {
return (0x3000 <= c && c <= 0x303f) || (0x3100 <= c && c <= 0x312f) || (0x3200 <= c && c <= 0x33ff) ||
(0xAC00 <= c && c <= 0xD7FF) || (0xFE30 <= c && c <= 0xFE4F) || (0xFF00 <= c && c <= 0xff60)
}
// Convert string to an array of codepoints using spread operator
// This correctly handles surrogate pairs and counts each codepoint as one
return unicode.utf8toCodepoints(str).reduce((acc, c) => acc + (isTripleWidth(c) ? 3 : isDoubleWidth(c) ? 2 : 1), 0)
}
Object.freeze(unicode);

View File

@@ -35,7 +35,7 @@ function print_prompt_text() {
print(" "+CURRENT_DRIVE+":")
con.color_pair(161,253)
con.addch(16);con.curs_right()
con.color_pair(0,253)
con.color_pair(240,253)
print(" \\"+shell_pwd.join("\\").substring(1)+" ")
if (errorlevel != 0 && errorlevel != "undefined" && errorlevel != undefined) {
con.color_pair(166,253)
@@ -61,7 +61,7 @@ function greet() {
con.clear()
con.color_pair(253,255)
print(' ');con.addch(17);con.curs_right()
con.color_pair(0,253)
con.color_pair(240,253)
print(" ".repeat(greetLeftPad)+welcome_text+" ".repeat(greetRightPad))
con.color_pair(253,255)
con.addch(16);con.curs_right();print(' ')
@@ -753,6 +753,25 @@ shell.execute = function(line) {
shell.execute(line)
})
}
else if ("ALIAS" == extension) {
// parse alias
// $0: all arguments
// $1..9: specific arguments
var lines = programCode.split('\n').filter(function(it) { return it.length > 0 }) // this return is not shell's return!
lines.forEach(function(line) {
var newLine = line
// replace $1..$9
for (let j = 1; j < 9; j++) {
newLine = newLine.replaceAll('$'+j, tokens[j])
}
// replace $0
newLine = newLine.replaceAll('$0', tokens.slice(1).join(' '))
shell.execute(newLine)
})
}
else if ("APP" == extension) {
let appexec = `A:${_TVDOS.variables.DOSDIR}\\sbin\\appexec.js`
let foundFile = searchFile.fullPath

View File

@@ -0,0 +1,5 @@
/**
* Hopper is a package manager for TSVM
* Created by CuriousTorvald on 2026-04-16
*/

View File

@@ -0,0 +1 @@
taut $0

View File

@@ -1,236 +0,0 @@
println("DEPRECATION NOTICE: MP3 Playback function will be removed for following reason")
println("\tMP3 does not really fit in the time TSVM targets to emulate")
return 1
const Mp3 = require('mp3dec')
const pcm = require("pcm")
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
function printdbg(s) { if (0) serial.println(s) }
class SequentialFileBuffer {
constructor(path, offset, length) {
if (Array.isArray(path)) throw Error("arg #1 is path(string), not array")
this.path = path
this.file = files.open(path)
this.offset = offset || 0
this.originalOffset = offset
this.length = length || this.file.size
this.seq = require("seqread")
this.seq.prepare(path)
}
/*readFull(n) {
throw Error()
let ptr = this.seq.readBytes(n)
return ptr
}*/
readStr(n) {
let ptr = this.seq.readBytes(n)
let s = ''
for (let i = 0; i < n; i++) {
if (i >= this.length) break
s += String.fromCharCode(sys.peek(ptr + i))
}
sys.free(ptr)
return s
}
readByteNumbers(n) {
let ptr = this.seq.readBytes(n)
try {
let s = []
for (let i = 0; i < n; i++) {
if (i >= this.length) break
s.push(sys.peek(ptr + i))
}
sys.free(ptr)
return s
}
catch (e) {
println(`n: ${n}; ptr: ${ptr}`)
println(e)
}
}
unread(diff) {
let newSkipLen = this.seq.getReadCount() - diff
this.seq.prepare(this.path)
this.seq.skip(newSkipLen)
}
rewind() {
this.seq.prepare(this.path)
}
seek(p) {
this.seq.prepare(this.path)
this.seq.skip(p)
}
get byteLength() {
return this.length
}
/*get remaining() {
return this.length - this.getReadCount()
}*/
}
con.curs_set(0)
let [cy, cx] = con.getyx()
let [__, CONSOLE_WIDTH] = con.getmaxyx()
let paintWidth = CONSOLE_WIDTH - 16
if (interactive) {
println("Decoding...")
}
printdbg("pre-decode...")
let filebuf = new SequentialFileBuffer(_G.shell.resolvePathInput(exec_args[1]).full)
const FILE_SIZE = filebuf.length
let decoder = Mp3.newDecoder(filebuf)
if (decoder === null) throw Error("decoder is null")
const HEADER_SIZE = decoder.headerSize + 3
const FRAME_SIZE = decoder.frameSize // only works reliably for CBR
//serial.println(`header size: ${HEADER_SIZE}`)
//serial.println(`frame size: ${FRAME_SIZE}`)
audio.resetParams(0)
audio.purgeQueue(0)
audio.setPcmMode(0)
audio.setPcmQueueCapacityIndex(0, 5) // queue size is now 24
const QUEUE_MAX = audio.getPcmQueueCapacity(0)
audio.setMasterVolume(0, 255)
audio.play(0)
let decodedLength = 0
let readPtr = sys.malloc(8000)
let decodePtr = sys.malloc(12000)
function bytesToSec(i) {
return i / (FRAME_SIZE * 1000 / bufRealTimeLen)
}
function secToReadable(n) {
let mins = ''+((n/60)|0)
let secs = ''+(n % 60)
return `${mins.padStart(2,'0')}:${secs.padStart(2,'0')}`
}
function decodeAndResample(inPtr, outPtr, inputLen) {
// TODO resample
for (let k = 0; k < inputLen / 2; k+=2) {
let sample = [
pcm.u16Tos16(sys.peek(inPtr + k*2 + 0) | (sys.peek(inPtr + k*2 + 1) << 8)),
pcm.u16Tos16(sys.peek(inPtr + k*2 + 2) | (sys.peek(inPtr + k*2 + 3) << 8))
]
sys.poke(outPtr + k, pcm.s16Tou8(sample[0]))
sys.poke(outPtr + k + 1, pcm.s16Tou8(sample[1]))
// soothing visualiser(????)
// printvis(`${sampleToVisual(sample[0])} | ${sampleToVisual(sample[1])}`)
}
}
function printPlayBar() {
}
let stopPlay = false
con.curs_set(0)
if (interactive) {
con.move(cy, cy)
println("Push and hold Backspace to exit")
}
[cy, cx] = con.getyx()
function printPlayBar(currently) {
if (interactive) {
// let currently = decodedLength
let total = FILE_SIZE - HEADER_SIZE
let currentlySec = Math.round(bytesToSec(currently))
let totalSec = Math.round(bytesToSec(total))
con.move(cy, 1)
print(' '.repeat(15))
con.move(cy, 1)
print(`${secToReadable(currentlySec)} / ${secToReadable(totalSec)}`)
con.move(cy, 15)
print(' ')
let progressbar = '\x84205u'.repeat(paintWidth + 1)
print(progressbar)
con.mvaddch(cy, 16 + Math.round(paintWidth * (currently / total)), 0xDB)
}
}
let t1 = sys.nanoTime()
let errorlevel = 0
let bufRealTimeLen = 36
try {
decoder.decode((ptr, len, pos)=>{
if (interactive) {
sys.poke(-40, 1)
if (sys.peek(-41) == 67) {
stopPlay = true
throw "STOP"
}
}
printPlayBar(pos)
let t2 = sys.nanoTime()
decodedLength += len
// serial.println(`Audio queue size: ${audio.getPosition(0)}/${QUEUE_MAX}`)
if (audio.getPosition(0) >= QUEUE_MAX) {
while (audio.getPosition(0) >= (QUEUE_MAX >>> 1)) {
printdbg(`Queue full, waiting until the queue has some space (${audio.getPosition(0)}/${QUEUE_MAX})`)
// serial.println(`Queue full, waiting until the queue has some space (${audio.getPosition(0)}/${QUEUE_MAX})`)
sys.sleep(bufRealTimeLen)
}
}
decodeAndResample(ptr, decodePtr, len)
audio.putPcmDataByPtr(decodePtr, len >> 1, 0)
audio.setSampleUploadLength(0, len >> 1)
audio.startSampleUpload(0)
let decodingTime = (t2 - t1) / 1000000.0
bufRealTimeLen = (len >> 1) / 64000.0 * 1000
t1 = t2
printdbg(`Decoded ${decodedLength} bytes; target: ${bufRealTimeLen} ms, lag: ${decodingTime - bufRealTimeLen} ms`)
}) // now you got decoded PCM data
}
catch (e) {
if (e != "STOP") {
printerrln(e)
errorlevel = 1
}
}
finally {
//audio.stop(0)
sys.free(readPtr)
sys.free(decodePtr)
}
return errorlevel

View File

@@ -1,4 +1,4 @@
// usage: playmov moviefile.mov [/i]
// usage: playmv1 moviefile.mv1 [/i]
const SND_BASE_ADDR = audio.getBaseAddr()
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
const WIDTH = 560
@@ -326,7 +326,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_LENGTH) {
// RAW PCM packets (decode on the fly)
else if (packetType == 0x1000 || packetType == 0x1001) {
let frame = seqread.readBytes(readLength)
audio.putPcmDataByPtr(frame, readLength, 0)
audio.putPcmDataByPtr(0, frame, readLength, 0)
audio.setSampleUploadLength(0, readLength)
audio.startSampleUpload(0)
sys.free(frame)

View File

@@ -162,7 +162,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_SIZE && readLength > 0) {
seqread.readBytes(readLength, readPtr)
audio.putPcmDataByPtr(readPtr, readLength, 0)
audio.putPcmDataByPtr(0, readPtr, readLength, 0)
audio.setSampleUploadLength(0, readLength)
audio.startSampleUpload(0)

View File

@@ -0,0 +1,361 @@
const SND_BASE_ADDR = audio.getBaseAddr()
const SND_MEM_ADDR = audio.getMemAddr()
const TAD_INPUT_ADDR = SND_MEM_ADDR - 262144 // TAD input buffer (matches TAV packet 0x24)
const TAD_DECODED_ADDR = SND_MEM_ADDR - 262144 + 65536 // TAD decoded buffer
if (!SND_BASE_ADDR) return 10
// Check for help flag or missing arguments
if (!exec_args[1] || exec_args[1] == "-h" || exec_args[1] == "--help") {
serial.println("Usage: playtad <file.tad> [-i | -d] [quality]")
serial.println(" -i Interactive mode (progress bar, press Backspace to exit)")
serial.println(" -d Dump mode (show first 3 chunks with payload hex and decoded samples)")
serial.println("")
serial.println("Examples:")
serial.println(" playtad audio.tad -i # Play with progress bar")
serial.println(" playtad audio.tad -d # Dump first 3 chunks for debugging")
return 0
}
const pcm = require("pcm")
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
const dumpCoeffs = exec_args[2] && exec_args[2].toLowerCase() == "-d"
function printdbg(s) { if (0) serial.println(s) }
class SequentialFileBuffer {
constructor(path, offset, length) {
if (Array.isArray(path)) throw Error("arg #1 is path(string), not array")
this.path = path
this.file = files.open(path)
this.offset = offset || 0
this.originalOffset = offset
this.length = length || this.file.size
this.seq = require("seqread")
this.seq.prepare(path)
}
readBytes(size, ptr) {
return this.seq.readBytes(size, ptr)
}
readByte() {
let ptr = this.seq.readBytes(1)
let val = sys.peek(ptr)
sys.free(ptr)
return val
}
readShort() {
let ptr = this.seq.readBytes(2)
let val = sys.peek(ptr) | (sys.peek(ptr + 1) << 8)
sys.free(ptr)
return val
}
readInt() {
let ptr = this.seq.readBytes(4)
let val = sys.peek(ptr) | (sys.peek(ptr + 1) << 8) | (sys.peek(ptr + 2) << 16) | (sys.peek(ptr + 3) << 24)
sys.free(ptr)
return val
}
readStr(n) {
let ptr = this.seq.readBytes(n)
let s = ''
for (let i = 0; i < n; i++) {
if (i >= this.length) break
s += String.fromCharCode(sys.peek(ptr + i))
}
sys.free(ptr)
return s
}
unread(diff) {
let newSkipLen = this.seq.getReadCount() - diff
this.seq.prepare(this.path)
this.seq.skip(newSkipLen)
}
rewind() {
this.seq.prepare(this.path)
}
seek(p) {
this.seq.prepare(this.path)
this.seq.skip(p)
}
get byteLength() {
return this.length
}
get fileHeader() {
return this.seq.fileHeader
}
getReadCount() {
return this.seq.getReadCount()
}
}
// Read TAD chunk header to determine format
let filebuf = new SequentialFileBuffer(_G.shell.resolvePathInput(exec_args[1]).full)
const FILE_SIZE = filebuf.length
if (FILE_SIZE < 7) {
serial.println(`ERROR: File too small (${FILE_SIZE} bytes). Expected TAD format.`)
return 1
}
// Read first chunk header (standalone TAD format: no TAV wrapper)
let firstSampleCount = filebuf.readShort()
let firstMaxIndex = filebuf.readByte()
let firstPayloadSize = filebuf.readInt()
// Validate first chunk
if (firstSampleCount < 0 || firstSampleCount > 65536) {
serial.println(`ERROR: Invalid sample count ${firstSampleCount}. File may be corrupted.`)
return 1
}
if (firstMaxIndex < 0 || firstMaxIndex > 255) {
serial.println(`ERROR: Invalid max index ${firstMaxIndex}. File may be corrupted.`)
return 1
}
if (firstPayloadSize < 1 || firstPayloadSize > 65536) {
serial.println(`ERROR: Invalid payload size ${firstPayloadSize}. File may be corrupted.`)
return 1
}
// Rewind to start
filebuf.rewind()
// Calculate approximate frame info
const AVG_CHUNK_SIZE = 7 + firstPayloadSize // TAD header (2+1+4) + payload
const SAMPLE_RATE = 32000
const bufRealTimeLen = Math.floor((firstSampleCount / SAMPLE_RATE) * 1000) // milliseconds per chunk
if (dumpCoeffs) {
serial.println(`TAD Coefficient Dump Mode`)
serial.println(`File: ${filebuf.file.name}`)
serial.println(`First chunk header:`)
serial.println(` Sample Count: ${firstSampleCount}`)
serial.println(` Max Index: ${firstMaxIndex}`)
serial.println(` Payload Size: ${firstPayloadSize} bytes`)
serial.println(`Chunk Duration: ${bufRealTimeLen} ms`)
serial.println(``)
}
let bytes_left = FILE_SIZE
let decodedLength = 0
let chunkNumber = 0
con.curs_set(0)
let [__, CONSOLE_WIDTH] = con.getmaxyx()
if (interactive) {
let [cy, cx] = con.getyx()
// file name
con.mvaddch(cy, 1)
con.prnch(0xC9);con.prnch(0xCD);con.prnch(0xB5)
print(filebuf.file.name)
con.prnch(0xC6);con.prnch(0xCD)
print("\x84205u".repeat(CONSOLE_WIDTH - 26 - filebuf.file.name.length))
con.prnch(0xB5)
print("Hold Bksp to Exit")
con.prnch(0xC6);con.prnch(0xCD);con.prnch(0xBB)
// L R pillar
con.prnch(0xBA)
con.mvaddch(cy+1, CONSOLE_WIDTH, 0xBA)
// media info
let mediaInfoStr = `TAD Q${firstMaxIndex} ${SAMPLE_RATE/1000}kHz`
con.move(cy+2,1)
con.prnch(0xC8)
print("\x84205u".repeat(CONSOLE_WIDTH - 5 - mediaInfoStr.length))
con.prnch(0xB5)
print(mediaInfoStr)
con.prnch(0xC6);con.prnch(0xCD);con.prnch(0xBC)
con.move(cy+1, 2)
}
let [cy, cx] = con.getyx()
let paintWidth = CONSOLE_WIDTH - 20
function bytesToSec(i) {
// Approximate: use first chunk's ratio
return Math.round((i / FILE_SIZE) * (FILE_SIZE / AVG_CHUNK_SIZE) * (bufRealTimeLen / 1000))
}
function secToReadable(n) {
let mins = ''+((n/60)|0)
let secs = ''+(n % 60)
return `${mins.padStart(2,'0')}:${secs.padStart(2,'0')}`
}
function printPlayBar() {
if (interactive) {
let currently = decodedLength
let total = FILE_SIZE
let currentlySec = bytesToSec(currently)
let totalSec = bytesToSec(total)
con.move(cy, 3)
print(' '.repeat(15))
con.move(cy, 3)
print(`${secToReadable(currentlySec)} / ${secToReadable(totalSec)}`)
con.move(cy, 17)
print(' ')
let progressbar = '\x84196u'.repeat(paintWidth + 1)
print(progressbar)
con.mvaddch(cy, 18 + Math.round(paintWidth * (currently / total)), 0xDB)
}
}
audio.resetParams(0)
audio.purgeQueue(0)
audio.setPcmMode(0)
audio.setPcmQueueCapacityIndex(0, 2) // queue size is now 8
const QUEUE_MAX = audio.getPcmQueueCapacity(0)
audio.setMasterVolume(0, 255)
audio.play(0)
let stopPlay = false
let errorlevel = 0
try {
while (bytes_left > 0 && !stopPlay) {
if (interactive) {
sys.poke(-40, 1)
if (sys.peek(-41) == 67) { // Backspace key
stopPlay = true
}
}
printPlayBar()
// Read TAD chunk header (standalone TAD format)
// Format: [sample_count][max_index][payload_size][payload]
let sampleCount = filebuf.readShort()
let maxIndex = filebuf.readByte()
let payloadSize = filebuf.readInt()
// Validate every chunk (not just first one)
if (sampleCount < 0 || sampleCount > 65536) {
serial.println(`ERROR: Chunk ${chunkNumber}: Invalid sample count ${sampleCount}. File may be corrupted.`)
errorlevel = 1
break
}
if (maxIndex < 0 || maxIndex > 255) {
serial.println(`ERROR: Chunk ${chunkNumber}: Invalid max index ${maxIndex}. File may be corrupted.`)
errorlevel = 1
break
}
if (payloadSize < 1 || payloadSize > 65536) {
serial.println(`ERROR: Chunk ${chunkNumber}: Invalid payload size ${payloadSize}. File may be corrupted.`)
errorlevel = 1
break
}
if (payloadSize + 7 > bytes_left) {
serial.println(`ERROR: Chunk ${chunkNumber}: Chunk size ${payloadSize + 7} exceeds remaining file size ${bytes_left}`)
errorlevel = 1
break
}
if (dumpCoeffs && chunkNumber < 3) {
serial.println(`=== Chunk ${chunkNumber} ===`)
serial.println(` Sample Count: ${sampleCount}`)
serial.println(` Max Index: ${maxIndex}`)
serial.println(` Payload Size: ${payloadSize} bytes`)
serial.println(` Bytes remaining in file: ${bytes_left}`)
}
// Rewind 7 bytes to re-read the header along with payload
// This allows reading the complete chunk (header + payload) in one call
filebuf.unread(7)
// Read entire chunk (header + payload) to TAD input buffer
// This matches TAV's approach for packet 0x24
let totalChunkSize = 7 + payloadSize
filebuf.readBytes(totalChunkSize, TAD_INPUT_ADDR)
if (dumpCoeffs && chunkNumber < 3) {
// Dump first 32 bytes of compressed payload (skip 7-byte header)
serial.print(` Compressed data (first 32 bytes): `)
for (let i = 0; i < Math.min(32, payloadSize); i++) {
let b = sys.peek(TAD_INPUT_ADDR + 7 + i)
serial.print(`${(b & 0xFF).toString(16).padStart(2, '0')} `)
}
serial.println('')
}
// Decode TAD chunk
audio.tadDecode()
if (dumpCoeffs && chunkNumber < 3) {
// After decoding, the decoded PCMu8 samples are in tadDecodedBin
serial.println(` Decoded ${sampleCount} samples`)
// Dump first 16 decoded samples (PCMu8 stereo interleaved)
serial.print(` Decoded (first 16 L samples): `)
for (let i = 0; i < 16; i++) {
serial.print(`${sys.peek(TAD_DECODED_ADDR + i * 2) & 0xFF} `)
}
serial.println('')
serial.print(` Decoded (first 16 R samples): `)
for (let i = 0; i < 16; i++) {
serial.print(`${sys.peek(TAD_DECODED_ADDR + i * 2 + 1) & 0xFF} `)
}
serial.println('')
serial.println('')
}
// Upload decoded audio to queue
audio.tadUploadDecoded(0, sampleCount)
if (!dumpCoeffs) {
// Sleep for the duration of the audio chunk to pace playback
// This prevents uploading everything at once
sys.sleep(bufRealTimeLen)
}
// Chunk size = header (7 bytes) + payload
let chunkSize = 7 + payloadSize
bytes_left -= chunkSize
decodedLength += chunkSize
chunkNumber++
// Limit coefficient dump to first 3 chunks
if (dumpCoeffs && chunkNumber >= 3) {
serial.println(`... (remaining chunks omitted)`)
// Keep playing but don't dump more
}
}
}
catch (e) {
printerrln(e)
errorlevel = 1
}
finally {
if (interactive) {
con.move(cy + 3, 1)
con.curs_set(1)
}
}
return errorlevel

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,17 @@
// Created by Claude on 2025-08-18.
// Created by CuriousTorvald and Claude on 2025-08-18.
// TSVM Enhanced Video (TEV) Format Decoder - YCoCg-R 4:2:0 Version
// Usage: playtev moviefile.tev [options]
// Options: -i (interactive), -debug-mv (show motion vector debug visualization)
// -deinterlace=algorithm (yadif or bwdif, default: yadif)
// -nodeblock (disble deblocking filter)
// -nodeblock (disable post-processing deblocking filter)
// -boundaryaware (enable boundary-aware decoding to prevent artifacts at DCT level)
const WIDTH = 560
const HEIGHT = 448
const BLOCK_SIZE = 16 // 16x16 blocks for YCoCg-R
const TEV_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x45, 0x56] // "\x1FTSVM TEV"
const TEV_VERSION_YCOCG = 2 // YCoCg-R version
const TEV_VERSION_XYB = 3 // XYB version
const TEV_VERSION_ICtCp = 3 // ICtCp version
const SND_BASE_ADDR = audio.getBaseAddr()
const pcm = require("pcm")
const MP2_FRAME_SIZE = [144,216,252,288,360,432,504,576,720,864,1008,1152,1440,1728]
@@ -25,7 +26,8 @@ const TEV_MODE_MOTION = 0x03
const TEV_PACKET_IFRAME = 0x10
const TEV_PACKET_PFRAME = 0x11
const TEV_PACKET_AUDIO_MP2 = 0x20
const TEV_PACKET_SUBTITLE = 0x30
const TEV_PACKET_SUBTITLE = 0x30 // Legacy SSF (frame-locked)
const TEV_PACKET_SUBTITLE_TC = 0x31 // SSF-TC (timecode-based)
const TEV_PACKET_SYNC = 0xFF
// Subtitle opcodes (SSF format)
@@ -41,11 +43,16 @@ let subtitleVisible = false
let subtitleText = ""
let subtitlePosition = 0 // 0=bottom center (default)
// SSF-TC subtitle event buffer
let subtitleEvents = [] // Array of {timecode_ns, index, opcode, text}
let nextSubtitleEventIndex = 0 // Next event to check
// Parse command line options
let interactive = false
let debugMotionVectors = false
let deinterlaceAlgorithm = "yadif"
let enableDeblocking = true // Default: enabled (use -nodeblock to disable)
let enableDeblocking = false // Default: disabled (use -deblock to enable)
let enableBoundaryAwareDecoding = false // Default: disabled (use -boundaryaware to enable) // suitable for still frame and slide shows, absolutely unsuitable for videos
if (exec_args.length > 2) {
for (let i = 2; i < exec_args.length; i++) {
@@ -54,8 +61,10 @@ if (exec_args.length > 2) {
interactive = true
} else if (arg === "-debug-mv") {
debugMotionVectors = true
} else if (arg === "-nodeblock") {
enableDeblocking = false
} else if (arg === "-deblock") {
enableDeblocking = true
} else if (arg === "-boundaryaware") {
enableBoundaryAwareDecoding = true
} else if (arg.startsWith("-deinterlace=")) {
deinterlaceAlgorithm = arg.substring(13)
}
@@ -70,18 +79,17 @@ let notifHideTimer = 0
const NOTIF_SHOWUPTIME = 3000000000
let [cy, cx] = con.getyx()
let seqreadserial = require("seqread")
let seqreadtape = require("seqreadtape")
let gui = require("playgui")
let seqread = undefined
let fullFilePathStr = fullFilePath.full
// Select seqread driver to use
if (fullFilePathStr.startsWith('$:/TAPE') || fullFilePathStr.startsWith('$:\\TAPE')) {
seqread = seqreadtape
seqread = require("seqreadtape")
seqread.prepare(fullFilePathStr)
seqread.seek(0)
} else {
seqread = seqreadserial
seqread = require("seqread")
seqread.prepare(fullFilePathStr)
}
@@ -97,6 +105,9 @@ audio.purgeQueue(0)
audio.setPcmMode(0)
audio.setMasterVolume(0, 255)
// set colour zero as half-opaque black
graphics.setPalette(0, 0, 0, 0, 9)
// Subtitle display functions
function clearSubtitleArea() {
// Clear the subtitle area at the bottom of the screen
@@ -268,6 +279,99 @@ function displaySubtitle(text, position = 0) {
con.color_pair(oldFgColor, oldBgColor)
}
// Parse SSF-TC subtitle packet and add to event buffer (0x31)
function parseSubtitlePacketTC(packetSize) {
// Read subtitle index (24-bit, little-endian)
let indexByte0 = seqread.readOneByte()
let indexByte1 = seqread.readOneByte()
let indexByte2 = seqread.readOneByte()
let index = indexByte0 | (indexByte1 << 8) | (indexByte2 << 16)
// Read timecode (64-bit, little-endian)
let timecode_ns = 0
for (let i = 0; i < 8; i++) {
let byte = seqread.readOneByte()
timecode_ns += byte * Math.pow(2, i * 8)
}
// Read opcode
let opcode = seqread.readOneByte()
let remainingBytes = packetSize - 12 // Subtract 3 (index) + 8 (timecode) + 1 (opcode)
// Read text if present
let text = null
if (remainingBytes > 1 && (opcode === SSF_OP_SHOW || (opcode >= 0x10 && opcode <= 0x2F))) {
let textBytes = seqread.readBytes(remainingBytes)
text = ""
for (let i = 0; i < remainingBytes - 1; i++) { // -1 for null terminator
let byte = sys.peek(textBytes + i)
if (byte === 0) break
text += String.fromCharCode(byte)
}
sys.free(textBytes)
} else if (remainingBytes > 0) {
// Skip remaining bytes
let skipBytes = seqread.readBytes(remainingBytes)
sys.free(skipBytes)
}
// Add event to buffer
subtitleEvents.push({
timecode_ns: timecode_ns,
index: index,
opcode: opcode,
text: text
})
}
// Process subtitle events based on current playback time
function processSubtitleEvents(currentTimeNs) {
// Process all events whose timecode has been reached
while (nextSubtitleEventIndex < subtitleEvents.length) {
let event = subtitleEvents[nextSubtitleEventIndex]
if (event.timecode_ns > currentTimeNs) {
break // Haven't reached this event yet
}
// Execute the subtitle event
switch (event.opcode) {
case SSF_OP_SHOW:
subtitleText = event.text || ""
subtitleVisible = true
displaySubtitle(subtitleText, subtitlePosition)
break
case SSF_OP_HIDE:
subtitleVisible = false
subtitleText = ""
clearSubtitleArea()
break
case SSF_OP_MOVE:
if (event.text && event.text.length > 0) {
let newPosition = event.text.charCodeAt(0)
if (newPosition >= 0 && newPosition <= 8) {
subtitlePosition = newPosition
if (subtitleVisible && subtitleText.length > 0) {
clearSubtitleArea()
displaySubtitle(subtitleText, subtitlePosition)
}
}
}
break
case SSF_OP_UPLOAD_LOW_FONT:
case SSF_OP_UPLOAD_HIGH_FONT:
// Font upload handled during packet parsing
break
}
nextSubtitleEventIndex++
}
}
// Process legacy frame-locked subtitle packet (0x30)
function processSubtitlePacket(packetSize) {
// Read subtitle packet data according to SSF format
// uint24 index + uint8 opcode + variable arguments
@@ -384,15 +488,15 @@ if (!magicMatching) {
// Read header
let version = seqread.readOneByte()
if (version !== TEV_VERSION_YCOCG && version !== TEV_VERSION_XYB) {
println(`Unsupported TEV version: ${version} (expected ${TEV_VERSION_YCOCG} for YCoCg-R or ${TEV_VERSION_XYB} for XYB)`)
if (version !== TEV_VERSION_YCOCG && version !== TEV_VERSION_ICtCp) {
println(`Unsupported TEV version: ${version} (expected ${TEV_VERSION_YCOCG} for YCoCg-R or ${TEV_VERSION_ICtCp} for ICtCp)`)
return 1
}
let colorSpace = (version === TEV_VERSION_XYB) ? "XYB" : "YCoCg-R"
let colorSpace = (version === TEV_VERSION_ICtCp) ? "ICtCp" : "YCoCg"
if (interactive) {
con.move(1,1)
println(`Push and hold Backspace to exit | TEV Format ${version} (${colorSpace}) | Deblocking: ${enableDeblocking ? 'ON' : 'OFF'}`)
println(`Push and hold Backspace to exit | ${colorSpace} | Deblock: ${enableDeblocking ? 'ON' : 'OFF'} | EdgeAware: ${enableBoundaryAwareDecoding ? 'ON' : 'OFF'}`);
}
let width = seqread.readShort()
@@ -417,6 +521,7 @@ serial.println(` FPS: ${(isNTSC) ? (fps * 1000 / 1001) : fps}`)
serial.println(` Duration: ${totalFrames / fps}`)
serial.println(` Audio: ${hasAudio ? "Yes" : "No"}`)
serial.println(` Resolution: ${width}x${height}, ${isInterlaced ? "interlaced" : "progressive"}`)
serial.println(` Quality: Y=${qualityY}, Co=${qualityCo}, Cg=${qualityCg}`)
// DEBUG interlace raw output
@@ -436,13 +541,14 @@ function updateDataRateBin(rate) {
}
}
function getVideoRate(rate) {
function getVideoRate() {
let baseRate = videoRateBin.reduce((a, c) => a + c, 0)
let mult = fps / videoRateBin.length
return baseRate * mult
}
let FRAME_TIME = 1.0 / fps
let FRAME_TIME_NS = (1000000000.0 / fps) // Frame time in nanoseconds for subtitle timing
// Ultra-fast approach: always render to display, use dedicated previous frame buffer
const FRAME_PIXELS = width * height
@@ -577,11 +683,12 @@ function rotateFieldBuffers() {
}
let frameDuped = false
let currentFrameType = "I"
// Main decoding loop - simplified for performance
try {
let t1 = sys.nanoTime()
while (!stopPlay && seqread.getReadCount() < FILE_LENGTH && trueFrameCount < totalFrames) {
while (!stopPlay && seqread.getReadCount() < FILE_LENGTH /*&& trueFrameCount < totalFrames*/) {
// Handle interactive controls
if (interactive) {
@@ -609,7 +716,7 @@ try {
PREV_RGB_ADDR = temp
} else if (packetType == TEV_PACKET_IFRAME || packetType == TEV_PACKET_PFRAME) {
// Video frame packet (always includes rate control factor)
// Video frame packet
let payloadLen = seqread.readInt()
let compressedPtr = seqread.readBytes(payloadLen)
updateDataRateBin(payloadLen)
@@ -624,11 +731,6 @@ try {
// Decompress using gzip
// Optimized buffer size calculation for TEV YCoCg-R blocks
let blocksX = (width + 15) >> 4 // 16x16 blocks
let blocksY = (height + 15) >> 4
let tevBlockSize = 1 + 4 + 2 + (256 * 2) + (64 * 2) + (64 * 2) // mode + mv + cbp + Y(16x16) + Co(8x8) + Cg(8x8)
let decompressedSize = Math.max(payloadLen * 4, blocksX * blocksY * tevBlockSize) // More efficient sizing
let actualSize
let decompressStart = sys.nanoTime()
try {
@@ -643,7 +745,7 @@ try {
continue
}
// Hardware-accelerated TEV decoding to RGB buffers (YCoCg-R or XYB based on version)
// Hardware-accelerated TEV decoding to RGB buffers (YCoCg-R or ICtCp based on version)
try {
// duplicate every 1000th frame (pass a turn every 1000n+501st) if NTSC
if (!isNTSC || frameCount % 1000 != 501 || frameDuped) {
@@ -655,14 +757,14 @@ try {
if (isInterlaced) {
// For interlaced: decode current frame into currentFieldAddr
// For display: use prevFieldAddr as current, currentFieldAddr as next
graphics.tevDecode(blockDataPtr, nextFieldAddr, currentFieldAddr, width, decodingHeight, [qualityY, qualityCo, qualityCg], trueFrameCount, debugMotionVectors, version, enableDeblocking)
graphics.tevDecode(blockDataPtr, nextFieldAddr, currentFieldAddr, width, decodingHeight, qualityY, qualityCo, qualityCg, trueFrameCount, debugMotionVectors, version, enableDeblocking, enableBoundaryAwareDecoding)
graphics.tevDeinterlace(trueFrameCount, width, decodingHeight, prevFieldAddr, currentFieldAddr, nextFieldAddr, CURRENT_RGB_ADDR, deinterlaceAlgorithm)
// Rotate field buffers for next frame: NEXT -> CURRENT -> PREV
rotateFieldBuffers()
} else {
// Progressive or first frame: normal decoding without temporal prediction
graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, decodingHeight, [qualityY, qualityCo, qualityCg], trueFrameCount, debugMotionVectors, version, enableDeblocking)
graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, decodingHeight, qualityY, qualityCo, qualityCg, trueFrameCount, debugMotionVectors, version, enableDeblocking, enableBoundaryAwareDecoding)
}
decodeTime = (sys.nanoTime() - decodeStart) / 1000000.0 // Convert to milliseconds
@@ -670,7 +772,7 @@ try {
// Upload RGB buffer to display framebuffer with dithering
let uploadStart = sys.nanoTime()
graphics.uploadRGBToFramebuffer(CURRENT_RGB_ADDR, width, height, frameCount, true)
graphics.uploadRGBToFramebuffer(CURRENT_RGB_ADDR, width, height, frameCount, false)
uploadTime = (sys.nanoTime() - uploadStart) / 1000000.0 // Convert to milliseconds
}
else {
@@ -679,6 +781,12 @@ try {
serial.println(`Frame ${frameCount}: Duplicating previous frame`)
}
// Process SSF-TC subtitle events based on current playback time
if (subtitleEvents.length > 0) {
let currentTimeNs = frameCount * FRAME_TIME_NS
processSubtitleEvents(currentTimeNs)
}
// Defer audio playback until a first frame is sent
if (isInterlaced) {
// fire audio after frame 1
@@ -710,6 +818,8 @@ try {
serial.println(`Frame ${frameCount}: Decompress=${decompressTime.toFixed(1)}ms, Decode=${decodeTime.toFixed(1)}ms, Upload=${uploadTime.toFixed(1)}ms, Bias=${biasTime.toFixed(1)}ms, Total=${totalTime.toFixed(1)}ms`)
}
currentFrameType = packetType == TEV_PACKET_IFRAME ? "I" : "P"
} else if (packetType == TEV_PACKET_AUDIO_MP2) {
// MP2 Audio packet
let audioLen = seqread.readInt()
@@ -724,9 +834,14 @@ try {
audio.mp2UploadDecoded(0)
} else if (packetType == TEV_PACKET_SUBTITLE) {
// Subtitle packet
// Legacy frame-locked subtitle packet (0x30)
let packetSize = seqread.readInt()
processSubtitlePacket(packetSize)
} else if (packetType == TEV_PACKET_SUBTITLE_TC) {
// SSF-TC subtitle packet (0x31) - parse and buffer for later playback
let packetSize = seqread.readInt()
parseSubtitlePacketTC(packetSize)
} else if (packetType == 0x00) {
// Silently discard, faulty subtitle creation can cause this as 0x00 is used as an argument terminator
} else {
@@ -743,27 +858,37 @@ try {
if (interactive) {
notifHideTimer += (t2 - t1)
if (!notifHidden && notifHideTimer > (NOTIF_SHOWUPTIME + FRAME_TIME)) {
con.move(1, 1)
print(' '.repeat(79))
// clearing function here
notifHidden = true
}
if (!hasSubtitle) {
con.move(31, 1)
graphics.setTextFore(161)
print(`Frame: ${frameCount}/${totalFrames} (${((frameCount / akku2 * 100)|0) / 100}f) `)
con.move(32, 1)
graphics.setTextFore(161)
print(`VRate: ${(getVideoRate() / 1024 * 8)|0} kbps `)
con.move(1, 1)
con.color_pair(253, 0)
let guiStatus = {
fps: fps,
videoRate: getVideoRate(),
frameCount: frameCount,
totalFrames: totalFrames,
frameMode: currentFrameType,
qY: qualityY,
qCo: qualityCo,
qCg: qualityCg,
akku: akku2,
fileName: fullFilePathStr,
fileOrd: 1,
resolution: `${width}x${height}${(isInterlaced) ? 'i' : ''}`,
colourSpace: colorSpace,
currentStatus: 1
}
gui.printBottomBar(guiStatus)
gui.printTopBar(guiStatus, 1)
}
t1 = t2
}
}
catch (e) {
printerrln(`TEV ${colorSpace} decode error: ${e}`)
serial.printerr(`TEV ${colorSpace} decode error: ${e}`)
errorlevel = 1
}
finally {
@@ -781,7 +906,10 @@ finally {
if (interactive) {
//con.clear()
}
// set colour zero as opaque black
}
graphics.setPalette(0, 0, 0, 0, 0)
con.move(cy, cx) // restore cursor
return errorlevel

View File

@@ -0,0 +1,358 @@
// TSVM Universal Cue Format (UCF) Player
// Created by CuriousTorvald and Claude on 2025-09-22
// Usage: playucf cuefile.ucf [options]
// Options: -i (interactive mode)
if (!exec_args[1]) {
serial.println("Usage: playucf cuefile.ucf [options]")
serial.println("Options: -i (interactive mode)")
return 1
}
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
const fullFilePath = _G.shell.resolvePathInput(exec_args[1])
if (!files.open(fullFilePath.full).exists) {
serial.println(`Error: File not found: ${fullFilePath.full}`)
return 2
}
// UCF Format constants
const UCF_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x55, 0x43, 0x46] // "\x1FTSVM UCF"
const UCF_VERSION = 1
const ADDRESSING_EXTERNAL = 0x01
const ADDRESSING_INTERNAL = 0x02
// Media player mappings based on file extensions
const PLAYER_MAP = {
'mp2': 'playmp2',
'wav': 'playwav',
'pcm': 'playpcm',
'mv1': 'playmv1',
'mv2': 'playtev',
'mv3': 'playtav'
}
// Helper class for UCF file reading with internal addressing support
class UCFSequentialReader {
constructor(path, baseOffset = 0) {
this.path = path
this.baseOffset = baseOffset
this.currentOffset = 0
// Detect if this is a TAPE device path
if (path.startsWith("$:/TAPE") || path.startsWith("$:\\TAPE")) {
this.seq = require("seqreadtape")
} else {
this.seq = require("seqread")
}
this.seq.prepare(path)
// Skip to the base offset for internal addressing
if (baseOffset > 0) {
this.seq.skip(baseOffset)
this.currentOffset = baseOffset
}
}
readBytes(length) {
this.currentOffset += length
return this.seq.readBytes(length)
}
readOneByte() {
this.currentOffset += 1
return this.seq.readOneByte()
}
readShort() {
this.currentOffset += 2
return this.seq.readShort()
}
readString(length) {
this.currentOffset += length
return this.seq.readString(length)
}
skip(n) {
this.currentOffset += n
this.seq.skip(n)
}
// Skip to absolute position from base offset
seekTo(position) {
let targetOffset = this.baseOffset + position
if (targetOffset < this.currentOffset) {
// Need to rewind and seek forward
this.seq.prepare(this.path)
this.currentOffset = 0
if (targetOffset > 0) {
this.seq.skip(targetOffset)
this.currentOffset = targetOffset
}
} else if (targetOffset > this.currentOffset) {
// Skip forward
let skipAmount = targetOffset - this.currentOffset
this.seq.skip(skipAmount)
this.currentOffset = targetOffset
}
}
getPosition() {
return this.currentOffset - this.baseOffset
}
}
// Parse UCF file
serial.println(`Playing UCF: ${fullFilePath.full}`)
let reader = new UCFSequentialReader(fullFilePath.full)
// Read and validate magic
let magic = []
for (let i = 0; i < 8; i++) {
magic.push(reader.readOneByte())
}
let magicValid = true
for (let i = 0; i < 8; i++) {
if (magic[i] !== UCF_MAGIC[i]) {
magicValid = false
break
}
}
if (!magicValid) {
serial.println("Error: Invalid UCF magic signature")
return 3
}
// Read header
let version = reader.readOneByte()
if (version !== UCF_VERSION) {
serial.println(`Error: Unsupported UCF version: ${version} (expected ${UCF_VERSION})`)
return 4
}
let numElements = reader.readShort()
// Skip reserved bytes (5 bytes)
reader.skip(5)
serial.println(`UCF Version: ${version}, Elements: ${numElements}`)
// Parse cue elements
let cueElements = []
for (let i = 0; i < numElements; i++) {
let element = {}
element.addressingModeAndIntent = reader.readOneByte()
element.addressingMode = element.addressingModeAndIntent & 15
let nameLength = reader.readShort()
element.name = reader.readString(nameLength)
if (element.addressingMode === ADDRESSING_EXTERNAL) {
let pathLength = reader.readShort()
element.path = reader.readString(pathLength)
serial.println(`Element ${i + 1}: ${element.name} -> ${element.path} (external)`)
} else if (element.addressingMode === ADDRESSING_INTERNAL) {
// Read 48-bit offset (6 bytes, little endian)
let offsetBytes = []
for (let j = 0; j < 6; j++) {
offsetBytes.push(reader.readOneByte())
}
element.offset = 0
for (let j = 0; j < 6; j++) {
element.offset |= (offsetBytes[j] << (j * 8))
}
serial.println(`Element ${i + 1}: ${element.name} -> offset ${element.offset} (internal)`)
} else {
serial.println(`Error: Unknown addressing mode: ${element.addressingMode}`)
return 5
}
cueElements.push(element)
}
// Function to get file extension
function getFileExtension(filename) {
let lastDot = filename.lastIndexOf('.')
if (lastDot === -1) return ''
return filename.substring(lastDot + 1).toLowerCase()
}
// Function to determine player for a file
function getPlayerForFile(filename) {
let ext = getFileExtension(filename)
return PLAYER_MAP[ext] || null
}
// Function to create a temporary file for internal addressing
function createTempFileForInternal(element, ucfPath) {
// Create a unique temporary filename
let tempFilename = `$:\\TMP\\temp_ucf_${Date.now()}_${element.name.replace(/[^a-zA-Z0-9]/g, '_')}`
// For internal addressing, we abuse seqread by creating a "virtual" file view
// We'll return a special path that our modified exec environment can handle
return {
isTemporary: true,
path: tempFilename,
ucfPath: ucfPath,
offset: element.offset,
name: element.name
}
}
// Play each cue element in sequence
for (let i = 0; i < cueElements.length; i++) {
let element = cueElements[i]
serial.println(`\nPlaying element ${i + 1}/${numElements}: ${element.name}`)
if (interactive && i > 0) {
serial.print("Press ENTER to continue, 'q' to quit: ")
let input = serial.readLine()
if (input && input.toLowerCase().startsWith('q')) {
serial.println("Playback stopped by user")
break
}
}
let playerFile = null
let targetPath = null
if (element.addressingMode === ADDRESSING_EXTERNAL) {
// External addressing - resolve relative path
let elementPath = element.path
if (!elementPath.startsWith('A:\\') && !elementPath.startsWith('A:/')) {
// Relative path - resolve relative to UCF file location
let ucfDir = fullFilePath.full.substring(0, fullFilePath.full.lastIndexOf('\\'))
targetPath = ucfDir + '\\' + elementPath.replace(/\//g, '\\')
} else {
targetPath = elementPath
}
if (!files.open(targetPath).exists) {
serial.println(`Warning: External file not found: ${targetPath}`)
continue
}
playerFile = getPlayerForFile(element.name)
} else if (element.addressingMode === ADDRESSING_INTERNAL) {
// Internal addressing - create temporary file reference
let tempFile = createTempFileForInternal(element, fullFilePath.full)
targetPath = tempFile.path
playerFile = getPlayerForFile(element.name)
// For internal addressing, we need to extract the data to a temporary location
// or use a specialized player that can handle offset-based reading
// Since we can't easily create temp files, we'll modify the exec_args for the player
// Create a new UCF reader positioned at the file offset
let fileReader = new UCFSequentialReader(fullFilePath.full, element.offset)
// We need to somehow pass this to the player...
// The most elegant solution is to create a wrapper that temporarily modifies
// the file system view or uses a custom SequentialFileBuffer
// For now, let's use a simpler approach: save exec_args and restore them
let originalExecArgs = [...exec_args]
// Modify the global environment to provide the offset reader
let originalFilesOpen = files.open
files.open = function(path) {
if (path === targetPath || path.endsWith(targetPath)) {
// Return a mock file object that uses our offset reader
return {
exists: true,
size: 2147483648, // Arbitrary large size
path: path,
_ucfReader: fileReader
}
}
return originalFilesOpen.call(this, path)
}
// Also modify seqread require to use our reader
let originalRequire = require
require = function(moduleName) {
if (moduleName === "seqread" || moduleName === "seqreadtape") {
return {
prepare: function(path) {
if (path === targetPath || path.endsWith(targetPath)) {
// Already prepared in fileReader
return 0
}
return fileReader.seq.prepare(path)
},
readBytes: function(length, ptr) { return fileReader.readBytes(length, ptr) },
readOneByte: function() { return fileReader.readOneByte() },
readShort: function() { return fileReader.readShort() },
readInt: function() { return fileReader.seq.readInt() },
readFourCC: function() { return fileReader.seq.readFourCC() },
readString: function(length) { return fileReader.readString(length) },
skip: function(n) { return fileReader.skip(n) },
getReadCount: function() { return fileReader.getPosition() },
fileHeader: fileReader.seq.fileHeader
}
}
return originalRequire.call(this, moduleName)
}
try {
// Execute the player with modified environment
exec_args[1] = targetPath
if (playerFile) {
let playerPath = `A:\\tvdos\\bin\\${playerFile}.js`
if (files.open(playerPath).exists) {
eval(files.readText(playerPath))
} else {
serial.println(`Warning: Player not found: ${playerFile}`)
}
} else {
serial.println(`Warning: No player found for file type: ${element.name}`)
}
} catch (e) {
serial.println(`Error playing ${element.name}: ${e.message}`)
} finally {
// Restore original environment
files.open = originalFilesOpen
require = originalRequire
exec_args = originalExecArgs
}
continue
}
if (!playerFile) {
serial.println(`Warning: No player found for file type: ${element.name}`)
continue
}
// Execute the appropriate player
let playerPath = `A:\\tvdos\\bin\\${playerFile}.js`
if (!files.open(playerPath).exists) {
serial.println(`Warning: Player script not found: ${playerPath}`)
continue
}
// Save and modify exec_args for the player
let originalExecArgs = [...exec_args]
exec_args[1] = targetPath
try {
eval(files.readText(playerPath))
} catch (e) {
serial.println(`Error playing ${element.name}: ${e.message}`)
} finally {
// Restore original exec_args
exec_args = originalExecArgs
}
}
serial.println("\nUCF playback completed")
return 0

View File

@@ -289,7 +289,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_SIZE - 8) {
let decodedSampleLength = decodeInfilePcm(readPtr, decodePtr, readLength)
printdbg(` decodedSampleLength: ${decodedSampleLength}`)
audio.putPcmDataByPtr(decodePtr, decodedSampleLength, 0)
audio.putPcmDataByPtr(0, decodePtr, decodedSampleLength, 0)
audio.setSampleUploadLength(0, decodedSampleLength)
audio.startSampleUpload(0)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
/**
* TAUT File Operations
* Sub-program launched by taut.js when the File tab is active.
* Rows 1-3 are owned by the parent; this program draws rows 4+.
*
* exec_args[1] = path to .taud file
* Sets _G.taut_nextPanel before returning to request a panel switch.
*
* Created by minjaesong on 2026-04-27
*/
const win = require("wintex")
const PANEL_COUNT = 7
const MY_PANEL = 6 // VIEW_FILE
const [SCRH, SCRW] = con.getmaxyx()
const PANEL_Y = 4
const PANEL_H = SCRH - PANEL_Y
const colStatus = 253
const colContent = 240
const colHdr = 230
function drawFileOpContents(wo) {
for (let y = PANEL_Y; y < SCRH; y++) {
con.move(y, 1)
con.color_pair(colContent, 255)
print(' '.repeat(SCRW))
}
con.move(PANEL_Y + 1, 3)
con.color_pair(colHdr, 255)
print('[ File ]')
con.move(PANEL_Y + 3, 3)
con.color_pair(colStatus, 255)
print('placeholder — not yet implemented')
}
function drawHints() {
con.move(SCRH, 1)
con.color_pair(colStatus, 255)
print(' '.repeat(SCRW - 1))
con.move(SCRH, 1)
con.color_pair(colHdr, 255); print('Tab ')
con.color_pair(colStatus, 255); print('Panel')
}
function fileOpInput(wo, event) {
// placeholder — no interaction yet
}
const panel = new win.WindowObject(1, PANEL_Y, SCRW, PANEL_H, fileOpInput, drawFileOpContents, undefined, ()=>{})
panel.drawContents()
drawHints()
let done = false
while (!done) {
input.withEvent(event => {
if (event[0] !== 'key_down') return
const keysym = event[1]
const keyJustHit = (1 == event[2])
const shiftDown = (event.includes(59) || event.includes(60))
if (!keyJustHit) return
if (keysym === '<TAB>') {
_G.taut_nextPanel = (MY_PANEL + (shiftDown ? -1 : 1) + PANEL_COUNT) % PANEL_COUNT
done = true
return
}
panel.processInput(event)
})
}
return 0

View File

@@ -0,0 +1,77 @@
/**
* TAUT Instrument Editor
* Sub-program launched by taut.js when the Instrmnt tab is active.
* Rows 1-3 are owned by the parent; this program draws rows 4+.
*
* exec_args[1] = path to .taud file
* Sets _G.taut_nextPanel before returning to request a panel switch.
*
* Created by minjaesong on 2026-04-27
*/
const win = require("wintex")
const PANEL_COUNT = 7
const MY_PANEL = 4 // VIEW_INSTRMNT
const [SCRH, SCRW] = con.getmaxyx()
const PANEL_Y = 4
const PANEL_H = SCRH - PANEL_Y
const colStatus = 253
const colContent = 240
const colHdr = 230
function drawInstEditContents(wo) {
for (let y = PANEL_Y; y < SCRH; y++) {
con.move(y, 1)
con.color_pair(colContent, 255)
print(' '.repeat(SCRW))
}
con.move(PANEL_Y + 1, 3)
con.color_pair(colHdr, 255)
print('[ Instrument Editor ]')
con.move(PANEL_Y + 3, 3)
con.color_pair(colStatus, 255)
print('placeholder — not yet implemented')
}
function drawHints() {
con.move(SCRH, 1)
con.color_pair(colStatus, 255)
print(' '.repeat(SCRW - 1))
con.move(SCRH, 1)
con.color_pair(colHdr, 255); print('Tab ')
con.color_pair(colStatus, 255); print('Panel')
}
function instEditInput(wo, event) {
// placeholder — no interaction yet
}
const panel = new win.WindowObject(1, PANEL_Y, SCRW, PANEL_H, instEditInput, drawInstEditContents, undefined, ()=>{})
panel.drawContents()
drawHints()
let done = false
while (!done) {
input.withEvent(event => {
if (event[0] !== 'key_down') return
const keysym = event[1]
const keyJustHit = (1 == event[2])
const shiftDown = (event.includes(59) || event.includes(60))
if (!keyJustHit) return
if (keysym === '<TAB>') {
_G.taut_nextPanel = (MY_PANEL + (shiftDown ? -1 : 1) + PANEL_COUNT) % PANEL_COUNT
done = true
return
}
panel.processInput(event)
})
}
return 0

View File

@@ -0,0 +1,77 @@
/**
* TAUT Sample Editor
* Sub-program launched by taut.js when the Samples tab is active.
* Rows 1-3 are owned by the parent; this program draws rows 4+.
*
* exec_args[1] = path to .taud file
* Sets _G.taut_nextPanel before returning to request a panel switch.
*
* Created by minjaesong on 2026-04-27
*/
const win = require("wintex")
const PANEL_COUNT = 7
const MY_PANEL = 3 // VIEW_SAMPLES
const [SCRH, SCRW] = con.getmaxyx()
const PANEL_Y = 4
const PANEL_H = SCRH - PANEL_Y
const colStatus = 253
const colContent = 240
const colHdr = 230
function drawSampleEditContents(wo) {
for (let y = PANEL_Y; y < SCRH; y++) {
con.move(y, 1)
con.color_pair(colContent, 255)
print(' '.repeat(SCRW))
}
con.move(PANEL_Y + 1, 3)
con.color_pair(colHdr, 255)
print('[ Sample Editor ]')
con.move(PANEL_Y + 3, 3)
con.color_pair(colStatus, 255)
print('placeholder — not yet implemented')
}
function drawHints() {
con.move(SCRH, 1)
con.color_pair(colStatus, 255)
print(' '.repeat(SCRW - 1))
con.move(SCRH, 1)
con.color_pair(colHdr, 255); print('Tab ')
con.color_pair(colStatus, 255); print('Panel')
}
function sampleEditInput(wo, event) {
// placeholder — no interaction yet
}
const panel = new win.WindowObject(1, PANEL_Y, SCRW, PANEL_H, sampleEditInput, drawSampleEditContents, undefined, ()=>{})
panel.drawContents()
drawHints()
let done = false
while (!done) {
input.withEvent(event => {
if (event[0] !== 'key_down') return
const keysym = event[1]
const keyJustHit = (1 == event[2])
const shiftDown = (event.includes(59) || event.includes(60))
if (!keyJustHit) return
if (keysym === '<TAB>') {
_G.taut_nextPanel = (MY_PANEL + (shiftDown ? -1 : 1) + PANEL_COUNT) % PANEL_COUNT
done = true
return
}
panel.processInput(event)
})
}
return 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD>洄洄洄洄洄泚<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD>_

View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

View File

@@ -1,4 +1,6 @@
const win = require("wintex")
const keys = require("keysym")
const COL_TEXT = 253
const COL_BACK = 255
const COL_BACK_SEL = 81
@@ -26,33 +28,45 @@ const COL_HL_EXT = {
"wav": 31,
"adpcm": 31,
"pcm": 32,
"mp3": 33,
// "mp3": 33,
"tad": 33,
"mp2": 34,
"mov": 213,
"mv2": 214,
"mv3": 214,
"mv1": 213,
"mv2": 213,
"mv3": 213,
"tav": 213,
"ipf": 190,
"ipf1": 190,
"ipf2": 191,
"ipf2": 190,
"im3": 190,
"tap": 190,
"txt": 223,
"md": 223,
"log": 223
"log": 223,
"taud":109,
}
const EXEC_FUNS = {
"wav": (f) => _G.shell.execute(`playwav "${f}" -i`),
"adpcm": (f) => _G.shell.execute(`playwav "${f}" -i`),
"mp3": (f) => _G.shell.execute(`playmp3 "${f}" -i`),
// "mp3": (f) => _G.shell.execute(`playmp3 "${f}" -i`),
"mp2": (f) => _G.shell.execute(`playmp2 "${f}" -i`),
"mov": (f) => _G.shell.execute(`playmov "${f}" -i`),
"mv1": (f) => _G.shell.execute(`playmv1 "${f}" -i`),
"mv2": (f) => _G.shell.execute(`playtev "${f}" -i`),
"mv3": (f) => _G.shell.execute(`playtev "${f}" -i`),
"mv3": (f) => _G.shell.execute(`playtav "${f}" -i`),
"tav": (f) => _G.shell.execute(`playtav "${f}" -i`),
"im3": (f) => _G.shell.execute(`playtav "${f}" -i`),
"tap": (f) => _G.shell.execute(`playtav "${f}" -i`),
"tad": (f) => _G.shell.execute(`playtad "${f}" -i`),
"pcm": (f) => _G.shell.execute(`playpcm "${f}" -i`),
"ipf": (f) => _G.shell.execute(`decodeipf "${f}" -i`),
"ipf1": (f) => _G.shell.execute(`decodeipf "${f}" -i`),
"ipf2": (f) => _G.shell.execute(`decodeipf "${f}" -i`),
"bas": (f) => _G.shell.execute(`basic "${f}"`),
"txt": (f) => _G.shell.execute(`less "${f}"`),
"md": (f) => _G.shell.execute(`less "${f}"`),
"log": (f) => _G.shell.execute(`less "${f}"`)
"log": (f) => _G.shell.execute(`less "${f}"`),
"taud": (f) => _G.shell.execute(`taut "${f}"`),
}
let windowMode = 0 // 0 == left, 1 == right
@@ -68,6 +82,7 @@ let cursor = [0, 0] // absolute position!
function bytesToReadable(i) {
return ''+ (
(i > 999999999) ? (((i / 10000000)|0)/100 + "G") :
(i > 999999) ? (((i / 10000)|0)/100 + "M") :
(i > 9999) ? (((i / 100)|0)/10 + "K") :
i
@@ -407,6 +422,8 @@ let filenavOninput = (window, event) => {
let keycodes = [event[3],event[4],event[5],event[6],event[7],event[8],event[9],event[10]]
let keycode = keycodes[0]
let scrollPeek = (LIST_HEIGHT / 3)|0
if (keyJustHit && keysym == "q") {
exit = true
}
@@ -415,19 +432,19 @@ let filenavOninput = (window, event) => {
redraw() // this would double-redraw (hence no panel switching) or something if redraw() is not merely a request to do so
}
else if (keysym == "<UP>") {
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(-1, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], 1)
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(-1, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], scrollPeek)
drawFilePanel()
}
else if (keysym == "<DOWN>") {
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(+1, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], 1)
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(+1, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], scrollPeek)
drawFilePanel()
}
else if (keysym == "<PAGE_UP>") {
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(-LIST_HEIGHT, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], 1)
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(-LIST_HEIGHT, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], scrollPeek)
drawFilePanel()
}
else if (keysym == "<PAGE_DOWN>") {
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(+LIST_HEIGHT, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], 1)
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(+LIST_HEIGHT, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], scrollPeek)
drawFilePanel()
}
else if (keyJustHit && keycode == 66) { // enter
@@ -474,6 +491,7 @@ let filenavOninput = (window, event) => {
firstRunLatch = true
con.curs_set(0);clearScr()
refreshFilePanelCache(windowMode)
redraw()
}
}
@@ -659,7 +677,7 @@ while (!exit) {
let keysym = event[1]
let keyJustHit = (1 == event[2])
if (keyJustHit && event[3] != 66) { // release the latch right away if the key is not Return
if (keyJustHit && event[3] != keys.ENTER) { // release the latch right away if the key is not Return
firstRunLatch = false
}

Binary file not shown.

Binary file not shown.

View File

@@ -1,37 +1,3 @@
let status = 0
let workarea = sys.malloc(1920)
// install LOCHRROM
let hangulRomL = files.open("A:/tvdos/i18n/hang_lo.chr")
if (!hangulRomL.exists) {
printerrln("hang_lo.chr not found")
sys.free(workarea)
return status
}
//dma.comToRam(filesystem._toPorts("A")[0], 0, workarea, 1920)
hangulRomL.pread(workarea, 1920, 0)
for (let i = 0; i < 1920; i++) sys.poke(-1300607 - i, sys.peek(workarea + i))
sys.poke(-1299460, 18)
// install HICHRROM
let hangulRomH = files.open("A:/tvdos/i18n/hang_hi.chr")
if (!hangulRomH.exists) {
printerrln("hang_hi.chr not found")
sys.free(workarea)
sys.poke(-1299460, 20) // clean up the crap
return status
}
//dma.comToRam(filesystem._toPorts("A")[0], 0, workarea, 1920)
hangulRomH.pread(workarea, 1920, 0)
for (let i = 0; i < 1920; i++) sys.poke(-1300607 - i, sys.peek(workarea + i))
sys.poke(-1299460, 19)
sys.free(workarea)
graphics.setHalfrowMode(true)
/*
* A character is defined as one of:
* 1. [I,x] (Initial only)
@@ -100,7 +66,37 @@ i:{ // Cell Indices: [c0,c2]
18:[5,0],
19:[5,11],
20:[0,14]
},f:{ // Cell Indices: [c3,c5]
},fvert:{ // Cell Indices: [c3,c5] for non-horizontal vowels (ㅏ,ㅐ,ㅑ,ㅒ and compound vowels)
// c3,c5:[null,ㄱ,ㄴ,ㄷ,...]
0:[0,0],
1:[0,1],
2:[1,1],
3:[1,7],
4:[0,2],
5:[2,9],
6:[2,14],
7:[0,3],
8:[0,4],
9:[4,1],
10:[4,5],
11:[4,6],
12:[4,7],
13:[4,12],
14:[4,13],
15:[4,14],
16:[0,5],
17:[0,6],
18:[6,7],
19:[0,7],
20:[7,7],
21:[0,8],
22:[0,9],
23:[0,10],
24:[0,11],
25:[0,12],
26:[0,13],
27:[0,14]
},fhorz:{ // Cell Indices: [c3,c5] for horizontal vowels (ㅗ,ㅛ,ㅜ,ㅠ,ㅡ)
// c3,c5:[null,ㄱ,ㄴ,ㄷ,...]
0:[0,0],
1:[1,0],
@@ -151,7 +147,7 @@ function toLineChar(i,p,f) {
let out = []
let ibuf = charmap.i[i]
let pbuf = charmap.p[p]
let fbuf = charmap.f[f]
let fbuf = ([8,12,13,17,18].includes(p)) ? charmap.fhorz[f] : charmap.fvert[f]
let dbl = 2*(ibuf.length == 2) // 0 or 2
/* 0 | 0 */out[0] = ibuf[0]
/* x | 2 */out[2] = ibuf[1]
@@ -189,7 +185,9 @@ let printHangul = (char) => {
if (i % 2 == 0)
con.curs_down()
else
cursReturn()
cursReturn()
//if (graphics.getCursorYX()[1] == 1) con.curs_down();
})
}
@@ -217,17 +215,18 @@ if (unicode.uniprint) {
let f = (c - 0xAC00) % 28
let char = toLineChar(i,p,f)
let w = Math.ceil(char.length / 2.0)|0
if (con.getyx()[1] + w > termw) println()
if (con.getyx()[1] + w > termw) print('\n\n');
printHangul(char)
}
}
])
println("조합한글 커널모듈이 로드되었습니다.")
return 0
}
else {
println("Failed to load Assembly Hangul kernel module: incompatible DOS version")
return 1
unicode.uniprint.unshift([
c => 0x20 == c,
c => {
if (con.getyx()[1] >= termw) print('\n\n');
else print(' ')
}
])
}

View File

@@ -0,0 +1,30 @@
let status = 0
let workarea = sys.malloc(1920)
// install LOCHRROM
let hangulRomL = files.open("A:/tvdos/i18n/hang_lo.chr")
if (!hangulRomL.exists) {
printerrln("hang_lo.chr not found")
sys.free(workarea)
return status
}
hangulRomL.pread(workarea, 1920, 0)
for (let i = 0; i < 1920; i++) sys.poke(-133121 - i, sys.peek(workarea + i))
sys.poke(-1299460, 18)
// install HICHRROM
let hangulRomH = files.open("A:/tvdos/i18n/hang_hi.chr")
if (!hangulRomH.exists) {
printerrln("hang_hi.chr not found")
sys.free(workarea)
sys.poke(-1299460, 20) // clean up the crap
return status
}
hangulRomH.pread(workarea, 1920, 0)
for (let i = 0; i < 1920; i++) sys.poke(-133121 - i, sys.peek(workarea + i))
sys.poke(-1299460, 19)
sys.free(workarea)

View File

@@ -0,0 +1,31 @@
function setHighRom(fullPath) {
const fontFile = files.open(fullPath)
// upload font
const fontData = fontFile.bread()
for (let i = 0; i < 1920; i++) sys.poke(-133121 - i, fontData[i])
sys.poke(-1299460, 19) // write to high rom
fontFile.close()
}
function setLowRom(fullPath) {
const fontFile = files.open(fullPath)
// upload font
const fontData = fontFile.bread()
for (let i = 0; i < 1920; i++) sys.poke(-133121 - i, fontData[i])
sys.poke(-1299460, 18) // write to low rom
fontFile.close()
}
function resetHighRom() {
sys.poke(-1299460, 21)
}
function resetLowRom() {
sys.poke(-1299460, 20)
}
exports = { setHighRom, setLowRom, resetHighRom, resetLowRom }

View File

@@ -0,0 +1,305 @@
/*
* getopt.js: node.js implementation of POSIX getopt() (and then some)
*
* Copyright 2011 David Pacheco. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
var ASSERT = require('assert').ok;
function goError(msg)
{
return (new Error('getopt: ' + msg));
}
/*
* The BasicParser is our primary interface to the outside world. The
* documentation for this object and its public methods is contained in
* the included README.md.
*/
function goBasicParser(optstring, argv, optind)
{
var ii;
ASSERT(optstring || optstring === '', 'optstring is required');
ASSERT(optstring.constructor === String, 'optstring must be a string');
ASSERT(argv, 'argv is required');
ASSERT(argv.constructor === Array, 'argv must be an array');
this.gop_argv = new Array(argv.length);
this.gop_options = {};
this.gop_aliases = {};
this.gop_optind = optind !== undefined ? optind : 2;
this.gop_subind = 0;
for (ii = 0; ii < argv.length; ii++) {
ASSERT(argv[ii].constructor === String,
'argv must be string array');
this.gop_argv[ii] = argv[ii];
}
this.parseOptstr(optstring);
}
exports.BasicParser = goBasicParser;
/*
* Parse the option string and update the following fields:
*
* gop_silent Whether to log errors to stderr. Silent mode is
* indicated by a leading ':' in the option string.
*
* gop_options Maps valid single-letter-options to booleans indicating
* whether each option is required.
*
* gop_aliases Maps valid long options to the corresponding
* single-letter short option.
*/
goBasicParser.prototype.parseOptstr = function (optstr)
{
var chr, cp, alias, arg, ii;
ii = 0;
if (optstr.length > 0 && optstr[0] == ':') {
this.gop_silent = true;
ii++;
} else {
this.gop_silent = false;
}
while (ii < optstr.length) {
chr = optstr[ii];
arg = false;
if (!/^[\w\d\u1000-\u1100]$/.test(chr))
throw (goError('invalid optstring: only alphanumeric ' +
'characters and unicode characters between ' +
'\\u1000-\\u1100 may be used as options: ' + chr));
if (ii + 1 < optstr.length && optstr[ii + 1] == ':') {
arg = true;
ii++;
}
this.gop_options[chr] = arg;
while (ii + 1 < optstr.length && optstr[ii + 1] == '(') {
ii++;
cp = optstr.indexOf(')', ii + 1);
if (cp == -1)
throw (goError('invalid optstring: missing ' +
'")" to match "(" at char ' + ii));
alias = optstr.substring(ii + 1, cp);
this.gop_aliases[alias] = chr;
ii = cp;
}
ii++;
}
};
goBasicParser.prototype.optind = function ()
{
return (this.gop_optind);
};
/*
* For documentation on what getopt() does, see README.md. The following
* implementation invariants are maintained by getopt() and its helper methods:
*
* this.gop_optind Refers to the element of gop_argv that contains
* the next argument to be processed. This may
* exceed gop_argv, in which case the end of input
* has been reached.
*
* this.gop_subind Refers to the character inside
* this.gop_options[this.gop_optind] which begins
* the next option to be processed. This may never
* exceed the length of gop_argv[gop_optind], so
* when incrementing this value we must always
* check if we should instead increment optind and
* reset subind to 0.
*
* That is, when any of these functions is entered, the above indices' values
* are as described above. getopt() itself and getoptArgument() may both be
* called at the end of the input, so they check whether optind exceeds
* argv.length. getoptShort() and getoptLong() are called only when the indices
* already point to a valid short or long option, respectively.
*
* getopt() processes the next option as follows:
*
* o If gop_optind > gop_argv.length, then we already parsed all arguments.
*
* o If gop_subind == 0, then we're looking at the start of an argument:
*
* o Check for special cases like '-', '--', and non-option arguments.
* If present, update the indices and return the appropriate value.
*
* o Check for a long-form option (beginning with '--'). If present,
* delegate to getoptLong() and return the result.
*
* o Otherwise, advance subind past the argument's leading '-' and
* continue as though gop_subind != 0 (since that's now the case).
*
* o Delegate to getoptShort() and return the result.
*/
goBasicParser.prototype.getopt = function ()
{
if (this.gop_optind >= this.gop_argv.length)
/* end of input */
return (undefined);
var arg = this.gop_argv[this.gop_optind];
if (this.gop_subind === 0) {
if (arg == '-' || arg === '' || arg[0] != '-')
return (undefined);
if (arg == '--') {
this.gop_optind++;
this.gop_subind = 0;
return (undefined);
}
if (arg[1] == '-')
return (this.getoptLong());
this.gop_subind++;
ASSERT(this.gop_subind < arg.length);
}
return (this.getoptShort());
};
/*
* Implements getopt() for the case where optind/subind point to a short option.
*/
goBasicParser.prototype.getoptShort = function ()
{
var arg, chr;
ASSERT(this.gop_optind < this.gop_argv.length);
arg = this.gop_argv[this.gop_optind];
ASSERT(this.gop_subind < arg.length);
chr = arg[this.gop_subind];
if (++this.gop_subind >= arg.length) {
this.gop_optind++;
this.gop_subind = 0;
}
if (!(chr in this.gop_options))
return (this.errInvalidOption(chr));
if (!this.gop_options[chr])
return ({ option: chr });
return (this.getoptArgument(chr));
};
/*
* Implements getopt() for the case where optind/subind point to a long option.
*/
goBasicParser.prototype.getoptLong = function ()
{
var arg, alias, chr, eq;
ASSERT(this.gop_subind === 0);
ASSERT(this.gop_optind < this.gop_argv.length);
arg = this.gop_argv[this.gop_optind];
ASSERT(arg.length > 2 && arg[0] == '-' && arg[1] == '-');
eq = arg.indexOf('=');
alias = arg.substring(2, eq == -1 ? arg.length : eq);
if (!(alias in this.gop_aliases))
return (this.errInvalidOption(alias));
chr = this.gop_aliases[alias];
ASSERT(chr in this.gop_options);
if (!this.gop_options[chr]) {
if (eq != -1)
return (this.errExtraArg(alias));
this.gop_optind++; /* eat this argument */
return ({ option: chr });
}
/*
* Advance optind/subind for the argument value and retrieve it.
*/
if (eq == -1)
this.gop_optind++;
else
this.gop_subind = eq + 1;
return (this.getoptArgument(chr));
};
/*
* For the given option letter 'chr' that takes an argument, assumes that
* optind/subind point to the argument (or denote the end of input) and return
* the appropriate getopt() return value for this option and argument (or return
* the appropriate error).
*/
goBasicParser.prototype.getoptArgument = function (chr)
{
var arg;
if (this.gop_optind >= this.gop_argv.length)
return (this.errMissingArg(chr));
arg = this.gop_argv[this.gop_optind].substring(this.gop_subind);
this.gop_optind++;
this.gop_subind = 0;
return ({ option: chr, optarg: arg });
};
goBasicParser.prototype.errMissingArg = function (chr)
{
if (this.gop_silent)
return ({ option: ':', optopt: chr });
process.stderr.write('option requires an argument -- ' + chr + '\n');
return ({ option: '?', optopt: chr, error: true });
};
goBasicParser.prototype.errInvalidOption = function (chr)
{
if (!this.gop_silent)
process.stderr.write('illegal option -- ' + chr + '\n');
return ({ option: '?', optopt: chr, error: true });
};
/*
* This error is not specified by POSIX, but neither is the notion of specifying
* long option arguments using "=" in the same argv-argument, but it's common
* practice and pretty convenient.
*/
goBasicParser.prototype.errExtraArg = function (chr)
{
if (!this.gop_silent)
process.stderr.write('option expects no argument -- ' +
chr + '\n');
return ({ option: '?', optopt: chr, error: true });
};

View File

@@ -1,7 +1,7 @@
/*
TVDOS Graphics Library
Has no affiliation with OpenGL by Khronos Group
/**
* LibGL — TVDOS Graphics Library
* Has no affiliation with OpenGL by Khronos Group
* @author CuriousTorvald
*/
@@ -45,7 +45,7 @@ exports.SpriteSheet = function(tilew, tileh, tex) {
return ty;
};
};
exports.drawTexPattern = function(texture, x, y, width, height, framebuffer, fgcol, bgcol) {
exports.drawTexPattern = function(texture, x, y, width = texture.width, height = texture.height, framebuffer = false, fgcol, bgcol) {
if (!(texture instanceof exports.Texture) && !(texture instanceof exports.MonoTex)) throw Error("Texture is not a GL Texture types");
let paint = (!framebuffer) ? graphics.plotPixel : graphics.plotPixel2

View File

@@ -0,0 +1,80 @@
/**
* These are key symbols returned by `input.withEvent`, NOT `con.getch()`
*/
exports = {
NUM_0:7,
NUM_1:8,
NUM_2:9,
NUM_3:10,
NUM_4:11,
NUM_5:12,
NUM_6:13,
NUM_7:14,
NUM_8:15,
NUM_9:16,
A:29,
ALT_LEFT:57,
ALT_RIGHT:58,
APOSTROPHE:75,
AT:77,
B:30,
BACK:4,
BACKSLASH:73,
C:31,
CAPS_LOCK:115,
COMMA:55,
D:32,
DEL:67,
BACKSPACE:67,
FORWARD_DEL:112,
DOWN:20,
LEFT:21,
RIGHT:22,
UP:19,
E:33,
ENTER:66,
EQUALS:70,
F:34,
G:35,
GRAVE:68,
H:36,
HOME:3,
I:37,
J:38,
K:39,
L:40,
LEFT_BRACKET:71,
M:41,
MINUS:69,
N:42,
O:43,
P:44,
PERIOD:56,
PLUS:81,
Q:45,
R:46,
RIGHT_BRACKET:72,
S:47,
SEMICOLON:74,
SHIFT_LEFT:59,
SHIFT_RIGHT:60,
SLASH:76,
SPACE:62,
SYM:63, // on MacOS, this is Command (⌘)
T:48,
TAB:61,
U:49,
V:50,
W:51,
X:52,
Y:53,
Z:54,
CONTROL_LEFT:129,
CONTROL_RIGHT:130,
ESCAPE:111,
END:123,
INSERT:124,
PAGE_UP:92,
PAGE_DOWN:93,
}

View File

@@ -1,3 +1,8 @@
/**
* LibPCM — PCM decoder for TSVM
* @author CuriousTorvald
*/
const HW_SAMPLING_RATE = 32000
function printdbg(s) { if (0) serial.println(s) }
function printvis(s) { if (0) println(s) }
@@ -29,7 +34,7 @@ function s16Tou8(i) {
}
function u16Tos16(i) { return (i > 32767) ? i - 65536 : i }
function randomRound(k) {
let rnd = (Math.random() + Math.random()) / 2.0 // this produces triangular distribution
let rnd = Math.random() // note to self: no triangular here
return (rnd < (k - (k|0))) ? Math.ceil(k) : Math.floor(k)
}
function lerp(start, end, x) {

View File

@@ -0,0 +1,289 @@
// Common GUI for media player
// Created by CuriousTorvald on 2025-09-30.
// Subtitle display functions
function clearSubtitleArea() {
// Clear the subtitle area at the bottom of the screen
// Text mode is 80x32, so clear the bottom few lines
let oldFgColour = con.get_color_fore()
let oldBgColour = con.get_color_back()
con.color_pair(255, 255) // transparent to clear
// Clear bottom 4 lines for subtitles
for (let row = 28; row <= 31; row++) {
con.move(row, 1)
for (let col = 1; col <= 80; col++) {
print(" ")
}
}
con.color_pair(oldFgColour, oldBgColour)
}
function getVisualLength(line) {
// Remove HTML tags and count the remaining text using unicode.strlen()
const withoutTags = line.replace(/<\/?[bi]>/gi, '')
return unicode.visualStrlen(withoutTags)
}
function displayFormattedLine(line, useUnicode) {
// Parse line and handle <b> and <i> tags with colour changes
// Default subtitle colour: yellow (231), formatted text: white (254)
let i = 0
let inBoldOrItalic = false
let buffer = "" // Accumulate characters for batch printing
// Helper function to flush the buffer
function flushBuffer() {
if (buffer.length > 0) {
useUnicode ? unicode.print(buffer) : print(buffer)
buffer = ""
}
}
// insert initial padding block
con.color_pair(0, 255)
con.prnch(0xDE)
con.color_pair(231, 0)
while (i < line.length) {
if (i < line.length - 2 && line[i] === '<') {
// Check for opening tags
if (line.substring(i, i + 3).toLowerCase() === '<b>' ||
line.substring(i, i + 3).toLowerCase() === '<i>') {
flushBuffer() // Flush before color change
con.color_pair(254, 0) // Switch to white for formatted text
inBoldOrItalic = true
i += 3
} else if (i < line.length - 3 &&
(line.substring(i, i + 4).toLowerCase() === '</b>' ||
line.substring(i, i + 4).toLowerCase() === '</i>')) {
flushBuffer() // Flush before color change
con.color_pair(231, 0) // Switch back to yellow for normal text
inBoldOrItalic = false
i += 4
} else {
// Not a formatting tag, add to buffer
buffer += line[i]
i++
}
} else {
// Regular character, add to buffer
buffer += line[i]
i++
}
}
// Flush any remaining buffered text
flushBuffer()
// insert final padding block
con.color_pair(0, 255)
con.prnch(0xDD)
con.color_pair(231, 0)
}
function displaySubtitle(text, useUnicode = false, position = 0) {
if (!text || text.length === 0) {
clearSubtitleArea()
return
}
// Set subtitle colours: yellow (231) on black (0)
let oldFgColour = con.get_color_fore()
let oldBgColour = con.get_color_back()
con.color_pair(231, 0)
// Split text into lines
let lines = text.split('\n')
// Calculate position based on subtitle position setting
let startRow, startCol
// Calculate visual length without formatting tags for positioning
let longestLineLength = lines.map(s => getVisualLength(s)).sort().last()
switch (position) {
case 2: // center left
case 6: // center right
case 8: // dead center
startRow = 16 - Math.floor(lines.length / 2)
break
case 3: // top left
case 4: // top center
case 5: // top right
startRow = 2
break
case 0: // bottom center
case 1: // bottom left
case 7: // bottom right
default:
startRow = 31 - lines.length
startRow = 31 - lines.length
startRow = 31 - lines.length // Default to bottom center
}
// Display each line
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim()
if (line.length === 0) continue
let row = startRow + i
if (row < 1) row = 1
if (row > 32) row = 32
// Calculate column based on alignment
switch (position) {
case 1: // bottom left
case 2: // center left
case 3: // top left
startCol = 1
break
case 5: // top right
case 6: // center right
case 7: // bottom right
startCol = Math.max(1, 78 - getVisualLength(line) - 2)
break
case 0: // bottom center
case 4: // top center
case 8: // dead center
default:
startCol = Math.max(1, Math.floor((80 - longestLineLength - 2) / 2) + 1)
break
}
con.move(row, startCol)
// Parse and display line with formatting tag support
displayFormattedLine(line, useUnicode)
}
con.color_pair(oldFgColour, oldBgColour)
}
function emit(c) {
return "\x84"+c+"u"
}
function formatTime(seconds) {
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = Math.floor(seconds % 60)
return [hours, minutes, secs]
.map(val => val.toString().padStart(2, '0'))
.join(':')
}
function drawProgressBar(progress, width) {
// Clamp progress between 0 and 1
progress = Math.max(0, Math.min(1, progress));
// Calculate position in "half-character" resolution
const position = progress * width * 2;
const charIndex = Math.floor(position / 2);
const isRightHalf = (position % 2) >= 1;
let bar = '';
for (let i = 0; i < width; i++) {
if (i == charIndex) {
bar += isRightHalf ? '\xDE' : '\xDD';
} else {
bar += '\xC4';
}
}
return bar;
}
/*
status = {
videoRate: int,
frameCount: int,
totalFrames: int,
fps: int,
frameMode: String,
qY: int,
qCo: int,
qCg: int,
akku: float,
fileName: String,
fileOrd: int,
currentStatus: int (0: stop/init, 1: play, 2: pause),
resolution: string,
colourSpace: string
}
*/
function printBottomBar(status) {
con.color_pair(253, 0)
con.move(32, 1)
const fullTimeInSec = status.totalFrames / status.fps
const progress = status.frameCount / (status.totalFrames - 1)
const elapsed = progress * fullTimeInSec
const remaining = (1 - progress) * fullTimeInSec
const BAR = '\xB3'
const statIcon = [emit(0xFE), emit(0x10), emit(0x13)]
let sLeft = `${emit(0x1E)}${status.fileOrd}${emit(0x1F)}${BAR}${statIcon[status.currentStatus]} `
let sRate = `${BAR}${(''+((status.videoRate/128)|0)).padStart(6, ' ')}`
let timeElapsed = formatTime(elapsed)
let timeRemaining = formatTime(remaining)
let barWidth = 80 - (sLeft.length - 8 - ((status.currentStatus == 0) ? 1 : 0) + timeElapsed.length + timeRemaining.length + sRate.length) - 2
let bar = drawProgressBar(progress, barWidth)
let s = sLeft + timeElapsed + ' ' + bar + ' ' + timeRemaining + sRate
print(s);con.addch(0x4B)
con.move(1, 1)
}
function printTopBar(status, moreInfo) {
con.color_pair(253, 0)
con.move(1)
const BAR = '\xB3'
if (moreInfo) {
let filename = status.fileName.split("\\").pop()
let sF = `F ${(''+status.frameCount).padStart((''+status.totalFrames).length, ' ')}${status.frameMode}/${status.totalFrames}`
let sQ = `Q${(''+status.qY).padStart(4,' ')},${(''+status.qCo).padStart(2,' ')},${(''+status.qCg).padStart(2,' ')}`
let sFPS = `${(status.frameCount / status.akku).toFixed(2)}f`
let sRes = `${status.resolution}`
let sCol = `${status.colourSpace}`
let sLeft = sF + BAR + sQ + BAR + sFPS + BAR + sRes + BAR + sCol + BAR
let filenameSpace = 80 - sLeft.length
if (filename.length > filenameSpace) {
filename = filename.slice(0, filenameSpace - 1) + '~'
}
let remainingSpc = filenameSpace - status.fileName.length
let sRight = (remainingSpc > 0) ? ' '.repeat(filenameSpace - status.fileName.length + 3) : ''
print(sLeft + filename + sRight)
} else {
let s = status.fileName
if (s.length > 80) {
s = s.slice(0, 79) + '~'
}
let spcs = 80 - s.length
let spcsLeft = (spcs / 2)|0
let spcsRight = spcs - spcsLeft
print(' '.repeat(spcsLeft))
print(s)
print(' '.repeat(spcsRight))
}
con.move(1, 1)
}
exports = {
clearSubtitleArea,
displaySubtitle,
printTopBar,
printBottomBar
}

View File

@@ -0,0 +1,414 @@
/**
* LibPSG — PSG emulator and mixer for TSVM
* Software-mixes various PSG channels and sends them to sound device as PCM
* @author CuriousTorvald
*/
const HW_SAMPLING_RATE = 32000
function clamp(val, low, hi) { return (val < low) ? low : (val > hi) ? hi : val }
function clampS16(i) { return clamp(i, -32768, 32767) }
const uNybToSnyb = [0,1,2,3,4,5,6,7,-8,-7,-6,-5,-4,-3,-2,-1]
// returns: [unsigned high, unsigned low, signed high, signed low]
function getNybbles(b) { return [b >> 4, b & 15, uNybToSnyb[b >> 4], uNybToSnyb[b & 15]] }
function s8Tou8(i) { return i + 128 }
function s16Tou8(i) {
// return s8Tou8((i >> 8) & 255)
// apply dithering
let ufval = (i / 65536.0) + 0.5
let ival = randomRound(ufval * 255.0)
return ival|0
}
function u16Tos16(i) { return (i > 32767) ? i - 65536 : i }
function randomRound(k) {
let rnd = Math.random() // note to self: no triangular here
return (rnd < (k - (k|0))) ? Math.ceil(k) : Math.floor(k)
}
function lerp(start, end, x) {
return (1 - x) * start + x * end
}
function lerpAndRound(start, end, x) {
return Math.round(lerp(start, end, x))
}
// output format: immediately uploadable into TSVM audio adapter
// ── Internal helpers ────────────────────────────────────────────────────────
function secToSamples(sec) { return Math.round(HW_SAMPLING_RATE * sec) }
function isNative(buf) { return buf.native }
function readU8(buf, ch, i) {
return isNative(buf) ? (sys.peek(buf[ch] + i) & 255) : buf[ch][i]
}
function writeU8(buf, ch, i, v) {
if (isNative(buf)) sys.poke(buf[ch] + i, v)
else buf[ch][i] = v
}
// ── Buffer management ───────────────────────────────────────────────────────
function makeBuffer(length) {
// returns [Uint8Array, Uint8Array] (stereo) that will be used to collect samples made by LibPSG.
// Length: seconds. Number of elements: round(HW_SAMPLING_RATE * length)
const n = secToSamples(length)
const L = new Uint8Array(n)
const R = new Uint8Array(n)
L.fill(128)
R.fill(128)
return { 0: L, 1: R, samples: n, native: false }
}
function makeBufferNative(length) {
// returns native buffer object (stereo) that will be used to collect samples made by LibPSG.
// Length: seconds. Number of elements: round(HW_SAMPLING_RATE * length)
// Free with freeBufferNative() when done.
const n = secToSamples(length)
const L = sys.malloc(n); sys.memset(L, 128, n)
const R = sys.malloc(n); sys.memset(R, 128, n)
return { 0: L, 1: R, samples: n, native: true }
}
function freeBufferNative(buf) {
sys.free(buf[0])
sys.free(buf[1])
}
function clearBuffer(buf, offsetSec, lengthSec) {
// Re-silence a buffer region (fill with 128) for re-use across frames.
const start = (offsetSec != null) ? secToSamples(offsetSec) : 0
const total = (lengthSec != null) ? secToSamples(lengthSec) : (buf.samples - start)
if (!buf.native) {
buf[0].fill(128, start, start + total)
buf[1].fill(128, start, start + total)
} else {
sys.memset(buf[0] + start, 128, total)
sys.memset(buf[1] + start, 128, total)
}
}
// ── Shared mix core ─────────────────────────────────────────────────────────
// sampleFn(i) must return a float in [-1, 1].
// Mixing maths: decode u8 → s16, apply op, clamp, dither back to u8.
function mixInto(buf, lengthSec, offsetSec, op, amp, pan, sampleFn) {
const startIdx = secToSamples(offsetSec)
const n = secToSamples(lengthSec)
// Linear pan law: centre (pan=0) → both channels at full amp
const gainL = Math.max(0, Math.min(1, 1.0 - pan))
const gainR = Math.max(0, Math.min(1, 1.0 + pan))
const opCode = (op === 'sub') ? 1 : (op === 'mul') ? 2 : 0 // default: add
for (let i = 0; i < n; i++) {
const v = sampleFn(i) // oscillator value in [-1, 1]
const oscBase = v * amp * 32767
const oscL = Math.round(oscBase * gainL) | 0
const oscR = Math.round(oscBase * gainR) | 0
for (let ch = 0; ch < 2; ch++) {
const osc = (ch === 0) ? oscL : oscR
const cur = (readU8(buf, ch, startIdx + i) - 128) << 8
let out
switch (opCode) {
case 0: out = cur + osc; break
case 1: out = cur - osc; break
case 2: out = (cur * osc) >> 15; break
}
writeU8(buf, ch, startIdx + i, s16Tou8(clampS16(out)))
}
}
}
// ── Waveform generators ─────────────────────────────────────────────────────
function makeSquare(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) {
// buffer: [Uint8Array, Uint8Array] or native buffer
// length: in seconds
// offset: in seconds
// duty: 0.0 to 1.0. default 0.5 (fraction of period where output is +1)
// freq: Hz
// op: add / mul / sub; default: add
// amp: 0.0 to 1.0; default: 0.5
// pan: -1.0 to 1.0; default: 0.0
// phaseOffset: optional absolute-time base (seconds) added to phase calc only,
// not to the buffer write position — use to ensure phase continuity
// across successive calls (e.g. frame boundaries).
if (duty == null) duty = 0.5
if (op == null) op = 'add'
if (amp == null) amp = 0.5
if (pan == null) pan = 0.0
const tBase = (phaseOffset || 0) + offset
mixInto(buf, length, offset, op, amp, pan, function(i) {
const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1
return (phase < duty) ? 1.0 : -1.0
})
}
function makeTriangle(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) {
// buffer: [Uint8Array, Uint8Array] or native buffer
// length: in seconds
// offset: in seconds
// duty: skew. -1.0 = falling sawtooth, 0.0 = symmetric triangle, 1.0 = rising sawtooth
// freq: Hz
// op: add / mul / sub; default: add
// amp: 0.0 to 1.0; default: 0.5
// pan: -1.0 to 1.0; default: 0.0
// phaseOffset: optional absolute-time base (seconds) added to phase calc only —
// see makeSquare for details.
if (duty == null) duty = 0.0
if (op == null) op = 'add'
if (amp == null) amp = 0.5
if (pan == null) pan = 0.0
// riseFrac: fraction of period spent rising from -1 to +1
// 0.0 → falling saw, 0.5 → symmetric triangle, 1.0 → rising saw
const riseFrac = (duty + 1.0) * 0.5
const tBase = (phaseOffset || 0) + offset
mixInto(buf, length, offset, op, amp, pan, function(i) {
const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1
if (riseFrac <= 0) {
return 1.0 - 2.0 * phase // falling saw
} else if (riseFrac >= 1) {
return -1.0 + 2.0 * phase // rising saw
} else if (phase < riseFrac) {
return -1.0 + 2.0 * (phase / riseFrac) // rising slope
} else {
return 1.0 - 2.0 * ((phase - riseFrac) / (1.0 - riseFrac)) // falling slope
}
})
}
function makeAliasedTriangle(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) {
// buffer: [Uint8Array, Uint8Array] or native buffer
// Famicom-style triangle — output is quantised to 16 DAC levels (4-bit, NES APU style).
// The staircase quantisation introduces harmonics that mimic NES character.
// length: in seconds
// offset: in seconds
// duty: skew. -1.0 = falling sawtooth, 0.0 = symmetric triangle, 1.0 = rising sawtooth
// freq: Hz
// op: add / mul / sub; default: add
// amp: 0.0 to 1.0; default: 0.5
// pan: -1.0 to 1.0; default: 0.0
// phaseOffset: optional absolute-time base (seconds) added to phase calc only —
// see makeSquare for details.
if (duty == null) duty = 0.0
if (op == null) op = 'add'
if (amp == null) amp = 0.5
if (pan == null) pan = 0.0
const riseFrac = (duty + 1.0) * 0.5
const tBase = (phaseOffset || 0) + offset
mixInto(buf, length, offset, op, amp, pan, function(i) {
const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1
let v
if (riseFrac <= 0) {
v = 1.0 - 2.0 * phase
} else if (riseFrac >= 1) {
v = -1.0 + 2.0 * phase
} else if (phase < riseFrac) {
v = -1.0 + 2.0 * (phase / riseFrac)
} else {
v = 1.0 - 2.0 * ((phase - riseFrac) / (1.0 - riseFrac))
}
// Quantise to 16 levels (NES triangle 4-bit DAC: 0..15 → -1..+1)
const level = Math.max(0, Math.min(15, Math.round((v + 1.0) * 7.5)))
return level / 7.5 - 1.0
})
}
// ── LFSR helpers (for noise types 1 and 2) ─────────────────────────────────
function lfsrStep(state, mode) {
// mode 0 (long/NES mode 0): feedback tap at bit 1; period 32767
// mode 1 (short/NES mode 1): feedback tap at bit 6; period 93 (metallic/tonal)
const bit0 = state & 1
const bitTap = (mode === 0) ? (state >> 1) & 1 : (state >> 6) & 1
const feed = bit0 ^ bitTap
return ((feed << 14) | (state >> 1)) & 0x7FFF
}
function lfsrAdvance(state, steps, mode) {
for (let k = 0; k < steps; k++) state = lfsrStep(state, mode)
return state
}
// NES APU documented LFSR periods
const LFSR_PERIOD_LONG = 32767 // mode 0
const LFSR_PERIOD_SHORT = 93 // mode 1
function makeNoise(buf, length, offset, freq, type, op, amp, pan, phaseOffset) {
// buffer: [Uint8Array, Uint8Array] or native buffer
// length: in seconds
// offset: in seconds
// type:
// -1: 8-bit white noise (random float per period, sample-and-hold)
// 0: 1-bit white noise (random ±1 per period, sample-and-hold)
// 1: 1-bit LFSR long mode — NES mode 0, tap=bit0^bit1, period 32767 (full-spectrum)
// 2: 1-bit LFSR short mode — NES mode 1, tap=bit0^bit6, period 93 (metallic/tonal)
// freq: Hz (clock rate of the noise generator)
// op: add / mul / sub; default: add
// amp: 0.0 to 1.0; default: 0.5
// pan: -1.0 to 1.0; default: 0.0
// phaseOffset: optional absolute-time base (seconds) added to phase/LFSR calc only —
// see makeSquare for details.
//
// LFSR types (1 and 2) are deterministic given (phaseOffset+offset, freq): calling
// with monotonically advancing phaseOffset+offset produces a seamless noise stream
// across frames. White noise types (-1, 0) are random per call.
if (op == null) op = 'add'
if (amp == null) amp = 0.5
if (pan == null) pan = 0.0
const tBase = (phaseOffset || 0) + offset
if (type === -1) {
// 8-bit white: new random float in [-1, 1] each clock period
let prevClock = -1
let noiseVal = 0.0
mixInto(buf, length, offset, op, amp, pan, function(i) {
const currentClock = Math.floor((tBase + i / HW_SAMPLING_RATE) * freq) | 0
if (currentClock !== prevClock) {
prevClock = currentClock
noiseVal = Math.random() * 2.0 - 1.0
}
return noiseVal
})
} else if (type === 0) {
// 1-bit white: random ±1 each clock period
let prevClock = -1
let noiseVal = 1.0
mixInto(buf, length, offset, op, amp, pan, function(i) {
const currentClock = Math.floor((tBase + i / HW_SAMPLING_RATE) * freq) | 0
if (currentClock !== prevClock) {
prevClock = currentClock
noiseVal = (Math.random() >= 0.5) ? 1.0 : -1.0
}
return noiseVal
})
} else {
// LFSR-based noise (types 1 and 2)
const mode = (type === 2) ? 1 : 0
const period = (mode === 0) ? LFSR_PERIOD_LONG : LFSR_PERIOD_SHORT
// Advance to deterministic position for this tBase so consecutive frame
// calls with monotonically advancing phaseOffset produce a seamless noise stream.
const startClock = Math.floor(tBase * freq) | 0
let lfsr = lfsrAdvance(1, startClock % period, mode)
let prevClock = startClock
mixInto(buf, length, offset, op, amp, pan, function(i) {
const currentClock = Math.floor((tBase + i / HW_SAMPLING_RATE) * freq) | 0
const delta = currentClock - prevClock
if (delta > 0) {
const steps = delta % period
if (steps > 0) lfsr = lfsrAdvance(lfsr, steps, mode)
prevClock = currentClock
}
return (lfsr & 1) ? 1.0 : -1.0
})
}
}
function makeAliasedTriangleNES(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) {
// NES APU triangle — quantised to the authentic 32-step, 4-bit (0..15) staircase.
// The 32-step sequence is: 15,14,...,1,0, 0,1,...,14,15 (descending then ascending).
// This mirrors the real NES triangle DAC which has 32 equal-height steps per period.
// duty parameter is accepted for API symmetry but ignored (NES triangle is always symmetric).
// phaseOffset: optional absolute-time base (seconds) — see makeSquare for details.
if (op == null) op = 'add'
if (amp == null) amp = 0.5
if (pan == null) pan = 0.0
const tBase = (phaseOffset || 0) + offset
mixInto(buf, length, offset, op, amp, pan, function(i) {
const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1
const step32 = Math.floor(phase * 32) | 0 // 0..31
// step 0..15: descend from 15 to 0; step 16..31: ascend from 0 to 15
const level = (step32 < 16) ? (15 - step32) : (step32 - 16)
return level / 7.5 - 1.0 // map 0..15 → -1..+1
})
}
// ── Send to audio hardware ──────────────────────────────────────────────────
function sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr) {
// Interleaves the L and R channels into a staging region (LRLRLR…) and uploads
// to the audio adapter pcmBin via the standard putPcmDataByPtr pipeline.
//
// offsetSec: start of region to send (default: 0)
// lengthSec: duration to send (default: entire buffer from offsetSec)
// stagingPtr: optional caller-owned native buffer (≥ min(chunk, 32768) * 2 bytes).
// Pass a pre-allocated pointer to avoid malloc/free per call —
// useful for the per-frame tvnes pattern.
//
// The function auto-chunks at 32768 stereo samples (pcmBin capacity).
// Blocks briefly if the audio queue is saturated (queue depth > 2).
const start = (offsetSec != null) ? secToSamples(offsetSec) : 0
const total = (lengthSec != null) ? secToSamples(lengthSec) : (buf.samples - start)
const MAX_CHUNK = 32768 // pcmBin = 65536 bytes; stereo → max 32768 samples per upload
const ownsStaging = (stagingPtr == null)
if (ownsStaging) stagingPtr = sys.malloc(Math.min(total, MAX_CHUNK) * 2)
let remaining = total
let cursor = start
while (remaining > 0) {
const take = Math.min(remaining, MAX_CHUNK)
// Interleave L, R into staging buffer
for (let i = 0; i < take; i++) {
sys.poke(stagingPtr + 2 * i, readU8(buf, 0, cursor + i))
sys.poke(stagingPtr + 2 * i + 1, readU8(buf, 1, cursor + i))
}
// Wait for room in the playback queue (mirrors playwav.js idiom)
// while (audio.getPosition(playhead) > 2) sys.sleep(2)
audio.putPcmDataByPtr(playhead, stagingPtr, take * 2, 0)
audio.setSampleUploadLength(playhead, take * 2)
audio.startSampleUpload(playhead)
remaining -= take
cursor += take
}
if (ownsStaging) sys.free(stagingPtr)
}
// Lazily-allocated JS-side interleave scratch; shared across sendBufferFast calls.
let _sendFastScratch = null
function sendBufferFast(buf, playhead, offsetSec, lengthSec, stagingPtr) {
// Like sendBuffer but interleaves L/R via a JS Uint8Array + one sys.pokeBytes per chunk,
// instead of ~2n sys.poke calls. Requires a non-native (JS-backed) buffer.
// Falls back to sendBuffer for native buffers.
if (isNative(buf)) { sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr); return }
const start = (offsetSec != null) ? secToSamples(offsetSec) : 0
const total = (lengthSec != null) ? secToSamples(lengthSec) : (buf.samples - start)
const MAX_CHUNK = 32768
const ownsStaging = (stagingPtr == null)
if (ownsStaging) stagingPtr = sys.malloc(Math.min(total, MAX_CHUNK) * 2)
const scratchNeeded = Math.min(total, MAX_CHUNK) * 2
if (_sendFastScratch == null || _sendFastScratch.length < scratchNeeded) {
_sendFastScratch = new Uint8Array(scratchNeeded)
}
let remaining = total
let cursor = start
while (remaining > 0) {
const take = Math.min(remaining, MAX_CHUNK)
const L = buf[0], R = buf[1], sc = _sendFastScratch
for (let i = 0; i < take; i++) {
sc[2 * i] = L[cursor + i]
sc[2 * i + 1] = R[cursor + i]
}
sys.pokeBytes(stagingPtr, sc.subarray(0, take * 2), take * 2)
// while (audio.getPosition(playhead) > 2) sys.sleep(2)
audio.putPcmDataByPtr(playhead, stagingPtr, take * 2, 0)
audio.setSampleUploadLength(playhead, take * 2)
audio.startSampleUpload(playhead)
remaining -= take
cursor += take
}
if (ownsStaging) sys.free(stagingPtr)
}
exports = {
HW_SAMPLING_RATE,
makeBuffer, makeBufferNative, freeBufferNative, clearBuffer,
makeSquare, makeTriangle, makeAliasedTriangle, makeAliasedTriangleNES, makeNoise,
sendBuffer, sendBufferFast
}

View File

@@ -1,4 +1,7 @@
/**
* LibSeqread — sequentially read files from disk drive
* @author CuriousTorvald
*/
let readCount = 0
let port = undefined
let fileHeader = new Uint8Array(4096)
@@ -155,4 +158,35 @@ function getReadCount() {
return readCount
}
exports = {fileHeader, prepare, readBytes, readInt, readShort, readFourCC, readOneByte, readString, skip, getReadCount}
function rewind() {
// Send REWIND command to reset stream position
com.sendMessage(port, "REWIND")
let statusCode = com.getStatusCode(port)
if (statusCode != 0) {
throw Error("REWIND failed with "+statusCode)
}
readCount = 0
}
function seek(position) {
if (position < 0) {
throw Error("seek: position must be non-negative")
}
let relPos = position - readCount
if (relPos == 0) {
return // Already at target position
} else if (relPos < 0) {
// Seeking backward - must rewind and skip forward
rewind()
if (position > 0) {
skip(position)
}
} else {
// Seeking forward - skip the difference
skip(relPos)
}
}
exports = {fileHeader, prepare, readBytes, readInt, readShort, readFourCC, readOneByte, readString, skip, getReadCount, seek, rewind}

View File

@@ -1,3 +1,8 @@
/**
* LibSeqread extension for Tape Drive — sequentially read tape
* @author CuriousTorvald
*/
// Sequential reader for HSDPA TAPE devices
// Unlike seqread.mjs which is limited to 4096 bytes per read due to serial communication,
// this module can read larger chunks efficiently from HSDPA devices.
@@ -203,7 +208,7 @@ function skip(n0) {
let n = n0
while (n > 0) {
let skiplen = Math.min(n, 16777215)
serial.println(`skip ${skiplen}; remaining: ${n}`)
// serial.println(`skip ${skiplen}; remaining: ${n}`)
hsdpaSkip(skiplen)
n -= skiplen
}
@@ -237,14 +242,23 @@ function isReady() {
}
function seek(position) {
if (position < 0) {
throw Error("seek: position must be non-negative")
}
let relPos = position - readCount
if (position == 0) {
return
} else if (position > 0) {
skip(relPos)
if (relPos == 0) {
return // Already at target position
} else if (relPos < 0) {
// Seeking backward - must rewind and skip forward
hsdpaRewind() // This resets readCount to 0
if (position > 0) {
skip(position)
}
} else {
hsdpaRewind()
skip(position)
// Seeking forward - skip the difference
skip(relPos)
}
}

View File

@@ -0,0 +1,312 @@
/*
* LibTaud — Helper functions for interaction between Taud format and TSVM Tracker
* Requires TVDOS to function.
* @author CuriousTorvald
*/
// ── Format constants ────────────────────────────────────────────────────────
const TAUD_MAGIC = [0x1F,0x54,0x53,0x56,0x4D,0x61,0x75,0x64] // \x1F TSVMaud
const TAUD_VERSION = 1
const TAUD_HEADER_SIZE = 32 // magic(8) + version(1) + numSongs(1) + compSize(4) + projOff(4) + sig(14)
const TAUD_SONG_ENTRY = 32 // see encodeSongEntry / decodeSongEntry below
const SAMPLEINST_SIZE = 786432 // 737280 sample + 49152 instrument (256 × 192)
const PATTERN_SIZE = 512 // bytes per pattern (64 rows × 8 bytes)
const NUM_PATTERNS_MAX = 256
const NUM_CUES = 1024
const CUE_SIZE = 32 // bytes per cue entry (packed 12-bit×20 voices + instruction + pad)
// Signature written into the file (14 bytes, space-padded)
const CAPTURE_SIGNATURE = "LibTaud/TSVM "
// ── Internal helpers ────────────────────────────────────────────────────────
function _peekU32LE(ptr, off) {
return ((sys.peek(ptr+off) & 0xFF) ) |
((sys.peek(ptr+off+1) & 0xFF) << 8 ) |
((sys.peek(ptr+off+2) & 0xFF) << 16 ) |
((sys.peek(ptr+off+3) & 0xFF) * 0x1000000) // avoid sign-extend
}
function _pokeU32LE(ptr, off, v) {
sys.poke(ptr+off, (v ) & 0xFF)
sys.poke(ptr+off+1, (v >>> 8) & 0xFF)
sys.poke(ptr+off+2, (v >>> 16) & 0xFF)
sys.poke(ptr+off+3, (v >>> 24) & 0xFF)
}
// ── uploadTaudFile ──────────────────────────────────────────────────────────
/**
* Load one song from a Taud file into the tracker hardware and configure the
* given playhead ready to play.
*
* @param inFile Full path with drive letter, e.g. "A:/music/song.taud"
* @param songIndex 0-based index of the song in the SONG TABLE
* @param playhead Playhead number (0-3) to configure
*/
function uploadTaudFile(inFile, songIndex, playhead) {
const drive = inFile[0].toUpperCase()
const diskPath = inFile.substring(2)
const memBase = audio.getMemAddr()
// -- 1. Read whole file into VM memory ------------------------------------
const fileHandle = files.open(inFile)
if (!fileHandle.exists) {
throw Error("taud: file not exists")
}
const fileSize = fileHandle.size
const filePtr = sys.malloc(fileSize)
fileHandle.pread(filePtr, fileSize, 0)
let pos = 0
// -- 2. Verify magic ------------------------------------------------------
for (let i = 0; i < 8; i++) {
let magicc = sys.peek(filePtr + i)
if (magicc !== TAUD_MAGIC[i]) {
sys.free(filePtr)
throw Error("taud: bad magic byte " + magicc.toString(16) + " at index " + i)
}
}
pos = 8
// -- 3. Parse header ------------------------------------------------------
// version(1) + numSongs(1) + compressedSize(4) + rsvd(2) + signature(16) = 24 bytes
let version = sys.peek(filePtr + pos) & 0xFF; pos++
let numSongs = sys.peek(filePtr + pos) & 0xFF; pos++
let compressedSize = _peekU32LE(filePtr, pos); pos += 4
pos += 18 // skip reserved(2) + signature(16)
// pos == 32 == TAUD_HEADER_SIZE
if (songIndex < 0 || songIndex >= numSongs) {
sys.free(filePtr)
throw Error("taud: songIndex " + songIndex + " out of range (numSongs=" + numSongs + ")")
}
// -- 4. Decompress and upload sample+instrument bin -----------------------
let decompPtr = sys.malloc(SAMPLEINST_SIZE)
gzip.decompFromTo(filePtr + pos, compressedSize, decompPtr)
pos += compressedSize
// Write decompressed data to peripheral memory (backwards addressing:
// peripheral byte k lives at memBase - k).
for (let i = 0; i < SAMPLEINST_SIZE; i++) {
// TODO use sys.memcpy
sys.poke(memBase - i, sys.peek(decompPtr + i))
}
sys.free(decompPtr)
// -- 5. Parse song-table entry for the requested song --------------------
let entryOff = pos + songIndex * TAUD_SONG_ENTRY
let songOffset = _peekU32LE(filePtr, entryOff)
let numVoices = sys.peek(filePtr + entryOff + 4) & 0xFF
let numPatsLo = sys.peek(filePtr + entryOff + 5) & 0xFF
let numPatsHi = sys.peek(filePtr + entryOff + 6) & 0xFF
let bpmStored = sys.peek(filePtr + entryOff + 7) & 0xFF
let tickRate = sys.peek(filePtr + entryOff + 8) & 0xFF
let mixerflags = sys.peek(filePtr + entryOff + 15) & 0xFF
let songGlobalVolume = sys.peek(filePtr + entryOff + 16) & 0xFF
let songMixingVolume = sys.peek(filePtr + entryOff + 17) & 0xFF
let patBinCompSize = _peekU32LE(filePtr, entryOff + 18)
let cueSheetCompSize = _peekU32LE(filePtr, entryOff + 22)
let bpm = bpmStored + 24
let patsToLoad = numPatsLo | (numPatsHi << 8)
// -- 6. Decompress + upload patterns --------------------------------------
let patBinSize = patsToLoad * PATTERN_SIZE
let patBinPtr = sys.malloc(patBinSize)
gzip.decompFromTo(filePtr + songOffset, patBinCompSize, patBinPtr)
let patBytes = new Array(PATTERN_SIZE)
for (let p = 0; p < patsToLoad; p++) {
for (let k = 0; k < PATTERN_SIZE; k++)
patBytes[k] = sys.peek(patBinPtr + p * PATTERN_SIZE + k) & 0xFF
audio.uploadPattern(p, patBytes)
}
sys.free(patBinPtr)
// -- 7. Decompress + upload cue sheet -------------------------------------
let cueSheetSize = NUM_CUES * CUE_SIZE
let cueSheetPtr = sys.malloc(cueSheetSize)
gzip.decompFromTo(filePtr + songOffset + patBinCompSize, cueSheetCompSize, cueSheetPtr)
let cueBytes = new Array(CUE_SIZE)
for (let c = 0; c < NUM_CUES; c++) {
for (let k = 0; k < CUE_SIZE; k++)
cueBytes[k] = sys.peek(cueSheetPtr + c * CUE_SIZE + k) & 0xFF
audio.uploadCue(c, cueBytes)
}
sys.free(cueSheetPtr)
// -- 8. Configure playhead ------------------------------------------------
audio.setTrackerMode(playhead)
audio.setBPM(playhead, bpm)
audio.setTickRate(playhead, tickRate > 0 ? tickRate : 6)
audio.setTrackerMixerFlags(playhead, mixerflags)
audio.setSongGlobalVolume(playhead, songGlobalVolume)
audio.setSongMixingVolume(playhead, songMixingVolume)
fileHandle.close()
sys.free(filePtr)
}
// ── captureTrackerDataToFile ────────────────────────────────────────────────
/**
* Dump the current tracker hardware state (sample bin, instruments, patterns
* in bank 0, cue sheet) to a single-song Taud file. BPM and tick-rate are
* taken from playhead 0.
*
* @param outFile Full path with drive letter, e.g. "A:/music/out.taud"
*/
function captureTrackerDataToFile(outFile) {
const drive = outFile[0].toUpperCase()
const diskPath = outFile.substring(2)
const memBase = audio.getMemAddr()
const baseAddr = audio.getBaseAddr()
// -- 1. Compress sample+instrument bin ------------------------------------
// sys.memcpy(negative_src, positive_dst, len) copies peripheral byte k from
// (memBase - k) into (sampleInstBuf + k).
let sampleInstBuf = sys.malloc(SAMPLEINST_SIZE)
sys.memcpy(memBase, sampleInstBuf, SAMPLEINST_SIZE)
let compBuf = sys.malloc(SAMPLEINST_SIZE + 4096) // headroom for incompressible data
let compressedSize = gzip.compFromTo(sampleInstBuf, SAMPLEINST_SIZE, compBuf)
sys.free(sampleInstBuf)
// -- 2. Find last non-empty pattern in bank 0 (all-zero = uninitialized) --
let numPatsActual = 0
outer: for (let p = NUM_PATTERNS_MAX - 1; p >= 0; p--) {
let patBase = 131072 + p * PATTERN_SIZE // offset within peripheral memory space
for (let k = 0; k < PATTERN_SIZE; k++) {
if ((sys.peek(memBase - (patBase + k)) & 0xFF) !== 0) {
numPatsActual = p + 1
break outer
}
}
}
if (numPatsActual === 0) numPatsActual = 1 // always emit at least one pattern slot
let numPats = numPatsActual // Uint16, 1-65535
let patsToSave = numPatsActual
// -- 3. BPM / tick-rate / volumes from playhead 0 -------------------------
let bpm = audio.getBPM(0) || 125
let tickRate = audio.getTickRate(0) || 6
let bpmStored = (bpm - 24) & 0xFF
let songGlobalVolume = audio.getSongGlobalVolume(0)
let songMixingVolume = audio.getSongMixingVolume(0)
if (songGlobalVolume === undefined || songGlobalVolume === null) songGlobalVolume = 0x80
if (songMixingVolume === undefined || songMixingVolume === null) songMixingVolume = 0x80
// -- 4. Compress pattern bin ----------------------------------------------
let patBinSize = patsToSave * PATTERN_SIZE
let patBuf = sys.malloc(patBinSize)
sys.memcpy(memBase - 131072, patBuf, patBinSize)
let patCompBuf = sys.malloc(patBinSize + 4096)
let patCompSize = gzip.compFromTo(patBuf, patBinSize, patCompBuf)
sys.free(patBuf)
// -- 5. Compress cue sheet ------------------------------------------------
// Cue entry c, byte k is at MMIO address 32768 + c*32 + k,
// accessed as sys.peek(baseAddr (32768 + c*32 + k)).
let cueSheetSize = NUM_CUES * CUE_SIZE
let cueBuf = sys.malloc(cueSheetSize)
for (let c = 0; c < NUM_CUES; c++) {
let cueOff = 32768 + c * CUE_SIZE
for (let k = 0; k < CUE_SIZE; k++)
sys.poke(cueBuf + c * CUE_SIZE + k,
sys.peek(baseAddr - (cueOff + k)) & 0xFF)
}
let cueCompBuf = sys.malloc(cueSheetSize + 4096)
let cueCompSize = gzip.compFromTo(cueBuf, cueSheetSize, cueCompBuf)
sys.free(cueBuf)
// -- 6. Compute song offset (absolute from file start) --------------------
// Layout: header(32) + compressed(compressedSize) + songTable(1 × TAUD_SONG_ENTRY)
let songOffset = TAUD_HEADER_SIZE + compressedSize + 1 * TAUD_SONG_ENTRY
// -- 7. Build header byte array (32 bytes) --------------------------------
let sigBytes = new Array(14)
for (let i = 0; i < 14; i++)
sigBytes[i] = i < CAPTURE_SIGNATURE.length ? CAPTURE_SIGNATURE.charCodeAt(i) : 0
let header = [
// Magic (8)
0x1F, 0x54, 0x53, 0x56, 0x4D, 0x61, 0x75, 0x64,
// version, numSongs
TAUD_VERSION, 1,
// compressedSize uint32 LE (4) -- sample+inst bin
(compressedSize ) & 0xFF,
(compressedSize >>> 8) & 0xFF,
(compressedSize >>> 16) & 0xFF,
(compressedSize >>> 24) & 0xFF,
// project data offset (4) -- not emitted
0x00, 0x00, 0x00, 0x00,
].concat(sigBytes) // 8 + 2 + 4 + 4 + 14 = 32 bytes
// -- 8. Build song-table row (32 bytes) -----------------------------------
let songTable = [
(songOffset ) & 0xFF,
(songOffset >>> 8) & 0xFF,
(songOffset >>> 16) & 0xFF,
(songOffset >>> 24) & 0xFF,
20, // numVoices
numPats & 0xFF, (numPats >>> 8) & 0xFF, // numPatterns Uint16 LE
bpmStored, // BPM with 24 bias
tickRate, // initial tick-rate
0x00,0xA0, // basenote (0xA000 -- C9)
0x00,0xAC,0x02,0x46, // basefreq (8363 Hz)
sys.peek(baseAddr - 7), // mixer flags
songGlobalVolume & 0xFF, // global volume
songMixingVolume & 0xFF, // mixing volume
// pattern bin compressed size (4)
(patCompSize ) & 0xFF,
(patCompSize >>> 8) & 0xFF,
(patCompSize >>> 16) & 0xFF,
(patCompSize >>> 24) & 0xFF,
// cue sheet compressed size (4)
(cueCompSize ) & 0xFF,
(cueCompSize >>> 8) & 0xFF,
(cueCompSize >>> 16) & 0xFF,
(cueCompSize >>> 24) & 0xFF,
// reserved (6)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
// -- 9. Write header (creates / truncates file) ---------------------------
const fileHandle = files.open(outFile)
fileHandle.bwrite(header)
// -- 10. Append compressed sample+inst bin --------------------------------
fileHandle.pwrite(compBuf, compressedSize, TAUD_HEADER_SIZE)
sys.free(compBuf)
// -- 11. Write song table -------------------------------------------------
fileHandle.bwrite(songTable)
// -- 12. Append compressed pattern bin ------------------------------------
fileHandle.pwrite(patCompBuf, patCompSize,
TAUD_HEADER_SIZE + compressedSize + songTable.length)
sys.free(patCompBuf)
// -- 13. Append compressed cue sheet --------------------------------------
fileHandle.pwrite(cueCompBuf, cueCompSize,
TAUD_HEADER_SIZE + compressedSize + songTable.length + patCompSize)
sys.free(cueCompBuf)
fileHandle.flush(); fileHandle.close()
}
exports = { uploadTaudFile, captureTrackerDataToFile }

View File

@@ -1,3 +1,8 @@
/**
* WinTex — TUI window management and renderer
* @author CuriousTorvald
*/
class WindowObject {
constructor(x, y, w, h, inputProcessor, drawContents, title, drawFrame) {
@@ -91,8 +96,6 @@ class WindowObject {
* @return [new cursor pos, new scroll pos]
*/
function scrollVert(dy, listSize, listHeight, currentCursorPos, currentScrollPos, scrollPeek) {
let peek = 1
// clamp dy
if (currentCursorPos + dy > listSize - 1)
dy = (listSize - 1) - currentCursorPos
@@ -103,13 +106,13 @@ function scrollVert(dy, listSize, listHeight, currentCursorPos, currentScrollPos
// update vertical scroll stats
if (dy != 0) {
let visible = listHeight - 1 - peek
let visible = listHeight - 1 - scrollPeek
if (nextRow - currentScrollPos > visible) {
currentScrollPos = nextRow - visible
}
else if (nextRow - currentScrollPos < 0 + peek) {
currentScrollPos = nextRow - peek // nextRow is less than zero
else if (nextRow - currentScrollPos < 0 + scrollPeek) {
currentScrollPos = nextRow - scrollPeek // nextRow is less than zero
}
// NOTE: future-proofing here -- scroll clamping is moved outside of go-up/go-down
@@ -140,8 +143,6 @@ function scrollVert(dy, listSize, listHeight, currentCursorPos, currentScrollPos
* @return [new cursor pos, new scroll pos]
*/
function scrollHorz(dx, stringSize, stringViewSize, currentCursorPos, currentScrollPos, scrollPeek) {
let peek = 1
// clamp dx
if (currentCursorPos + dx > stringSize - 1)
dx = (stringSize - 1) - currentCursorPos
@@ -152,13 +153,13 @@ function scrollHorz(dx, stringSize, stringViewSize, currentCursorPos, currentScr
// update vertical scroll stats
if (dx != 0) {
let visible = stringViewSize - 1 - peek
let visible = stringViewSize - 1 - scrollPeek
if (nextCol - currentScrollPos > visible) {
currentScrollPos = nextCol - visible
}
else if (nextCol - currentScrollPos < 0 + peek) {
currentScrollPos = nextCol - peek // nextCol is less than zero
else if (nextCol - currentScrollPos < 0 + scrollPeek) {
currentScrollPos = nextCol - scrollPeek // nextCol is less than zero
}
// NOTE: future-proofing here -- scroll clamping is moved outside of go-up/go-down

View File

@@ -0,0 +1,721 @@
// TAV Packet Inspector - JavaScript port for TSVM
// Ported from tav_inspector.c by CuriousTorvald and Claude
// Usage: tav_inspector <input.tav> <output.txt> [options]
const seqread = require('seqread')
// Frame mode constants
const FRAME_MODE_SKIP = 0x00
const FRAME_MODE_INTRA = 0x01
const FRAME_MODE_DELTA = 0x02
// Packet type constants
const TAV_PACKET_IFRAME = 0x10
const TAV_PACKET_PFRAME = 0x11
const TAV_PACKET_GOP_UNIFIED = 0x12
const TAV_PACKET_GOP_UNIFIED_MOTION = 0x13
const TAV_PACKET_PFRAME_RESIDUAL = 0x14
const TAV_PACKET_BFRAME_RESIDUAL = 0x15
const TAV_PACKET_PFRAME_ADAPTIVE = 0x16
const TAV_PACKET_BFRAME_ADAPTIVE = 0x17
const TAV_PACKET_AUDIO_MP2 = 0x20
const TAV_PACKET_AUDIO_PCM8 = 0x21
const TAV_PACKET_AUDIO_TAD = 0x24
const TAV_PACKET_SUBTITLE = 0x30
const TAV_PACKET_SUBTITLE_TC = 0x31
const TAV_PACKET_VIDEOTEX = 0x3F
const TAV_PACKET_AUDIO_TRACK = 0x40
const TAV_PACKET_VIDEO_CH2_I = 0x70
const TAV_PACKET_VIDEO_CH2_P = 0x71
const TAV_PACKET_VIDEO_CH3_I = 0x72
const TAV_PACKET_VIDEO_CH3_P = 0x73
const TAV_PACKET_VIDEO_CH4_I = 0x74
const TAV_PACKET_VIDEO_CH4_P = 0x75
const TAV_PACKET_VIDEO_CH5_I = 0x76
const TAV_PACKET_VIDEO_CH5_P = 0x77
const TAV_PACKET_VIDEO_CH6_I = 0x78
const TAV_PACKET_VIDEO_CH6_P = 0x79
const TAV_PACKET_VIDEO_CH7_I = 0x7A
const TAV_PACKET_VIDEO_CH7_P = 0x7B
const TAV_PACKET_VIDEO_CH8_I = 0x7C
const TAV_PACKET_VIDEO_CH8_P = 0x7D
const TAV_PACKET_VIDEO_CH9_I = 0x7E
const TAV_PACKET_VIDEO_CH9_P = 0x7F
const TAV_PACKET_EXIF = 0xE0
const TAV_PACKET_ID3V1 = 0xE1
const TAV_PACKET_ID3V2 = 0xE2
const TAV_PACKET_VORBIS_COMMENT = 0xE3
const TAV_PACKET_CD_TEXT = 0xE4
const TAV_PACKET_EXTENDED_HDR = 0xEF
const TAV_PACKET_LOOP_START = 0xF0
const TAV_PACKET_LOOP_END = 0xF1
const TAV_PACKET_SCREEN_MASK = 0xF2
const TAV_PACKET_GOP_SYNC = 0xFC
const TAV_PACKET_TIMECODE = 0xFD
const TAV_PACKET_SYNC_NTSC = 0xFE
const TAV_PACKET_SYNC = 0xFF
const TAV_PACKET_NOOP = 0x00
const QLUT = [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,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,256,264,272,280,288,296,304,312,320,328,336,344,352,360,368,376,384,392,400,408,416,424,432,440,448,456,464,472,480,488,496,504,512,528,544,560,576,592,608,624,640,656,672,688,704,720,736,752,768,784,800,816,832,848,864,880,896,912,928,944,960,976,992,1008,1024,1056,1088,1120,1152,1184,1216,1248,1280,1312,1344,1376,1408,1440,1472,1504,1536,1568,1600,1632,1664,1696,1728,1760,1792,1824,1856,1888,1920,1952,1984,2016,2048,2112,2176,2240,2304,2368,2432,2496,2560,2624,2688,2752,2816,2880,2944,3008,3072,3136,3200,3264,3328,3392,3456,3520,3584,3648,3712,3776,3840,3904,3968,4032,4096]
const CLAYOUT = ["Luma-Chroma", "Luma-Chroma-Alpha", "Luma", "Luma-Alpha", "Chroma", "Chroma-Alpha"]
const VERDESC = ["null", "YCoCg tiled, uniform", "ICtCp tiled, uniform", "YCoCg monoblock, uniform", "ICtCp monoblock, uniform", "YCoCg monoblock, perceptual", "ICtCp monoblock, perceptual", "YCoCg tiled, perceptual", "ICtCp tiled, perceptual"]
const TEMPORAL_WAVELET = ["Haar", "CDF 5/3"]
function getPacketTypeName(type) {
switch (type) {
case TAV_PACKET_IFRAME: return "I-FRAME"
case TAV_PACKET_PFRAME: return "P-FRAME"
case TAV_PACKET_GOP_UNIFIED: return "GOP (3D DWT Unified)"
case TAV_PACKET_GOP_UNIFIED_MOTION: return "GOP (3D DWT Unified with Motion Data)"
case TAV_PACKET_PFRAME_RESIDUAL: return "P-FRAME (residual)"
case TAV_PACKET_BFRAME_RESIDUAL: return "B-FRAME (residual)"
case TAV_PACKET_PFRAME_ADAPTIVE: return "P-FRAME (quadtree)"
case TAV_PACKET_BFRAME_ADAPTIVE: return "B-FRAME (quadtree)"
case TAV_PACKET_AUDIO_MP2: return "AUDIO MP2"
case TAV_PACKET_AUDIO_PCM8: return "AUDIO PCM8 (zstd)"
case TAV_PACKET_AUDIO_TAD: return "AUDIO TAD (zstd)"
case TAV_PACKET_SUBTITLE: return "SUBTITLE (SSF frame-locked)"
case TAV_PACKET_SUBTITLE_TC: return "SUBTITLE (SSF-TC timecoded)"
case TAV_PACKET_VIDEOTEX: return "VIDEOTEX (text-mode video)"
case TAV_PACKET_AUDIO_TRACK: return "AUDIO TRACK (Separate MP2)"
case TAV_PACKET_EXIF: return "METADATA (EXIF)"
case TAV_PACKET_ID3V1: return "METADATA (ID3v1)"
case TAV_PACKET_ID3V2: return "METADATA (ID3v2)"
case TAV_PACKET_VORBIS_COMMENT: return "METADATA (Vorbis)"
case TAV_PACKET_CD_TEXT: return "METADATA (CD-Text)"
case TAV_PACKET_EXTENDED_HDR: return "EXTENDED HEADER"
case TAV_PACKET_LOOP_START: return "LOOP START"
case TAV_PACKET_LOOP_END: return "LOOP END"
case TAV_PACKET_SCREEN_MASK: return "SCREEN MASK"
case TAV_PACKET_GOP_SYNC: return "GOP SYNC"
case TAV_PACKET_TIMECODE: return "TIMECODE"
case TAV_PACKET_SYNC_NTSC: return "SYNC (NTSC)"
case TAV_PACKET_SYNC: return "SYNC"
case TAV_PACKET_NOOP: return "NO-OP"
default:
if (type >= 0x70 && type <= 0x7F) {
return "MUX VIDEO"
}
return "UNKNOWN"
}
}
// Read int64 (little-endian)
function readInt64() {
let lo = seqread.readInt() >>> 0
let hi = seqread.readInt() >>> 0
return lo + hi * 4294967296
}
// Read uint24 (little-endian)
function readUint24() {
let b0 = seqread.readOneByte()
let b1 = seqread.readOneByte()
let b2 = seqread.readOneByte()
return b0 | (b1 << 8) | (b2 << 16)
}
// Get frame info from compressed data
function getFrameInfo(compressedSize) {
let info = { mode: -1, quantiser: 0xFF }
if (compressedSize === 0) return info
// Read compressed data into memory
let compressedPtr = sys.malloc(compressedSize)
if (compressedPtr === 0) {
seqread.skip(compressedSize)
return info
}
seqread.readBytes(compressedSize, compressedPtr)
// Decompress (max 2MB buffer)
let decompressedSize = 2 * 1024 * 1024
let decompressedPtr = sys.malloc(decompressedSize)
if (decompressedPtr === 0) {
sys.free(compressedPtr)
return info
}
try {
let actualSize = gzip.decompFromTo(compressedPtr, compressedSize, decompressedPtr)
if (actualSize >= 1) {
info.mode = sys.peek(decompressedPtr) & 0xFF
}
if (info.mode !== FRAME_MODE_SKIP && actualSize >= 2) {
info.quantiser = sys.peek(decompressedPtr + 1) & 0xFF
}
} catch (e) {
// Decompression failed, keep default values
}
sys.free(decompressedPtr)
sys.free(compressedPtr)
return info
}
// Parse extended header
function parseExtendedHeader(output) {
let numPairs = seqread.readShort()
output.push(` - ${numPairs} key-value pairs:\n`)
for (let i = 0; i < numPairs; i++) {
let key = seqread.readFourCC()
let valueType = seqread.readOneByte()
let valueTypeStr = "Unknown"
switch (valueType) {
case 0x00: valueTypeStr = "Int16"; break
case 0x01: valueTypeStr = "Int24"; break
case 0x02: valueTypeStr = "Int32"; break
case 0x03: valueTypeStr = "Int48"; break
case 0x04: valueTypeStr = "Int64"; break
case 0x10: valueTypeStr = "Bytes"; break
}
output.push(` ${key} (type: ${valueTypeStr} (0x${valueType.toString(16).padStart(2,'0')})): `)
if (valueType === 0x04) { // Int64
let value = readInt64()
if (key === "CDAT") {
let timeSec = Math.floor(value / 1000000)
let date = new Date(timeSec * 1000)
output.push(date.toUTCString())
} else {
output.push((value / 1000000000).toFixed(6) + " seconds")
}
} else if (valueType === 0x10) { // Bytes
let length = seqread.readShort()
let data = seqread.readString(length)
output.push(`"${data}"`)
} else {
output.push("Unknown type")
}
if (i < numPairs - 1) {
output.push("\n")
}
}
}
// Parse subtitle packet
function parseSubtitlePacket(size, isTimecoded, output) {
let index = readUint24()
let timecodeNs = 0
let headerSize = 4 // 3 bytes index + 1 byte opcode
if (isTimecoded) {
timecodeNs = readInt64()
headerSize += 8
}
let opcode = seqread.readOneByte()
output.push(` [Index=${index}`)
if (isTimecoded) {
output.push(`, Time=${(timecodeNs / 1000000000).toFixed(3)}s`)
}
output.push(`, Opcode=0x${opcode.toString(16).padStart(2,'0')}`)
switch (opcode) {
case 0x01: output.push(" (SHOW)"); break
case 0x02: output.push(" (HIDE)"); break
case 0x03: output.push(" (MOVE)"); break
case 0x80: output.push(" (UPLOAD LOW FONT)"); break
case 0x81: output.push(" (UPLOAD HIGH FONT)"); break
default:
if (opcode >= 0x10 && opcode <= 0x2F) output.push(" (SHOW LANG)")
else if (opcode >= 0x30 && opcode <= 0x41) output.push(" (REVEAL)")
break
}
output.push("]")
// Read text content for SHOW commands
let remaining = size - headerSize
if ((opcode === 0x01 || (opcode >= 0x10 && opcode <= 0x2F) || (opcode >= 0x30 && opcode <= 0x41)) && remaining > 0) {
let text = seqread.readString(remaining)
// Clean up control characters
text = text.replace(/[\n\r\t]/g, ' ')
output.push(` Text: "${text}"`)
} else {
seqread.skip(remaining)
}
}
// Parse videotex packet
function parseVideotexPacket(size, output) {
let compressedPtr = sys.malloc(size)
if (compressedPtr === 0) {
seqread.skip(size)
output.push(` - size=${size} bytes`)
return
}
seqread.readBytes(size, compressedPtr)
let decompressSize = 8192
let decompressedPtr = sys.malloc(decompressSize)
if (decompressedPtr === 0) {
sys.free(compressedPtr)
output.push(` - size=${size} bytes`)
return
}
try {
let actualSize = gzip.decompFromTo(compressedPtr, size, decompressedPtr)
if (actualSize >= 2) {
let rows = sys.peek(decompressedPtr) & 0xFF
let cols = sys.peek(decompressedPtr + 1) & 0xFF
let ratio = (actualSize / size).toFixed(2)
output.push(` - size=${size} bytes (decompressed: ${actualSize} bytes, grid: ${cols}x${rows}, ratio: ${ratio}:1)`)
} else {
output.push(` - size=${size} bytes (decompression failed)`)
}
} catch (e) {
output.push(` - size=${size} bytes (decompression failed)`)
}
sys.free(decompressedPtr)
sys.free(compressedPtr)
}
// Main function
function main() {
if (exec_args.length < 3) {
println("Usage: tav_inspector <input.tav> <output.txt>")
println(" Analyzes TAV file packets and writes report to output file")
return 1
}
let inputPath = _G.shell.resolvePathInput(exec_args[1]).full
let outputPath = _G.shell.resolvePathInput(exec_args[2]).full
const FILE_LENGTH = files.open(inputPath).size
// Prepare sequential reader
try {
seqread.prepare(inputPath)
} catch (e) {
println(`Error: Cannot open file ${inputPath}`)
println(e.toString())
return 1
}
let output = []
// Read and verify TAV header (32 bytes)
let magic = seqread.readString(8)
let expectedMagic = "\x1FTSVMTAV"
if (magic !== expectedMagic) {
println("Error: Invalid TAV magic number")
return 1
}
// Parse header fields
let version = seqread.readOneByte()
let baseVersion = (version > 8) ? (version - 8) : version
let temporalMotionCoder = (version > 8) ? 1 : 0
let width = seqread.readShort()
let height = seqread.readShort()
let fps = seqread.readOneByte()
let totalFrames = seqread.readInt()
let wavelet = seqread.readOneByte()
let decompLevels = seqread.readOneByte()
let quantY = seqread.readOneByte()
let quantCo = seqread.readOneByte()
let quantCg = seqread.readOneByte()
let extraFlags = seqread.readOneByte()
let videoFlags = seqread.readOneByte()
let quality = seqread.readOneByte()
let channelLayout = seqread.readOneByte()
let entropyCoder = seqread.readOneByte()
let encoderPreset = seqread.readOneByte()
seqread.skip(3) // Reserved bytes
let waveletNames = ["LGT 5/3", "CDF 9/7", "CDF 13/7", "Reserved", "Reserved",
"Reserved", "Reserved", "Reserved", "Reserved",
"Reserved", "Reserved", "Reserved", "Reserved",
"Reserved", "Reserved", "Reserved", "DD-4"]
// Write header information
output.push("TAV Packet Inspector\n")
output.push(`File: ${inputPath}\n`)
output.push("==================================================\n\n")
output.push("TAV Header:\n")
output.push(` Version: ${version} (base: ${baseVersion} - ${VERDESC[baseVersion]}, temporal: ${TEMPORAL_WAVELET[temporalMotionCoder]})\n`)
output.push(` Resolution: ${width}x${height}\n`)
output.push(` Frame rate: ${fps} fps`)
if (videoFlags & 0x02) output.push(" (NTSC)")
output.push("\n")
output.push(` Total frames: ${totalFrames}\n`)
output.push(` Wavelet: ${wavelet}`)
if (wavelet < 17) output.push(` (${waveletNames[wavelet === 16 ? 16 : wavelet]})`)
if (wavelet === 255) output.push(" (Haar)")
output.push("\n")
output.push(` Decomp levels: ${decompLevels}\n`)
output.push(` Quantisers: Y=${QLUT[quantY]}, Co=${QLUT[quantCo]}, Cg=${QLUT[quantCg]} (Index=${quantY},${quantCo},${quantCg})\n`)
if (quality > 0)
output.push(` Quality: ${quality - 1}\n`)
else
output.push(" Quality: n/a\n")
output.push(` Channel layout: ${CLAYOUT[channelLayout]}\n`)
output.push(` Entropy coder: ${entropyCoder === 0 ? "Twobit-map" : "EZBC"}\n`)
output.push(" Encoder preset: ")
if (encoderPreset === 0) {
output.push("Default\n")
} else {
let presets = []
if (encoderPreset & 0x01) presets.push("Sports")
if (encoderPreset & 0x02) presets.push("Anime")
output.push(presets.join(", ") + "\n")
}
output.push(" Flags:\n")
output.push(` Has audio: ${(extraFlags & 0x01) ? "Yes" : "No"}\n`)
output.push(` Has subtitles: ${(extraFlags & 0x02) ? "Yes" : "No"}\n`)
output.push(` Progressive: ${(videoFlags & 0x01) ? "No (interlaced)" : "Yes"}\n`)
output.push(` Lossless: ${(videoFlags & 0x04) ? "Yes" : "No"}\n`)
if (extraFlags & 0x04) output.push(" Progressive TX: Enabled\n")
if (extraFlags & 0x08) output.push(" ROI encoding: Enabled\n")
output.push("\nPackets:\n")
output.push("==================================================\n")
// Statistics
let stats = {
iframeCount: 0,
pframeCount: 0,
pframeIntraCount: 0,
pframeDeltaCount: 0,
pframeSkipCount: 0,
gopUnifiedCount: 0,
gopUnifiedMotionCount: 0,
gopSyncCount: 0,
totalGopFrames: 0,
audioCount: 0,
audioMp2Count: 0,
audioPcm8Count: 0,
audioTadCount: 0,
audioTrackCount: 0,
subtitleCount: 0,
videotexCount: 0,
timecodeCount: 0,
syncCount: 0,
syncNtscCount: 0,
extendedHeaderCount: 0,
metadataCount: 0,
loopPointCount: 0,
muxVideoCount: 0,
unknownCount: 0,
totalVideoBytes: 0,
totalAudioBytes: 0,
audioMp2Bytes: 0,
audioPcm8Bytes: 0,
audioTadBytes: 0,
audioTrackBytes: 0,
videotexBytes: 0
}
let packetNum = 0
let currentFrame = 0
// Parse packets
try {
while (seqread.getReadCount() < FILE_LENGTH) {
let packetOffset = seqread.getReadCount()
let packetType = seqread.readOneByte()
output.push(`Packet ${packetNum} (offset 0x${packetOffset.toString(16).toUpperCase()}): Type 0x${packetType.toString(16).padStart(2,'0').toUpperCase()} (${getPacketTypeName(packetType)})`)
switch (packetType) {
case TAV_PACKET_EXTENDED_HDR:
stats.extendedHeaderCount++
parseExtendedHeader(output)
break
case TAV_PACKET_TIMECODE:
stats.timecodeCount++
let timecodeNs = readInt64()
let timecodeSec = (timecodeNs / 1000000000).toFixed(6)
output.push(` - ${timecodeSec} seconds (Frame ${currentFrame})`)
break
case TAV_PACKET_GOP_UNIFIED:
case TAV_PACKET_GOP_UNIFIED_MOTION:
let gopSize = seqread.readOneByte()
let size0 = 0
if (packetType === TAV_PACKET_GOP_UNIFIED_MOTION) {
size0 = seqread.readInt()
stats.totalVideoBytes += size0
stats.gopUnifiedMotionCount++
seqread.skip(size0)
}
let size1 = seqread.readInt()
stats.totalVideoBytes += size1
seqread.skip(size1)
stats.totalGopFrames += gopSize
if (packetType === TAV_PACKET_GOP_UNIFIED) {
stats.gopUnifiedCount++
}
let totalSize = size0 + size1
let bytesPerFrame = (totalSize / gopSize).toFixed(2)
output.push(` - GOP size=${gopSize}, data size=${totalSize} bytes (${bytesPerFrame} bytes/frame)`)
break
case TAV_PACKET_GOP_SYNC:
let frameCount = seqread.readOneByte()
stats.gopSyncCount++
currentFrame += frameCount
output.push(` - ${frameCount} frames decoded from GOP block`)
break
case TAV_PACKET_IFRAME:
case TAV_PACKET_PFRAME:
case TAV_PACKET_VIDEO_CH2_I:
case TAV_PACKET_VIDEO_CH2_P:
case TAV_PACKET_VIDEO_CH3_I:
case TAV_PACKET_VIDEO_CH3_P:
case TAV_PACKET_VIDEO_CH4_I:
case TAV_PACKET_VIDEO_CH4_P:
case TAV_PACKET_VIDEO_CH5_I:
case TAV_PACKET_VIDEO_CH5_P:
case TAV_PACKET_VIDEO_CH6_I:
case TAV_PACKET_VIDEO_CH6_P:
case TAV_PACKET_VIDEO_CH7_I:
case TAV_PACKET_VIDEO_CH7_P:
case TAV_PACKET_VIDEO_CH8_I:
case TAV_PACKET_VIDEO_CH8_P:
case TAV_PACKET_VIDEO_CH9_I:
case TAV_PACKET_VIDEO_CH9_P:
let size = seqread.readInt()
stats.totalVideoBytes += size
let frameInfo = getFrameInfo(size)
if (packetType === TAV_PACKET_PFRAME ||
(packetType >= 0x71 && packetType <= 0x7F && (packetType & 1))) {
// P-frame
if (packetType === TAV_PACKET_PFRAME) {
stats.pframeCount++
if (frameInfo.mode === FRAME_MODE_INTRA) stats.pframeIntraCount++
else if (frameInfo.mode === FRAME_MODE_DELTA) stats.pframeDeltaCount++
else if (frameInfo.mode === FRAME_MODE_SKIP) stats.pframeSkipCount++
currentFrame++
} else {
stats.muxVideoCount++
}
} else {
// I-frame
if (packetType === TAV_PACKET_IFRAME) {
stats.iframeCount++
currentFrame++
} else {
stats.muxVideoCount++
}
}
output.push(` - size=${size} bytes`)
if (frameInfo.mode >= 0) {
if (frameInfo.mode === FRAME_MODE_SKIP) output.push(" [SKIP]")
else if (frameInfo.mode === FRAME_MODE_DELTA) output.push(" [DELTA]")
else if (frameInfo.mode === FRAME_MODE_INTRA) output.push(" [INTRA]")
if (frameInfo.mode !== FRAME_MODE_SKIP) {
if (frameInfo.quantiser !== 0xFF) {
output.push(` [Q=${frameInfo.quantiser}]`)
}
}
}
if (packetType >= 0x70 && packetType <= 0x7F) {
let channel = Math.floor((packetType - 0x70) / 2) + 2
output.push(` (Channel ${channel})`)
}
break
case TAV_PACKET_AUDIO_MP2:
stats.audioCount++
stats.audioMp2Count++
let mp2Size = seqread.readInt()
stats.totalAudioBytes += mp2Size
stats.audioMp2Bytes += mp2Size
output.push(` - size=${mp2Size} bytes`)
seqread.skip(mp2Size)
break
case TAV_PACKET_AUDIO_PCM8:
stats.audioCount++
stats.audioPcm8Count++
let pcm8Size = seqread.readInt()
stats.totalAudioBytes += pcm8Size
stats.audioPcm8Bytes += pcm8Size
output.push(` - size=${pcm8Size} bytes (zstd compressed)`)
seqread.skip(pcm8Size)
break
case TAV_PACKET_AUDIO_TAD:
stats.audioCount++
stats.audioTadCount++
let sampleCount0 = seqread.readShort()
let payloadSizePlus7 = seqread.readInt()
let sampleCount = seqread.readShort()
let quantiser = seqread.readOneByte()
let compressedSize = seqread.readInt()
stats.totalAudioBytes += compressedSize
stats.audioTadBytes += compressedSize
output.push(` - samples=${sampleCount}, size=${compressedSize} bytes, quantiser=${quantiser * 2 + 1} steps (index ${quantiser})`)
seqread.skip(compressedSize)
break
case TAV_PACKET_AUDIO_TRACK:
stats.audioCount++
stats.audioTrackCount++
let trackSize = seqread.readInt()
stats.totalAudioBytes += trackSize
stats.audioTrackBytes += trackSize
output.push(` - size=${trackSize} bytes (separate track)`)
seqread.skip(trackSize)
break
case TAV_PACKET_SUBTITLE:
case TAV_PACKET_SUBTITLE_TC:
stats.subtitleCount++
let subSize = seqread.readInt()
output.push(` - size=${subSize} bytes`)
parseSubtitlePacket(subSize, packetType === TAV_PACKET_SUBTITLE_TC, output)
break
case TAV_PACKET_VIDEOTEX:
stats.videotexCount++
let vtSize = seqread.readInt()
stats.videotexBytes += vtSize
parseVideotexPacket(vtSize, output)
break
case TAV_PACKET_EXIF:
case TAV_PACKET_ID3V1:
case TAV_PACKET_ID3V2:
case TAV_PACKET_VORBIS_COMMENT:
case TAV_PACKET_CD_TEXT:
stats.metadataCount++
let metaSize = seqread.readInt()
output.push(` - size=${metaSize} bytes`)
seqread.skip(metaSize)
break
case TAV_PACKET_LOOP_START:
case TAV_PACKET_LOOP_END:
stats.loopPointCount++
output.push(" (no payload)")
break
case TAV_PACKET_SCREEN_MASK:
let frameNumber = seqread.readInt()
let top = seqread.readShort()
let right = seqread.readShort()
let bottom = seqread.readShort()
let left = seqread.readShort()
output.push(` - Frame=${frameNumber} [top=${top}, right=${right}, bottom=${bottom}, left=${left}]`)
break
case TAV_PACKET_SYNC:
stats.syncCount++
break
case TAV_PACKET_SYNC_NTSC:
stats.syncNtscCount++
break
case TAV_PACKET_NOOP:
// Silent no-op
break
default:
stats.unknownCount++
output.push(" (UNKNOWN)")
break
}
output.push("\n")
packetNum++
}
} catch (e) {
output.push(`\nError during packet parsing: ${e}\n`)
}
// Print summary
output.push("\n==================================================\n")
output.push("Summary Statistics:\n")
output.push("==================================================\n")
output.push(`Total packets: ${packetNum}\n`)
output.push("\nVideo:\n")
output.push(` I-frames: ${stats.iframeCount}\n`)
output.push(` P-frames: ${stats.pframeCount}`)
if (stats.pframeCount > 0) {
output.push(` (INTRA: ${stats.pframeIntraCount}, DELTA: ${stats.pframeDeltaCount}, SKIP: ${stats.pframeSkipCount}`)
let knownModes = stats.pframeIntraCount + stats.pframeDeltaCount + stats.pframeSkipCount
if (knownModes < stats.pframeCount) {
output.push(`, Unknown: ${stats.pframeCount - knownModes}`)
}
output.push(")")
}
output.push("\n")
if (stats.gopUnifiedCount + stats.gopUnifiedMotionCount > 0) {
let avgFramesPerGop = (stats.totalGopFrames / (stats.gopUnifiedCount + stats.gopUnifiedMotionCount)).toFixed(1)
output.push(` 3D GOP packets: ${stats.gopUnifiedCount + stats.gopUnifiedMotionCount} (total frames: ${stats.totalGopFrames}, avg ${avgFramesPerGop} frames/GOP)\n`)
output.push(` GOP sync packets: ${stats.gopSyncCount}\n`)
}
output.push(` Mux video: ${stats.muxVideoCount}\n`)
output.push(` Total video bytes: ${stats.totalVideoBytes} (${(stats.totalVideoBytes / 1024 / 1024).toFixed(2)} MB)\n`)
output.push("\nAudio:\n")
output.push(` Total packets: ${stats.audioCount}\n`)
if (stats.audioMp2Count > 0) {
output.push(` MP2: ${stats.audioMp2Count} packets, ${stats.audioMp2Bytes} bytes (${(stats.audioMp2Bytes / 1024 / 1024).toFixed(2)} MB)\n`)
}
if (stats.audioPcm8Count > 0) {
output.push(` PCM8 (zstd): ${stats.audioPcm8Count} packets, ${stats.audioPcm8Bytes} bytes (${(stats.audioPcm8Bytes / 1024 / 1024).toFixed(2)} MB)\n`)
}
if (stats.audioTadCount > 0) {
output.push(` TAD32 (zstd): ${stats.audioTadCount} packets, ${stats.audioTadBytes} bytes (${(stats.audioTadBytes / 1024 / 1024).toFixed(2)} MB)\n`)
}
if (stats.audioTrackCount > 0) {
output.push(` Separate track: ${stats.audioTrackCount} packets, ${stats.audioTrackBytes} bytes (${(stats.audioTrackBytes / 1024 / 1024).toFixed(2)} MB)\n`)
}
output.push(` Total audio bytes: ${stats.totalAudioBytes} (${(stats.totalAudioBytes / 1024 / 1024).toFixed(2)} MB)\n`)
output.push("\nOther:\n")
output.push(` Timecodes: ${stats.timecodeCount}\n`)
output.push(` Subtitles: ${stats.subtitleCount}\n`)
if (stats.videotexCount > 0) {
output.push(` Videotex frames: ${stats.videotexCount} (${stats.videotexBytes} bytes, ${(stats.videotexBytes / 1024 / 1024).toFixed(2)} MB)\n`)
}
output.push(` Extended headers: ${stats.extendedHeaderCount}\n`)
output.push(` Metadata packets: ${stats.metadataCount}\n`)
output.push(` Loop points: ${stats.loopPointCount}\n`)
output.push(` Sync packets: ${stats.syncCount}\n`)
output.push(` NTSC sync packets: ${stats.syncNtscCount}\n`)
output.push(` Unknown packets: ${stats.unknownCount}\n`)
// Write output to file
try {
let outputStr = output.join("")
files.open(outputPath).swrite(outputStr)
println(`Analysis complete. Report written to ${outputPath}`)
return 0
} catch (e) {
println(`Error writing output file: ${e}`)
return 1
}
}
return main()

View File

@@ -0,0 +1,6 @@
CC = gcc
CFLAGS = -std=c99 -O3 -Wall -Wextra -Ofast -D_GNU_SOURCE
font_rom_builder:
rm -f font_rom_builder
$(CC) $(CFLAGS) font_rom_builder.c -o font_rom_builder

View File

@@ -0,0 +1,202 @@
/*
* font_rom_builder.c
* Build TSVM 7x14 font ROM from human-readable images (.png, .tga)
*
* Input: Image with no gaps between characters (7x14 pixels per glyph)
* Output: TSVM-compatible font ROM file(s) padded to 1920 bytes
*
* Usage:
* gcc -O2 -std=c99 -Wall font_rom_builder.c -o font_rom_builder
* ./font_rom_builder <input.png|tga> <output_prefix>
*
* For 128-char images: outputs <output_prefix>_high.chr
* For 256-char images: outputs <output_prefix>_low.chr and <output_prefix>_high.chr
*
* Image layout:
* - 128 chars: 16 columns × 8 rows = 112×112 pixels
* - 256 chars: 16 columns × 16 rows = 112×224 pixels
* or 32 columns × 8 rows = 224×112 pixels
*
* ROM format:
* - Each glyph: 14 bytes (one byte per row)
* - Bit 6 = leftmost pixel, Bit 0 = rightmost pixel
* - Each ROM padded to 1920 bytes
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#define GLYPH_W 7
#define GLYPH_H 14
#define GLYPH_BYTES 14
#define ROM_PADDED_SIZE 1920
static void die(const char *msg) {
fprintf(stderr, "Error: %s\n", msg);
exit(1);
}
static void write_rom(const char *filename, const uint8_t *glyphs, int glyph_count) {
FILE *out = fopen(filename, "wb");
if (!out) {
fprintf(stderr, "Failed to open output file: %s\n", filename);
exit(1);
}
// Write glyph data
size_t data_size = glyph_count * GLYPH_BYTES;
fwrite(glyphs, 1, data_size, out);
// Pad to 1920 bytes
if (data_size < ROM_PADDED_SIZE) {
size_t padding = ROM_PADDED_SIZE - data_size;
uint8_t *pad = calloc(padding, 1);
fwrite(pad, 1, padding, out);
free(pad);
fprintf(stderr, " Wrote %zu bytes + %zu bytes padding = %d bytes total\n",
data_size, padding, ROM_PADDED_SIZE);
} else {
fprintf(stderr, " Wrote %zu bytes (no padding needed)\n", data_size);
}
fclose(out);
fprintf(stderr, " Output: %s\n", filename);
}
int main(int argc, char **argv) {
if (argc < 3) {
fprintf(stderr, "Usage: %s <input.png|tga> <output_prefix>\n", argv[0]);
fprintf(stderr, "\n");
fprintf(stderr, "Converts human-readable font images to TSVM font ROM format.\n");
fprintf(stderr, "\n");
fprintf(stderr, "Input requirements:\n");
fprintf(stderr, " - Image with no gaps between characters\n");
fprintf(stderr, " - Each character is 7x14 pixels\n");
fprintf(stderr, " - 128 chars: typically 112x112 (16 cols × 8 rows)\n");
fprintf(stderr, " - 256 chars: typically 112x224 (16 cols × 16 rows)\n");
fprintf(stderr, "\n");
fprintf(stderr, "Output:\n");
fprintf(stderr, " - 128 chars: <prefix>_high.chr (high ROM only)\n");
fprintf(stderr, " - 256 chars: <prefix>_low.chr + <prefix>_high.chr\n");
fprintf(stderr, " - Each ROM padded to 1920 bytes\n");
return 1;
}
const char *input_path = argv[1];
const char *output_prefix = argv[2];
// Get image dimensions using ImageMagick identify
char cmd[1024];
snprintf(cmd, sizeof(cmd), "identify -format '%%w %%h' \"%s\" 2>/dev/null", input_path);
FILE *pipe = popen(cmd, "r");
if (!pipe) die("Failed to run 'identify' command (ImageMagick required)");
int img_w = 0, img_h = 0;
if (fscanf(pipe, "%d %d", &img_w, &img_h) != 2) {
pclose(pipe);
die("Failed to read image dimensions (is ImageMagick installed?)");
}
pclose(pipe);
fprintf(stderr, "Input: %s (%dx%d)\n", input_path, img_w, img_h);
// Calculate grid dimensions
int cols = img_w / GLYPH_W;
int rows = img_h / GLYPH_H;
int total_chars = cols * rows;
if (img_w % GLYPH_W != 0 || img_h % GLYPH_H != 0) {
fprintf(stderr, "Warning: Image dimensions not evenly divisible by %dx%d\n",
GLYPH_W, GLYPH_H);
}
fprintf(stderr, "Grid: %d columns × %d rows = %d characters\n", cols, rows, total_chars);
// Validate character count
if (total_chars != 128 && total_chars != 256) {
fprintf(stderr, "Error: Expected 128 or 256 characters, got %d\n", total_chars);
fprintf(stderr, " For 128 chars: use 112x112 (16×8) or similar layout\n");
fprintf(stderr, " For 256 chars: use 112x224 (16×16) or 224x112 (32×8)\n");
return 1;
}
// Read image as grayscale using ImageMagick convert
// IMPORTANT: Flatten alpha onto black background first, so transparent pixels become black
size_t img_size = img_w * img_h;
uint8_t *img_data = malloc(img_size);
if (!img_data) die("Memory allocation failed");
snprintf(cmd, sizeof(cmd),
"convert \"%s\" -background black -alpha remove -colorspace Gray -depth 8 gray:- 2>/dev/null",
input_path);
pipe = popen(cmd, "r");
if (!pipe) die("Failed to run 'convert' command (ImageMagick required)");
if (fread(img_data, 1, img_size, pipe) != img_size) {
pclose(pipe);
die("Failed to read image data from ImageMagick");
}
pclose(pipe);
fprintf(stderr, "Read %zu bytes of grayscale data\n", img_size);
// Extract glyphs
uint8_t *glyphs = calloc(total_chars, GLYPH_BYTES);
if (!glyphs) die("Memory allocation failed");
for (int gy = 0; gy < rows; gy++) {
for (int gx = 0; gx < cols; gx++) {
int glyph_idx = gy * cols + gx;
uint8_t *glyph = &glyphs[glyph_idx * GLYPH_BYTES];
for (int row = 0; row < GLYPH_H; row++) {
uint8_t byte = 0;
for (int col = 0; col < GLYPH_W; col++) {
int px = gx * GLYPH_W + col;
int py = gy * GLYPH_H + row;
uint8_t pixel = img_data[py * img_w + px];
// Threshold: >= 128 is foreground (white/lit)
int is_set = (pixel >= 128) ? 1 : 0;
// Pack: bit 6 = leftmost, bit 0 = rightmost
if (is_set) {
byte |= (1u << (6 - col));
}
}
glyph[row] = byte;
}
}
}
free(img_data);
fprintf(stderr, "Extracted %d glyphs\n", total_chars);
// Write output ROM file(s)
char out_path[1024];
if (total_chars == 128) {
// High ROM only (chars 128-255)
snprintf(out_path, sizeof(out_path), "%s.chr", output_prefix);
fprintf(stderr, "\nWriting high ROM (128 chars):\n");
write_rom(out_path, glyphs, 128);
} else {
// 256 chars: low ROM (0-127) and high ROM (128-255)
snprintf(out_path, sizeof(out_path), "%s_low.chr", output_prefix);
fprintf(stderr, "\nWriting low ROM (chars 0-127):\n");
write_rom(out_path, glyphs, 128);
snprintf(out_path, sizeof(out_path), "%s_high.chr", output_prefix);
fprintf(stderr, "\nWriting high ROM (chars 128-255):\n");
write_rom(out_path, &glyphs[128 * GLYPH_BYTES], 128);
}
free(glyphs);
fprintf(stderr, "\nDone.\n");
return 0;
}

View File

@@ -22,6 +22,7 @@ cp -r "../out/$RUNTIME" $DESTDIR/
# Copy over all the assets and a jarfile
cp -r "../out/TerranBASIC.jar" $DESTDIR/
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/
# Pack everything to AppImage
ARCH=arm_aarch64 "./$APPIMAGETOOL" $DESTDIR "out/$DESTDIR.AppImage" || { echo 'Building AppImage failed' >&2; exit 1; }

View File

@@ -22,6 +22,7 @@ cp -r "../out/$RUNTIME" $DESTDIR/
# Copy over all the assets and a jarfile
cp -r "../out/TerranBASIC.jar" $DESTDIR/
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/
# Pack everything to AppImage
"./$APPIMAGETOOL" $DESTDIR "out/$DESTDIR.AppImage" || { echo 'Building AppImage failed' >&2; exit 1; }

View File

@@ -23,5 +23,6 @@ cp -r "../out/$RUNTIME" $DESTDIR/Contents/MacOS/
# Copy over all the assets and a jarfile
cp -r "../out/TerranBASIC.jar" $DESTDIR/Contents/MacOS/
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/Contents/MacOS/
echo "Build successful: $DESTDIR"

View File

@@ -23,5 +23,6 @@ cp -r "../out/$RUNTIME" $DESTDIR/Contents/MacOS/
# Copy over all the assets and a jarfile
cp -r "../out/TerranBASIC.jar" $DESTDIR/Contents/MacOS/
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/Contents/MacOS/
echo "Build successful: $DESTDIR"

View File

@@ -18,6 +18,7 @@ cp -r "../out/$RUNTIME" $DESTDIR/
# Copy over all the assets and a jarfile
cp -r "../out/TerranBASIC.jar" $DESTDIR/
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/
# Temporary solution: zip everything
zip -r -9 -l "out/$DESTDIR.zip" $DESTDIR

View File

@@ -1,3 +1,4 @@
#!/bin/bash
cd "${0%/*}"
./runtime-linux-arm/bin/java -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
./runtime-linux-arm/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar

View File

@@ -1,3 +1,4 @@
#!/bin/bash
cd "${0%/*}"
./runtime-linux-x86/bin/java -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
./runtime-linux-x86/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar

View File

@@ -1,3 +1,4 @@
#!/bin/bash
cd "${0%/*}"
./runtime-osx-arm/bin/java -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
./runtime-osx-arm/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar

View File

@@ -1,3 +1,4 @@
#!/bin/bash
cd "${0%/*}"
./runtime-osx-x86/bin/java -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
./runtime-osx-x86/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar

View File

@@ -1,2 +1,3 @@
cd /D "%~dp0"
.\runtime-windows-x86\bin\java -Xms128M -Xmx2G -jar .\TerranBASIC.jar
set GRAAL_MODULE_PATH=compiler-23.1.10.jar;compiler-management-23.1.10.jar;truffle-compiler-23.1.10.jar;truffle-api-23.1.10.jar;truffle-runtime-23.1.10.jar;polyglot-23.1.10.jar;collections-23.1.10.jar;word-23.1.10.jar;nativeimage-23.1.10.jar;jniutils-23.1.10.jar
.\runtime-windows-x86\bin\java --upgrade-module-path=%GRAAL_MODULE_PATH% -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -Xms128M -Xmx2G -jar .\TerranBASIC.jar

View File

@@ -662,7 +662,7 @@ TODO
\endlastfoot
\centering
\begin{tabulary}{\textwidth}{rl}
{\ttfamily 0} & {\ttfamily \#000F} \\
{\ttfamily 0} & {\ttfamily \#0007} \\
{\ttfamily 1} & {\ttfamily \#004F} \\
{\ttfamily 2} & {\ttfamily \#008F} \\
{\ttfamily 3} & {\ttfamily \#00BF} \\

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

81
ipf_encoder/Makefile Normal file
View File

@@ -0,0 +1,81 @@
# Makefile for iPF (TSVM Interchangeable Picture Format) Encoder
# Created by CuriousTorvald and Claude on 2025-12-19.
CC = gcc
CFLAGS = -std=c99 -Wall -Wextra -O2 -D_GNU_SOURCE
DBGFLAGS =
PREFIX = /usr/local
# Zstd flags (use pkg-config if available, fallback for cross-platform compatibility)
ZSTD_CFLAGS = $(shell pkg-config --cflags libzstd 2>/dev/null || echo "")
ZSTD_LIBS = $(shell pkg-config --libs libzstd 2>/dev/null || echo "-lzstd")
LIBS = -lm $(ZSTD_LIBS)
# Targets
TARGETS = encoder_ipf decoder_ipf
# Build all (default)
all: $(TARGETS)
encoder_ipf: encoder_ipf.c
rm -f encoder_ipf
$(CC) $(CFLAGS) $(ZSTD_CFLAGS) -o encoder_ipf encoder_ipf.c $(LIBS)
@echo "iPF encoder built: encoder_ipf"
decoder_ipf: decoder_ipf.c
rm -f decoder_ipf
$(CC) $(CFLAGS) $(ZSTD_CFLAGS) -o decoder_ipf decoder_ipf.c $(LIBS)
@echo "iPF decoder built: decoder_ipf"
# Build with debug symbols
debug: CFLAGS += -g -DDEBUG -fsanitize=address -fno-omit-frame-pointer
debug: DBGFLAGS += -fsanitize=address -fno-omit-frame-pointer
debug: clean $(TARGETS)
# Build with optimizations
release: CFLAGS = -std=c99 -Wall -Wextra -O3 -D_GNU_SOURCE -march=native
release: clean $(TARGETS)
# Clean build artifacts
clean:
rm -f $(TARGETS) *.o
# Install
install: $(TARGETS)
cp encoder_ipf $(PREFIX)/bin/
cp decoder_ipf $(PREFIX)/bin/
# Check for required dependencies
check-deps:
@echo "Checking dependencies..."
@pkg-config --exists libzstd || (echo "Error: libzstd-dev not found. Install libzstd-dev or equivalent" && exit 1)
@which ffmpeg >/dev/null 2>&1 || (echo "Error: ffmpeg not found in PATH" && exit 1)
@which ffprobe >/dev/null 2>&1 || (echo "Error: ffprobe not found in PATH" && exit 1)
@echo "All dependencies found."
# Help
help:
@echo "iPF (TSVM Interchangeable Picture Format) Tools"
@echo ""
@echo "Targets:"
@echo " all - Build encoder and decoder (default)"
@echo " encoder_ipf - Build encoder only"
@echo " decoder_ipf - Build decoder only"
@echo " debug - Build with debug symbols and AddressSanitizer"
@echo " release - Build with full optimizations"
@echo " clean - Remove build artifacts"
@echo " install - Install to /usr/local/bin"
@echo " check-deps - Check for required dependencies"
@echo " help - Show this help"
@echo ""
@echo "Requirements:"
@echo " - GCC with C99 support"
@echo " - libzstd-dev (Zstd compression library)"
@echo " - FFmpeg (for image encoding/decoding)"
@echo ""
@echo "Usage:"
@echo " make # Build all"
@echo " ./encoder_ipf -i input.png -o output.ipf # Encode"
@echo " ./decoder_ipf -i output.ipf -o decoded.png # Decode"
.PHONY: all clean install check-deps help debug release

592
ipf_encoder/decoder_ipf.c Normal file
View File

@@ -0,0 +1,592 @@
/**
* iPF Decoder - TSVM Interchangeable Picture Format Decoder
*
* Decodes iPF format (Type 1 or Type 2) images to standard formats via FFmpeg.
*
* Created by CuriousTorvald and Claude on 2025-12-19.
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <math.h>
#include <getopt.h>
#include <zstd.h>
// =============================================================================
// Constants
// =============================================================================
#define IPF_MAGIC "\x1F\x54\x53\x56\x4D\x69\x50\x46" // "\x1FTSVMiPF"
#define IPF_HEADER_SIZE 28 // 8 magic + 2 width + 2 height + 1 flags + 1 type + 10 reserved + 4 uncompressed
#define IPF_TYPE_1 0 // 4:2:0 chroma subsampling
#define IPF_TYPE_2 1 // 4:2:2 chroma subsampling
#define IPF_FLAG_ALPHA 0x01
#define IPF_FLAG_ZSTD 0x10
#define IPF_FLAG_PROGRESSIVE 0x80
#define MAX_PATH 4096
// =============================================================================
// Structures
// =============================================================================
typedef struct {
uint16_t width;
uint16_t height;
uint8_t flags;
uint8_t type;
uint32_t uncompressed_size;
} ipf_header_t;
typedef struct {
char *input_file;
char *output_file;
int verbose;
int raw_output; // Output raw RGB instead of using FFmpeg
} decoder_config_t;
// =============================================================================
// Utility Functions
// =============================================================================
static void print_usage(const char *program) {
printf("iPF Decoder - TSVM Interchangeable Picture Format\n");
printf("\nUsage: %s -i input.ipf -o output.png [options]\n\n", program);
printf("Required:\n");
printf(" -i, --input FILE Input iPF file\n");
printf(" -o, --output FILE Output image file (any format FFmpeg supports)\n");
printf("\nOptions:\n");
printf(" --raw Output raw RGB24/RGBA data instead of image file\n");
printf(" -v, --verbose Verbose output\n");
printf(" -h, --help Show this help\n");
printf("\nExamples:\n");
printf(" %s -i photo.ipf -o photo.png\n", program);
printf(" %s -i logo.ipf -o logo.jpg -v\n", program);
}
static float clampf(float v, float lo, float hi) {
return v < lo ? lo : (v > hi ? hi : v);
}
// =============================================================================
// iPF File Reading
// =============================================================================
static int read_ipf_header(FILE *fp, ipf_header_t *header) {
uint8_t magic[8];
if (fread(magic, 1, 8, fp) != 8) {
fprintf(stderr, "Error: Failed to read magic\n");
return -1;
}
if (memcmp(magic, IPF_MAGIC, 8) != 0) {
fprintf(stderr, "Error: Invalid iPF magic\n");
return -1;
}
// Read width (uint16 LE)
if (fread(&header->width, 2, 1, fp) != 1) return -1;
// Read height (uint16 LE)
if (fread(&header->height, 2, 1, fp) != 1) return -1;
// Read flags
if (fread(&header->flags, 1, 1, fp) != 1) return -1;
// Read type
if (fread(&header->type, 1, 1, fp) != 1) return -1;
// Skip reserved (10 bytes)
fseek(fp, 10, SEEK_CUR);
// Read uncompressed size (uint32 LE)
if (fread(&header->uncompressed_size, 4, 1, fp) != 1) return -1;
return 0;
}
// =============================================================================
// YCoCg to RGB Conversion
// =============================================================================
/**
* Convert YCoCg to RGB for 4 pixels sharing the same chroma.
* y_values: 4 Y values packed as nibbles (Y0|Y1 in low byte, Y2|Y3 in high byte style)
* a_values: 4 alpha values packed similarly
* co, cg: 4-bit chroma values [0..15]
*
* Output: fills rgb array with R,G,B[,A] values for 4 pixels
*/
static void ycocg_to_rgb_quad(int co, int cg, int y0, int y1, int y2, int y3,
int a0, int a1, int a2, int a3,
int has_alpha, uint8_t *rgb) {
// Convert chroma from [0..15] to [-1..1]
float co_f = (co - 7) / 8.0f;
float cg_f = (cg - 7) / 8.0f;
int ys[4] = {y0, y1, y2, y3};
int as[4] = {a0, a1, a2, a3};
int stride = has_alpha ? 4 : 3;
for (int i = 0; i < 4; i++) {
float y = ys[i] / 15.0f;
// YCoCg to RGB conversion
float tmp = y - cg_f / 2.0f;
float g = clampf(cg_f + tmp, 0.0f, 1.0f);
float b = clampf(tmp - co_f / 2.0f, 0.0f, 1.0f);
float r = clampf(b + co_f, 0.0f, 1.0f);
rgb[i * stride + 0] = (uint8_t)(r * 255.0f + 0.5f);
rgb[i * stride + 1] = (uint8_t)(g * 255.0f + 0.5f);
rgb[i * stride + 2] = (uint8_t)(b * 255.0f + 0.5f);
if (has_alpha) {
rgb[i * stride + 3] = (uint8_t)(as[i] * 17); // Scale 0-15 to 0-255
}
}
}
/**
* Decode iPF1 block (4:2:0 chroma subsampling).
* Input: 12 bytes (or 20 with alpha)
* Output: 16 pixels in RGB24/RGBA format
*/
static void decode_ipf1_block(const uint8_t *block, int has_alpha, uint8_t *pixels, int stride) {
// Read chroma (4 values for 2x2 regions)
int co1 = block[0] & 0x0F;
int co2 = (block[0] >> 4) & 0x0F;
int co3 = block[1] & 0x0F;
int co4 = (block[1] >> 4) & 0x0F;
int cg1 = block[2] & 0x0F;
int cg2 = (block[2] >> 4) & 0x0F;
int cg3 = block[3] & 0x0F;
int cg4 = (block[3] >> 4) & 0x0F;
// Read Y values (16 values)
// Layout: [Y1|Y0|Y5|Y4], [Y3|Y2|Y7|Y6], [Y9|Y8|YD|YC], [YB|YA|YF|YE]
int Y[16];
Y[0] = block[4] & 0x0F;
Y[1] = (block[4] >> 4) & 0x0F;
Y[4] = block[5] & 0x0F;
Y[5] = (block[5] >> 4) & 0x0F;
Y[2] = block[6] & 0x0F;
Y[3] = (block[6] >> 4) & 0x0F;
Y[6] = block[7] & 0x0F;
Y[7] = (block[7] >> 4) & 0x0F;
Y[8] = block[8] & 0x0F;
Y[9] = (block[8] >> 4) & 0x0F;
Y[12] = block[9] & 0x0F;
Y[13] = (block[9] >> 4) & 0x0F;
Y[10] = block[10] & 0x0F;
Y[11] = (block[10] >> 4) & 0x0F;
Y[14] = block[11] & 0x0F;
Y[15] = (block[11] >> 4) & 0x0F;
// Read alpha values if present
int A[16];
if (has_alpha) {
A[0] = block[12] & 0x0F;
A[1] = (block[12] >> 4) & 0x0F;
A[4] = block[13] & 0x0F;
A[5] = (block[13] >> 4) & 0x0F;
A[2] = block[14] & 0x0F;
A[3] = (block[14] >> 4) & 0x0F;
A[6] = block[15] & 0x0F;
A[7] = (block[15] >> 4) & 0x0F;
A[8] = block[16] & 0x0F;
A[9] = (block[16] >> 4) & 0x0F;
A[12] = block[17] & 0x0F;
A[13] = (block[17] >> 4) & 0x0F;
A[10] = block[18] & 0x0F;
A[11] = (block[18] >> 4) & 0x0F;
A[14] = block[19] & 0x0F;
A[15] = (block[19] >> 4) & 0x0F;
} else {
for (int i = 0; i < 16; i++) A[i] = 15;
}
int channels = has_alpha ? 4 : 3;
uint8_t quad[16]; // 4 pixels max
// Decode 4 quads (2x2 regions), each sharing one chroma pair
// Top-left quad (pixels 0,1,4,5) uses co1/cg1
ycocg_to_rgb_quad(co1, cg1, Y[0], Y[1], Y[4], Y[5], A[0], A[1], A[4], A[5], has_alpha, quad);
memcpy(pixels + 0 * stride + 0 * channels, quad + 0 * channels, channels);
memcpy(pixels + 0 * stride + 1 * channels, quad + 1 * channels, channels);
memcpy(pixels + 1 * stride + 0 * channels, quad + 2 * channels, channels);
memcpy(pixels + 1 * stride + 1 * channels, quad + 3 * channels, channels);
// Top-right quad (pixels 2,3,6,7) uses co2/cg2
ycocg_to_rgb_quad(co2, cg2, Y[2], Y[3], Y[6], Y[7], A[2], A[3], A[6], A[7], has_alpha, quad);
memcpy(pixels + 0 * stride + 2 * channels, quad + 0 * channels, channels);
memcpy(pixels + 0 * stride + 3 * channels, quad + 1 * channels, channels);
memcpy(pixels + 1 * stride + 2 * channels, quad + 2 * channels, channels);
memcpy(pixels + 1 * stride + 3 * channels, quad + 3 * channels, channels);
// Bottom-left quad (pixels 8,9,12,13) uses co3/cg3
ycocg_to_rgb_quad(co3, cg3, Y[8], Y[9], Y[12], Y[13], A[8], A[9], A[12], A[13], has_alpha, quad);
memcpy(pixels + 2 * stride + 0 * channels, quad + 0 * channels, channels);
memcpy(pixels + 2 * stride + 1 * channels, quad + 1 * channels, channels);
memcpy(pixels + 3 * stride + 0 * channels, quad + 2 * channels, channels);
memcpy(pixels + 3 * stride + 1 * channels, quad + 3 * channels, channels);
// Bottom-right quad (pixels 10,11,14,15) uses co4/cg4
ycocg_to_rgb_quad(co4, cg4, Y[10], Y[11], Y[14], Y[15], A[10], A[11], A[14], A[15], has_alpha, quad);
memcpy(pixels + 2 * stride + 2 * channels, quad + 0 * channels, channels);
memcpy(pixels + 2 * stride + 3 * channels, quad + 1 * channels, channels);
memcpy(pixels + 3 * stride + 2 * channels, quad + 2 * channels, channels);
memcpy(pixels + 3 * stride + 3 * channels, quad + 3 * channels, channels);
}
/**
* Decode iPF2 block (4:2:2 chroma subsampling).
* Input: 16 bytes (or 24 with alpha)
* Output: 16 pixels in RGB24/RGBA format
*/
static void decode_ipf2_block(const uint8_t *block, int has_alpha, uint8_t *pixels, int stride) {
// Read chroma (8 values for horizontal pairs)
int co[8], cg[8];
co[0] = block[0] & 0x0F;
co[1] = (block[0] >> 4) & 0x0F;
co[2] = block[1] & 0x0F;
co[3] = (block[1] >> 4) & 0x0F;
co[4] = block[2] & 0x0F;
co[5] = (block[2] >> 4) & 0x0F;
co[6] = block[3] & 0x0F;
co[7] = (block[3] >> 4) & 0x0F;
cg[0] = block[4] & 0x0F;
cg[1] = (block[4] >> 4) & 0x0F;
cg[2] = block[5] & 0x0F;
cg[3] = (block[5] >> 4) & 0x0F;
cg[4] = block[6] & 0x0F;
cg[5] = (block[6] >> 4) & 0x0F;
cg[6] = block[7] & 0x0F;
cg[7] = (block[7] >> 4) & 0x0F;
// Read Y values (16 values) - same layout as iPF1
int Y[16];
Y[0] = block[8] & 0x0F;
Y[1] = (block[8] >> 4) & 0x0F;
Y[4] = block[9] & 0x0F;
Y[5] = (block[9] >> 4) & 0x0F;
Y[2] = block[10] & 0x0F;
Y[3] = (block[10] >> 4) & 0x0F;
Y[6] = block[11] & 0x0F;
Y[7] = (block[11] >> 4) & 0x0F;
Y[8] = block[12] & 0x0F;
Y[9] = (block[12] >> 4) & 0x0F;
Y[12] = block[13] & 0x0F;
Y[13] = (block[13] >> 4) & 0x0F;
Y[10] = block[14] & 0x0F;
Y[11] = (block[14] >> 4) & 0x0F;
Y[14] = block[15] & 0x0F;
Y[15] = (block[15] >> 4) & 0x0F;
// Read alpha values if present
int A[16];
if (has_alpha) {
A[0] = block[16] & 0x0F;
A[1] = (block[16] >> 4) & 0x0F;
A[4] = block[17] & 0x0F;
A[5] = (block[17] >> 4) & 0x0F;
A[2] = block[18] & 0x0F;
A[3] = (block[18] >> 4) & 0x0F;
A[6] = block[19] & 0x0F;
A[7] = (block[19] >> 4) & 0x0F;
A[8] = block[20] & 0x0F;
A[9] = (block[20] >> 4) & 0x0F;
A[12] = block[21] & 0x0F;
A[13] = (block[21] >> 4) & 0x0F;
A[10] = block[22] & 0x0F;
A[11] = (block[22] >> 4) & 0x0F;
A[14] = block[23] & 0x0F;
A[15] = (block[23] >> 4) & 0x0F;
} else {
for (int i = 0; i < 16; i++) A[i] = 15;
}
int channels = has_alpha ? 4 : 3;
// iPF2: 4:2:2 - each horizontal pair shares chroma
// Row 0: pixels 0,1 share co[0]/cg[0], pixels 2,3 share co[1]/cg[1]
// Row 1: pixels 4,5 share co[2]/cg[2], pixels 6,7 share co[3]/cg[3]
// Row 2: pixels 8,9 share co[4]/cg[4], pixels 10,11 share co[5]/cg[5]
// Row 3: pixels 12,13 share co[6]/cg[6], pixels 14,15 share co[7]/cg[7]
int pixel_map[8][4] = {
{0, 1, 0, 1}, // co/cg index 0: pixels 0,1
{2, 3, 2, 3}, // co/cg index 1: pixels 2,3
{4, 5, 4, 5}, // co/cg index 2: pixels 4,5
{6, 7, 6, 7}, // co/cg index 3: pixels 6,7
{8, 9, 8, 9}, // co/cg index 4: pixels 8,9
{10, 11, 10, 11}, // co/cg index 5: pixels 10,11
{12, 13, 12, 13}, // co/cg index 6: pixels 12,13
{14, 15, 14, 15} // co/cg index 7: pixels 14,15
};
for (int ci = 0; ci < 8; ci++) {
int p0 = pixel_map[ci][0];
int p1 = pixel_map[ci][1];
uint8_t quad[16]; // 4 pixels max (ycocg_to_rgb_quad writes 4 pixels)
ycocg_to_rgb_quad(co[ci], cg[ci], Y[p0], Y[p1], Y[p0], Y[p1],
A[p0], A[p1], A[p0], A[p1], has_alpha, quad);
int row = p0 / 4;
int col0 = p0 % 4;
int col1 = p1 % 4;
memcpy(pixels + row * stride + col0 * channels, quad + 0 * channels, channels);
memcpy(pixels + row * stride + col1 * channels, quad + 1 * channels, channels);
}
}
// =============================================================================
// Main Decoding
// =============================================================================
static int decode_ipf(const decoder_config_t *cfg) {
FILE *fp = fopen(cfg->input_file, "rb");
if (!fp) {
fprintf(stderr, "Error: Failed to open input file: %s\n", cfg->input_file);
return -1;
}
// Read header
ipf_header_t header;
if (read_ipf_header(fp, &header) < 0) {
fclose(fp);
return -1;
}
int has_alpha = (header.flags & IPF_FLAG_ALPHA) != 0;
int use_zstd = (header.flags & IPF_FLAG_ZSTD) != 0;
int progressive = (header.flags & IPF_FLAG_PROGRESSIVE) != 0;
if (cfg->verbose) {
printf("iPF Header:\n");
printf(" Size: %dx%d\n", header.width, header.height);
printf(" Type: iPF%d (%s)\n", header.type + 1,
header.type == 0 ? "4:2:0" : "4:2:2");
printf(" Flags: %s%s%s\n",
has_alpha ? "alpha " : "",
use_zstd ? "zstd " : "",
progressive ? "progressive " : "");
printf(" Uncompressed size: %u bytes\n", header.uncompressed_size);
}
if (progressive) {
fprintf(stderr, "Warning: Progressive mode not implemented, decoding as sequential\n");
}
// Read compressed/raw block data
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
fseek(fp, IPF_HEADER_SIZE, SEEK_SET);
size_t compressed_size = file_size - IPF_HEADER_SIZE;
uint8_t *compressed_data = malloc(compressed_size);
if (!compressed_data) {
fclose(fp);
fprintf(stderr, "Error: Failed to allocate memory\n");
return -1;
}
if (fread(compressed_data, 1, compressed_size, fp) != compressed_size) {
free(compressed_data);
fclose(fp);
fprintf(stderr, "Error: Failed to read block data\n");
return -1;
}
fclose(fp);
// Decompress if needed
uint8_t *block_data;
size_t block_data_size;
if (use_zstd) {
block_data_size = header.uncompressed_size;
block_data = malloc(block_data_size);
if (!block_data) {
free(compressed_data);
fprintf(stderr, "Error: Failed to allocate decompression buffer\n");
return -1;
}
size_t result = ZSTD_decompress(block_data, block_data_size,
compressed_data, compressed_size);
if (ZSTD_isError(result)) {
fprintf(stderr, "Error: Zstd decompression failed: %s\n",
ZSTD_getErrorName(result));
free(block_data);
free(compressed_data);
return -1;
}
if (cfg->verbose) {
printf("Decompressed: %zu -> %zu bytes\n", compressed_size, block_data_size);
}
free(compressed_data);
} else {
block_data = compressed_data;
block_data_size = compressed_size;
}
// Allocate output image
int channels = has_alpha ? 4 : 3;
size_t image_size = (size_t)header.width * header.height * channels;
uint8_t *image = malloc(image_size);
if (!image) {
free(block_data);
fprintf(stderr, "Error: Failed to allocate image buffer\n");
return -1;
}
// Decode blocks
int blocks_x = (header.width + 3) / 4;
int blocks_y = (header.height + 3) / 4;
int block_size = (header.type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16);
int row_stride = header.width * channels;
int block_stride = 4 * channels; // 4 pixels per block row
size_t block_offset = 0;
for (int by = 0; by < blocks_y; by++) {
for (int bx = 0; bx < blocks_x; bx++) {
// Calculate output position
uint8_t *block_pixels = image + by * 4 * row_stride + bx * block_stride;
if (header.type == IPF_TYPE_1) {
decode_ipf1_block(block_data + block_offset, has_alpha, block_pixels, row_stride);
} else {
decode_ipf2_block(block_data + block_offset, has_alpha, block_pixels, row_stride);
}
block_offset += block_size;
}
}
free(block_data);
if (cfg->verbose) {
printf("Decoded %d blocks (%dx%d)\n", blocks_x * blocks_y, blocks_x, blocks_y);
}
// Output image
int result = 0;
if (cfg->raw_output) {
// Write raw RGB/RGBA data
FILE *out = fopen(cfg->output_file, "wb");
if (!out) {
fprintf(stderr, "Error: Failed to open output file: %s\n", cfg->output_file);
result = -1;
} else {
fwrite(image, 1, image_size, out);
fclose(out);
if (cfg->verbose) {
printf("Wrote %zu bytes raw %s data\n", image_size, has_alpha ? "RGBA" : "RGB24");
}
}
} else {
// Use FFmpeg to write output image
char cmd[MAX_PATH * 2];
const char *pix_fmt = has_alpha ? "rgba" : "rgb24";
snprintf(cmd, sizeof(cmd),
"ffmpeg -hide_banner -v quiet -y -f rawvideo -pix_fmt %s -s %dx%d "
"-i - \"%s\"",
pix_fmt, header.width, header.height, cfg->output_file);
if (cfg->verbose) {
printf("FFmpeg command: %s\n", cmd);
}
FILE *pipe = popen(cmd, "w");
if (!pipe) {
fprintf(stderr, "Error: Failed to start FFmpeg\n");
result = -1;
} else {
fwrite(image, 1, image_size, pipe);
int status = pclose(pipe);
if (status != 0) {
fprintf(stderr, "Error: FFmpeg failed with status %d\n", status);
result = -1;
}
}
}
free(image);
return result;
}
// =============================================================================
// Main Entry Point
// =============================================================================
int main(int argc, char *argv[]) {
decoder_config_t cfg = {
.input_file = NULL,
.output_file = NULL,
.verbose = 0,
.raw_output = 0
};
static struct option long_options[] = {
{"input", required_argument, 0, 'i'},
{"output", required_argument, 0, 'o'},
{"raw", no_argument, 0, 'R'},
{"verbose", no_argument, 0, 'v'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0}
};
int opt;
while ((opt = getopt_long(argc, argv, "i:o:vh", long_options, NULL)) != -1) {
switch (opt) {
case 'i':
cfg.input_file = optarg;
break;
case 'o':
cfg.output_file = optarg;
break;
case 'R':
cfg.raw_output = 1;
break;
case 'v':
cfg.verbose = 1;
break;
case 'h':
print_usage(argv[0]);
return 0;
default:
print_usage(argv[0]);
return 1;
}
}
// Validate required arguments
if (!cfg.input_file || !cfg.output_file) {
fprintf(stderr, "Error: Input and output files are required\n\n");
print_usage(argv[0]);
return 1;
}
int result = decode_ipf(&cfg);
if (result == 0) {
printf("Successfully decoded: %s\n", cfg.output_file);
}
return result == 0 ? 0 : 1;
}

787
ipf_encoder/encoder_ipf.c Normal file
View File

@@ -0,0 +1,787 @@
/**
* iPF Encoder - TSVM Interchangeable Picture Format Encoder
*
* Encodes images to iPF format (Type 1 or Type 2) with:
* - YCoCg colour space with chroma subsampling
* - 4x4 block encoding
* - Optional Zstd compression
* - Optional alpha channel
* - Optional Adam7 progressive ordering
*
* Created by CuriousTorvald and Claude on 2025-12-19.
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <math.h>
#include <getopt.h>
#include <zstd.h>
// =============================================================================
// Constants
// =============================================================================
#define IPF_MAGIC "\x1F\x54\x53\x56\x4D\x69\x50\x46" // "\x1FTSVMiPF"
#define IPF_HEADER_SIZE 28 // 8 magic + 2 width + 2 height + 1 flags + 1 type + 10 reserved + 4 uncompressed size
#define DEFAULT_WIDTH 560
#define DEFAULT_HEIGHT 448
#define IPF_TYPE_1 0 // 4:2:0 chroma subsampling (12 bytes per block, +8 with alpha)
#define IPF_TYPE_2 1 // 4:2:2 chroma subsampling (16 bytes per block, +8 with alpha)
#define IPF_FLAG_ALPHA 0x01 // Has alpha channel
#define IPF_FLAG_ZSTD 0x10 // Zstd compressed
#define IPF_FLAG_PROGRESSIVE 0x80 // Adam7 progressive ordering
#define MAX_PATH 4096
// Bayer dithering kernel (4x4)
static const float BAYER_4X4[16] = {
0.0f/16.0f, 8.0f/16.0f, 2.0f/16.0f, 10.0f/16.0f,
12.0f/16.0f, 4.0f/16.0f, 14.0f/16.0f, 6.0f/16.0f,
3.0f/16.0f, 11.0f/16.0f, 1.0f/16.0f, 9.0f/16.0f,
15.0f/16.0f, 7.0f/16.0f, 13.0f/16.0f, 5.0f/16.0f
};
// Adam7 interlace pattern - pass number (1-7) for each pixel in 8x8 block
// 0 = not in this standard pattern, we'll adapt for 4x4 blocks
static const int ADAM7_PASS[8][8] = {
{1, 6, 4, 6, 2, 6, 4, 6},
{7, 7, 7, 7, 7, 7, 7, 7},
{5, 6, 5, 6, 5, 6, 5, 6},
{7, 7, 7, 7, 7, 7, 7, 7},
{3, 6, 4, 6, 3, 6, 4, 6},
{7, 7, 7, 7, 7, 7, 7, 7},
{5, 6, 5, 6, 5, 6, 5, 6},
{7, 7, 7, 7, 7, 7, 7, 7}
};
// =============================================================================
// Structures
// =============================================================================
typedef struct {
char *input_file;
char *output_file;
int width;
int height;
int ipf_type; // 0 = iPF1, 1 = iPF2
int use_zstd; // 1 = compress with Zstd
int force_alpha; // 1 = force alpha channel in output
int no_alpha; // 1 = strip alpha even if present in input
int progressive; // 1 = Adam7 progressive ordering
int dither; // Bayer dither pattern index (-1 = no dithering)
int verbose;
} encoder_config_t;
typedef struct {
uint8_t *data; // RGB or RGBA data
int width;
int height;
int channels; // 3 = RGB, 4 = RGBA
int has_alpha; // 1 if input image has meaningful alpha
} image_t;
// =============================================================================
// Utility Functions
// =============================================================================
static void print_usage(const char *program) {
printf("iPF Encoder - TSVM Interchangeable Picture Format\n");
printf("\nUsage: %s -i input.png -o output.ipf [options]\n\n", program);
printf("Required:\n");
printf(" -i, --input FILE Input image file (any format FFmpeg supports)\n");
printf(" -o, --output FILE Output iPF file\n");
printf("\nOptions:\n");
printf(" -s, --size WxH Output size (default: %dx%d)\n", DEFAULT_WIDTH, DEFAULT_HEIGHT);
printf(" -t, --type N iPF type: 1 (4:2:0, default) or 2 (4:2:2)\n");
printf(" --no-zstd Disable Zstd compression (default: enabled)\n");
printf(" --alpha Force alpha channel in output\n");
printf(" --no-alpha Strip alpha channel from input\n");
printf(" -p, --progressive Use Adam7 progressive ordering\n");
printf(" -d, --dither N Bayer dither pattern (0=4x4, -1=none, default: 0)\n");
printf(" -v, --verbose Verbose output\n");
printf(" -h, --help Show this help\n");
printf("\nExamples:\n");
printf(" %s -i photo.jpg -o photo.ipf\n", program);
printf(" %s -i logo.png -o logo.ipf --alpha\n", program);
printf(" %s -i image.png -o image.ipf -s 280x224 -t 2\n", program);
}
static int clampi(int v, int lo, int hi) {
return v < lo ? lo : (v > hi ? hi : v);
}
// Convert chroma value [-1..1] to 4-bit [0..15]
static int chroma_to_four_bits(float f) {
return clampi((int)roundf(f * 8.0f) + 7, 0, 15);
}
// =============================================================================
// Image Loading via FFmpeg
// =============================================================================
/**
* Probe input image dimensions using FFmpeg.
* Returns 0 on success, -1 on error.
*/
static int probe_image_dimensions(const char *input_file, int *width, int *height, int *has_alpha) {
char cmd[MAX_PATH * 2];
// Use ffprobe to get dimensions and pixel format
snprintf(cmd, sizeof(cmd),
"ffprobe -v quiet -select_streams v:0 -show_entries stream=width,height,pix_fmt "
"-of csv=p=0:s=x \"%s\" 2>/dev/null",
input_file);
FILE *fp = popen(cmd, "r");
if (!fp) {
fprintf(stderr, "Error: Failed to run ffprobe\n");
return -1;
}
char buffer[256];
if (fgets(buffer, sizeof(buffer), fp) == NULL) {
pclose(fp);
fprintf(stderr, "Error: Failed to read image info\n");
return -1;
}
pclose(fp);
// Parse "width x height x pix_fmt"
char pix_fmt[64] = "";
if (sscanf(buffer, "%dx%dx%63s", width, height, pix_fmt) < 2) {
// Try alternate format without pix_fmt
if (sscanf(buffer, "%dx%d", width, height) != 2) {
fprintf(stderr, "Error: Failed to parse image dimensions\n");
return -1;
}
}
// Check if pixel format indicates alpha
*has_alpha = (strstr(pix_fmt, "rgba") != NULL ||
strstr(pix_fmt, "argb") != NULL ||
strstr(pix_fmt, "bgra") != NULL ||
strstr(pix_fmt, "abgr") != NULL ||
strstr(pix_fmt, "ya") != NULL ||
strstr(pix_fmt, "pal8") != NULL || // palette may have alpha
strstr(pix_fmt, "yuva") != NULL);
return 0;
}
/**
* Load and resize image using FFmpeg.
* Maintains aspect ratio and crops to target size.
* Returns image data or NULL on error.
*/
static image_t* load_image(const char *input_file, int target_width, int target_height,
int want_alpha, int verbose) {
int src_width, src_height, src_has_alpha;
// Probe source dimensions
if (probe_image_dimensions(input_file, &src_width, &src_height, &src_has_alpha) < 0) {
return NULL;
}
if (verbose) {
printf("Source image: %dx%d, alpha: %s\n",
src_width, src_height, src_has_alpha ? "yes" : "no");
}
// Determine if we need alpha channel
int use_alpha = want_alpha || src_has_alpha;
int channels = use_alpha ? 4 : 3;
const char *pix_fmt = use_alpha ? "rgba" : "rgb24";
// Build FFmpeg command with scale and crop filter
char cmd[MAX_PATH * 2];
snprintf(cmd, sizeof(cmd),
"ffmpeg -hide_banner -v quiet -i \"%s\" -f rawvideo -pix_fmt %s -vf "
"\"scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d\" -frames:v 1 -",
input_file, pix_fmt, target_width, target_height, target_width, target_height);
if (verbose) {
printf("FFmpeg command: %s\n", cmd);
}
FILE *fp = popen(cmd, "r");
if (!fp) {
fprintf(stderr, "Error: Failed to start FFmpeg\n");
return NULL;
}
// Allocate image
image_t *img = malloc(sizeof(image_t));
if (!img) {
pclose(fp);
return NULL;
}
size_t data_size = (size_t)target_width * target_height * channels;
img->data = malloc(data_size);
if (!img->data) {
free(img);
pclose(fp);
return NULL;
}
img->width = target_width;
img->height = target_height;
img->channels = channels;
img->has_alpha = use_alpha;
// Read image data
size_t bytes_read = fread(img->data, 1, data_size, fp);
pclose(fp);
if (bytes_read != data_size) {
fprintf(stderr, "Error: Expected %zu bytes, got %zu\n", data_size, bytes_read);
free(img->data);
free(img);
return NULL;
}
if (verbose) {
printf("Loaded %dx%d image, %d channels, %zu bytes\n",
img->width, img->height, img->channels, data_size);
}
return img;
}
static void free_image(image_t *img) {
if (img) {
free(img->data);
free(img);
}
}
// =============================================================================
// iPF Block Encoding
// =============================================================================
/**
* Encode a 4x4 block to YCoCg with dithering.
* Returns arrays of Y (16 values), A (16 values), Co (16 values), Cg (16 values).
*/
static void encode_block_to_ycocg(const image_t *img, int block_x, int block_y,
int dither_pattern,
int *Y_out, int *A_out, float *Co_out, float *Cg_out) {
for (int py = 0; py < 4; py++) {
for (int px = 0; px < 4; px++) {
int ox = block_x * 4 + px;
int oy = block_y * 4 + py;
// Handle out-of-bounds (extend edge pixels)
ox = clampi(ox, 0, img->width - 1);
oy = clampi(oy, 0, img->height - 1);
// Get dither threshold
float t = 0.0f;
if (dither_pattern >= 0) {
t = BAYER_4X4[(py % 4) * 4 + (px % 4)];
}
// Read pixel
int offset = (oy * img->width + ox) * img->channels;
float r0 = img->data[offset + 0] / 255.0f;
float g0 = (img->channels >= 3) ? img->data[offset + 1] / 255.0f : r0;
float b0 = (img->channels >= 3) ? img->data[offset + 2] / 255.0f : r0;
float a0 = (img->channels == 4) ? img->data[offset + 3] / 255.0f : 1.0f;
// Apply dithering
float r = floorf((t / 15.0f + r0) * 15.0f) / 15.0f;
float g = floorf((t / 15.0f + g0) * 15.0f) / 15.0f;
float b = floorf((t / 15.0f + b0) * 15.0f) / 15.0f;
float a = floorf((t / 15.0f + a0) * 15.0f) / 15.0f;
// Convert to YCoCg
float co = r - b; // [-1..1]
float tmp = b + co / 2.0f;
float cg = g - tmp; // [-1..1]
float y = tmp + cg / 2.0f; // [0..1]
int index = py * 4 + px;
Y_out[index] = (int)roundf(y * 15.0f);
A_out[index] = (int)roundf(a * 15.0f);
Co_out[index] = co;
Cg_out[index] = cg;
}
}
}
/**
* Encode iPF1 block (4:2:0 chroma subsampling).
* Returns 12 bytes (or 20 with alpha).
*/
static int encode_ipf1_block(const int *Ys, const int *As, const float *COs, const float *CGs,
int has_alpha, uint8_t *out) {
// Subsample Co/Cg by averaging 2x2 regions (4:2:0)
int cos1 = chroma_to_four_bits((COs[0] + COs[1] + COs[4] + COs[5]) / 4.0f);
int cos2 = chroma_to_four_bits((COs[2] + COs[3] + COs[6] + COs[7]) / 4.0f);
int cos3 = chroma_to_four_bits((COs[8] + COs[9] + COs[12] + COs[13]) / 4.0f);
int cos4 = chroma_to_four_bits((COs[10] + COs[11] + COs[14] + COs[15]) / 4.0f);
int cgs1 = chroma_to_four_bits((CGs[0] + CGs[1] + CGs[4] + CGs[5]) / 4.0f);
int cgs2 = chroma_to_four_bits((CGs[2] + CGs[3] + CGs[6] + CGs[7]) / 4.0f);
int cgs3 = chroma_to_four_bits((CGs[8] + CGs[9] + CGs[12] + CGs[13]) / 4.0f);
int cgs4 = chroma_to_four_bits((CGs[10] + CGs[11] + CGs[14] + CGs[15]) / 4.0f);
// Pack according to iPF1 format
// uint16 [Co4 | Co3 | Co2 | Co1]
out[0] = (cos2 << 4) | cos1;
out[1] = (cos4 << 4) | cos3;
// uint16 [Cg4 | Cg3 | Cg2 | Cg1]
out[2] = (cgs2 << 4) | cgs1;
out[3] = (cgs4 << 4) | cgs3;
// Y values: [Y1|Y0|Y5|Y4], [Y3|Y2|Y7|Y6], [Y9|Y8|YD|YC], [YB|YA|YF|YE]
out[4] = (Ys[1] << 4) | Ys[0];
out[5] = (Ys[5] << 4) | Ys[4];
out[6] = (Ys[3] << 4) | Ys[2];
out[7] = (Ys[7] << 4) | Ys[6];
out[8] = (Ys[9] << 4) | Ys[8];
out[9] = (Ys[13] << 4) | Ys[12];
out[10] = (Ys[11] << 4) | Ys[10];
out[11] = (Ys[15] << 4) | Ys[14];
int block_size = 12;
if (has_alpha) {
// Alpha values: same layout as Y
out[12] = (As[1] << 4) | As[0];
out[13] = (As[5] << 4) | As[4];
out[14] = (As[3] << 4) | As[2];
out[15] = (As[7] << 4) | As[6];
out[16] = (As[9] << 4) | As[8];
out[17] = (As[13] << 4) | As[12];
out[18] = (As[11] << 4) | As[10];
out[19] = (As[15] << 4) | As[14];
block_size = 20;
}
return block_size;
}
/**
* Encode iPF2 block (4:2:2 chroma subsampling).
* Returns 16 bytes (or 24 with alpha).
*/
static int encode_ipf2_block(const int *Ys, const int *As, const float *COs, const float *CGs,
int has_alpha, uint8_t *out) {
// Subsample Co/Cg horizontally only (4:2:2) - 8 values each
int cos1 = chroma_to_four_bits((COs[0] + COs[1]) / 2.0f);
int cos2 = chroma_to_four_bits((COs[2] + COs[3]) / 2.0f);
int cos3 = chroma_to_four_bits((COs[4] + COs[5]) / 2.0f);
int cos4 = chroma_to_four_bits((COs[6] + COs[7]) / 2.0f);
int cos5 = chroma_to_four_bits((COs[8] + COs[9]) / 2.0f);
int cos6 = chroma_to_four_bits((COs[10] + COs[11]) / 2.0f);
int cos7 = chroma_to_four_bits((COs[12] + COs[13]) / 2.0f);
int cos8 = chroma_to_four_bits((COs[14] + COs[15]) / 2.0f);
int cgs1 = chroma_to_four_bits((CGs[0] + CGs[1]) / 2.0f);
int cgs2 = chroma_to_four_bits((CGs[2] + CGs[3]) / 2.0f);
int cgs3 = chroma_to_four_bits((CGs[4] + CGs[5]) / 2.0f);
int cgs4 = chroma_to_four_bits((CGs[6] + CGs[7]) / 2.0f);
int cgs5 = chroma_to_four_bits((CGs[8] + CGs[9]) / 2.0f);
int cgs6 = chroma_to_four_bits((CGs[10] + CGs[11]) / 2.0f);
int cgs7 = chroma_to_four_bits((CGs[12] + CGs[13]) / 2.0f);
int cgs8 = chroma_to_four_bits((CGs[14] + CGs[15]) / 2.0f);
// Pack according to iPF2 format
// uint32 [Co8 | Co7 | Co6 | Co5 | Co4 | Co3 | Co2 | Co1]
out[0] = (cos2 << 4) | cos1;
out[1] = (cos4 << 4) | cos3;
out[2] = (cos6 << 4) | cos5;
out[3] = (cos8 << 4) | cos7;
// uint32 [Cg8 | Cg7 | Cg6 | Cg5 | Cg4 | Cg3 | Cg2 | Cg1]
out[4] = (cgs2 << 4) | cgs1;
out[5] = (cgs4 << 4) | cgs3;
out[6] = (cgs6 << 4) | cgs5;
out[7] = (cgs8 << 4) | cgs7;
// Y values: same as iPF1
out[8] = (Ys[1] << 4) | Ys[0];
out[9] = (Ys[5] << 4) | Ys[4];
out[10] = (Ys[3] << 4) | Ys[2];
out[11] = (Ys[7] << 4) | Ys[6];
out[12] = (Ys[9] << 4) | Ys[8];
out[13] = (Ys[13] << 4) | Ys[12];
out[14] = (Ys[11] << 4) | Ys[10];
out[15] = (Ys[15] << 4) | Ys[14];
int block_size = 16;
if (has_alpha) {
// Alpha values: same layout as Y
out[16] = (As[1] << 4) | As[0];
out[17] = (As[5] << 4) | As[4];
out[18] = (As[3] << 4) | As[2];
out[19] = (As[7] << 4) | As[6];
out[20] = (As[9] << 4) | As[8];
out[21] = (As[13] << 4) | As[12];
out[22] = (As[11] << 4) | As[10];
out[23] = (As[15] << 4) | As[14];
block_size = 24;
}
return block_size;
}
// =============================================================================
// Adam7 Progressive Ordering
// =============================================================================
/**
* Get Adam7 pass number for a block at (block_x, block_y).
* For blocks, we use a simplified version based on block position.
*/
static int get_adam7_pass(int block_x, int block_y) {
// Use Adam7 pattern for 8x8 blocks, but adapt for 4x4 block indices
int px = (block_x * 4) % 8;
int py = (block_y * 4) % 8;
return ADAM7_PASS[py][px];
}
/**
* Encode blocks in Adam7 progressive order.
* Returns the encoded block data in progressive order.
*/
static uint8_t* encode_progressive(const image_t *img, const encoder_config_t *cfg,
int has_alpha, size_t *out_size) {
int blocks_x = (img->width + 3) / 4;
int blocks_y = (img->height + 3) / 4;
int total_blocks = blocks_x * blocks_y;
int block_size = (cfg->ipf_type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16);
size_t max_size = (size_t)total_blocks * block_size;
uint8_t *output = malloc(max_size);
if (!output) return NULL;
// Temporary storage for all encoded blocks
uint8_t *all_blocks = malloc(max_size);
if (!all_blocks) {
free(output);
return NULL;
}
// Encode all blocks first
size_t offset = 0;
for (int by = 0; by < blocks_y; by++) {
for (int bx = 0; bx < blocks_x; bx++) {
int Ys[16], As[16];
float COs[16], CGs[16];
encode_block_to_ycocg(img, bx, by, cfg->dither, Ys, As, COs, CGs);
if (cfg->ipf_type == IPF_TYPE_1) {
encode_ipf1_block(Ys, As, COs, CGs, has_alpha, all_blocks + offset);
} else {
encode_ipf2_block(Ys, As, COs, CGs, has_alpha, all_blocks + offset);
}
offset += block_size;
}
}
// Reorder blocks according to Adam7 progressive order (7 passes)
size_t out_offset = 0;
for (int pass = 1; pass <= 7; pass++) {
for (int by = 0; by < blocks_y; by++) {
for (int bx = 0; bx < blocks_x; bx++) {
if (get_adam7_pass(bx, by) == pass) {
int block_idx = by * blocks_x + bx;
memcpy(output + out_offset, all_blocks + block_idx * block_size, block_size);
out_offset += block_size;
}
}
}
}
free(all_blocks);
*out_size = out_offset;
return output;
}
/**
* Encode blocks in sequential (raster) order.
*/
static uint8_t* encode_sequential(const image_t *img, const encoder_config_t *cfg,
int has_alpha, size_t *out_size) {
int blocks_x = (img->width + 3) / 4;
int blocks_y = (img->height + 3) / 4;
int total_blocks = blocks_x * blocks_y;
int block_size = (cfg->ipf_type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16);
size_t max_size = (size_t)total_blocks * block_size;
uint8_t *output = malloc(max_size);
if (!output) return NULL;
size_t offset = 0;
for (int by = 0; by < blocks_y; by++) {
for (int bx = 0; bx < blocks_x; bx++) {
int Ys[16], As[16];
float COs[16], CGs[16];
encode_block_to_ycocg(img, bx, by, cfg->dither, Ys, As, COs, CGs);
if (cfg->ipf_type == IPF_TYPE_1) {
offset += encode_ipf1_block(Ys, As, COs, CGs, has_alpha, output + offset);
} else {
offset += encode_ipf2_block(Ys, As, COs, CGs, has_alpha, output + offset);
}
}
}
*out_size = offset;
return output;
}
// =============================================================================
// iPF File Writing
// =============================================================================
static int write_ipf_file(const char *output_file, const encoder_config_t *cfg,
const image_t *img, int verbose) {
// Determine if we use alpha
int has_alpha = 0;
if (cfg->force_alpha) {
has_alpha = 1;
} else if (!cfg->no_alpha && img->has_alpha) {
has_alpha = 1;
}
// Encode blocks
size_t block_data_size;
uint8_t *block_data;
if (cfg->progressive) {
block_data = encode_progressive(img, cfg, has_alpha, &block_data_size);
} else {
block_data = encode_sequential(img, cfg, has_alpha, &block_data_size);
}
if (!block_data) {
fprintf(stderr, "Error: Failed to encode image blocks\n");
return -1;
}
if (verbose) {
printf("Encoded %zu bytes of block data\n", block_data_size);
}
// Prepare output data (may be compressed)
uint8_t *output_data = block_data;
size_t output_size = block_data_size;
uint8_t *compressed_data = NULL;
if (cfg->use_zstd) {
size_t max_compressed = ZSTD_compressBound(block_data_size);
compressed_data = malloc(max_compressed);
if (!compressed_data) {
free(block_data);
fprintf(stderr, "Error: Failed to allocate compression buffer\n");
return -1;
}
output_size = ZSTD_compress(compressed_data, max_compressed,
block_data, block_data_size, 7);
if (ZSTD_isError(output_size)) {
fprintf(stderr, "Error: Zstd compression failed: %s\n",
ZSTD_getErrorName(output_size));
free(block_data);
free(compressed_data);
return -1;
}
output_data = compressed_data;
if (verbose) {
printf("Compressed: %zu -> %zu bytes (%.1f%%)\n",
block_data_size, output_size,
100.0 * output_size / block_data_size);
}
}
// Open output file
FILE *fp = fopen(output_file, "wb");
if (!fp) {
fprintf(stderr, "Error: Failed to open output file: %s\n", output_file);
free(block_data);
if (compressed_data) free(compressed_data);
return -1;
}
// Build flags byte
uint8_t flags = 0;
if (has_alpha) flags |= IPF_FLAG_ALPHA;
if (cfg->use_zstd) flags |= IPF_FLAG_ZSTD;
if (cfg->progressive) flags |= IPF_FLAG_PROGRESSIVE | IPF_FLAG_ZSTD; // Progressive always sets zstd flag
// Write header
// Magic: "\x1FTSVMiPF" (8 bytes)
fwrite(IPF_MAGIC, 1, 8, fp);
// Width (uint16 LE)
uint16_t width_le = (uint16_t)cfg->width;
fwrite(&width_le, 2, 1, fp);
// Height (uint16 LE)
uint16_t height_le = (uint16_t)cfg->height;
fwrite(&height_le, 2, 1, fp);
// Flags (uint8)
fwrite(&flags, 1, 1, fp);
// Type (uint8)
uint8_t type_byte = (uint8_t)cfg->ipf_type;
fwrite(&type_byte, 1, 1, fp);
// Reserved (10 bytes)
uint8_t reserved[10] = {0};
fwrite(reserved, 1, 10, fp);
// Uncompressed size (uint32 LE)
uint32_t uncompressed_size_le = (uint32_t)block_data_size;
fwrite(&uncompressed_size_le, 4, 1, fp);
// Write block data
fwrite(output_data, 1, output_size, fp);
fclose(fp);
if (verbose) {
printf("Wrote %zu bytes to %s\n", IPF_HEADER_SIZE + output_size, output_file);
printf(" Format: iPF%d, %dx%d\n", cfg->ipf_type + 1, cfg->width, cfg->height);
printf(" Flags: %s%s%s\n",
has_alpha ? "alpha " : "",
cfg->use_zstd ? "zstd " : "",
cfg->progressive ? "progressive " : "");
}
free(block_data);
if (compressed_data) free(compressed_data);
return 0;
}
// =============================================================================
// Main Entry Point
// =============================================================================
static int parse_size(const char *arg, int *width, int *height) {
return sscanf(arg, "%dx%d", width, height) == 2 ? 0 : -1;
}
int main(int argc, char *argv[]) {
encoder_config_t cfg = {
.input_file = NULL,
.output_file = NULL,
.width = DEFAULT_WIDTH,
.height = DEFAULT_HEIGHT,
.ipf_type = IPF_TYPE_1,
.use_zstd = 1,
.force_alpha = 0,
.no_alpha = 0,
.progressive = 0,
.dither = 0,
.verbose = 0
};
static struct option long_options[] = {
{"input", required_argument, 0, 'i'},
{"output", required_argument, 0, 'o'},
{"size", required_argument, 0, 's'},
{"type", required_argument, 0, 't'},
{"no-zstd", no_argument, 0, 'Z'},
{"alpha", no_argument, 0, 'A'},
{"no-alpha", no_argument, 0, 'N'},
{"progressive", no_argument, 0, 'p'},
{"dither", required_argument, 0, 'd'},
{"verbose", no_argument, 0, 'v'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0}
};
int opt;
while ((opt = getopt_long(argc, argv, "i:o:s:t:pd:vh", long_options, NULL)) != -1) {
switch (opt) {
case 'i':
cfg.input_file = optarg;
break;
case 'o':
cfg.output_file = optarg;
break;
case 's':
if (parse_size(optarg, &cfg.width, &cfg.height) != 0) {
fprintf(stderr, "Error: Invalid size format (use WxH)\n");
return 1;
}
break;
case 't':
cfg.ipf_type = atoi(optarg) - 1; // User specifies 1 or 2
if (cfg.ipf_type < 0 || cfg.ipf_type > 1) {
fprintf(stderr, "Error: Invalid iPF type (use 1 or 2)\n");
return 1;
}
break;
case 'Z':
cfg.use_zstd = 0;
break;
case 'A':
cfg.force_alpha = 1;
break;
case 'N':
cfg.no_alpha = 1;
break;
case 'p':
cfg.progressive = 1;
break;
case 'd':
cfg.dither = atoi(optarg);
break;
case 'v':
cfg.verbose = 1;
break;
case 'h':
print_usage(argv[0]);
return 0;
default:
print_usage(argv[0]);
return 1;
}
}
// Validate required arguments
if (!cfg.input_file || !cfg.output_file) {
fprintf(stderr, "Error: Input and output files are required\n\n");
print_usage(argv[0]);
return 1;
}
// Load image
if (cfg.verbose) {
printf("Loading image: %s\n", cfg.input_file);
}
image_t *img = load_image(cfg.input_file, cfg.width, cfg.height,
cfg.force_alpha, cfg.verbose);
if (!img) {
fprintf(stderr, "Error: Failed to load image\n");
return 1;
}
// Encode and write iPF file
int result = write_ipf_file(cfg.output_file, &cfg, img, cfg.verbose);
free_image(img);
if (result == 0) {
printf("Successfully encoded: %s\n", cfg.output_file);
}
return result == 0 ? 0 : 1;
}

1820
it2taud.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,200 @@
import kotlin.math.ceil
object Random {
fun uniformRand(low: Int, high: Int) = (Math.random() * (high + 1)).toInt()
fun triangularRand(low: Float, high: Float): Float {
val a = (Math.random() + Math.random()) / 2.0
return ((high - low) * a + low).toFloat()
}
fun gaussianRand(avg: Float, stddev: Float): Float {
// Box-Muller transform to generate random numbers with standard normal distribution
// This implementation uses the polar form for better efficiency
// We need two uniform random values between 0 and 1
val random = kotlin.random.Random
// Using the polar form of the Box-Muller transformation
var u: Double
var v: Double
var s: Double
do {
// Generate two uniform random numbers between -1 and 1
u = Math.random() * 2 - 1
v = Math.random() * 2 - 1
// Calculate sum of squares
s = u * u + v * v
} while (s >= 1 || s == 0.0)
// Calculate polar transformation
val multiplier = kotlin.math.sqrt(-2.0 * kotlin.math.ln(s) / s)
// Transform to the desired mean and standard deviation
// We only use one of the two generated values here
return (avg + stddev * u * multiplier).toFloat()
}
}
sealed class SeekSimulator {
abstract fun computeSeekTime(currentSector: Int, targetSector: Int): Float
class Tape(
val totalSectors: Int,
val tapeLengthMeters: Float = 200f,
val baseSeekTime: Float = 0.5f, // seconds base inertia
val tapeSpeedMetersPerSec: Float = 2.0f, // normal speed
) : SeekSimulator() {
override fun computeSeekTime(currentSector: Int, targetSector: Int): Float {
val posCurrent = (currentSector.toFloat() / totalSectors) * tapeLengthMeters
val posTarget = (targetSector.toFloat() / totalSectors) * tapeLengthMeters
val distance = kotlin.math.abs(posTarget - posCurrent)
// Inject random tape jitter
val effectiveSpeed = tapeSpeedMetersPerSec * Random.triangularRand(0.9f, 1.1f)
return baseSeekTime + (distance / effectiveSpeed)
}
}
class Disc(
val totalTracks: Int,
val armSeekBaseTime: Float = 0.005f, // fast seek, seconds
val armSeekMultiplier: Float = 0.002f, // slower for bigger jumps
val rotationLatencyAvg: Float = 0.008f, // seconds (half-rotation average)
) : SeekSimulator() {
override fun computeSeekTime(currentSector: Int, targetSector: Int): Float {
val cylCurrent = sectorToTrack(currentSector)
val cylTarget = sectorToTrack(targetSector)
val deltaTracks = kotlin.math.abs(cylTarget - cylCurrent)
val armSeek = armSeekBaseTime + (armSeekMultiplier * kotlin.math.sqrt(deltaTracks.toFloat()))
val rotationLatency = Random.gaussianRand(rotationLatencyAvg, rotationLatencyAvg * 0.2f)
return armSeek + rotationLatency
}
private fun sectorToTrack(sector: Int): Int {
// Simplistic assumption: sector layout maps 1:1 to track at this level
return sector % totalTracks
}
}
class Drum(
val rpm: Float = 3000f
) : SeekSimulator() {
override fun computeSeekTime(currentSector: Int, targetSector: Int): Float {
val degreesPerSector = 360.0f / 10000.0f // Assume 10k sectors per drum circumference
val angleCurrent = currentSector * degreesPerSector
val angleTarget = targetSector * degreesPerSector
val deltaAngle = kotlin.math.abs(angleTarget - angleCurrent) % 360f
val rotationLatencySeconds = (deltaAngle / 360f) * (60f / rpm)
// Add a little mechanical jitter
val jitteredLatency = rotationLatencySeconds * Random.triangularRand(0.95f, 1.05f)
return jitteredLatency
}
}
}
class SeekLatencySampler(
val simulator: SeekSimulator,
val totalSectors: Int,
val sampleCount: Int = 10000
) {
data class Sample(val fromSector: Int, val toSector: Int, val latency: Float)
val samples = mutableListOf<Sample>()
fun runSampling() {
samples.clear()
var lastSector = Random.uniformRand(0, totalSectors - 1)
repeat(sampleCount) {
val nextSector = Random.uniformRand(0, totalSectors - 1)
val latency = simulator.computeSeekTime(lastSector, nextSector)
samples.add(Sample(lastSector, nextSector, latency))
lastSector = nextSector
}
}
fun analyzeAndPrint() {
if (samples.isEmpty()) {
println("No samples generated. Run runSampling() first.")
return
}
val latencies = samples.map { it.latency }
val minLatency = latencies.minOrNull() ?: 0f
val maxLatency = latencies.maxOrNull() ?: 0f
val avgLatency = latencies.average().toFloat()
val stddevLatency = kotlin.math.sqrt(latencies.map { (it - avgLatency).let { diff -> diff * diff } }.average()).toFloat()
println("=== Seek Latency Stats ===")
println("Samples: $sampleCount")
println("Min: ${"%.4f".format(minLatency)} s")
println("Max: ${"%.4f".format(maxLatency)} s")
println("Avg: ${"%.4f".format(avgLatency)} s")
println("Stddev: ${"%.4f".format(stddevLatency)} s")
printSimpleHistogram(latencies)
}
private fun printSimpleHistogram(latencies: List<Float>, bins: Int = 30) {
val min = latencies.minOrNull() ?: return
val max = latencies.maxOrNull() ?: return
val binSize = (max - min) / bins
val histogram = IntArray(bins) { 0 }
latencies.forEach { latency ->
val bin = kotlin.math.min(((latency - min) / binSize).toInt(), bins - 1)
histogram[bin]++
}
println("--- Latency Distribution ---")
histogram.forEachIndexed { index, count ->
val lower = min + binSize * index
val upper = lower + binSize
val bar = "#".repeat(count / (sampleCount / 200)) // Scale bar length
println("${"%.4f".format(lower)} - ${"%.4f".format(upper)} s: $bar")
}
}
}
fun main() {
val tapeSimulator = SeekSimulator.Tape(
totalSectors = 100000,
tapeLengthMeters = 200f,
baseSeekTime = 0.2f,
tapeSpeedMetersPerSec = 5.0f
)
val discSimulator = SeekSimulator.Disc(
totalTracks = 3810,
armSeekBaseTime = 0.005f,
armSeekMultiplier = 0.002f,
rotationLatencyAvg = 0.008f
)
val drumSimulator = SeekSimulator.Drum(
rpm = 3000f
)
listOf(tapeSimulator, discSimulator, drumSimulator).forEach { sim ->
SeekLatencySampler(
simulator = sim,
totalSectors = 100000,
sampleCount = 5000
).also {
it.runSampling()
it.analyzeAndPrint()
}
}
}

View File

@@ -1,20 +1,8 @@
## How To Edit the Graaljs Jars
## GraalJS JAR Editing (OBSOLETE)
0. Download following from Maven:
The META-INF/services cross-registration hack was needed for GraalJS 22.3.1 where
`js` and `regex` JARs each needed the other's `TruffleLanguage$Provider` registered.
org.graalvm.js:js:00.0.0
org.graalvm.js:js-scriptengine:00.0.0
1. grab `js-00.0.0.jar`
2. on `META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider`, edit as shown:
com.oracle.truffle.js.lang.JavaScriptLanguageProvider (existing line)
com.oracle.truffle.regex.RegexLanguageProvider (<< add this line)
3. grab `regex-00.0.0.jar`
4. on `META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider`, edit as shown:
com.oracle.truffle.regex.RegexLanguageProvider (existing line)
com.oracle.truffle.js.lang.JavaScriptLanguageProvider (<< add this line)
5. Re-zip two files
As of GraalJS 24.1.2, the service discovery mechanism changed to
`TruffleLanguageProvider` and each JAR registers its own provider independently.
No JAR editing is required.

Binary file not shown.

Binary file not shown.

BIN
lib/collections-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

BIN
lib/compiler-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More