mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
Compare commits
503 Commits
new_movie_
...
db3ffdedb6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db3ffdedb6 | ||
|
|
5b9b96c8de | ||
|
|
8e8374ba99 | ||
|
|
34fba4b2f2 | ||
|
|
1d28c89937 | ||
|
|
61524b3685 | ||
|
|
e6f77c4789 | ||
|
|
00c0e18c1a | ||
|
|
135c7b9c4e | ||
|
|
295c1f7fe2 | ||
|
|
e74a373605 | ||
|
|
b1a0a9f801 | ||
|
|
bdc2578072 | ||
|
|
e3bd4a1b59 | ||
|
|
70d953a784 | ||
|
|
f3ece28a10 | ||
|
|
3ecf842ac0 | ||
|
|
6004060344 | ||
|
|
7d89605302 | ||
|
|
11bc1ca125 | ||
|
|
6a72a81198 | ||
|
|
8380d1e845 | ||
|
|
4ea9ade060 | ||
|
|
46ae6511f6 | ||
|
|
577d46d31e | ||
|
|
2ffdf32c91 | ||
|
|
a28fcbcefc | ||
|
|
ebba33a5c3 | ||
|
|
ab0b215759 | ||
|
|
2c2ad70a23 | ||
|
|
fb42ab4413 | ||
|
|
2177ddbd6b | ||
|
|
aa32c70d8a | ||
|
|
72761c0552 | ||
|
|
2cdd731c3b | ||
|
|
b27ef0dbf9 | ||
|
|
ddeab1c782 | ||
|
|
f69108c40d | ||
|
|
74cba0a893 | ||
|
|
bc235ebb17 | ||
|
|
9f01bdfee9 | ||
|
|
3c57e33f8f | ||
|
|
935fbe04a6 | ||
|
|
6b02d73600 | ||
|
|
8e6f597e9b | ||
|
|
ed3bbb6ffe | ||
|
|
27b0f2e63f | ||
|
|
dcd191b734 | ||
|
|
d706f27e18 | ||
|
|
e49140902b | ||
|
|
3182ae9146 | ||
|
|
34b3b83d65 | ||
|
|
a767eebc2e | ||
|
|
6ce8d2cc1e | ||
|
|
9017b76f6d | ||
|
|
449885c1ea | ||
|
|
59bbe9e503 | ||
|
|
cc492c4ead | ||
|
|
ec0f41b574 | ||
|
|
937d3e27ed | ||
|
|
e64e335db3 | ||
|
|
0124b062d0 | ||
|
|
18881a6d16 | ||
|
|
5a4d200fdc | ||
|
|
75ddfcde0f | ||
|
|
d058f11329 | ||
|
|
60b07a325a | ||
|
|
1e482e32a8 | ||
|
|
4ff48bba1c | ||
|
|
2dcdff83c8 | ||
|
|
89d3c5d776 | ||
|
|
517d0ad9a7 | ||
|
|
9524bf36e0 | ||
|
|
8e17256224 | ||
|
|
ac409bf961 | ||
|
|
94e3ce55ce | ||
|
|
9a9893b9a3 | ||
|
|
789c78f1e7 | ||
|
|
c7e7ee650d | ||
|
|
5d968fecf5 | ||
|
|
aaf3cc28b2 | ||
|
|
24375727db | ||
|
|
6a7ef670d9 | ||
|
|
1bbf0de381 | ||
|
|
5e6ac17146 | ||
|
|
d2b1e792b9 | ||
|
|
219ca1e475 | ||
|
|
902ab00132 | ||
|
|
5dc87a80be | ||
|
|
2b91251d6e | ||
|
|
f84d317f95 | ||
|
|
f295223f15 | ||
|
|
6de9476c4f | ||
|
|
e317d79a21 | ||
|
|
fe59df18f7 | ||
|
|
a4adc428d0 | ||
|
|
31e46b78ce | ||
|
|
ac94a52329 | ||
|
|
01ff4b1d47 | ||
|
|
50802186ce | ||
|
|
7184392521 | ||
|
|
018b9f5eb3 | ||
|
|
bb0810798d | ||
|
|
909f970d60 | ||
|
|
80c26c6b35 | ||
|
|
515e0268e6 | ||
|
|
606fa736af | ||
|
|
89effb5b24 | ||
|
|
376c3c4766 | ||
|
|
0a247897e4 | ||
|
|
b838b35525 | ||
|
|
1148454fb3 | ||
|
|
cfb7b97bf0 | ||
|
|
176aa824fe | ||
|
|
d33484c3c8 | ||
|
|
737b1cebe7 | ||
|
|
176766b793 | ||
|
|
191c733913 | ||
|
|
895f1b27ef | ||
|
|
538d718568 | ||
|
|
b3c5719e3a | ||
|
|
27e4bc1ae5 | ||
|
|
2282e0c10b | ||
|
|
e7287fae37 | ||
|
|
65d89db9c6 | ||
|
|
53173a359c | ||
|
|
8d7d534bc8 | ||
|
|
dc3c73252e | ||
|
|
2053526dfa | ||
|
|
6bc49e3f0b | ||
|
|
02c4f9590c | ||
|
|
34afa95137 | ||
|
|
284108f07f | ||
|
|
76011d4fa9 | ||
|
|
b44d9c6b68 | ||
|
|
e47e9e1259 | ||
|
|
c5789ec28b | ||
|
|
93f7f436a3 | ||
|
|
3ca31e57a1 | ||
|
|
1f630aee62 | ||
|
|
3f3644d165 | ||
|
|
e29f9c3032 | ||
|
|
85b8586a3a | ||
|
|
92b9984ef8 | ||
|
|
3f98d25828 | ||
|
|
d4ea9b2d29 | ||
|
|
a1b62f3155 | ||
|
|
4802e10dfc | ||
|
|
6d70960e5c | ||
|
|
3d99568359 | ||
|
|
1fe966ca09 | ||
|
|
037b2c1a16 | ||
|
|
ea09065802 | ||
|
|
8d28bde119 | ||
|
|
755afb7df4 | ||
|
|
539df453ec | ||
|
|
5e3ffea6d3 | ||
|
|
4bda55d511 | ||
|
|
852c0e6e80 | ||
|
|
25309cf5b6 | ||
|
|
44f11120d8 | ||
|
|
bc16ffabb4 | ||
|
|
ad5e5b62bc | ||
|
|
887c2fbfba | ||
|
|
e58eb2c12b | ||
|
|
3a91edb379 | ||
|
|
74d94b350c | ||
|
|
4559e4f3f6 | ||
|
|
c5bc0d6526 | ||
|
|
a92727862e | ||
|
|
31d25d940b | ||
|
|
460046c4ed | ||
|
|
6eeccb4baa | ||
|
|
a7c44bd05f | ||
|
|
4e647f9fe1 | ||
|
|
b2faab9377 | ||
|
|
e833d75b2c | ||
|
|
f84ea5e68a | ||
|
|
5374ca43c3 | ||
|
|
bef85f6e2f | ||
|
|
f02ad1de79 | ||
|
|
7f0ff3e653 | ||
|
|
8702104bfe | ||
|
|
7d899936e2 | ||
|
|
6aa2542bb8 | ||
|
|
2ac084acd7 | ||
|
|
1208690c4f | ||
|
|
ca977b074d | ||
|
|
102801d8b0 | ||
|
|
10351bafb1 | ||
|
|
9310885260 | ||
|
|
b10d5d3a34 | ||
|
|
86b44565e0 | ||
|
|
54b61fb436 | ||
|
|
4afe3816c7 | ||
|
|
f09dd66185 | ||
|
|
b590415231 | ||
|
|
237d3d6fd2 | ||
|
|
3421d71012 | ||
|
|
5d5576c077 | ||
|
|
64026be133 | ||
|
|
96d697e158 | ||
|
|
1680137b7d | ||
|
|
c71920b95d | ||
|
|
629ed5fb12 | ||
|
|
4f6efbe000 | ||
|
|
4362610c70 | ||
|
|
ed63af903b | ||
|
|
c742bc354a | ||
|
|
e893ca2df5 | ||
|
|
ca037c8e74 | ||
|
|
1144338059 | ||
|
|
71dff4b0e0 | ||
|
|
9e1191c0c2 | ||
|
|
3a19b6aea8 | ||
|
|
67413f2749 | ||
|
|
4929d84cec | ||
|
|
3d76006ad9 | ||
|
|
506fcbe79d | ||
|
|
42341b4e10 | ||
|
|
b9d9d221dd | ||
|
|
01a89f3b36 | ||
|
|
300f88a44c | ||
|
|
2d6167393a | ||
|
|
f1d1e36164 | ||
|
|
50092aef60 | ||
|
|
017aef26ab | ||
|
|
efdb915208 | ||
|
|
5d99191b5e | ||
|
|
621c312922 | ||
|
|
c6c50c2ebe | ||
|
|
34a1f0e3db | ||
|
|
9b72a62cdb | ||
|
|
9e2c9e6efd | ||
|
|
0907e22f53 | ||
|
|
3828bd7fbc | ||
|
|
189646a8dc | ||
|
|
7f951366da | ||
|
|
a2233aedaf | ||
|
|
dad1da741f | ||
|
|
94ae24e9e4 | ||
|
|
d3cc05789f | ||
|
|
bc5779d4f5 | ||
|
|
046fa98025 | ||
|
|
196b9a0c01 | ||
|
|
7848357cf2 | ||
|
|
a3f8be9773 | ||
|
|
8adf44e5ae | ||
|
|
602b42c6ff | ||
|
|
79eb81e8d0 | ||
|
|
c522244574 | ||
|
|
28b391cfd4 | ||
|
|
26d216ca13 | ||
|
|
f598daec1e | ||
|
|
3f8cf6a38c | ||
|
|
902d971ae7 | ||
|
|
5ecf2dcadd | ||
|
|
9edeca929d | ||
|
|
3b401139e9 | ||
|
|
acaade1062 | ||
|
|
e3099195e4 | ||
|
|
08bb33bf27 | ||
|
|
6132012e74 | ||
|
|
dbbb471a11 | ||
|
|
53b87ff735 | ||
|
|
d49ec39b73 | ||
|
|
dd60b2c569 | ||
|
|
1c7ab17b1c | ||
|
|
e928d2d3ec | ||
|
|
5129e354bb | ||
|
|
be1b92f188 | ||
|
|
2533b2dc19 | ||
|
|
a61a21d28b | ||
|
|
92274a8e19 | ||
|
|
44cc54264a | ||
|
|
8199cbc955 | ||
|
|
aa7e20695d | ||
|
|
5c87325366 | ||
|
|
64e100e532 | ||
|
|
233f1e7dcd | ||
|
|
19f813eb7d | ||
|
|
a45a919c84 | ||
|
|
9247471bf2 | ||
|
|
6add391d07 | ||
|
|
b19afdae3a | ||
|
|
bd530f803f | ||
|
|
901f6b52b4 | ||
|
|
bff5021a7a | ||
|
|
9425c58e53 | ||
|
|
c1d6a959f5 | ||
|
|
edb951fb1a | ||
|
|
0f5875d45b | ||
|
|
0e6f2162c8 | ||
|
|
28e9a88f8d | ||
|
|
3f97f1a59e | ||
|
|
c0d1d54bed | ||
|
|
aa9ecee7ca | ||
|
|
8878d37e5b | ||
|
|
e743fbf3c0 | ||
|
|
d9d395c62c | ||
|
|
00c882aa8d | ||
|
|
af3679921d | ||
|
|
332e8760ad | ||
|
|
c85b007ba9 | ||
|
|
61b0bdaed7 | ||
|
|
9d98cc1a21 | ||
|
|
76c42f20b3 | ||
|
|
e871264ae5 | ||
|
|
f3b68e1164 | ||
|
|
755d4deb95 | ||
|
|
46ad919407 | ||
|
|
c61bf7750f | ||
|
|
991d035bcc | ||
|
|
480d2d8538 | ||
|
|
4a6edeca09 | ||
|
|
692defdbb8 | ||
|
|
ee2ddef1c1 | ||
|
|
999e1deda0 | ||
|
|
a67d8b5f08 | ||
|
|
f06f339d99 | ||
|
|
86864c4b7a | ||
|
|
86de627734 | ||
|
|
c6de68291d | ||
|
|
b3a91bf6cb | ||
|
|
1d0f369827 | ||
|
|
9c27d114fc | ||
|
|
67f7c091eb | ||
|
|
370d511f44 | ||
|
|
9fcb7fc95c | ||
|
|
52f25f7d04 | ||
|
|
69583e5f1e | ||
|
|
56a1bac19a | ||
|
|
3adc50365b | ||
|
|
cd88885fbf | ||
|
|
9dc71095a0 | ||
|
|
a9319fd812 | ||
|
|
6f669f4fd9 | ||
|
|
e147072d03 | ||
|
|
53da0bfcee | ||
|
|
34427d61d7 | ||
|
|
7f7222fe54 | ||
|
|
4265891093 | ||
|
|
758b134abd | ||
|
|
4eec98cdca | ||
|
|
9ac0424be3 | ||
|
|
f0ad0ef034 | ||
|
|
9553b281af | ||
|
|
019f0aaed5 | ||
|
|
120058be6d | ||
|
|
3b9e02b17f | ||
|
|
93622fc8ca | ||
|
|
0cf1173dd6 | ||
|
|
cc2f3e4d57 | ||
|
|
e179a15f33 | ||
|
|
e19af854dc | ||
|
|
ea72dec996 | ||
|
|
7e248bc83d | ||
|
|
b40b2ff0a1 | ||
|
|
5dcf2177d5 | ||
|
|
871d7bcdfe | ||
|
|
4c48d761b9 | ||
|
|
94749a3ad6 | ||
|
|
e705d274de | ||
|
|
222b9866a8 | ||
|
|
0b7b8cdd35 | ||
|
|
31457974be | ||
|
|
0284b37678 | ||
|
|
912e35a122 | ||
|
|
78a7cdc08f | ||
|
|
1a072f6a0c | ||
|
|
17b5063ef0 | ||
|
|
9826efd98a | ||
|
|
f980f23efe | ||
|
|
67445b040c | ||
|
|
d08511a39d | ||
|
|
f918cd429c | ||
|
|
769b6481da | ||
|
|
00e390d879 | ||
|
|
7474a9d472 | ||
|
|
abce002cdd | ||
|
|
e36d4041ce | ||
|
|
da084c0074 | ||
|
|
3c9441e67f | ||
|
|
581b270c31 | ||
|
|
fd62df99a4 | ||
|
|
ad232d1c84 | ||
|
|
0b066a693e | ||
|
|
cdec0fe020 | ||
|
|
21e3fe4c1e | ||
|
|
364e33eede | ||
|
|
5d4e775ad0 | ||
|
|
f7d98e74e3 | ||
|
|
d6019019dc | ||
|
|
6c83c925cb | ||
|
|
5c3a3a112c | ||
|
|
71102f1d70 | ||
|
|
6222e9d8bd | ||
|
|
60e0ff9e61 | ||
|
|
7d61074a13 | ||
|
|
5f14169e6b | ||
|
|
29b3da1dbc | ||
|
|
27ad3361ea | ||
|
|
d4fae0071b | ||
|
|
0666734c9d | ||
|
|
f1cad6d9fa | ||
|
|
027d3289ca | ||
|
|
b9f38dfa08 | ||
|
|
3e40b048a7 | ||
|
|
70dfc7bf13 | ||
|
|
8c7550e581 | ||
|
|
ff6821eb55 | ||
|
|
4e219d1a71 | ||
|
|
8688fc3742 | ||
|
|
0f784eb741 | ||
|
|
7f22ec8cc7 | ||
|
|
b457be4bbf | ||
|
|
41a8b578b5 | ||
|
|
836e69a40b | ||
|
|
8b808ca297 | ||
|
|
7608e7433a | ||
|
|
907cc37b01 | ||
|
|
1d3d218238 | ||
|
|
5012ca4085 | ||
|
|
66909537a0 | ||
|
|
01278815c7 | ||
|
|
65a01f36a4 | ||
|
|
6ff634cc12 | ||
|
|
d85f8002cc | ||
|
|
c50d015515 | ||
|
|
efab1c3a88 | ||
|
|
4d9981ec23 | ||
|
|
ca18595134 | ||
|
|
b4e9d84f5f | ||
|
|
e2dd0744d2 | ||
|
|
2b59d5dd8d | ||
|
|
bb3f715ad6 | ||
|
|
699c6394a1 | ||
|
|
d117b15e0f | ||
|
|
5564fa5c9b | ||
|
|
21fd10d2b6 | ||
|
|
338a0b2e5d | ||
|
|
0b3497b013 | ||
|
|
a9ba57c09a | ||
|
|
05101ecd08 | ||
|
|
e001445095 | ||
|
|
4851f61c56 | ||
|
|
be43384968 | ||
|
|
28624309d7 | ||
|
|
3584520ff9 | ||
|
|
613b8a97dd | ||
|
|
5d48cc62eb | ||
|
|
206e43a308 | ||
|
|
d3a18c081a | ||
|
|
c14b692114 | ||
|
|
89aada888d | ||
|
|
b373471629 | ||
|
|
35f1a0c2f2 | ||
|
|
f4b03b55b6 | ||
|
|
8279b15b43 | ||
|
|
9652143d93 | ||
|
|
89e8fc39ce | ||
|
|
9ca575eee4 | ||
|
|
953de6feb6 | ||
|
|
ae59946883 | ||
|
|
a639e116c5 | ||
|
|
9e8aeeb112 | ||
|
|
47f93194a7 | ||
|
|
be193269d8 | ||
|
|
391adffad4 | ||
|
|
dab56ee55d | ||
|
|
3011c73168 | ||
|
|
a5da200507 | ||
|
|
54f335e3de | ||
|
|
4bb234a89b | ||
|
|
4c0a282de7 | ||
|
|
113c01b851 | ||
|
|
1343dd10cf | ||
|
|
b497570a3b | ||
|
|
9f901681a6 | ||
|
|
ded609e65e | ||
|
|
34cf5cb591 | ||
|
|
9c2aa96b73 | ||
|
|
d446a4e2f5 | ||
|
|
db57516a46 | ||
|
|
712506c91c | ||
|
|
722e8e893f | ||
|
|
dca09cf4a3 | ||
|
|
62d6ee94cf | ||
|
|
198e951102 | ||
|
|
1f5f72733a | ||
|
|
957522a460 | ||
|
|
433e3ea3ae | ||
|
|
dc223fe00b | ||
|
|
190cb130bf | ||
|
|
29907ec357 | ||
|
|
8601f614b4 | ||
|
|
3f9747ebf0 | ||
|
|
c498526a90 | ||
|
|
5a5ac8ef74 | ||
|
|
9f7a4ef2e7 | ||
|
|
3495dfca5e | ||
|
|
cf1ee80aa1 | ||
|
|
b80d5b858a |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -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
|
||||
|
||||
54
.idea/artifacts/TerranBASIC.xml
generated
54
.idea/artifacts/TerranBASIC.xml
generated
@@ -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
133
.idea/cody_history.xml
generated
Normal 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>
|
||||
11
.idea/libraries/badlogicgames_gdx.xml
generated
Normal file
11
.idea/libraries/badlogicgames_gdx.xml
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
<component name="libraryTable">
|
||||
<library name="badlogicgames.gdx" type="repository">
|
||||
<properties maven-id="com.badlogicgames.gdx:gdx:1.12.1" />
|
||||
<CLASSES>
|
||||
<root url="jar://$MAVEN_REPOSITORY$/com/badlogicgames/gdx/gdx/1.12.1/gdx-1.12.1.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/com/badlogicgames/gdx/gdx-jnigen-loader/2.3.1/gdx-jnigen-loader-2.3.1.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
62
.idea/libraries/badlogicgames_gdx_backend_lwjgl3.xml
generated
Normal file
62
.idea/libraries/badlogicgames_gdx_backend_lwjgl3.xml
generated
Normal file
@@ -0,0 +1,62 @@
|
||||
<component name="libraryTable">
|
||||
<library name="badlogicgames.gdx.backend.lwjgl3" type="repository">
|
||||
<properties maven-id="com.badlogicgames.gdx:gdx-backend-lwjgl3:1.12.1" />
|
||||
<CLASSES>
|
||||
<root url="jar://$MAVEN_REPOSITORY$/com/badlogicgames/gdx/gdx-backend-lwjgl3/1.12.1/gdx-backend-lwjgl3-1.12.1.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/com/badlogicgames/gdx/gdx/1.12.1/gdx-1.12.1.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/com/badlogicgames/gdx/gdx-jnigen-loader/2.3.1/gdx-jnigen-loader-2.3.1.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3-natives-linux.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3-natives-linux-arm32.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3-natives-linux-arm64.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3-natives-macos.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3-natives-macos-arm64.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3-natives-windows.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3-natives-windows-x86.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3-natives-linux.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3-natives-linux-arm32.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3-natives-linux-arm64.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3-natives-macos.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3-natives-macos-arm64.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3-natives-windows.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3-natives-windows-x86.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3-natives-linux.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3-natives-linux-arm32.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3-natives-linux-arm64.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3-natives-macos.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3-natives-macos-arm64.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3-natives-windows.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3-natives-windows-x86.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3-natives-linux.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3-natives-linux-arm32.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3-natives-linux-arm64.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3-natives-macos.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3-natives-macos-arm64.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3-natives-windows.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3-natives-windows-x86.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3-natives-linux.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3-natives-linux-arm32.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3-natives-linux-arm64.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3-natives-macos.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3-natives-macos-arm64.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3-natives-windows.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3-natives-windows-x86.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3-natives-linux.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3-natives-linux-arm32.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3-natives-linux-arm64.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3-natives-macos.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3-natives-macos-arm64.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3-natives-windows.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3-natives-windows-x86.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/com/badlogicgames/jlayer/jlayer/1.0.1-gdx/jlayer-1.0.1-gdx.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jcraft/jorbis/0.0.17/jorbis-0.0.17.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
9
.idea/markdown.xml
generated
Normal file
9
.idea/markdown.xml
generated
Normal 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
2
.idea/misc.xml
generated
@@ -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
17
.idea/runConfigurations/AppLoader.xml
generated
Normal 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
16
.idea/runConfigurations/TerranBASIC.xml
generated
Normal 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
16
.idea/runConfigurations/TsvmEmulator.xml
generated
Normal 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
2
.idea/vcs.xml
generated
@@ -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>
|
||||
12
2taud.sh
Executable file
12
2taud.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env fish
|
||||
|
||||
for f in *.mod; python3 mod2taud.py $f assets/disk0/home/music/(basename $f .mod).taud; end
|
||||
for f in *.MOD; python3 mod2taud.py $f assets/disk0/home/music/(basename $f .MOD).taud; end
|
||||
for f in *.s3m; python3 s3m2taud.py $f assets/disk0/home/music/(basename $f .s3m).taud; end
|
||||
for f in *.S3M; python3 s3m2taud.py $f assets/disk0/home/music/(basename $f .S3M).taud; end
|
||||
for f in *.it; python3 it2taud.py $f assets/disk0/home/music/(basename $f .it).taud; end
|
||||
for f in *.IT; python3 it2taud.py $f assets/disk0/home/music/(basename $f .IT).taud; end
|
||||
for f in *.xm; python3 xm2taud.py $f assets/disk0/home/music/(basename $f .xm).taud; end
|
||||
for f in *.XM; python3 xm2taud.py $f assets/disk0/home/music/(basename $f .XM).taud; end
|
||||
for f in *.mon; python3 mon2taud.py $f assets/disk0/home/music/(basename $f .mon).taud; end
|
||||
for f in *.MON; python3 mon2taud.py $f assets/disk0/home/music/(basename $f .MON).taud; end
|
||||
420
CLAUDE.md
Normal file
420
CLAUDE.md
Normal file
@@ -0,0 +1,420 @@
|
||||
# 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[n−1] / x[n−2] 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
|
||||
- `reference_materials/pt2-clone` — Open-source re-implementation of ProTracker 2
|
||||
|
||||
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
224
README.md
@@ -1,8 +1,222 @@
|
||||

|
||||
|
||||
**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 0–16 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`.
|
||||
|
||||
1423
TAUD_NOTE_EFFECTS.md
Normal file
1423
TAUD_NOTE_EFFECTS.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,5 +10,7 @@
|
||||
<orderEntry type="module" module-name="tsvm_core" />
|
||||
<orderEntry type="library" name="TerranVirtualDisk" level="project" />
|
||||
<orderEntry type="library" name="lib" level="project" />
|
||||
<orderEntry type="library" name="badlogicgames.gdx" level="project" />
|
||||
<orderEntry type="library" name="badlogicgames.gdx.backend.lwjgl3" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -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.
@@ -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();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
let p=_BIOS.FIRST_BOOTABLE_PORT;com.sendMessage(p[0],"DEVRST\x17");com.sendMessage(p[0],'OPENR"tvdos/hyve.SYS",'+p[1]);let r=com.getStatusCode(p[0]);if(0==r)if(com.sendMessage(p[0],"READ"),r=com.getStatusCode(p[0]),0==r){let g=com.pullMessage(p[0]);eval(g)}else println("I/O Error");else println("TVDOS.SYS not found");println("Shutting down...");println("It is now safe to turn off the power")
|
||||
let p=_BIOS.FIRST_BOOTABLE_PORT;com.sendMessage(p[0],"DEVRST\x17");com.sendMessage(p[0],'OPENR"tvdos/TVDOS.SYS",'+p[1]);let r=com.getStatusCode(p[0]);if(0==r)if(com.sendMessage(p[0],"READ"),r=com.getStatusCode(p[0]),0==r){let g=com.pullMessage(p[0]);eval(g)}else println("I/O Error");else println("TVDOS.SYS not found");println("Shutting down...");println("It is now safe to turn off the power")
|
||||
@@ -1,8 +1,12 @@
|
||||
echo "Starting TVDOS..."
|
||||
|
||||
rem put set-xxx commands here:
|
||||
set PATH=\tvdos\installer;\tvdos\tuidev;$PATH
|
||||
set PATH=\tvdos\installer;\tvdos\tuidev;\tbas;\hopper\bin;$PATH
|
||||
set INCLPATH=\hopper\include;$INCLPATH
|
||||
set HELPPATH=\hopper\help;$HELPPATH
|
||||
set KEYBOARD=us_colemak
|
||||
|
||||
rem this line specifies which shell to be presented after the boot precess:
|
||||
tvdos/i18n/korean
|
||||
zfm
|
||||
command -fancy
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2020-2024 CuriousTorvald
|
||||
Copyright (c) 2020-2026 CuriousTorvald
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
TVDOS (c) 2020-2024 CuriousTorvald
|
||||
TVDOS (c) 2020-2026 CuriousTorvald
|
||||
|
||||
TVDOS is provided "as is", without warranty of any kind; in no event shall the authors or copyright holders be liable for any claim, damages or other liabilities. Run 'less COPYING' for more information.
|
||||
@@ -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)
|
||||
@@ -1,2 +1,2 @@
|
||||
println("몬스터 시트라, 이 이름은 특이하게 생긴 프랑스 자동차나 스칸디나비아 보드카에서 온 것은 아닙니다. 고대부터 유래된 과일 시트론에서 영감을 받아 태어난 이 제품은 레몬과 비슷하지만 더 원초적이고 투박합니다. 마치 몬스터 에너지처럼요. 이 고대의 과일과 선조들에게서 영감을 얻은 우리는 전형적인 드링크를 새롭게 해석한 울트라 시트라를 만들었습니다. 울트라 시트라는 새콤달콤한 맛이 입안에서 잔잔하게 퍼지며 상쾌한 맛으로 마무리하죠. 저칼로리에 무설탕이지만 몬스터 에너지만의 블렌드는 변함없이 가득 담겨있답니다.")
|
||||
println("멕시코에서는 매년 할로윈 이후 '죽은 자의 날'을 기념합니다. 신비한 분위기 속의 메리골드 꽃과 추억들은 떠난 이들을 축제로 이끕니다. 누구나 매혹될 이국적인 천사의 주스 블렌드, 망고 로코. 환상적인 맛과 몬스터 에너지 만의 마법으로 파티는 계속될 것입니다.")
|
||||
unicode.println("몬스터 시트라, 이 이름은 특이하게 생긴 프랑스 자동차나 스칸디나비아 보드카에서 온 것은 아닙니다. 고대부터 유래된 과일 시트론에서 영감을 받아 태어난 이 제품은 레몬과 비슷하지만 더 원초적이고 투박합니다. 마치 몬스터 에너지처럼요. 이 고대의 과일과 선조들에게서 영감을 얻은 우리는 전형적인 드링크를 새롭게 해석한 울트라 시트라를 만들었습니다. 울트라 시트라는 새콤달콤한 맛이 입안에서 잔잔하게 퍼지며 상쾌한 맛으로 마무리하죠. 저칼로리에 무설탕이지만 몬스터 에너지만의 블렌드는 변함없이 가득 담겨있답니다.")
|
||||
unicode.println("멕시코에서는 매년 할로윈 이후 '죽은 자의 날'을 기념합니다. 신비한 분위기 속의 메리골드 꽃과 추억들은 떠난 이들을 축제로 이끕니다. 누구나 매혹될 이국적인 천사의 주스 블렌드, 망고 로코. 환상적인 맛과 몬스터 에너지 만의 마법으로 파티는 계속될 것입니다.")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// a simple, symmetric obfuscator with infinite-length key
|
||||
|
||||
function seq(s) {
|
||||
let out = ""
|
||||
let cnt = 0
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
6
assets/disk0/hopper/bin/hopper.js
Normal file
6
assets/disk0/hopper/bin/hopper.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Hopper is a package manager for TSVM
|
||||
* Created by CuriousTorvald on 2026-04-16
|
||||
*/
|
||||
|
||||
println("Hopper - Package manager for TSVM")
|
||||
@@ -32,7 +32,7 @@ if (exec_args !== undefined && exec_args[1] !== undefined && exec_args[1].starts
|
||||
return 0
|
||||
}
|
||||
|
||||
const THEVERSION = "1.2.1"
|
||||
const THEVERSION = "1.2.2"
|
||||
|
||||
const PROD = true
|
||||
let INDEX_BASE = 0
|
||||
@@ -4197,7 +4197,7 @@ bF.load = function(args) { // LOAD function
|
||||
if (args[1] === undefined) throw lang.missingOperand
|
||||
var fileOpened = fs.open(args[1], "R")
|
||||
|
||||
|
||||
serial.printerr('load '+args[1])
|
||||
if (replUsrConfirmed || cmdbuf.length == 0) {
|
||||
if (!fileOpened) {
|
||||
fileOpened = fs.open(args[1]+".BAS", "R")
|
||||
@@ -4241,7 +4241,7 @@ bF.yes = function() {
|
||||
}
|
||||
}
|
||||
bF.catalog = function(args) { // CATALOG function
|
||||
if (args[1] === undefined) args[1] = "\\"
|
||||
if (args[1] === undefined) args[1] = BASIC_HOME_PATH
|
||||
var pathOpened = fs.open(args[1], 'R')
|
||||
if (!pathOpened) {
|
||||
throw lang.noSuchFile
|
||||
@@ -4251,6 +4251,57 @@ bF.catalog = function(args) { // CATALOG function
|
||||
com.sendMessage(port, "LIST")
|
||||
println(com.pullMessage(port))
|
||||
}
|
||||
// Load a file by absolute disk path (bypasses BASIC_HOME_PATH).
|
||||
// Used by COMPILE to fetch /tbas/compile.js.
|
||||
bF._slurpAbsolute = function(path) {
|
||||
var port = _BIOS.FIRST_BOOTABLE_PORT
|
||||
com.sendMessage(port[0], "FLUSH")
|
||||
com.sendMessage(port[0], "CLOSE")
|
||||
com.sendMessage(port[0], 'OPENR"' + path + '",' + port[1])
|
||||
if (com.getStatusCode(port[0]) != 0) return undefined
|
||||
com.sendMessage(port[0], "READ")
|
||||
if (com.getStatusCode(port[0]) >= 128) return undefined
|
||||
var s = com.pullMessage(port[0])
|
||||
com.sendMessage(port[0], "FLUSH"); com.sendMessage(port[0], "CLOSE")
|
||||
return s
|
||||
}
|
||||
bF.compile = function(args) { // COMPILE "OUT.JS" -- transpile cmdbuf to JS
|
||||
if (args[1] === undefined) {
|
||||
println("Usage: COMPILE \"out.js\""); return
|
||||
}
|
||||
if (cmdbuf.length === 0) {
|
||||
println("No program loaded"); return
|
||||
}
|
||||
if (bS._compileImpl === undefined) {
|
||||
// Lazy-load compile.js from /tbas/compile.js
|
||||
var src = bF._slurpAbsolute("/tbas/compile.js")
|
||||
if (src === undefined) {
|
||||
println("Cannot load /tbas/compile.js")
|
||||
return
|
||||
}
|
||||
try { eval(src) } catch (e) {
|
||||
println("Failed to load compiler: " + e); return
|
||||
}
|
||||
if (bS._compileImpl === undefined) {
|
||||
println("compile.js loaded but did not define bS._compileImpl"); return
|
||||
}
|
||||
}
|
||||
var outpath = args[1]
|
||||
// Strip surrounding quotes if any
|
||||
if ((outpath.charAt(0) === '"' || outpath.charAt(0) === "'") &&
|
||||
outpath.charAt(outpath.length - 1) === outpath.charAt(0)) {
|
||||
outpath = outpath.substring(1, outpath.length - 1)
|
||||
}
|
||||
// Default to .js extension if missing
|
||||
if (!/\.[A-Za-z0-9]+$/.test(outpath)) outpath += ".js"
|
||||
try {
|
||||
var n = bS._compileImpl(outpath)
|
||||
println("Wrote " + n + " bytes to " + outpath)
|
||||
} catch (e) {
|
||||
serial.printerr(e + "\n" + (e.stack || ""))
|
||||
println("Compile error: " + e)
|
||||
}
|
||||
}
|
||||
Object.freeze(bF)
|
||||
|
||||
if (exec_args !== undefined && exec_args[1] !== undefined) {
|
||||
|
||||
564
assets/disk0/tbas/compile.js
Normal file
564
assets/disk0/tbas/compile.js
Normal file
@@ -0,0 +1,564 @@
|
||||
// Terran BASIC -> JavaScript compiler
|
||||
// Loaded into basic.js's context by `bF.compile`. Re-uses bF._interpretLine
|
||||
// (tokeniser + elaborator + parser + pruner) verbatim and emits a self-
|
||||
// contained JS program that does its work via `let bS = require("tbas")`.
|
||||
//
|
||||
// On load, attaches `bS._compileImpl` to the live bS object.
|
||||
|
||||
;(function() {
|
||||
|
||||
// ---------- helpers ----------------------------------------------------------
|
||||
|
||||
function isValidJsId(s) {
|
||||
return /^[A-Z_][A-Z0-9_]*$/i.test(s)
|
||||
}
|
||||
function varRef(name) {
|
||||
const u = String(name).toUpperCase()
|
||||
return isValidJsId(u) ? `bS.__state.vars.${u}` : `bS.__state.vars[${JSON.stringify(u)}]`
|
||||
}
|
||||
function jsLit(v) { return JSON.stringify(v) }
|
||||
|
||||
// Resolve a literal AST node down to a raw JS value at compile time. Used
|
||||
// for harvesting DATA constants. Only constant-propagatable types are
|
||||
// permitted; otherwise compile-time evaluation fails.
|
||||
function literalValue(node) {
|
||||
if (!node) return undefined
|
||||
switch (node.astType) {
|
||||
case "num": return Number(node.astValue)
|
||||
case "string": return String(node.astValue)
|
||||
case "bool": return Boolean(node.astValue)
|
||||
case "null": return undefined
|
||||
case "lit": return String(node.astValue) // bare identifier in DATA: keep as string
|
||||
default:
|
||||
throw Error("DATA: unsupported literal node type: " + node.astType)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the maximum varIndex used at the immediate scope of a lambda body,
|
||||
// hence its arity.
|
||||
function lambdaArity(body) {
|
||||
let maxIdx = -1
|
||||
function walk(t, level) {
|
||||
if (!t || !t.astType) return
|
||||
if (t.astType === "defun_args" && t.astValue[0] === level) {
|
||||
if (t.astValue[1] > maxIdx) maxIdx = t.astValue[1]
|
||||
}
|
||||
// descend into nested usrdefun (its body lives in astValue, not leaves)
|
||||
if (t.astType === "usrdefun" && t.astValue && t.astValue.astLeaves !== undefined) {
|
||||
walk(t.astValue, level + 1)
|
||||
}
|
||||
// generic descent
|
||||
if (t.astLeaves) {
|
||||
for (let i = 0; i < t.astLeaves.length; i++) walk(t.astLeaves[i], level)
|
||||
}
|
||||
}
|
||||
walk(body, 0)
|
||||
return maxIdx + 1
|
||||
}
|
||||
|
||||
// ---------- expression lowering ---------------------------------------------
|
||||
|
||||
// `depth` tracks the number of enclosing lambdas during emission. When we
|
||||
// emit a lambda we increment it; defun_args [d, i] becomes _aN_i where
|
||||
// N = depth - 1 - d (the absolute lambda index of the binding scope).
|
||||
function compileExpr(tree, depth) {
|
||||
if (tree === undefined || tree === null) return "undefined"
|
||||
|
||||
// Empty parens / wrapper node: descend into the single child
|
||||
if (tree.astType === "null") {
|
||||
if (tree.astLeaves && tree.astLeaves[0] !== undefined) return compileExpr(tree.astLeaves[0], depth)
|
||||
return "undefined"
|
||||
}
|
||||
if (tree.astValue === undefined && tree.astLeaves && tree.astLeaves.length === 1) {
|
||||
return compileExpr(tree.astLeaves[0], depth)
|
||||
}
|
||||
|
||||
switch (tree.astType) {
|
||||
case "num": return String(Number(tree.astValue))
|
||||
case "string": return jsLit(String(tree.astValue))
|
||||
case "bool": return tree.astValue ? "true" : "false"
|
||||
case "lit": return compileLit(tree)
|
||||
case "defun_args": {
|
||||
const d = tree.astValue[0], i = tree.astValue[1]
|
||||
const scope = depth - 1 - d
|
||||
if (scope < 0) throw Error("defun_args refers to a scope outside the program (depth=" + depth + ", d=" + d + ")")
|
||||
return "_a" + scope + "_" + i
|
||||
}
|
||||
case "usrdefun": return compileLambdaExpr(tree, depth)
|
||||
case "array": return compileArrayRef(tree, depth)
|
||||
case "function": return compileFunctionExpr(tree, depth)
|
||||
case "op": return compileOpExpr(tree, depth)
|
||||
default:
|
||||
throw Error("Cannot compile expression node of type: " + tree.astType + " (value=" + tree.astValue + ")")
|
||||
}
|
||||
}
|
||||
|
||||
function compileLit(tree) {
|
||||
const name = String(tree.astValue).toUpperCase()
|
||||
// Built-in zero-arg / pass-as-value functions: when a builtin name is
|
||||
// referenced as a value (e.g. assigned to a variable for later use as a
|
||||
// higher-order arg), emit a JS function reference. For a plain variable
|
||||
// read, emit the vars table lookup.
|
||||
// Heuristic: if the name matches a builtin we know about, prefer the
|
||||
// function; otherwise, vars lookup.
|
||||
if (RUNTIME_BUILTINS.has(name)) {
|
||||
return "bS." + (isValidJsId(name) ? name : `[${jsLit(name)}]`)
|
||||
}
|
||||
return varRef(name)
|
||||
}
|
||||
|
||||
function compileArrayRef(tree, depth) {
|
||||
// tree.astValue = array variable name; tree.astLeaves = index expressions
|
||||
if (!tree.astLeaves || tree.astLeaves.length === 0) {
|
||||
return varRef(tree.astValue)
|
||||
}
|
||||
const indices = tree.astLeaves.map(l => compileExpr(l, depth))
|
||||
return `bS.__arrGet(${varRef(tree.astValue)}, [${indices.join(",")}])`
|
||||
}
|
||||
|
||||
function compileFunctionExpr(tree, depth) {
|
||||
const name = String(tree.astValue).toUpperCase()
|
||||
|
||||
if (name === "PRINT" || name === "EMIT") {
|
||||
// PRINT/EMIT used as expression — emit as IIFE returning undefined
|
||||
return "(" + compilePrintLike(tree, name, depth) + ", undefined)"
|
||||
}
|
||||
// user function call by name: <varname>(args) — when astType is "function"
|
||||
// and astValue is a string that matches a variable, the parser may have
|
||||
// generated this. Treat it as: invoke the var.
|
||||
if (!RUNTIME_BUILTINS.has(name)) {
|
||||
// Not a known builtin: treat as a user defined function call
|
||||
const args = (tree.astLeaves || []).map(l => compileExpr(l, depth))
|
||||
return `bS.__runFn(${varRef(name)}, [${args.join(",")}])`
|
||||
}
|
||||
|
||||
const args = (tree.astLeaves || []).map(l => compileExpr(l, depth))
|
||||
return `bS.${isValidJsId(name) ? name : `[${jsLit(name)}]`}(${args.join(",")})`
|
||||
}
|
||||
|
||||
const ARITH_OP = {
|
||||
"+": (l,r) => `bS.__add(${l},${r})`,
|
||||
"-": (l,r) => `((${l})-(${r}))`,
|
||||
"*": (l,r) => `((${l})*(${r}))`,
|
||||
"/": (l,r) => `bS.__div(${l},${r})`,
|
||||
"\\": (l,r) => `bS.__intdiv(${l},${r})`,
|
||||
"MOD":(l,r) => `bS.__mod(${l},${r})`,
|
||||
"^": (l,r) => `bS.__pow(${l},${r})`,
|
||||
"==": (l,r) => `((${l})==(${r}))`,
|
||||
"<>": (l,r) => `((${l})!=(${r}))`,
|
||||
"><": (l,r) => `((${l})!=(${r}))`,
|
||||
"<": (l,r) => `((${l})<(${r}))`,
|
||||
">": (l,r) => `((${l})>(${r}))`,
|
||||
"<=": (l,r) => `((${l})<=(${r}))`,
|
||||
"=<": (l,r) => `((${l})<=(${r}))`,
|
||||
">=": (l,r) => `((${l})>=(${r}))`,
|
||||
"=>": (l,r) => `((${l})>=(${r}))`,
|
||||
"AND":(l,r) => `bS.AND(${l},${r})`,
|
||||
"OR": (l,r) => `bS.OR(${l},${r})`,
|
||||
"<<": (l,r) => `((${l})<<(${r}))`,
|
||||
">>": (l,r) => `((${l})>>>(${r}))`,
|
||||
"BAND":(l,r) => `((${l})&(${r}))`,
|
||||
"BOR": (l,r) => `((${l})|(${r}))`,
|
||||
"BXOR":(l,r) => `((${l})^(${r}))`,
|
||||
}
|
||||
const UNARY_OP = {
|
||||
"UNARYMINUS": (a) => `(-(${a}))`,
|
||||
"UNARYPLUS": (a) => `(+(${a}))`,
|
||||
"UNARYLOGICNOT":(a) => `(!(${a}))`,
|
||||
"UNARYBNOT": (a) => `(~(${a}))`,
|
||||
}
|
||||
|
||||
function compileOpExpr(tree, depth) {
|
||||
const op = String(tree.astValue)
|
||||
const leaves = tree.astLeaves || []
|
||||
|
||||
// Unary
|
||||
if (UNARY_OP[op] && (leaves.length === 1 || leaves[1] === undefined)) {
|
||||
return UNARY_OP[op](compileExpr(leaves[0], depth))
|
||||
}
|
||||
|
||||
// Binary arithmetic / comparison / logic
|
||||
if (ARITH_OP[op] && leaves.length === 2) {
|
||||
return ARITH_OP[op](compileExpr(leaves[0], depth), compileExpr(leaves[1], depth))
|
||||
}
|
||||
|
||||
// Generator / range
|
||||
if (op === "TO" && leaves.length === 2) {
|
||||
return `new bS.__ForGen(${compileExpr(leaves[0], depth)}, ${compileExpr(leaves[1], depth)}, 1)`
|
||||
}
|
||||
if (op === "STEP" && leaves.length === 2) {
|
||||
return `bS.STEP(${compileExpr(leaves[0], depth)}, ${compileExpr(leaves[1], depth)})`
|
||||
}
|
||||
|
||||
// List ops
|
||||
if ((op === "!" || op === "~" || op === "#") && leaves.length === 2) {
|
||||
const fn = (op === "!") ? "['!']" : (op === "~") ? "['~']" : "['#']"
|
||||
return `bS${fn}(${compileExpr(leaves[0], depth)}, ${compileExpr(leaves[1], depth)})`
|
||||
}
|
||||
|
||||
// Assignment as expression — returns the assigned value
|
||||
if (op === "=" && leaves.length === 2) {
|
||||
return "(" + compileAssignExpr(tree, depth) + ")"
|
||||
}
|
||||
if (op === "IN" && leaves.length === 2) {
|
||||
// Used inside FOR/FOREACH; compileFor unwraps these. As a value, treat
|
||||
// as { asgnVarName, asgnValue } so a stray IN still works.
|
||||
const name = jsLit(String(leaves[0].astValue).toUpperCase())
|
||||
const rhs = compileExpr(leaves[1], depth)
|
||||
return `({asgnVarName: ${name}, asgnValue: ${rhs}})`
|
||||
}
|
||||
|
||||
// Functional / monad ops
|
||||
if ((op === ">>=" || op === ">>~" || op === "." || op === "$" ||
|
||||
op === "&" || op === "~<" || op === "<*>" || op === "<$>" ||
|
||||
op === "<~>") && leaves.length === 2) {
|
||||
return `bS[${jsLit(op)}](${compileExpr(leaves[0], depth)}, ${compileExpr(leaves[1], depth)})`
|
||||
}
|
||||
if (op === "@" && leaves.length === 1) {
|
||||
// Monad return as prefix
|
||||
return `bS.MRET(${compileExpr(leaves[0], depth)})`
|
||||
}
|
||||
if (op === "~>") {
|
||||
throw Error("Compiler: bare ~> survived prune (should be usrdefun)")
|
||||
}
|
||||
|
||||
throw Error("Cannot compile op '" + op + "' with " + leaves.length + " operand(s)")
|
||||
}
|
||||
|
||||
function compileLambdaExpr(tree, depth) {
|
||||
// tree.astType === "usrdefun"; tree.astValue holds the body AST; if
|
||||
// tree.astLeaves is non-empty, this is an immediate application.
|
||||
const body = tree.astValue
|
||||
if (!body || !body.astType) throw Error("Malformed usrdefun")
|
||||
|
||||
const arity = lambdaArity(body)
|
||||
const newDepth = depth + 1
|
||||
const params = []
|
||||
for (let i = 0; i < arity; i++) params.push("_a" + (newDepth - 1) + "_" + i)
|
||||
const bodyJs = compileExpr(body, newDepth)
|
||||
const arrow = `((${params.join(",")}) => (${bodyJs}))`
|
||||
|
||||
if (tree.astLeaves && tree.astLeaves.length > 0) {
|
||||
const args = tree.astLeaves.map(l => compileExpr(l, depth))
|
||||
return `${arrow}(${args.join(",")})`
|
||||
}
|
||||
return arrow
|
||||
}
|
||||
|
||||
function compileAssignExpr(tree, depth) {
|
||||
// op "=" with leaves[0] as target, leaves[1] as RHS
|
||||
const lhs = tree.astLeaves[0]
|
||||
const rhs = compileExpr(tree.astLeaves[1], depth)
|
||||
|
||||
if (lhs.astType === "lit") {
|
||||
const name = String(lhs.astValue).toUpperCase()
|
||||
return `(${varRef(name)} = ${rhs})`
|
||||
}
|
||||
// The parser emits "function" or "array" for `A(i,j) = ...` — both mean
|
||||
// "store into element of A".
|
||||
if (lhs.astType === "array" || lhs.astType === "function") {
|
||||
const indices = lhs.astLeaves.map(l => compileExpr(l, depth))
|
||||
return `(bS.__arrSet(${varRef(lhs.astValue)}, [${indices.join(",")}], ${rhs}), ${rhs})`
|
||||
}
|
||||
throw Error("Cannot assign to LHS of type " + lhs.astType)
|
||||
}
|
||||
|
||||
// ---------- statement lowering ----------------------------------------------
|
||||
|
||||
function compilePrintLike(tree, fname, depth) {
|
||||
const leaves = (tree.astLeaves || []).slice()
|
||||
const seps = (tree.astSeps || []).slice()
|
||||
|
||||
let suppressNewline = false
|
||||
if (leaves.length > 0 && leaves[leaves.length - 1] !== undefined &&
|
||||
leaves[leaves.length - 1].astType === "null") {
|
||||
suppressNewline = true
|
||||
leaves.pop()
|
||||
}
|
||||
|
||||
const valueExprs = leaves.map(l => compileExpr(l, depth))
|
||||
if (suppressNewline) valueExprs.push("bS.__PRINT_NONL")
|
||||
const sepArr = seps.slice(0, leaves.length - 1)
|
||||
|
||||
return `bS.${fname}([${valueExprs.join(", ")}], ${jsLit(sepArr)})`
|
||||
}
|
||||
|
||||
function setPc(pc) {
|
||||
if (pc[0] === Infinity) return "pc=[Infinity,0];"
|
||||
return "pc=[" + pc[0] + "," + pc[1] + "];"
|
||||
}
|
||||
|
||||
function compileStatement(tree, lnum, stmt, nextPc) {
|
||||
if (!tree) return setPc(nextPc)
|
||||
if (tree.astType === "null" && tree.astLeaves && tree.astLeaves[0]) {
|
||||
return compileStatement(tree.astLeaves[0], lnum, stmt, nextPc)
|
||||
}
|
||||
|
||||
const isFn = (tree.astType === "function" || tree.astType === "op")
|
||||
const fname = isFn ? String(tree.astValue).toUpperCase() : null
|
||||
|
||||
switch (fname) {
|
||||
case "GOTO": {
|
||||
const target = compileGotoTarget(tree.astLeaves[0])
|
||||
return `pc=${target};`
|
||||
}
|
||||
case "GOSUB": {
|
||||
const target = compileGotoTarget(tree.astLeaves[0])
|
||||
return `gosubStack.push([${nextPc[0]},${nextPc[1]}]); pc=${target};`
|
||||
}
|
||||
case "RETURN":
|
||||
return `pc=gosubStack.pop(); if(!pc) throw new Error("RETURN without GOSUB");`
|
||||
case "END":
|
||||
return "pc=[Infinity,0];"
|
||||
case "IF":
|
||||
return compileIf(tree, lnum, stmt, nextPc)
|
||||
case "ON":
|
||||
return compileOn(tree, lnum, stmt, nextPc)
|
||||
case "FOR":
|
||||
case "FOREACH":
|
||||
return compileFor(tree, lnum, stmt, nextPc, fname === "FOREACH")
|
||||
case "NEXT":
|
||||
return compileNext(tree, lnum, stmt, nextPc)
|
||||
case "READ": {
|
||||
const target = tree.astLeaves[0]
|
||||
if (target.astType !== "lit") throw Error("READ: target must be a variable")
|
||||
return `${varRef(target.astValue)}=bS.__readData(); ${setPc(nextPc)}`
|
||||
}
|
||||
case "RESTORE":
|
||||
return `bS.__state.dataCursor=0; ${setPc(nextPc)}`
|
||||
case "DATA":
|
||||
case "LABEL":
|
||||
return setPc(nextPc) // harvested at compile time
|
||||
case "DIM":
|
||||
return compileDim(tree, lnum, stmt, nextPc)
|
||||
case "PRINT":
|
||||
case "EMIT":
|
||||
return `${compilePrintLike(tree, fname, 0)}; ${setPc(nextPc)}`
|
||||
case "OPTIONBASE":
|
||||
return `bS.OPTIONBASE(${compileExpr(tree.astLeaves[0], 0)}); ${setPc(nextPc)}`
|
||||
case "OPTIONDEBUG":
|
||||
return `bS.OPTIONDEBUG(${compileExpr(tree.astLeaves[0], 0)}); ${setPc(nextPc)}`
|
||||
case "OPTIONTRACE":
|
||||
return `bS.OPTIONTRACE(${compileExpr(tree.astLeaves[0], 0)}); ${setPc(nextPc)}`
|
||||
case "INPUT": {
|
||||
// INPUT <var> -> read into var
|
||||
const target = tree.astLeaves[tree.astLeaves.length - 1]
|
||||
if (target.astType !== "lit") throw Error("INPUT: target must be a variable")
|
||||
return `${varRef(target.astValue)}=bS.INPUT(); ${setPc(nextPc)}`
|
||||
}
|
||||
case "=":
|
||||
return `${compileAssignExpr(tree, 0)}; ${setPc(nextPc)}`
|
||||
case "IN":
|
||||
// bare IN as a statement is unusual but harmless
|
||||
return `${compileExpr(tree, 0)}; ${setPc(nextPc)}`
|
||||
case "REM":
|
||||
return setPc(nextPc)
|
||||
}
|
||||
|
||||
// Default: evaluate as an expression for side effect, then advance
|
||||
return `${compileExpr(tree, 0)}; ${setPc(nextPc)}`
|
||||
}
|
||||
|
||||
function compileGotoTarget(leaf) {
|
||||
// Always route through __resolveTarget so non-existent line numbers snap
|
||||
// upward to the next existing line — matching basic.js's main loop,
|
||||
// which increments lnum until it finds a populated cmdbuf entry.
|
||||
if (leaf.astType === "num") return `bS.__resolveTarget(${Number(leaf.astValue)})`
|
||||
if (leaf.astType === "string") return `bS.__resolveTarget(${jsLit(leaf.astValue)})`
|
||||
if (leaf.astType === "lit") {
|
||||
const name = String(leaf.astValue)
|
||||
return `bS.__resolveTarget(bS.__state.gotoLabels[${jsLit(name)}]!==undefined ? ${jsLit(name)} : ${varRef(name)})`
|
||||
}
|
||||
return `bS.__resolveTarget(${compileExpr(leaf, 0)})`
|
||||
}
|
||||
|
||||
function compileIf(tree, lnum, stmt, nextPc) {
|
||||
const test = compileExpr(tree.astLeaves[0], 0)
|
||||
const thenStmt = compileStatement(tree.astLeaves[1], lnum, stmt, nextPc)
|
||||
const elseStmt = (tree.astLeaves[2])
|
||||
? compileStatement(tree.astLeaves[2], lnum, stmt, nextPc)
|
||||
: setPc(nextPc)
|
||||
return `if(bS.__test(${test})){${thenStmt}}else{${elseStmt}}`
|
||||
}
|
||||
|
||||
function compileOn(tree, lnum, stmt, nextPc) {
|
||||
// children: testExpr, jumpFnLit, target0, target1, ...
|
||||
const testExpr = compileExpr(tree.astLeaves[0], 0)
|
||||
const jmpFn = String(tree.astLeaves[1].astValue).toUpperCase()
|
||||
const targets = tree.astLeaves.slice(2)
|
||||
|
||||
const cases = targets.map((t, i) => {
|
||||
const tgt = compileGotoTarget(t)
|
||||
if (jmpFn === "GOSUB") {
|
||||
return `case ${i}: gosubStack.push([${nextPc[0]},${nextPc[1]}]); pc=${tgt}; break;`
|
||||
}
|
||||
return `case ${i}: pc=${tgt}; break;`
|
||||
})
|
||||
return `{const _o=(${testExpr})-bS.__state.indexBase; switch(_o){${cases.join(" ")} default: ${setPc(nextPc)}}}`
|
||||
}
|
||||
|
||||
function compileFor(tree, lnum, stmt, nextPc, isForEach) {
|
||||
const child = tree.astLeaves[0]
|
||||
if (child.astType !== "op" || (child.astValue !== "=" && child.astValue !== "IN")) {
|
||||
throw Error("FOR/FOREACH: expected = or IN, got " + child.astType + ":" + child.astValue)
|
||||
}
|
||||
const varname = String(child.astLeaves[0].astValue).toUpperCase()
|
||||
let iter = compileExpr(child.astLeaves[1], 0)
|
||||
if (isForEach) {
|
||||
// ensure we coerce generators into arrays for FOREACH semantics
|
||||
iter = `(function(_x){return bS.__isGenerator(_x)?bS.__genToArray(_x):_x})(${iter})`
|
||||
}
|
||||
// Pass nextPc — the PC of the loop body's first statement — so NEXT can
|
||||
// jump straight back without relying on fall-through.
|
||||
return `bS.__forSetup(${jsLit(varname)}, ${iter}, ${nextPc[0]}, ${nextPc[1]}); ${setPc(nextPc)}`
|
||||
}
|
||||
|
||||
function compileNext(tree, lnum, stmt, nextPc) {
|
||||
let argExpr = "undefined"
|
||||
const leaves = tree.astLeaves || []
|
||||
if (leaves.length === 1 && leaves[0] && leaves[0].astType === "lit") {
|
||||
argExpr = jsLit(String(leaves[0].astValue).toUpperCase())
|
||||
}
|
||||
return `{const _n=bS.__forNext(${argExpr}); if(_n){pc=_n;}else{${setPc(nextPc)}}}`
|
||||
}
|
||||
|
||||
function compileDim(tree, lnum, stmt, nextPc) {
|
||||
// tree.astLeaves contains array constructor calls: each leaf is either
|
||||
// an `array` node OR a `function` node (the parser doesn't distinguish
|
||||
// `A(5)` from a function call until runtime). astValue is the variable
|
||||
// name and astLeaves are the dimension expressions.
|
||||
const stmts = []
|
||||
for (let i = 0; i < tree.astLeaves.length; i++) {
|
||||
const leaf = tree.astLeaves[i]
|
||||
if (leaf.astType !== "array" && leaf.astType !== "function") {
|
||||
throw Error("DIM: expected array decl, got " + leaf.astType)
|
||||
}
|
||||
const name = String(leaf.astValue).toUpperCase()
|
||||
const dims = leaf.astLeaves.map(l => compileExpr(l, 0))
|
||||
stmts.push(`${varRef(name)}=bS.__dim([${dims.join(",")}]);`)
|
||||
}
|
||||
return stmts.join(" ") + " " + setPc(nextPc)
|
||||
}
|
||||
|
||||
// ---------- top-level entry --------------------------------------------------
|
||||
|
||||
// Set of builtin names exposed by tbas.mjs. Used to decide whether a `lit`
|
||||
// in expression position is a variable or a function reference.
|
||||
const RUNTIME_BUILTINS = new Set([
|
||||
"PRINT","EMIT","INPUT","CIN",
|
||||
"ABS","SGN","INT","FLOOR","CEIL","FIX","ROUND","SQR","CBR",
|
||||
"SIN","COS","TAN","ASN","ACO","ATN","SINH","COSH","TANH",
|
||||
"EXP","LOG","MIN","MAX","RND",
|
||||
"SPC","LEFT","RIGHT","MID","CHR",
|
||||
"LEN","HEAD","TAIL","INIT","LAST","MAP","FOLD","FILTER","ARRAY",
|
||||
"CLS","CLPX","PLOT","GOTOYX","TEXTFORE","TEXTBACK",
|
||||
"POKE","PEEK","GETKEYSDOWN","CPUT","CGET","CSTA",
|
||||
"TYPEOF","OPTIONBASE","OPTIONDEBUG","OPTIONTRACE",
|
||||
"MRET","MLIST","MJOIN",
|
||||
"AND","OR","NOT",
|
||||
"DO","CLEAR","END","TO","STEP",
|
||||
"FOR","FOREACH","NEXT","IF","ON","GOTO","GOSUB","RETURN",
|
||||
"DIM","DATA","READ","RESTORE","LABEL","REM",
|
||||
"TEST",
|
||||
])
|
||||
|
||||
bS._compileImpl = function(outpath) {
|
||||
if (typeof cmdbuf === "undefined") throw Error("compile.js: cmdbuf not available")
|
||||
if (typeof bF === "undefined") throw Error("compile.js: bF not available")
|
||||
if (typeof bF._interpretLine !== "function") throw Error("compile.js: bF._interpretLine not available")
|
||||
|
||||
// Reset parser-side state so we don't pollute the live interpreter
|
||||
if (typeof lambdaBoundVars !== "undefined") lambdaBoundVars.length = 0
|
||||
const savedPrescan = (typeof prescan !== "undefined") ? prescan : false
|
||||
if (typeof prescan !== "undefined") prescan = true // suppress execution of LABEL/DATA prescan side-effects
|
||||
|
||||
// ---- pass 1: parse every line ----
|
||||
const programTrees = [] // [lnum] -> array of statements
|
||||
for (let lnum = 0; lnum < cmdbuf.length; lnum++) {
|
||||
const linestr = cmdbuf[lnum]
|
||||
if (linestr === undefined) continue
|
||||
const trees = bF._interpretLine(lnum, String(linestr).trim())
|
||||
if (trees !== undefined) programTrees[lnum] = trees
|
||||
}
|
||||
if (typeof prescan !== "undefined") prescan = savedPrescan
|
||||
|
||||
// ---- pass 2: ordered list of populated lnums and successor table ----
|
||||
const linenums = []
|
||||
for (let lnum = 0; lnum < programTrees.length; lnum++) {
|
||||
if (programTrees[lnum] !== undefined) linenums.push(lnum)
|
||||
}
|
||||
|
||||
function nextPcOf(idx, stmtIdx) {
|
||||
const lnum = linenums[idx]
|
||||
const stmts = programTrees[lnum]
|
||||
if (stmtIdx + 1 < stmts.length) return [lnum, stmtIdx + 1]
|
||||
if (idx + 1 < linenums.length) return [linenums[idx + 1], 0]
|
||||
return [Infinity, 0]
|
||||
}
|
||||
|
||||
// ---- pass 3: harvest DATA constants and LABEL definitions ----
|
||||
const dataConsts = []
|
||||
const labelMap = {}
|
||||
for (let i = 0; i < linenums.length; i++) {
|
||||
const lnum = linenums[i]
|
||||
const stmts = programTrees[lnum]
|
||||
for (let s = 0; s < stmts.length; s++) {
|
||||
const t = stmts[s]
|
||||
if (!t) continue
|
||||
if (t.astValue === "DATA") {
|
||||
for (let k = 0; k < t.astLeaves.length; k++) {
|
||||
dataConsts.push(literalValue(t.astLeaves[k]))
|
||||
}
|
||||
} else if (t.astValue === "LABEL") {
|
||||
const lblNode = t.astLeaves[0]
|
||||
if (!lblNode) throw Error("LABEL with no name on line " + lnum)
|
||||
const lblName = String(lblNode.astValue)
|
||||
labelMap[lblName] = [lnum, s]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- pass 4: emit case bodies ----
|
||||
const cases = []
|
||||
for (let i = 0; i < linenums.length; i++) {
|
||||
const lnum = linenums[i]
|
||||
const stmts = programTrees[lnum]
|
||||
for (let s = 0; s < stmts.length; s++) {
|
||||
const next = nextPcOf(i, s)
|
||||
const body = compileStatement(stmts[s], lnum, s, next)
|
||||
cases.push(` case ${lnum}*32+${s}: { ${body} break; }`)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- pass 5: assemble final output ----
|
||||
const firstPc = (linenums.length > 0) ? `[${linenums[0]},0]` : `[Infinity,0]`
|
||||
const labelMapJs = "{" + Object.keys(labelMap).map(k =>
|
||||
`${jsLit(k)}: [${labelMap[k][0]}, ${labelMap[k][1]}]`
|
||||
).join(", ") + "}"
|
||||
|
||||
const out =
|
||||
`// Compiled by Terran BASIC -> JS compiler (assets/disk0/tbas/compile.js)
|
||||
// Source line count: ${linenums.length}
|
||||
let bS = require("tbas")
|
||||
bS.__reset()
|
||||
bS.__data(${jsLit(dataConsts)})
|
||||
bS.__labels(${labelMapJs})
|
||||
bS.__setLines(${jsLit(linenums)})
|
||||
let pc = ${firstPc}
|
||||
const gosubStack = []
|
||||
while (pc[0] !== Infinity) {
|
||||
switch (pc[0]*32 + pc[1]) {
|
||||
${cases.join("\n")}
|
||||
default: pc = [Infinity, 0]; break;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// ---- write to disk via basic.js's fs (writes under BASIC_HOME_PATH) ----
|
||||
const opened = fs.open(outpath, "W")
|
||||
if (!opened) throw Error("Cannot open " + outpath + " for writing")
|
||||
fs.write(out)
|
||||
return out.length
|
||||
}
|
||||
|
||||
})();
|
||||
21
assets/disk0/tracker_play.js
Normal file
21
assets/disk0/tracker_play.js
Normal 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)
|
||||
186
assets/disk0/tracker_test.js
Normal file
186
assets/disk0/tracker_test.js
Normal 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.NOP = 0x0000; // no-op (empty row)
|
||||
t.OFF = 0x0001; // key-off
|
||||
t.CUT = 0x0002; // note cut (immediate)
|
||||
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 + ")");
|
||||
@@ -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
|
||||
|
||||
@@ -147,10 +147,12 @@ _TVDOS.variables = {
|
||||
LANG: "EN",
|
||||
KEYBOARD: "us_qwerty",
|
||||
PATH: "\\tvdos\\bin;\\home",
|
||||
PATHEXT: ".com;.bat;.app;.js",
|
||||
INCLPATH: "\\tvdos\\include;\\home",
|
||||
PATHEXT: ".com;.bat;.app;.js;.alias",
|
||||
HELPPATH: "\\tvdos\\help",
|
||||
OS_NAME: "TSVM Disk Operating System",
|
||||
OS_VERSION: _TVDOS.VERSION
|
||||
OS_VERSION: _TVDOS.VERSION,
|
||||
USERCONFIGPATH: "\\home\\config",
|
||||
};
|
||||
Object.freeze(_TVDOS);
|
||||
|
||||
@@ -225,8 +227,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 +244,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 +425,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 +875,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 +1029,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 +1251,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 +1266,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);
|
||||
@@ -1494,9 +1407,6 @@ let requireFromMemory = (ptr) => {
|
||||
}*/
|
||||
|
||||
|
||||
var GL = require("A:/tvdos/include/gl.mjs")
|
||||
|
||||
|
||||
// @param cmdsrc JS source code
|
||||
// @param args arguments for the program, must be Array, and args[0] is always the name of the program, e.g.
|
||||
// for command line 'echo foo bar', args[0] must be 'echo'
|
||||
@@ -1509,7 +1419,7 @@ var execApp = (cmdsrc, args, appname) => {
|
||||
`var ${appname}=function(exec_args){${injectIntChk(cmdsrc, intchkFunName)}\n};` +
|
||||
`${appname}`); // making 'exec_args' a app-level global
|
||||
|
||||
execAppPrg(args);
|
||||
return execAppPrg(args);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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(' ')
|
||||
@@ -577,6 +577,59 @@ shell.coreutils = {
|
||||
ver: function(args) {
|
||||
println(welcome_text)
|
||||
},
|
||||
which: function(args) {
|
||||
if (args[1] === undefined) {
|
||||
printerrln(`Usage: ${args[0].toUpperCase()} program_name`)
|
||||
return 1
|
||||
}
|
||||
let cmd = args[1]
|
||||
|
||||
if (shell.coreutils[cmd.toLowerCase()] !== undefined) {
|
||||
println(`${cmd}: shell built-in command`)
|
||||
return 0
|
||||
}
|
||||
|
||||
var fileExists = false
|
||||
var searchFile
|
||||
var searchPath = ""
|
||||
|
||||
if (shell.isValidDriveLetter(cmd[0]) && cmd[1] == ':') {
|
||||
searchFile = files.open(cmd)
|
||||
searchPath = trimStartRevSlash(searchFile.path)
|
||||
fileExists = searchFile.exists
|
||||
}
|
||||
else {
|
||||
var searchDir = (cmd.startsWith("/")) ? [""] : ["/"+shell_pwd.join("/")].concat(_TVDOS.getPath())
|
||||
|
||||
var pathExt = []
|
||||
if (cmd.split(".")[1] === undefined)
|
||||
_TVDOS.variables.PATHEXT.split(';').forEach(function(it) { pathExt.push(it); pathExt.push(it.toUpperCase()); })
|
||||
else
|
||||
pathExt.push("")
|
||||
|
||||
searchLoop:
|
||||
for (var i = 0; i < searchDir.length; i++) {
|
||||
for (var j = 0; j < pathExt.length; j++) {
|
||||
let search = searchDir[i]; if (!search.endsWith('\\')) search += '\\'
|
||||
searchPath = trimStartRevSlash(search + cmd + pathExt[j])
|
||||
|
||||
searchFile = files.open(`${CURRENT_DRIVE}:\\${searchPath}`)
|
||||
if (searchFile.exists) {
|
||||
fileExists = true
|
||||
break searchLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileExists) {
|
||||
printerrln(`${cmd}: not found`)
|
||||
return 1
|
||||
}
|
||||
|
||||
println(searchFile.fullPath)
|
||||
return 0
|
||||
},
|
||||
panic: function(args) {
|
||||
throw Error("Panicking command.js")
|
||||
}
|
||||
@@ -590,6 +643,7 @@ shell.coreutils.ls = shell.coreutils.dir
|
||||
shell.coreutils.time = shell.coreutils.date
|
||||
shell.coreutils.md = shell.coreutils.mkdir
|
||||
shell.coreutils.move = shell.coreutils.mv
|
||||
shell.coreutils.where = shell.coreutils.which
|
||||
// end of command aliases
|
||||
Object.freeze(shell.coreutils)
|
||||
shell.stdio = {
|
||||
@@ -614,13 +668,25 @@ require = function(path) {
|
||||
if (path[1] == ":") return shell.require(path)
|
||||
else {
|
||||
// if the path starts with ".", look for the current directory
|
||||
// if the path starts with [A-Za-z0-9], look for the DOSDIR/includes
|
||||
// if the path starts with [A-Za-z0-9], search through INCLPATH
|
||||
if (path[0] == '.') return shell.require(shell.resolvePathInput(path).full + ".mjs")
|
||||
else return shell.require(`A:${_TVDOS.variables.DOSDIR}/include/${path}.mjs`)
|
||||
else {
|
||||
let inclDirs = (_TVDOS.variables.INCLPATH || "").split(';').filter(function(it) { return it.length > 0 })
|
||||
for (let i = 0; i < inclDirs.length; i++) {
|
||||
let dir = inclDirs[i]
|
||||
if (!dir.endsWith('\\') && !dir.endsWith('/')) dir += '\\'
|
||||
let candidate = `${CURRENT_DRIVE}:${dir}${path}.mjs`
|
||||
if (files.open(candidate).exists) return shell.require(candidate)
|
||||
}
|
||||
// no match found; defer to shell.require with the first entry so the error mentions a sensible path
|
||||
let firstDir = inclDirs[0] || `${_TVDOS.variables.DOSDIR}\\include`
|
||||
if (!firstDir.endsWith('\\') && !firstDir.endsWith('/')) firstDir += '\\'
|
||||
return shell.require(`${CURRENT_DRIVE}:${firstDir}${path}.mjs`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shell.execute = function(line) {
|
||||
shell.execute = function(line, nameOverride) {
|
||||
if (0 == line.size) return
|
||||
let parsedTokens = shell.parse(line) // echo, "hai", |, less
|
||||
let statements = [] // [[echo, "hai"], [less]]
|
||||
@@ -753,6 +819,34 @@ shell.execute = function(line) {
|
||||
shell.execute(line)
|
||||
})
|
||||
}
|
||||
else if ("ALIAS" == extension) {
|
||||
// parse alias
|
||||
// $0: all arguments
|
||||
// $1..9: specific arguments
|
||||
// Tokens that contain whitespace or shell metacharacters must be re-quoted
|
||||
// before re-execution, otherwise the re-parse splits them on spaces.
|
||||
var quoteAliasArg = function(s) {
|
||||
if (s === undefined || s === null) return ""
|
||||
s = ''+s
|
||||
if (s.length === 0) return ""
|
||||
if (/[\s"|><&]/.test(s)) return '"' + s.replaceAll('"', '^"') + '"'
|
||||
return s
|
||||
}
|
||||
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, quoteAliasArg(tokens[j]))
|
||||
}
|
||||
|
||||
// replace $0
|
||||
newLine = newLine.replaceAll('$0', tokens.slice(1).map(quoteAliasArg).join(' '))
|
||||
|
||||
shell.execute(newLine, cmd)
|
||||
})
|
||||
}
|
||||
else if ("APP" == extension) {
|
||||
let appexec = `A:${_TVDOS.variables.DOSDIR}\\sbin\\appexec.js`
|
||||
let foundFile = searchFile.fullPath
|
||||
@@ -767,6 +861,10 @@ shell.execute = function(line) {
|
||||
errorlevel = 0 // reset the number
|
||||
|
||||
if (_G.shellProgramTitles === undefined) _G.shellProgramTitles = []
|
||||
if (nameOverride !== undefined) {
|
||||
tokens[0] = (''+nameOverride)
|
||||
cmd = tokens[0]
|
||||
}
|
||||
_G.shellProgramTitles.push(cmd.toUpperCase())
|
||||
sendLcdMsg(_G.shellProgramTitles[_G.shellProgramTitles.length - 1])
|
||||
//serial.println(_G.shellProgramTitles)
|
||||
@@ -866,6 +964,18 @@ _G.shell = shell
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// ensure USERCONFIGPATH directory exists
|
||||
try {
|
||||
let userConfigPath = `${CURRENT_DRIVE}:${_TVDOS.variables.USERCONFIGPATH}`
|
||||
let userConfigDir = files.open(userConfigPath)
|
||||
if (!userConfigDir.exists) {
|
||||
debugprintln(`command.js > creating USERCONFIGPATH at ${userConfigPath}`)
|
||||
userConfigDir.mkDir()
|
||||
}
|
||||
} catch (e) {
|
||||
debugprintln("command.js > USERCONFIGPATH creation failed: " + e.message)
|
||||
}
|
||||
|
||||
if (exec_args[1] !== undefined) {
|
||||
// only meaningful switches would be either -c or -k anyway
|
||||
var firstSwitch = exec_args[1].toLowerCase()
|
||||
|
||||
1
assets/disk0/tvdos/bin/microtone.alias
Normal file
1
assets/disk0/tvdos/bin/microtone.alias
Normal file
@@ -0,0 +1 @@
|
||||
taut $0
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
363
assets/disk0/tvdos/bin/playtad.js
Normal file
363
assets/disk0/tvdos/bin/playtad.js
Normal file
@@ -0,0 +1,363 @@
|
||||
const SND_BASE_ADDR = audio.getBaseAddr()
|
||||
const SND_MEM_ADDR = audio.getMemAddr()
|
||||
// tadInputBin lives at audio-local offset 917504 and tadDecodedBin at 983040
|
||||
// (post-bef85f6 memory map; the old 262144 offset now hits the enlarged sampleBin).
|
||||
const TAD_INPUT_ADDR = SND_MEM_ADDR - 917504 // TAD input buffer (matches TAV packet 0x24)
|
||||
const TAD_DECODED_ADDR = SND_MEM_ADDR - 983040 // 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
|
||||
2467
assets/disk0/tvdos/bin/playtav.js
Normal file
2467
assets/disk0/tvdos/bin/playtav.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
358
assets/disk0/tvdos/bin/playucf.js
Normal file
358
assets/disk0/tvdos/bin/playucf.js
Normal 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.variables.DOSDIR}/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.variables.DOSDIR}/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
|
||||
@@ -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)
|
||||
|
||||
|
||||
3971
assets/disk0/tvdos/bin/taut.js
Normal file
3971
assets/disk0/tvdos/bin/taut.js
Normal file
File diff suppressed because it is too large
Load Diff
77
assets/disk0/tvdos/bin/taut_fileop.js
Normal file
77
assets/disk0/tvdos/bin/taut_fileop.js
Normal 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.UI.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.UI.NEXTPANEL = (MY_PANEL + (shiftDown ? -1 : 1) + PANEL_COUNT) % PANEL_COUNT
|
||||
done = true
|
||||
return
|
||||
}
|
||||
|
||||
panel.processInput(event)
|
||||
})
|
||||
}
|
||||
|
||||
return 0
|
||||
416
assets/disk0/tvdos/bin/taut_helpmsg.js
Normal file
416
assets/disk0/tvdos/bin/taut_helpmsg.js
Normal file
@@ -0,0 +1,416 @@
|
||||
if (!_G.TAUT) _G.TAUT = {};
|
||||
let help = {}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/*
|
||||
Tags:
|
||||
<b> - print the text in emphasis colour (colVoiceHdr aka 230)
|
||||
<c> - centre the line. If the line spans multiple lines, centre each line
|
||||
<r> - align right
|
||||
<l> - align left
|
||||
<o> - create virtual typesetting box. Left anchor: where the text cursor is. Right anchor: end of the line
|
||||
µtone; - replace with the brand string (<col 211>Micro</col><col 239>tone</col>)
|
||||
|
||||
&bul; - replace with bullet (\u00F9)
|
||||
&ddot; - replace with double-dot (\u008419u)
|
||||
&mdot; - replace with BIGDOT (\u00FA)
|
||||
&updn; - up-down arrow (\u008418u)
|
||||
&udlr; - four direction arrow (\u008428u\u008429u)
|
||||
|
||||
&keyoffsym; - pattern view key-off symbol (\u00A0\u00B1\u00B1\u00A1)
|
||||
¬ecutsym; - pattern view note-cut symbol (\u00A4\u00A4\u00A4\u00A4)
|
||||
|
||||
&demisharp;
|
||||
♯
|
||||
&sesquisharp;
|
||||
&doublesharp;
|
||||
&triplesharp;
|
||||
&quadsharp;
|
||||
&demiflat;
|
||||
♭
|
||||
&sesquiflat;
|
||||
&doubleflat;
|
||||
&tripleflat;
|
||||
&quadflat;
|
||||
&accuptick;
|
||||
&accdntick;
|
||||
&accupup;
|
||||
&accdndn;
|
||||
|
||||
- nonbreakable space (only meaningful for typesetters)
|
||||
­ - soft hyphen (only meaningful for typesetters)
|
||||
|
||||
default alignment: fully justified
|
||||
*/
|
||||
|
||||
let helpNotation = `<c>CONTROL NOTATION</c>
|
||||
<c>\u00B7${'\u00B8'.repeat(16)}\u00B9</c>
|
||||
µtone; <O>shortcuts differentiate normal and shifted shortcuts.</O>
|
||||
&bul;<b>a</b>&ddot;<b>z</b> : <O>alphabet without shift-in</O>
|
||||
&bul;<b>A</b>&ddot;<b>Z</b> : <O>alphabet with shift-in</O>
|
||||
&bul;<b>^q</b> : <O>hit 'q' with control key</O>
|
||||
&bul;<b>^Q</b> : <O>hit 'q' with control and shift key</O>
|
||||
`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
let helpJam = `<c>NOTE JAMMING</c>
|
||||
<c>\u00B7${'\u00B8'.repeat(12)}\u00B9</c>
|
||||
Push keys to play or insert notes.
|
||||
w e t y u
|
||||
a s d f g h j k
|
||||
`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
let helpCommon = `<c>COMMON CONTROLS</c>
|
||||
<c>\u00B7${'\u00B8'.repeat(15)}\u00B9</c>
|
||||
&bul;<b>!</b> : <O>show this help message</O>
|
||||
&bul;<b>Y</b> : <O>plays the entire song from the current cue</O>
|
||||
&bul;<b>U</b> : <O>plays the current cue then stop</O>
|
||||
&bul;<b>I</b> : <O>plays the current row</O>
|
||||
&bul;<b>O</b> : <O>stops the playback</O>
|
||||
&bul;<b>tab</b> : <O>switchs forward a tab</O>
|
||||
&bul;<b>TAB</b> : <O>switchs backward a tab</O>
|
||||
&bul;<b>q</b> : <O>closes µtone;</O>
|
||||
`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
let helpTimeline = `<c>TIMELINE VIEW</c>
|
||||
<c>\u00B7${'\u00B8'.repeat(13)}\u00B9</c>
|
||||
Timeline has two distinct modes: view and edit mode. Two modes are toggled using the space bar.
|
||||
|
||||
<b> VIEW MODE</b>
|
||||
<b>\u00B7${'\u00B8'.repeat(9)}\u00B9</b>
|
||||
&bul;Note jamming : <O>plays the note</O>
|
||||
&bul;<b>&udlr;</b> : <O>moves the viewing cursor by voices and rows</O>
|
||||
&bul;<b>pg&updn;</b> : <O>goes to previous/next cue</O>
|
||||
&bul;<b>W</b>&mdot;<b>E</b>&mdot;<b>R</b> : <O>toggles timeline view mode. W-most detailed, R-most abridged</O>
|
||||
&bul;<b>n</b> : <O>toggles soloing of the selected voice</O>
|
||||
&bul;<b>m</b> : <O>toggles muting of the selected voice</O>
|
||||
&bul;<b>[</b>&mdot;<b>]</b> : <O>changes tick rate of playhead</O>
|
||||
|
||||
<b> EDIT MODE</b>
|
||||
<b>\u00B7${'\u00B8'.repeat(9)}\u00B9</b>
|
||||
&bul;Note jamming : <O>(note column) inserts the note</O>
|
||||
&bul;<b>{</b>&mdot;<b>}</b> : <O>(note column) lowers/raises a note by one octave (or period)</O>
|
||||
&bul;<b>[</b>&mdot;<b>]</b> : <O>(note column) lowers/raises a note by one unit</O>
|
||||
&bul;<b>z</b> : <O>(note column) inserts a key-off &keyoffsym;</O>
|
||||
&bul;<b>x</b> : <O>(note column) inserts a note-cut ¬ecutsym;</O>
|
||||
&bul;<b>.</b> : <O>clears fields</O>
|
||||
&bul;<b>bksp</b> : <O>deletes one character on the selected column</O>
|
||||
&bul;<b>0</b>&ddot;<b>9</b> <b>a</b>&ddot;<b>f</b> : <O>inserts a (hexa)decimal number</O>
|
||||
&bul;<b>0</b>&ddot;<b>9</b> <b>a</b>&ddot;<b>z</b> : <O>(fx column) inserts an effect</O>
|
||||
&bul;<b>^</b>&mdot;<b>v</b> : <O>(volume column) slide up/down</O>
|
||||
&bul;<b><</b>&mdot;<b>></b>: <O>(panning column) slide left/right</O>
|
||||
&bul;<b>-</b>&mdot;<b>=</b> : <O>(vol/pan col) fine slide down/up</O>
|
||||
&bul;<b>&udlr;</b> : <O>moves the viewing cursor by columns and rows</O>
|
||||
&bul;<b>pg&updn;</b> : <O>goes to previous/next cue</O>
|
||||
|
||||
<b> ACCIDENTALS</b>
|
||||
<b>\u00B7${'\u00B8'.repeat(11)}\u00B9</b>
|
||||
&demisharp; ♯ &doublesharp; &triplesharp; &quadsharp; &demiflat; ♭ &doubleflat; &tripleflat; &accuptick; &accupup; &accdntick; &accdndn;
|
||||
<b>C c cx x xx B b bb bbb ^ ^^ v vv</b>
|
||||
|
||||
<b> GLOBAL EDIT</b>
|
||||
<b>\u00B7${'\u00B8'.repeat(11)}\u00B9</b>
|
||||
&bul;<b>Q</b> : <O>retunes current song into different tuning and strategy. In general, nearest-note works best for macrotonals, nearest-harmonic and nearest-delta works best for highly microtonals (31+); 17- and 19-TET takes nearest-harmonic pretty well, while 22-TET seem to only benefit from the nearest-note</O>
|
||||
`
|
||||
|
||||
let helpProjectFlags = `<c>MIXER FLAGS</c>
|
||||
<c>\u00B7${'\u00B8'.repeat(11)}\u00B9</c>
|
||||
Mixer flags define how should the mixer behave.
|
||||
|
||||
<b> TONE MODE</b>
|
||||
<b>\u00B7${'\u00B8'.repeat(9)}\u00B9</b>
|
||||
&bul;Linear pitch : <O>pitch shift effects operate on linear pitch scale. The default and recommended setting for a new project</O>
|
||||
&bul;Amiga pitch : <O>pitch shift effects operate on Amiga period scale. Backwards compatible setting for MOD/S3M/XM/IT formats</O>
|
||||
&bul;Linear freq : <O>pitch shift effects operate on linear frequency scale. Backwards compatible setting for MONOTONE format</O>
|
||||
|
||||
<b> INTERPOLATION</b>
|
||||
<b>\u00B7${'\u00B8'.repeat(13)}\u00B9</b>
|
||||
&bul;Default : <O>three-tap fast sinc interpolation. The default and recommended setting for a new project</O>
|
||||
&bul;None : <O>zeroth-order hold</O>
|
||||
&bul;A500 : <O>emulates what Paula chip of Amiga 500 does. <b>S 0x00</b> effects only work with this and Amiga 1200 mode</O>
|
||||
&bul;A1200 : <O>emulates what Paula chip of Amiga 1200 does</O>
|
||||
&bul;SNES : <O>four-tap gaussian interpolation used by SNES</O>
|
||||
&bul;DPCM : <O>simulates Differential Pulse Code Modulation used by NES</O>
|
||||
`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// assemble help text pieces to complete help message
|
||||
|
||||
const SCRW = con.getmaxyx()[1]
|
||||
const HRULE = '\u00B4\u00B5'.repeat((_G.TAUT.HELPMSG_WIDTH) >>> 1) + '\n'
|
||||
|
||||
// Display-command palette. taut.js's popup uses (HELP_COL_TEXT on background) as the
|
||||
// default colour pair, so embedded `\x1B[38;5;Nm` codes switch foreground only.
|
||||
const HELP_COL_TEXT = 239 // popup body default (== colWHITE)
|
||||
const HELP_COL_EMPH = 230 // <b>...</b> highlight (== colVoiceHdr)
|
||||
const HELP_COL_BRAND = 211 // first half of "Microtone"
|
||||
const HELP_COL_BRAND_DIM = 239 // second half of "Microtone"
|
||||
|
||||
const fgEsc = (n) => `\x1B[38;5;${n}m`
|
||||
const ESC_DEFAULT = fgEsc(HELP_COL_TEXT)
|
||||
const ESC_EMPH = fgEsc(HELP_COL_EMPH)
|
||||
const MICROTONE = `${fgEsc(HELP_COL_BRAND)}Micro${fgEsc(HELP_COL_BRAND_DIM)}tone${ESC_DEFAULT}`
|
||||
|
||||
// Replace &xxx; entities with their final printable representations.
|
||||
function expandEntities(s) {
|
||||
return s
|
||||
.replaceAll('µtone;', MICROTONE)
|
||||
.replaceAll('&bul;', '\u00F9')
|
||||
.replaceAll('&ddot;', '\u008419u')
|
||||
.replaceAll('&mdot;', '\u00FA')
|
||||
.replaceAll('&updn;', '\u008418u')
|
||||
.replaceAll('&udlr;', '\u008428u\u008429u')
|
||||
.replaceAll('&keyoffsym;', '\u00A0\u00B1\u00B1\u00A1')
|
||||
.replaceAll('¬ecutsym;', '\u00A4\u00A4\u00A4\u00A4')
|
||||
.replaceAll(' ', '\u007F')
|
||||
.replaceAll('­', '')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('&demisharp;', '\u0080\u0081')
|
||||
.replaceAll('♯', '\u0082\u0083')
|
||||
.replaceAll('&sesquisharp;', '\u0084132u\u0085')
|
||||
.replaceAll('&doublesharp;', '\u0086\u0087')
|
||||
.replaceAll('&triplesharp;', '\u0088\u0089')
|
||||
.replaceAll('&quadsharp;', '\u008A\u008B')
|
||||
.replaceAll('&demiflat;', '\u008C\u008D')
|
||||
.replaceAll('♭', '\u008E\u008F')
|
||||
.replaceAll('&sesquiflat;', '\u0090\u0091')
|
||||
.replaceAll('&doubleflat;', '\u0092\u0093')
|
||||
.replaceAll('&tripleflat;', '\u0094\u0095')
|
||||
.replaceAll('&quadflat;', '\u0096\u0097')
|
||||
.replaceAll('&accuptick;', '\u009A')
|
||||
.replaceAll('&accdntick;', '\u009B')
|
||||
.replaceAll('&accupup;', '\u009C')
|
||||
.replaceAll('&accdndn;', '\u009D')
|
||||
}
|
||||
|
||||
// Tokenise a (post-entity-expansion) line. Returns an array of:
|
||||
// {type:'word', text:String, w:int} - non-breakable run of visible chars (may carry ANSI escapes)
|
||||
// {type:'sp'} - a single soft space (eligible for break/expansion)
|
||||
// {type:'anchor', open:Boolean} - <o>/</o> markers (zero width)
|
||||
//
|
||||
// Width accounting:
|
||||
// - ANSI escapes (`\x1B[...m`) : 0 visible chars
|
||||
// - TSVM unicode escapes (`..u`) : 1 visible char
|
||||
// - non-breaking space ( ) : 1 visible char (consumed as part of a word)
|
||||
// - soft hyphen () : dropped (not implemented as a break point)
|
||||
// - everything else : 1 visible char
|
||||
function tokenise(line) {
|
||||
const tokens = []
|
||||
let buf = ''
|
||||
let bufW = 0
|
||||
let i = 0
|
||||
|
||||
const flushWord = () => {
|
||||
if (buf.length > 0) {
|
||||
tokens.push({type: 'word', text: buf, w: bufW})
|
||||
buf = ''
|
||||
bufW = 0
|
||||
}
|
||||
}
|
||||
|
||||
while (i < line.length) {
|
||||
// inline tags (case-sensitive for <b>, case-insensitive for <o>)
|
||||
if (line.slice(i, i + 3) === '<b>') { buf += ESC_EMPH; i += 3; continue }
|
||||
if (line.slice(i, i + 4) === '</b>') { buf += ESC_DEFAULT; i += 4; continue }
|
||||
const head3 = line.slice(i, i + 3).toLowerCase()
|
||||
const head4 = line.slice(i, i + 4).toLowerCase()
|
||||
if (head3 === '<o>') { flushWord(); tokens.push({type: 'anchor', open: true}); i += 3; continue }
|
||||
if (head4 === '</o>') { flushWord(); tokens.push({type: 'anchor', open: false}); i += 4; continue }
|
||||
|
||||
const c = line[i]
|
||||
const cc = line.charCodeAt(i)
|
||||
|
||||
if (cc === 0x1B) {
|
||||
// pre-existing ANSI escape - copy verbatim, zero visible width
|
||||
const m = line.indexOf('m', i)
|
||||
const end = (m < 0) ? line.length : m + 1
|
||||
buf += line.slice(i, end)
|
||||
i = end
|
||||
}
|
||||
else if (cc === 0x84) {
|
||||
// TSVM <digits>u escape - copy verbatim, one visible char
|
||||
const u = line.indexOf('u', i)
|
||||
const end = (u < 0) ? line.length : u + 1
|
||||
buf += line.slice(i, end)
|
||||
bufW += 1
|
||||
i = end
|
||||
}
|
||||
else if (c === ' ') {
|
||||
flushWord()
|
||||
tokens.push({type: 'sp'})
|
||||
i += 1
|
||||
}
|
||||
else if (cc === 0x00AD) {
|
||||
// soft hyphen: drop (no break-point handling for now)
|
||||
i += 1
|
||||
}
|
||||
else {
|
||||
buf += c
|
||||
bufW += 1
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
flushWord()
|
||||
return tokens
|
||||
}
|
||||
|
||||
// Build wrapped lines from a token stream then format each one according to alignment.
|
||||
// Returns an array of strings, each exactly `width` visible chars wide (padded with
|
||||
// trailing spaces) so the caller can blit them without further math.
|
||||
function wrapAndAlign(tokens, width, alignment) {
|
||||
const lines = [] // each: {tokens, indent, contentW}
|
||||
let curTokens = []
|
||||
let curW = 0
|
||||
let curIndent = 0
|
||||
let nextIndent = 0 // indent the *next* flushed line should use
|
||||
|
||||
const flushLine = () => {
|
||||
// strip trailing soft spaces
|
||||
while (curTokens.length > 0 && curTokens[curTokens.length - 1].type === 'sp') {
|
||||
curTokens.pop()
|
||||
curW -= 1
|
||||
}
|
||||
lines.push({tokens: curTokens, indent: curIndent, contentW: curW})
|
||||
curTokens = []
|
||||
curW = 0
|
||||
curIndent = nextIndent
|
||||
}
|
||||
|
||||
for (const tok of tokens) {
|
||||
if (tok.type === 'anchor') {
|
||||
// anchor opens at the current visible column (accounting for indent)
|
||||
if (tok.open) nextIndent = curIndent + curW
|
||||
else nextIndent = 0
|
||||
continue
|
||||
}
|
||||
|
||||
if (tok.type === 'sp') {
|
||||
// ignore leading soft spaces on a fresh line
|
||||
if (curW === 0) continue
|
||||
// hard wrap if the line is already at the right edge
|
||||
if (curIndent + curW + 1 > width) { flushLine(); continue }
|
||||
curTokens.push(tok)
|
||||
curW += 1
|
||||
continue
|
||||
}
|
||||
|
||||
// word
|
||||
const tw = tok.w
|
||||
if (curIndent + curW + tw > width) {
|
||||
flushLine()
|
||||
// word too wide for the wrapped line: emit it on its own row (possibly clipped by terminal)
|
||||
if (curIndent + tw > width) {
|
||||
curTokens.push(tok)
|
||||
curW += tw
|
||||
flushLine()
|
||||
continue
|
||||
}
|
||||
}
|
||||
curTokens.push(tok)
|
||||
curW += tw
|
||||
}
|
||||
|
||||
if (curTokens.length > 0 || lines.length === 0) flushLine()
|
||||
|
||||
return lines.map((line, i) => formatLine(line, width, alignment, i === lines.length - 1))
|
||||
}
|
||||
|
||||
function formatLine(line, totalWidth, alignment, isLast) {
|
||||
if (line.tokens.length === 0) return ' '.repeat(totalWidth)
|
||||
|
||||
const indent = ' '.repeat(line.indent)
|
||||
const remaining = totalWidth - line.indent - line.contentW
|
||||
const pad = (n) => (n > 0) ? ' '.repeat(n) : ''
|
||||
const flatText = () => line.tokens.map(t => (t.type === 'sp') ? ' ' : t.text).join('')
|
||||
|
||||
if (alignment === 'c') {
|
||||
const left = remaining >> 1
|
||||
return indent + pad(left) + flatText() + pad(remaining - left)
|
||||
}
|
||||
if (alignment === 'r') return indent + pad(remaining) + flatText()
|
||||
if (alignment === 'l') return indent + flatText() + pad(remaining)
|
||||
|
||||
// justified: only expand spaces when there's slack and we're not on the
|
||||
// last (or single) wrapped line
|
||||
if (isLast || remaining <= 0) return indent + flatText() + pad(remaining)
|
||||
|
||||
const spaceCount = line.tokens.reduce((n, t) => n + (t.type === 'sp' ? 1 : 0), 0)
|
||||
if (spaceCount === 0) return indent + flatText() + pad(remaining)
|
||||
|
||||
const baseExtra = (remaining / spaceCount) | 0
|
||||
let leftover = remaining - baseExtra * spaceCount
|
||||
|
||||
let out = indent
|
||||
for (const tok of line.tokens) {
|
||||
if (tok.type === 'sp') {
|
||||
const extra = baseExtra + (leftover > 0 ? 1 : 0)
|
||||
if (leftover > 0) leftover -= 1
|
||||
out += ' '.repeat(1 + extra)
|
||||
} else {
|
||||
out += tok.text
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Process a single source line: peel a leading <c>/<r>/<l> alignment tag (if present),
|
||||
// strip its matching close tag, then tokenise + wrap.
|
||||
function typesetSourceLine(line, width) {
|
||||
if (line.length === 0) return [' '.repeat(width)]
|
||||
|
||||
let alignment = 'j' // justified default
|
||||
const startMatch = line.match(/^<([crl])>/i)
|
||||
if (startMatch) {
|
||||
alignment = startMatch[1].toLowerCase()
|
||||
line = line.slice(startMatch[0].length)
|
||||
const closeRe = new RegExp(`</${alignment}>$`, 'i')
|
||||
line = line.replace(closeRe, '')
|
||||
}
|
||||
|
||||
const tokens = tokenise(line)
|
||||
return wrapAndAlign(tokens, width, alignment)
|
||||
}
|
||||
|
||||
function typesetText(text, width) {
|
||||
text = expandEntities(text)
|
||||
const out = []
|
||||
for (const srcLine of text.split('\n')) {
|
||||
for (const outLine of typesetSourceLine(srcLine, width)) out.push(outLine)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function typeset(text, customWidth) {
|
||||
let typesetWidth = customWidth
|
||||
if (typesetWidth === undefined) typesetWidth = _G.TAUT.HELPMSG_WIDTH
|
||||
if (typesetWidth === undefined) {
|
||||
const currentPosX = con.getyx()[1] // 1-indexed
|
||||
typesetWidth = SCRW - currentPosX + 1
|
||||
}
|
||||
return typesetText(text, typesetWidth)
|
||||
}
|
||||
|
||||
let helpMessages = [ // index: taut.js PANEL_NAMES
|
||||
/* Timeline */[helpJam, helpTimeline, helpCommon, helpNotation].join(HRULE),
|
||||
/* Cues */[helpCommon, helpNotation].join(HRULE), // placeholder
|
||||
/* Patterns */[helpCommon, helpNotation].join(HRULE), // placeholder
|
||||
/* Samples */[helpCommon, helpNotation].join(HRULE), // placeholder
|
||||
/* Instruments */[helpCommon, helpNotation].join(HRULE), // placeholder
|
||||
/* Project */[helpProjectFlags, helpCommon, helpNotation].join(HRULE), // placeholder
|
||||
/* File */[helpCommon, helpNotation].join(HRULE), // placeholder
|
||||
]
|
||||
|
||||
help.MSG_BY_TABS = helpMessages.map(it => typeset(it))
|
||||
help.typeset = typeset
|
||||
help.COL_TEXT = HELP_COL_TEXT
|
||||
help.COL_EMPH = HELP_COL_EMPH
|
||||
|
||||
if (!_G.TAUT.HELPMSG) _G.TAUT.HELPMSG=help;
|
||||
77
assets/disk0/tvdos/bin/taut_instredit.js
Normal file
77
assets/disk0/tvdos/bin/taut_instredit.js
Normal 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.UI.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.UI.NEXTPANEL = (MY_PANEL + (shiftDown ? -1 : 1) + PANEL_COUNT) % PANEL_COUNT
|
||||
done = true
|
||||
return
|
||||
}
|
||||
|
||||
panel.processInput(event)
|
||||
})
|
||||
}
|
||||
|
||||
return 0
|
||||
0
assets/disk0/tvdos/bin/taut_notationedit.js
Normal file
0
assets/disk0/tvdos/bin/taut_notationedit.js
Normal file
77
assets/disk0/tvdos/bin/taut_sampleedit.js
Normal file
77
assets/disk0/tvdos/bin/taut_sampleedit.js
Normal 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.UI.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.UI.NEXTPANEL = (MY_PANEL + (shiftDown ? -1 : 1) + PANEL_COUNT) % PANEL_COUNT
|
||||
done = true
|
||||
return
|
||||
}
|
||||
|
||||
panel.processInput(event)
|
||||
})
|
||||
}
|
||||
|
||||
return 0
|
||||
BIN
assets/disk0/tvdos/bin/tautbtn.png
Normal file
BIN
assets/disk0/tvdos/bin/tautbtn.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 518 B |
1
assets/disk0/tvdos/bin/tautbtn.r8
Normal file
1
assets/disk0/tvdos/bin/tautbtn.r8
Normal 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>_
|
||||
1
assets/disk0/tvdos/bin/tautbtn0.r8
Normal file
1
assets/disk0/tvdos/bin/tautbtn0.r8
Normal 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>
|
||||
BIN
assets/disk0/tvdos/bin/tautfont.kra
LFS
Normal file
BIN
assets/disk0/tvdos/bin/tautfont.kra
LFS
Normal file
Binary file not shown.
BIN
assets/disk0/tvdos/bin/tautfont_high.chr
Normal file
BIN
assets/disk0/tvdos/bin/tautfont_high.chr
Normal file
Binary file not shown.
BIN
assets/disk0/tvdos/bin/tautfont_low.chr
Normal file
BIN
assets/disk0/tvdos/bin/tautfont_low.chr
Normal file
Binary file not shown.
BIN
assets/disk0/tvdos/bin/tauthdr.png
Normal file
BIN
assets/disk0/tvdos/bin/tauthdr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 490 B |
BIN
assets/disk0/tvdos/bin/tauthdr.r8
Normal file
BIN
assets/disk0/tvdos/bin/tauthdr.r8
Normal file
Binary file not shown.
@@ -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,35 +28,96 @@ 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(`microtone "${f}"`),
|
||||
}
|
||||
|
||||
function makeExecFun(template) {
|
||||
return (f) => _G.shell.execute(template.replaceAll("{0}", `"${f}"`))
|
||||
}
|
||||
|
||||
function loadZfmrc() {
|
||||
try {
|
||||
let zfmrcPath = `A:${_TVDOS.variables.USERCONFIGPATH}\\zfmrc`
|
||||
let zfmrcFile = files.open(zfmrcPath)
|
||||
if (!zfmrcFile.exists) return
|
||||
|
||||
let content = zfmrcFile.sread()
|
||||
let lines = content.split(/\r?\n/)
|
||||
let currentSection = null
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i].trim()
|
||||
if (line.length === 0 || line.startsWith("#") || line.startsWith(";")) continue
|
||||
|
||||
if (line.startsWith("[") && line.endsWith("]")) {
|
||||
currentSection = line.substring(1, line.length - 1).toUpperCase()
|
||||
continue
|
||||
}
|
||||
|
||||
if (currentSection === "EXEC_FUNS") {
|
||||
let commaIdx = line.indexOf(",")
|
||||
if (commaIdx < 0) continue
|
||||
let ext = line.substring(0, commaIdx).trim().toLowerCase()
|
||||
let template = line.substring(commaIdx + 1).trim()
|
||||
if (ext.length === 0 || template.length === 0) continue
|
||||
EXEC_FUNS[ext] = makeExecFun(template)
|
||||
}
|
||||
else if (currentSection === "COL_HL_EXT") {
|
||||
let commaIdx = line.indexOf(",")
|
||||
if (commaIdx < 0) continue
|
||||
let ext = line.substring(0, commaIdx).trim().toLowerCase()
|
||||
let colStr = line.substring(commaIdx + 1).trim()
|
||||
if (ext.length === 0 || colStr.length === 0) continue
|
||||
let col = parseInt(colStr, 10)
|
||||
if (isNaN(col)) continue
|
||||
COL_HL_EXT[ext] = col
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
serial.println("zfm: failed to load zfmrc: " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
loadZfmrc()
|
||||
|
||||
let windowMode = 0 // 0 == left, 1 == right
|
||||
let windowFocus = [0] // is a stack; 0: files window, 1: palette window, 2: popup window
|
||||
|
||||
@@ -68,6 +131,8 @@ let cursor = [0, 0] // absolute position!
|
||||
|
||||
function bytesToReadable(i) {
|
||||
return ''+ (
|
||||
(i > 999999999999) ? (((i / 10000000000)|0)/100 + "T") :
|
||||
(i > 999999999) ? (((i / 10000000)|0)/100 + "G") :
|
||||
(i > 999999) ? (((i / 10000)|0)/100 + "M") :
|
||||
(i > 9999) ? (((i / 100)|0)/10 + "K") :
|
||||
i
|
||||
@@ -407,6 +472,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 +482,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 +541,7 @@ let filenavOninput = (window, event) => {
|
||||
|
||||
firstRunLatch = true
|
||||
con.curs_set(0);clearScr()
|
||||
refreshFilePanelCache(windowMode)
|
||||
redraw()
|
||||
}
|
||||
}
|
||||
@@ -659,11 +727,11 @@ 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 && keysym != "q") { // release the latch right away if the key is neither Return nor 'q'
|
||||
firstRunLatch = false
|
||||
}
|
||||
|
||||
if (keyJustHit && firstRunLatch) { // filter out the initial ENTER key as they would cause unwanted behaviours
|
||||
if (keyJustHit && firstRunLatch) { // filter out the initial ENTER/'q' key as they would cause unwanted behaviours
|
||||
firstRunLatch = false
|
||||
}
|
||||
else {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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(' ')
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
30
assets/disk0/tvdos/i18n/korean_font_upload.js
Normal file
30
assets/disk0/tvdos/i18n/korean_font_upload.js
Normal 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)
|
||||
31
assets/disk0/tvdos/include/font.mjs
Normal file
31
assets/disk0/tvdos/include/font.mjs
Normal 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 }
|
||||
1129
assets/disk0/tvdos/include/fs.mjs
Normal file
1129
assets/disk0/tvdos/include/fs.mjs
Normal file
File diff suppressed because it is too large
Load Diff
305
assets/disk0/tvdos/include/getopt.mjs
Normal file
305
assets/disk0/tvdos/include/getopt.mjs
Normal 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 });
|
||||
};
|
||||
@@ -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
|
||||
|
||||
80
assets/disk0/tvdos/include/keysym.mjs
Normal file
80
assets/disk0/tvdos/include/keysym.mjs
Normal 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,
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
289
assets/disk0/tvdos/include/playgui.mjs
Normal file
289
assets/disk0/tvdos/include/playgui.mjs
Normal 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
|
||||
}
|
||||
414
assets/disk0/tvdos/include/psg.mjs
Normal file
414
assets/disk0/tvdos/include/psg.mjs
Normal 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
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
321
assets/disk0/tvdos/include/taud.mjs
Normal file
321
assets/disk0/tvdos/include/taud.mjs
Normal file
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
* 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
|
||||
// Sample+instrument image: 8 MB sample pool (banked, 16 × 512 K) + 64 K instrument bin = 8256 kB total.
|
||||
// (terranmon.txt:1985-1997, 2533-2564 — bank-switched via MMIO 46.)
|
||||
const SAMPLE_BANK_SIZE = 524288 // 512 K — size of the sample-bin window
|
||||
const SAMPLE_BANK_COUNT = 16 // 16 banks × 512 K = 8 MB
|
||||
const SAMPLEBIN_SIZE = SAMPLE_BANK_SIZE * SAMPLE_BANK_COUNT // 8 MB
|
||||
const INSTBIN_SIZE = 65536 // 256 inst × 256 bytes
|
||||
const SAMPLEINST_SIZE = SAMPLEBIN_SIZE + INSTBIN_SIZE // 8454144 = 8256 kB
|
||||
const SAMPLEBIN_WINDOW_OFFSET = 0 // peripheral memory window for the active sample bank
|
||||
const INSTBIN_WINDOW_OFFSET = 720896 // peripheral memory offset of instrument bin
|
||||
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 -----------------------
|
||||
// The decompressed image is 8256 kB (8 MB samples bank-major + 64 K instruments)
|
||||
// which exceeds the 8 MB user-space cap, so we route through a hardware helper
|
||||
// that decompresses straight into the adapter's native sample/instrument
|
||||
// storage instead of staging a buffer in user memory.
|
||||
audio.uploadSampleInstBlob(filePtr + pos, compressedSize)
|
||||
audio.setSampleBank(0)
|
||||
pos += compressedSize
|
||||
|
||||
// -- 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 + 25
|
||||
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 ------------------------------------
|
||||
// The 8256 kB raw image (8 MB samples + 64 K instruments) cannot fit in the
|
||||
// 8 MB user space, so we hand the entire compress step to a hardware helper
|
||||
// that reads directly out of the adapter's native sample/instrument storage.
|
||||
// Realistic sample data compresses well under both gzip and zstd; we cap the
|
||||
// destination at "uncompressed size + 8 K" headroom which suffices for any
|
||||
// sane musical content.
|
||||
const COMP_BUF_CAP = 1024 * 1024 * 4 // 4 MiB cap for compressed sample+inst blob
|
||||
let compBuf = sys.malloc(COMP_BUF_CAP)
|
||||
let compressedSize = audio.captureSampleInstBlob(compBuf, COMP_BUF_CAP)
|
||||
if (compressedSize > COMP_BUF_CAP) {
|
||||
sys.free(compBuf)
|
||||
throw Error("taud: compressed sample+inst blob exceeded " + COMP_BUF_CAP + " bytes (got " + compressedSize + ")")
|
||||
}
|
||||
|
||||
// -- 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 - 25) & 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 −25 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 }
|
||||
621
assets/disk0/tvdos/include/tbas.mjs
Normal file
621
assets/disk0/tvdos/include/tbas.mjs
Normal file
@@ -0,0 +1,621 @@
|
||||
// Terran BASIC runtime helper for compiled programs
|
||||
// Compiled-by: assets/disk0/tbas/compile.js
|
||||
// Loaded at runtime by `let bS = require("tbas")`
|
||||
//
|
||||
// Contract with compiler:
|
||||
// - The compiler has lowered every BASIC expression to a JS expression
|
||||
// that produces the *raw* JS value (number, string, array, ForGen,
|
||||
// function, BasicMemoMonad, …). Builtins take such raw values, NOT
|
||||
// SyntaxTreeReturnObj wrappers.
|
||||
// - Variable reads: bS.__state.vars.X (key always uppercased)
|
||||
// - Variable writes: bS.__state.vars.X = v
|
||||
// - Control flow (GOTO/GOSUB/RETURN/FOR/NEXT/IF/ON/END/READ/RESTORE/LABEL/DATA)
|
||||
// is *not* exposed here — the compiler emits inline JS that updates the
|
||||
// `pc` and `gosubStack` directly.
|
||||
//
|
||||
// Naming: BASIC builtins exposed under their UPPERCASE name (bS.PRINT,
|
||||
// bS.PLOT, bS.SIN). Compiler-only helpers prefixed with __.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types & helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isNumable(s) {
|
||||
if (Array.isArray(s)) return false
|
||||
if (s === undefined) return false
|
||||
if (typeof s.trim == "function" && s.trim().length == 0) return false
|
||||
return !isNaN(s)
|
||||
}
|
||||
const tonum = (t) => t * 1.0
|
||||
|
||||
function ForGen(s, e, t) {
|
||||
this.start = s
|
||||
this.end = e
|
||||
this.step = t || 1
|
||||
this.current = this.start
|
||||
this.stepsgn = (this.step > 0) ? 1 : -1
|
||||
}
|
||||
const isGenerator = (o) =>
|
||||
o !== undefined && o !== null &&
|
||||
o.start !== undefined && o.end !== undefined &&
|
||||
o.step !== undefined && o.stepsgn !== undefined
|
||||
const genToArray = (gen) => {
|
||||
let a = []
|
||||
let cur = gen.start
|
||||
while (cur * gen.stepsgn + gen.step * gen.stepsgn <= (gen.end + gen.step) * gen.stepsgn) {
|
||||
a.push(cur)
|
||||
cur += gen.step
|
||||
}
|
||||
return a
|
||||
}
|
||||
const genHasNext = (o) => o.current * o.stepsgn + o.step * o.stepsgn <= (o.end + o.step) * o.stepsgn
|
||||
const genGetNext = (gen, mutated) => {
|
||||
if (mutated !== undefined) gen.current = tonum(mutated)
|
||||
gen.current += gen.step
|
||||
return genHasNext(gen) ? gen.current : undefined
|
||||
}
|
||||
|
||||
function BasicMemoMonad(m) { this.mType = "value"; this.mVal = m }
|
||||
function BasicListMonad(m) { this.mType = "list"; this.mVal = [m] }
|
||||
function BasicFunSeq(f) { this.mType = "funseq"; this.mVal = f }
|
||||
const isMonad = (o) => o !== undefined && o !== null && o.mType !== undefined
|
||||
|
||||
function arrayToString(a) {
|
||||
let acc = ""
|
||||
for (let k = 0; k < a.length; k++) {
|
||||
if (k > 0) acc += ","
|
||||
acc += (Array.isArray(a[k])) ? arrayToString(a[k]) : a[k]
|
||||
}
|
||||
return "{" + acc + "}"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State container
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const _initialConsts = () => ({
|
||||
NIL: [],
|
||||
PI: Math.PI,
|
||||
TAU: Math.PI * 2,
|
||||
EULER: Math.E,
|
||||
UNDEFINED: undefined,
|
||||
TRUE: true,
|
||||
FALSE: false,
|
||||
// ID is identity-function: emitted as JS arrow when needed
|
||||
ID: (x) => x,
|
||||
})
|
||||
|
||||
const state = {
|
||||
vars: _initialConsts(),
|
||||
indexBase: 0,
|
||||
dataConsts: [],
|
||||
dataCursor: 0,
|
||||
gotoLabels: {}, // labelName -> [lnum, stmt]
|
||||
lineList: [], // sorted ascending list of existing source lines (for GOTO snap)
|
||||
rnd: Math.random(),
|
||||
forVar: {}, // varname -> generator|array (the iterable we still owe to FOR/FOREACH)
|
||||
forLnums: {}, // varname -> [lnum, stmt of the FOR/FOREACH header]
|
||||
forStack: [],
|
||||
trace: false,
|
||||
debug: false,
|
||||
}
|
||||
|
||||
function __reset() {
|
||||
state.vars = _initialConsts()
|
||||
state.indexBase = 0
|
||||
state.dataConsts = []
|
||||
state.dataCursor = 0
|
||||
state.gotoLabels = {}
|
||||
state.lineList = []
|
||||
state.rnd = Math.random()
|
||||
state.forVar = {}
|
||||
state.forLnums = {}
|
||||
state.forStack = []
|
||||
}
|
||||
|
||||
function __data(values) { state.dataConsts = values.slice() }
|
||||
function __labels(map) { state.gotoLabels = Object.assign({}, map) }
|
||||
function __setLines(arr) { state.lineList = arr.slice() }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compiler-emitted operator helpers (need behaviour not directly expressible
|
||||
// in raw JS without losing semantics)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function __add(lh, rh) {
|
||||
return (!isNaN(lh) && !isNaN(rh)) ? (tonum(lh) + tonum(rh)) : (lh + rh)
|
||||
}
|
||||
function __div(lh, rh) { if (rh == 0) throw Error("Division by zero"); return lh / rh }
|
||||
function __intdiv(lh, rh) { if (rh == 0) throw Error("Division by zero"); return (lh / rh) | 0 }
|
||||
function __mod(lh, rh) { if (rh == 0) throw Error("Division by zero"); return lh % rh }
|
||||
function __pow(lh, rh) {
|
||||
let r = Math.pow(lh, rh)
|
||||
if (isNaN(r)) throw Error("Illegal function call")
|
||||
if (!isFinite(r)) throw Error("Division by zero")
|
||||
return r
|
||||
}
|
||||
|
||||
function __test(v) { return !!v } // matches builtin TEST: string "false" is truthy
|
||||
|
||||
function __dim(dims) {
|
||||
let revdims = dims.slice().reverse()
|
||||
let inner = new Array(revdims[0]).fill(0)
|
||||
for (let k = 1; k < revdims.length; k++) {
|
||||
const sz = revdims[k]
|
||||
const prev = inner
|
||||
inner = new Array(sz).fill(0).map(_ => JSON.parse(JSON.stringify(prev)))
|
||||
}
|
||||
return inner
|
||||
}
|
||||
|
||||
function __subscriptError(idx, dim) {
|
||||
return Error("Subscript out of range (index " + idx + ", dim " + dim + ")")
|
||||
}
|
||||
function __arrGet(arr, idx) {
|
||||
let v = arr
|
||||
for (let i = 0; i < idx.length; i++) {
|
||||
if (v === undefined || v === null) throw __subscriptError(idx[i], i)
|
||||
v = v[idx[i] - state.indexBase]
|
||||
}
|
||||
return v
|
||||
}
|
||||
function __arrSet(arr, idx, value) {
|
||||
let v = arr
|
||||
for (let i = 0; i < idx.length - 1; i++) {
|
||||
if (v === undefined || v === null) throw __subscriptError(idx[i], i)
|
||||
v = v[idx[i] - state.indexBase]
|
||||
}
|
||||
if (v === undefined || v === null) throw __subscriptError(idx[idx.length - 1], idx.length - 1)
|
||||
v[idx[idx.length - 1] - state.indexBase] = value
|
||||
}
|
||||
|
||||
// FOR / FOREACH setup. Lowered as:
|
||||
// __forSetup(varname, iterable, bodyLnum, bodyStmt)
|
||||
// where iterable is a ForGen (FOR…TO…STEP) OR an Array (FOREACH IN…), and
|
||||
// (bodyLnum, bodyStmt) is the PC of the statement immediately following the
|
||||
// FOR header — i.e. where NEXT should jump back to. The compiler supplies
|
||||
// this directly so the state machine doesn't rely on fall-through.
|
||||
function __forSetup(varname, iterable, bodyLnum, bodyStmt) {
|
||||
const v = varname.toUpperCase()
|
||||
if (isGenerator(iterable)) {
|
||||
state.vars[v] = iterable.start
|
||||
state.forVar[v] = iterable
|
||||
} else if (Array.isArray(iterable)) {
|
||||
state.vars[v] = iterable[0]
|
||||
state.forVar[v] = iterable.slice(1) // remainder
|
||||
} else {
|
||||
throw Error("FOR: not a generator or array")
|
||||
}
|
||||
state.forLnums[v] = [bodyLnum, bodyStmt]
|
||||
state.forStack.push(v)
|
||||
}
|
||||
|
||||
// NEXT [varname]. Without varname, pops the most recent.
|
||||
// Returns [lnum, stmt] to jump back to (just-after the FOR header) if more
|
||||
// iterations remain, or undefined if the loop is exhausted (caller falls
|
||||
// through).
|
||||
function __forNext(varname) {
|
||||
let v
|
||||
if (varname === undefined || varname === null) {
|
||||
v = state.forStack.pop()
|
||||
} else {
|
||||
v = varname.toUpperCase()
|
||||
// remove this varname from the stack
|
||||
const idx = state.forStack.lastIndexOf(v)
|
||||
if (idx >= 0) state.forStack.splice(idx, 1)
|
||||
}
|
||||
if (v === undefined) throw Error("NEXT without FOR")
|
||||
|
||||
const it = state.forVar[v]
|
||||
let nextVal
|
||||
if (isGenerator(it)) {
|
||||
nextVal = genGetNext(it, state.vars[v])
|
||||
} else {
|
||||
nextVal = it.shift()
|
||||
}
|
||||
|
||||
if (nextVal !== undefined) {
|
||||
state.vars[v] = nextVal
|
||||
state.forStack.push(v)
|
||||
return state.forLnums[v] // already the PC of the loop body
|
||||
} else {
|
||||
if (isGenerator(it)) state.vars[v] = it.current
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function __readData() {
|
||||
const r = state.dataConsts[state.dataCursor++]
|
||||
if (r === undefined) throw Error("Out of DATA")
|
||||
return r
|
||||
}
|
||||
|
||||
// Resolve a GOTO/GOSUB target — accepts numeric line, label string, or
|
||||
// already-evaluated expression. For numeric targets that don't match an
|
||||
// existing source line, snap upward to the next one (matches the
|
||||
// interpreter's behaviour, where the main loop simply increments lnum until
|
||||
// it finds a populated cmdbuf entry).
|
||||
function __resolveTarget(t) {
|
||||
if (typeof t === "string" && state.gotoLabels[t] !== undefined) {
|
||||
return state.gotoLabels[t]
|
||||
}
|
||||
let target
|
||||
if (typeof t === "number") target = t
|
||||
else if (isNumable(t)) target = tonum(t)
|
||||
else throw Error("Invalid jump target: " + t)
|
||||
|
||||
const lines = state.lineList
|
||||
if (lines.length === 0) return [target, 0]
|
||||
// linear scan is fine for the line counts BASIC programs reach
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i] >= target) return [lines[i], 0]
|
||||
}
|
||||
return [Infinity, 0]
|
||||
}
|
||||
|
||||
// Invoke a usrdefun (compiled to a JS function), or — when the parser
|
||||
// couldn't tell array-indexing apart from function-call (e.g. `A(5)` for an
|
||||
// unknown identifier) — index into an array. Used by MAP/FOLD/FILTER, monad
|
||||
// operators, and the compiler's default `function` lowering.
|
||||
function __runFn(fn, args) {
|
||||
if (typeof fn === "function") return fn.apply(null, args)
|
||||
if (Array.isArray(fn)) return __arrGet(fn, args)
|
||||
if (isMonad(fn) && fn.mType === "funseq") {
|
||||
let arg = args[0]
|
||||
for (let i = 0; i < fn.mVal.length; i++) arg = __runFn(fn.mVal[i], [arg])
|
||||
return arg
|
||||
}
|
||||
throw Error("Not a callable: " + fn)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Operator builtins (where JS doesn't already do the right thing)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function _AND(a, b) { if (typeof a !== "boolean" || typeof b !== "boolean") throw Error("Type mismatch"); return a && b }
|
||||
function _OR (a, b) { if (typeof a !== "boolean" || typeof b !== "boolean") throw Error("Type mismatch"); return a || b }
|
||||
function _NOT(a) { return !a }
|
||||
|
||||
function _CONS(lh, rh) { // !
|
||||
if (Array.isArray(rh)) return [lh].concat(rh)
|
||||
if (rh && rh.mType === "list") { rh.mVal = [lh].concat(rh.mVal); return rh }
|
||||
throw Error("Type mismatch")
|
||||
}
|
||||
function _PUSH(lh, rh) { // ~
|
||||
if (Array.isArray(lh)) return lh.concat([rh])
|
||||
if (lh && lh.mType === "list") { lh.mVal = [lh.mVal].concat([rh]); return lh }
|
||||
throw Error("Type mismatch")
|
||||
}
|
||||
function _CONCAT(lh, rh) { // #
|
||||
if (Array.isArray(lh) && Array.isArray(rh)) return lh.concat(rh)
|
||||
if (lh && rh && lh.mType === "list" && rh.mType === "list") return new BasicListMonad(lh.mVal.concat(rh.mVal))
|
||||
throw Error("Type mismatch")
|
||||
}
|
||||
|
||||
function _TO(from, to) { return new ForGen(from, to, 1) }
|
||||
function _STEP(gen, step) {
|
||||
if (!isGenerator(gen)) throw Error("Type mismatch (STEP)")
|
||||
return new ForGen(gen.start, gen.end, step)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// I/O builtins
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// PRINT(values, seps) — values: array of resolved JS values; seps: array of
|
||||
// length values.length-1 with "," | ";" between each consecutive pair.
|
||||
// Trailing semicolon? The compiler signals "no newline" by passing a final
|
||||
// `null` element in `values` and "noNewline" flag — we use the convention
|
||||
// that the LAST entry of `values` being a marker `__noNewline` suppresses
|
||||
// the newline (matches basic.js trailing-null behaviour).
|
||||
const __PRINT_NONL = Symbol("PRINT_NONL")
|
||||
function PRINT(values, seps) {
|
||||
seps = seps || []
|
||||
if (values.length === 0) {
|
||||
println()
|
||||
return
|
||||
}
|
||||
let suppressNewline = false
|
||||
let realLen = values.length
|
||||
if (values[realLen - 1] === __PRINT_NONL) {
|
||||
suppressNewline = true
|
||||
realLen -= 1
|
||||
}
|
||||
for (let i = 0; i < realLen; i++) {
|
||||
if (i >= 1 && seps[i - 1] === ",") print("\t")
|
||||
const v = values[i]
|
||||
let s
|
||||
if (Array.isArray(v)) s = arrayToString(v)
|
||||
else if (v === undefined || v === "") s = ""
|
||||
else if (v.toString !== undefined) s = v.toString()
|
||||
else s = v
|
||||
print(s)
|
||||
}
|
||||
if (!suppressNewline) println()
|
||||
}
|
||||
function EMIT(values, seps) {
|
||||
seps = seps || []
|
||||
if (values.length === 0) { println(); return }
|
||||
let suppressNewline = false
|
||||
let realLen = values.length
|
||||
if (values[realLen - 1] === __PRINT_NONL) { suppressNewline = true; realLen -= 1 }
|
||||
for (let i = 0; i < realLen; i++) {
|
||||
if (i >= 1 && seps[i - 1] === ",") print("\t")
|
||||
const v = values[i]
|
||||
if (v === undefined) print("")
|
||||
else if (isNumable(v)) {
|
||||
const c = con.getyx()
|
||||
con.addch(tonum(v))
|
||||
con.move(c[0], c[1] + 1)
|
||||
} else if (v.toString !== undefined) print(v.toString())
|
||||
else print(v)
|
||||
}
|
||||
if (!suppressNewline) println()
|
||||
}
|
||||
|
||||
function INPUT(promptOrVarname) {
|
||||
print("? ")
|
||||
let r = sys.read().trim()
|
||||
if (!isNaN(r)) r = tonum(r)
|
||||
return r
|
||||
}
|
||||
function CIN() { return sys.read().trim() }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Numeric builtins
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const _num = (f) => (x) => { if (!isNumable(x)) throw Error("Type mismatch"); return f(tonum(x)) }
|
||||
const _num2 = (f) => (a, b) => {
|
||||
if (!isNumable(a) || !isNumable(b)) throw Error("Type mismatch")
|
||||
return f(tonum(a), tonum(b))
|
||||
}
|
||||
|
||||
const ABS = _num(Math.abs)
|
||||
const SGN = _num(x => x > 0 ? 1 : x < 0 ? -1 : 0)
|
||||
const INT = _num(Math.floor)
|
||||
const FLOOR = _num(Math.floor)
|
||||
const CEIL = _num(Math.ceil)
|
||||
const FIX = _num(x => x | 0)
|
||||
const ROUND = _num(Math.round)
|
||||
const SQR = _num(Math.sqrt)
|
||||
const CBR = _num(Math.cbrt)
|
||||
const SIN = _num(Math.sin)
|
||||
const COS = _num(Math.cos)
|
||||
const TAN = _num(Math.tan)
|
||||
const ASN = _num(Math.asin)
|
||||
const ACO = _num(Math.acos)
|
||||
const ATN = _num(Math.atan)
|
||||
const SINH = _num(Math.sinh)
|
||||
const COSH = _num(Math.cosh)
|
||||
const TANH = _num(Math.tanh)
|
||||
const EXP = _num(Math.exp)
|
||||
const LOG = _num(Math.log)
|
||||
const MIN = _num2((a,b) => a > b ? b : a)
|
||||
const MAX = _num2((a,b) => a < b ? b : a)
|
||||
|
||||
function RND(x) {
|
||||
// matches basic.js:1199 — only re-roll when arg !== 0
|
||||
if (!(x === 0)) state.rnd = Math.random()
|
||||
return state.rnd
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// String builtins
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function SPC(n) { return " ".repeat(n) }
|
||||
function LEFT(s, n) { return String(s).substring(0, n) }
|
||||
function RIGHT(s, n) { return String(s).substring(String(s).length - n) }
|
||||
function MID(s, start, len) { return String(s).substring(start - state.indexBase, start - state.indexBase + len) }
|
||||
function CHR(n) { return String.fromCharCode(n) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// List builtins
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function LEN(x) { if (x === undefined || x.length === undefined) throw Error("Type mismatch"); return x.length }
|
||||
function HEAD(x) { if (!x || x.length < 1) throw Error("Type mismatch"); return x[0] }
|
||||
function TAIL(x) { if (!x || x.length < 1) throw Error("Type mismatch"); return x.slice(1) }
|
||||
function INIT(x) { if (!x || x.length < 1) throw Error("Type mismatch"); return x.slice(0, x.length - 1) }
|
||||
function LAST(x) { if (!x || x.length < 1) throw Error("Type mismatch"); return x[x.length - 1] }
|
||||
|
||||
function MAP(fn, functor) {
|
||||
if (typeof fn !== "function" && !(isMonad(fn) && fn.mType === "funseq")) throw Error("MAP: not a function")
|
||||
if (isGenerator(functor)) functor = genToArray(functor)
|
||||
if (!Array.isArray(functor)) throw Error("MAP: not iterable")
|
||||
return functor.map(it => __runFn(fn, [it]))
|
||||
}
|
||||
function FOLD(fn, init, functor) {
|
||||
if (typeof fn !== "function" && !(isMonad(fn) && fn.mType === "funseq")) throw Error("FOLD: not a function")
|
||||
if (isGenerator(functor)) functor = genToArray(functor)
|
||||
if (!Array.isArray(functor)) throw Error("FOLD: not iterable")
|
||||
let akku = init
|
||||
for (let i = 0; i < functor.length; i++) akku = __runFn(fn, [akku, functor[i]])
|
||||
return akku
|
||||
}
|
||||
function FILTER(fn, functor) {
|
||||
if (typeof fn !== "function" && !(isMonad(fn) && fn.mType === "funseq")) throw Error("FILTER: not a function")
|
||||
if (isGenerator(functor)) functor = genToArray(functor)
|
||||
if (!Array.isArray(functor)) throw Error("FILTER: not iterable")
|
||||
return functor.filter(it => __runFn(fn, [it]))
|
||||
}
|
||||
|
||||
// Array literal constructor — emitted by the compiler for `[a,b,c]` syntax
|
||||
function ARRAY() { return Array.prototype.slice.call(arguments) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Graphics / system
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function CLS() { con.clear() }
|
||||
function CLPX() { graphics.clearPixels(255) }
|
||||
function PLOT(x, y, c) { graphics.plotPixel(x, y, c) }
|
||||
function GOTOYX(y, x) { con.move(y + (1 - state.indexBase), x + (1 - state.indexBase)) }
|
||||
function TEXTFORE(c) { print(String.fromCharCode(27, 91) + "38;5;" + (c | 0) + "m") }
|
||||
function TEXTBACK(c) { print(String.fromCharCode(27, 91) + "48;5;" + (c | 0) + "m") }
|
||||
function POKE(addr, v) { sys.poke(addr, v) }
|
||||
function PEEK(addr) { return sys.peek(addr) }
|
||||
function GETKEYSDOWN() {
|
||||
const keys = []
|
||||
sys.poke(-40, 255)
|
||||
for (let k = -41; k >= -48; k--) keys.push(sys.peek(k))
|
||||
return keys
|
||||
}
|
||||
|
||||
function CPUT(devnum, msg) { com.sendMessage(devnum, msg); return com.getStatusCode(devnum) }
|
||||
function CGET(devnum, ptr) {
|
||||
const msg = com.pullMessage(devnum)
|
||||
const len = msg.length | 0
|
||||
for (let i = 0; i < len; i++) sys.poke(ptr + i, msg.charCodeAt(i))
|
||||
return len
|
||||
}
|
||||
function CSTA(devnum) { return com.getStatusCode(devnum) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type / debug
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function TYPEOF(v) {
|
||||
if (v === undefined) return "null"
|
||||
if (typeof v === "boolean") return "bool"
|
||||
if (Array.isArray(v)) return "array"
|
||||
if (isGenerator(v)) return "generator"
|
||||
if (isMonad(v)) return v.mType + "-monad"
|
||||
if (typeof v === "function") return "usrdefun"
|
||||
if (isNumable(v)) return "num"
|
||||
if (typeof v === "string") return "string"
|
||||
return typeof v
|
||||
}
|
||||
|
||||
function OPTIONBASE(n) {
|
||||
if (n != 0 && n != 1) throw Error("Syntax error: OPTIONBASE")
|
||||
state.indexBase = n | 0
|
||||
}
|
||||
function OPTIONDEBUG(n) { state.debug = (n | 0) === 1 }
|
||||
function OPTIONTRACE(n) { state.trace = (n | 0) === 1 }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Monad / functional ops (best-effort port)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function MRET(v) { return new BasicMemoMonad(v) }
|
||||
function MLIST(v) { return new BasicListMonad(v) }
|
||||
function MJOIN(m) { if (!isMonad(m)) throw Error("Type mismatch"); return m.mVal }
|
||||
|
||||
function _BIND(ma, fn) { // >>=
|
||||
if (!isMonad(ma)) throw Error(">>=: left is not a monad")
|
||||
if (typeof fn !== "function") throw Error(">>=: right is not a function")
|
||||
const mb = __runFn(fn, [ma.mVal])
|
||||
if (!isMonad(mb)) throw Error(">>=: function did not return a monad")
|
||||
return mb
|
||||
}
|
||||
function _SEQ(ma, mb) { // >>~
|
||||
if (!isMonad(ma) || !isMonad(mb)) throw Error("Type mismatch")
|
||||
return mb
|
||||
}
|
||||
function _COMPOSE(fa, fb) { // .
|
||||
const ma = (typeof fa === "function") ? [fa] : fa.mVal
|
||||
const mb = (typeof fb === "function") ? [fb] : fb.mVal
|
||||
return new BasicFunSeq(mb.concat(ma))
|
||||
}
|
||||
function _APPLY(fn, value) { // $
|
||||
return __runFn(fn, [value])
|
||||
}
|
||||
function _PIPE(value, fn) { // &
|
||||
return _APPLY(fn, value)
|
||||
}
|
||||
function _CURRY(fn, value) { // ~<
|
||||
if (typeof fn !== "function") throw Error("~<: left is not a function")
|
||||
return function() {
|
||||
const rest = Array.prototype.slice.call(arguments)
|
||||
return fn.apply(null, [value].concat(rest))
|
||||
}
|
||||
}
|
||||
function _SEQAPP(fns, functor) { // <*>
|
||||
if (!Array.isArray(fns)) throw Error("<*>: first arg must be an array of functions")
|
||||
if (isGenerator(functor)) functor = genToArray(functor)
|
||||
if (!Array.isArray(functor)) throw Error("<*>: not iterable")
|
||||
let ret = []
|
||||
for (let i = 0; i < fns.length; i++) ret = ret.concat(functor.map(it => __runFn(fns[i], [it])))
|
||||
return ret
|
||||
}
|
||||
function _SEQCURRYMAP(fns, functor) { // <~>
|
||||
if (typeof fns === "function") fns = [fns]
|
||||
if (!Array.isArray(fns)) throw Error("<~>: first arg must be a function or array of functions")
|
||||
if (isGenerator(functor)) functor = genToArray(functor)
|
||||
if (!Array.isArray(functor)) throw Error("<~>: not iterable")
|
||||
let ret = []
|
||||
for (let i = 0; i < fns.length; i++) ret = ret.concat(functor.map(it => _CURRY(fns[i], it)))
|
||||
return ret
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exports
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
exports = {
|
||||
// state & introspection
|
||||
__state: state, __reset, __data, __labels, __setLines,
|
||||
__PRINT_NONL,
|
||||
|
||||
// operator helpers
|
||||
__add, __div, __intdiv, __mod, __pow, __test,
|
||||
__dim, __arrGet, __arrSet,
|
||||
__forSetup, __forNext, __readData, __resolveTarget,
|
||||
__runFn,
|
||||
|
||||
// type ctors
|
||||
__ForGen: ForGen, __isGenerator: isGenerator, __genToArray: genToArray,
|
||||
__isMonad: isMonad,
|
||||
|
||||
// operators
|
||||
AND: _AND, OR: _OR, NOT: _NOT,
|
||||
UNARYLOGICNOT: _NOT,
|
||||
UNARYBNOT: (a) => ~a,
|
||||
UNARYMINUS: (a) => -a,
|
||||
UNARYPLUS: (a) => +a,
|
||||
BAND: (a,b)=>a&b, BOR: (a,b)=>a|b, BXOR: (a,b)=>a^b,
|
||||
"<<": (a,b)=>a<<b, ">>": (a,b)=>a>>>b,
|
||||
"!": _CONS, "~": _PUSH, "#": _CONCAT,
|
||||
TO: _TO, STEP: _STEP,
|
||||
|
||||
// i/o
|
||||
PRINT, EMIT, INPUT, CIN,
|
||||
|
||||
// numeric
|
||||
ABS, SGN, INT, FLOOR, CEIL, FIX, ROUND, SQR, CBR,
|
||||
SIN, COS, TAN, ASN, ACO, ATN, SINH, COSH, TANH,
|
||||
EXP, LOG, MIN, MAX, RND,
|
||||
|
||||
// strings
|
||||
SPC, LEFT, RIGHT, MID, CHR,
|
||||
|
||||
// lists
|
||||
LEN, HEAD, TAIL, INIT, LAST, MAP, FOLD, FILTER,
|
||||
ARRAY,
|
||||
|
||||
// graphics / system
|
||||
CLS, CLPX, PLOT, GOTOYX, TEXTFORE, TEXTBACK,
|
||||
POKE, PEEK, GETKEYSDOWN, CPUT, CGET, CSTA,
|
||||
|
||||
// type / option
|
||||
TYPEOF, OPTIONBASE, OPTIONDEBUG, OPTIONTRACE,
|
||||
|
||||
// monads / functional
|
||||
MRET, MLIST, MJOIN,
|
||||
">>=": _BIND, ">>~": _SEQ,
|
||||
".": _COMPOSE, "$": _APPLY, "&": _PIPE, "~<": _CURRY,
|
||||
"<*>": _SEQAPP, "<$>": MAP, "<~>": _SEQCURRYMAP,
|
||||
|
||||
// misc
|
||||
DO: function() { return arguments[arguments.length - 1] },
|
||||
CLEAR: function() { state.vars = _initialConsts() },
|
||||
END: function() { /* compiler emits pc=[Infinity,0] */ },
|
||||
LABEL: function() { /* harvested at compile time */ },
|
||||
DATA: function() { /* harvested at compile time */ },
|
||||
// DIM as an expression (e.g. `WS = DIM(H, V)`): allocate and return a
|
||||
// freshly zero-filled N-D array. The statement form `DIM A(H, V)` is
|
||||
// compiled inline and never reaches this entry.
|
||||
DIM: function() { return __dim(Array.prototype.slice.call(arguments)) },
|
||||
}
|
||||
@@ -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
|
||||
|
||||
721
assets/disk0/tvdos/moviedev/tav_inspector.js
Normal file
721
assets/disk0/tvdos/moviedev/tav_inspector.js
Normal 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()
|
||||
6
assets/disk0/tvdos/tuidev/Makefile
Normal file
6
assets/disk0/tvdos/tuidev/Makefile
Normal 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
|
||||
202
assets/disk0/tvdos/tuidev/font_rom_builder.c
Normal file
202
assets/disk0/tvdos/tuidev/font_rom_builder.c
Normal 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;
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} \\
|
||||
|
||||
BIN
doc/tsvmpal.png
BIN
doc/tsvmpal.png
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
81
ipf_encoder/Makefile
Normal file
81
ipf_encoder/Makefile
Normal 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
592
ipf_encoder/decoder_ipf.c
Normal 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
787
ipf_encoder/encoder_ipf.c
Normal 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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user