mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
Compare commits
355 Commits
new_movie_
...
6d70960e5c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
4
.gitignore
vendored
4
.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,5 @@ assets/disk0/home/basic/*
|
||||
assets/disk0/movtestimg/*.jpg
|
||||
assets/disk0/*.mov
|
||||
assets/diskMediabin/*
|
||||
|
||||
video_encoder/*
|
||||
|
||||
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>
|
||||
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 --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 --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>
|
||||
395
CLAUDE.md
Normal file
395
CLAUDE.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# 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`
|
||||
|
||||
## 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 17 runtimes to `~/Documents/openjdk/*` with specific naming:
|
||||
- `jdk-17.0.1-x86` (Linux AMD64)
|
||||
- `jdk-17.0.1-arm` (Linux Aarch64)
|
||||
- `jdk-17.0.1-windows` (Windows AMD64)
|
||||
- `jdk-17.0.1.jdk-arm` (macOS Apple Silicon)
|
||||
- `jdk-17.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
|
||||
802
TAUD_NOTE_EFFECTS.md
Normal file
802
TAUD_NOTE_EFFECTS.md
Normal file
@@ -0,0 +1,802 @@
|
||||
# Taud Tracker Effect Command Reference
|
||||
|
||||
Taud is a tracker-style music format derived from ScreamTracker 3's pattern command set, extended to 16-bit effect arguments and a 4096-tone equal-temperament pitch grid. This document defines every effect command a Taud engine must implement. Each command entry has three parts: a plain explanation for composers, compatibility notes for converting patterns from ScreamTracker 3 (ST3), ImpulseTracker (IT) or ProTracker (PT), and implementation details for engine writers.
|
||||
|
||||
---
|
||||
|
||||
## 1. Sound device
|
||||
|
||||
- **Bit depth:** 8-bit unsigned throughout, including the final mixdown.
|
||||
- **Sample rate:** fixed at 32000 Hz.
|
||||
- **Output channels:** strictly stereo; the mix bus always produces a two-channel frame even for mono-source samples.
|
||||
|
||||
Internal accumulators may widen to 16 or 32 bits during mixing and effect computation, but stored samples and final output are 8-bit.
|
||||
|
||||
## 2. Pitch system — 4096-TET
|
||||
|
||||
One octave spans **4096 pitch units** ($1000 exactly). A 12-TET semitone therefore equals **4096 ÷ 12 ≈ 341.333 units** (≈ $0155.55), which is not an integer; this irrationality is a deliberate consequence of choosing a microtonal native grid. Implementations store channel pitch as a signed integer in Taud units, and convert to playback rate using
|
||||
|
||||
```
|
||||
playback_rate = reference_rate × 2 ^ (pitch_units / 4096)
|
||||
```
|
||||
|
||||
Commonly used intervals in Taud units are listed below; all are rounded to the nearest integer.
|
||||
|
||||
| Interval | Units (exact) | Hex (rounded) |
|
||||
|---|---|---|
|
||||
| Octave | 4096 | $1000 |
|
||||
| Perfect fifth (7 ST) | 2389.33 | $0955 |
|
||||
| Tritone (6 ST) | 2048 | $0800 |
|
||||
| Major third (4 ST) | 1365.33 | $0555 |
|
||||
| Minor third (3 ST) | 1024 | $0400 |
|
||||
| 1 semitone | 341.33 | $0155 |
|
||||
| 1/8 semitone (1 finetune) | 42.67 | $002B |
|
||||
| 1/16 semitone | 21.33 | $0015 |
|
||||
| 1/64 semitone | 5.33 | $0005 |
|
||||
| 1 cent (1/100 semitone) | 3.41 | $0003 |
|
||||
|
||||
## 3. Volume system
|
||||
|
||||
Per-note and per-channel volume runs from **$00 (silent) to $3F (full)**, a 6-bit range narrower than ST3's 0..$40. Global volume (effect V) runs 0..$FF; this wider range lets the mix bus scale the summed channel output without disturbing individual note volumes. The per-frame mix chain per channel is
|
||||
|
||||
```
|
||||
mix = sample × note_vol × channel_vol × global_vol >> normalisation_shift
|
||||
```
|
||||
|
||||
with saturation applied before the 8-bit stereo output.
|
||||
|
||||
## 4. Rows, ticks, patterns, orders
|
||||
|
||||
A pattern is a rectangular grid of rows and channels; each cell holds one note event. Playback divides each row into `speed` ticks (effect A); tempo (effect T) sets the duration of one tick. At 125 BPM and speed 6, one row takes 120 ms and one tick 20 ms. Songs play patterns in an order sequence; effects B and C navigate this sequence.
|
||||
|
||||
## 5. Default parameters at song start
|
||||
|
||||
| Parameter | Value |
|
||||
|---|---|
|
||||
| Speed | $06 (6 ticks/row) |
|
||||
| Tempo byte | $65 (125 BPM; see effect T for the $18 offset) |
|
||||
| Global volume | $80 (mid-scale) |
|
||||
| Channel volume | $3F (full) |
|
||||
| Pan (all channels) | $80 (centre) |
|
||||
| Order index | $0000 |
|
||||
|
||||
## 6. Effect memory groups
|
||||
|
||||
Most effects recall their last non-zero argument when re-issued with $0000. Unlike ST3, which shares one memory slot across most effects, Taud groups memories into four cohorts plus private slots:
|
||||
|
||||
- **E and F share one slot** (pitch slide down and up). Issuing E $0000 recalls the last E-or-F argument and re-applies it as a down-slide; F $0000 does the same as an up-slide.
|
||||
- **G has its own slot** (tone portamento).
|
||||
- **H and U share one slot** (vibrato speed and depth are jointly recalled; the last-written values persist across both commands).
|
||||
- **R has its own slot** (tremolo).
|
||||
|
||||
Every other memory-carrying effect (D, I, J, K, L, O, Q, and others) has a private slot.
|
||||
|
||||
## 7. Opcode and argument format
|
||||
|
||||
Opcodes are single base-36 digits (0-9, then A-Z); arguments are 16-bit hexadecimal values prefixed with `$`. A cell is notated `OPCODE $HHLL` where HH is the high byte and LL is the low byte. Where an effect partitions its argument into sub-fields (for instance, H's speed and depth), the split is spelled out in the command description.
|
||||
|
||||
---
|
||||
|
||||
# The effects
|
||||
|
||||
## A $xx00 — Set tick speed to $xx
|
||||
|
||||
**Plain.** Sets how many ticks each row contains. Lower values make rows shorter and per-tick effects (slides, vibrato) develop faster; higher values stretch the row and give effects more iterations.
|
||||
|
||||
**Compatibility.** ST3 `Axx` maps one-to-one: Taud `A $xx00`. ST3 `A00` is a no-op; Taud `A $0000` is likewise ignored. ProTracker `Fxx` with `xx < $20` maps to Taud `A $xx00`; `Fxx` with `xx ≥ $20` maps to T instead (see T).
|
||||
|
||||
**Implementation.** If the high byte is non-zero, write it to `ticks_per_row`; the low byte is reserved and must be zero. The change takes effect from the row on which the A command appears. There is no memory for A.
|
||||
|
||||
---
|
||||
|
||||
## B $xxyy — Jump to order $xxyy
|
||||
|
||||
**Plain.** Finishes the current row, then continues playback at row 0 of the pattern at order position $xxyy. Use this to create song-level jumps, loops, or branching structures.
|
||||
|
||||
**Compatibility.** ST3 `Bxx` jumps to an 8-bit order and maps to Taud `B $00xx`. The extended 16-bit range means Taud songs may have up to $10000 order entries.
|
||||
|
||||
**Implementation.** On the last tick of the current row, set the next order index to the argument and the next row to 0. If the argument exceeds the song length, wrap to the song's defined restart position (order $0000 by default). Jumps are detected by a visited `(order, row)` set so that pathological loops do not prevent song-length computation, though they do not interrupt actual playback. There is no memory for B.
|
||||
|
||||
**Simultaneous B and C on the same row.** If a B command appears in the same row as a C command (on any channel), both fire: B chooses the order, C chooses the row within that order. If the two commands appear on different channels, channel priority is **ascending channel index** — the lowest-numbered channel carrying either effect wins its parameter. If both appear on the same channel row (only possible if one is a volume-column equivalent), the effect column takes precedence.
|
||||
|
||||
---
|
||||
|
||||
## C $xxyy — Break pattern to row $xxyy
|
||||
|
||||
**Plain.** Finishes the current row, then skips ahead to row $xxyy of the **next** pattern in the order sequence.
|
||||
|
||||
**Compatibility.** ST3 stores `Cxx` as **BCD** (so on-disk `$10` means decimal row 10); Taud stores the argument as plain binary. When converting from ST3, decode with `row = (byte >> 4) × 10 + (byte & $0F)`. Valid ST3 source bytes are those representing decimal 0..63; out-of-range BCD bytes should clamp to row 0 on import. When exporting back to ST3, encode with `byte = ((row / 10) << 4) | (row % 10)`, clamped at row 63.
|
||||
|
||||
**Implementation.** On the last tick of the current row, advance the order index by 1 (or honour a co-occurring B), then set the next row to the argument. If the argument exceeds the destination pattern's row count, start the destination pattern at row 0. There is no memory for C.
|
||||
|
||||
---
|
||||
|
||||
## D — Volume slide (multiple forms)
|
||||
|
||||
D's 16-bit argument encodes four mutually exclusive modes using the top nibble and the following byte. All forms operate on the channel's current volume and clip to $00..$3F after each step.
|
||||
|
||||
### D $0y00 — Volume slide down by $y per non-first tick
|
||||
|
||||
**Plain.** Each tick after tick 0, volume decreases by $y. A D $0400 at speed 8 reduces volume by $1C over the row.
|
||||
|
||||
**Compatibility.** ST3 `Dx0` (volume slide down) maps to Taud `D $0x00`. The ST3 volume cap was $40; Taud's is $3F — a very high-volume sample reaching $40 in ST3 will snap to $3F in Taud.
|
||||
|
||||
**Implementation.** On ticks > 0, subtract the low nibble of the high byte from `channel_volume`; clamp at $00. Memory is private to D and is keyed on the full original byte (so D $0000 recalls whatever form last ran).
|
||||
|
||||
### D $x000 — Volume slide up by $x per non-first tick
|
||||
|
||||
**Plain.** Each tick after tick 0, volume increases by $x. Capped at $3F.
|
||||
|
||||
**Compatibility.** ST3 `D0y` (volume slide up) maps to Taud `D $y000`.
|
||||
|
||||
**Implementation.** On ticks > 0, add the high nibble of the high byte to `channel_volume`; clamp at $3F.
|
||||
|
||||
### D $Fy00 — Fine volume slide down by $y on tick 0
|
||||
|
||||
**Plain.** Applies a one-shot volume reduction of $y on tick 0 only. Independent of speed. A D $FF00 behaves as a fine slide up by $F (so a request for "down by F" is reinterpreted; see below).
|
||||
|
||||
**Compatibility.** ST3 `DFy` maps directly. The $FF edge case is preserved: ST3 treats `DFF` as fine slide up by $F rather than fine slide down by $F, and Taud follows suit.
|
||||
|
||||
**Implementation.** On tick 0 only, subtract the low nibble of the high byte from `channel_volume`. If the low nibble is $0, treat as fine-slide-up by $F. If the high byte is $FF, treat as fine-slide-up by $F.
|
||||
|
||||
### D $xF00 — Fine volume slide up by $x on tick 0
|
||||
|
||||
**Plain.** One-shot volume increase of $x on tick 0 only.
|
||||
|
||||
**Compatibility.** ST3 `DxF` maps directly. Volume cap is $3F, lower than ST3's $40.
|
||||
|
||||
**Implementation.** On tick 0 only, add the high nibble to `channel_volume`; clamp at $3F.
|
||||
|
||||
---
|
||||
|
||||
## E $xxxx — Pitch slide down by $xxxx (linear)
|
||||
|
||||
**Plain.** Lowers the channel's pitch by the argument per tick. Taud's pitch slides are **linear in the 4096-TET grid** — the slide value is subtracted directly from the stored pitch, without any period-table indirection. A coarse slide uses the full value range; a fine slide applies only once per row; an extra-fine slide is not provided (the 16-bit argument already gives microtonal precision below 1/64 semitone).
|
||||
|
||||
Coarse and fine modes are distinguished by the high nibble of the argument:
|
||||
|
||||
- `E $0001..$EFFF` — coarse slide: subtracts the full argument from pitch each tick after tick 0. A slide of $0155 drops pitch by one semitone per tick.
|
||||
- `E $F000..$FFFF` — fine slide: on tick 0 only, subtracts `arg & $0FFF` from pitch.
|
||||
- `E $0000` — recalls the last E-or-F argument and applies it as a down-slide, preserving the original form (coarse or fine).
|
||||
|
||||
**Compatibility.** This is **the single intentionally ST3-incompatible command in Taud**. ST3 pitch slides operate on Amiga periods or linear slide units; Taud operates directly on 4096-TET pitch units. Conversion from ST3 linear-mode slides uses 1 ST3 slide unit ≈ $0005 Taud units (1/64 semitone):
|
||||
|
||||
- ST3 `Exx` coarse (where `xx < $E0`) → Taud `E $00xx × $0015` (one ST3 coarse unit = 1/16 semitone ≈ $0015 Taud units).
|
||||
- ST3 `EFx` fine → Taud `E $F0xx × $0015` with appropriate range packing.
|
||||
- ST3 `EEx` extra-fine → Taud `E $F0xx × $0005` (one ST3 extra-fine unit = 1/64 semitone ≈ $0005 Taud units).
|
||||
|
||||
ST3 Amiga-mode slides do not have a clean conversion and should be treated as linear-mode equivalents during import.
|
||||
|
||||
Because E and F share memory in Taud (narrower than ST3's broad shared memory), an ST3 song that used `E00` or `F00` to recall a D, G, or Q argument will break on import; the converter must eagerly resolve ST3 recalls into explicit Taud arguments rather than relying on memory.
|
||||
|
||||
**Implementation.** Per-tick processing:
|
||||
|
||||
```
|
||||
on row start:
|
||||
raw = arg
|
||||
if raw == 0: raw = memory_EF
|
||||
else: memory_EF = raw
|
||||
if (raw & $F000) == $F000: # fine
|
||||
pitch -= (raw & $0FFF)
|
||||
mode_this_row = FINE
|
||||
else: # coarse
|
||||
slide_amount_this_row = raw
|
||||
mode_this_row = COARSE
|
||||
|
||||
on tick > 0:
|
||||
if mode_this_row == COARSE:
|
||||
pitch -= slide_amount_this_row
|
||||
```
|
||||
|
||||
Glissando control (S $1x) snaps the output pitch to the nearest semitone after every slide application; see S $1x.
|
||||
|
||||
---
|
||||
|
||||
## F $xxxx — Pitch slide up by $xxxx (linear)
|
||||
|
||||
**Plain.** Raises the channel's pitch by the argument per tick, with the same mode-selection scheme as E. Coarse, fine, and memory behaviour are identical in form but inverted in direction.
|
||||
|
||||
**Compatibility.** Same as E. ST3 `Fxx` coarse, `FFx` fine, and `FEx` extra-fine convert with the same scaling factors ($0015 and $0005). F and E share one memory slot in Taud.
|
||||
|
||||
**Implementation.** As for E, but add instead of subtract. No upper pitch cap is defined by the effect itself, but the sample-rate conversion at the mixer will saturate well before arithmetic overflow at reasonable playing ranges.
|
||||
|
||||
---
|
||||
|
||||
## G $xxxx — Tone portamento with speed $xxxx
|
||||
|
||||
**Plain.** Slides the channel's current pitch toward the note specified in the same row, at $xxxx Taud units per tick (after tick 0), stopping when the target is reached. A row with G and a note does **not** re-trigger the sample — the note's pitch becomes the portamento target and the already-sounding sample continues at its current pitch.
|
||||
|
||||
**Compatibility.** ST3 `Gxx` uses an 8-bit value in period-table units; convert to Taud using the same $0015-per-unit scale as E/F coarse (1/16 semitone per ST3 slide unit). ST3 linear mode is the expected import source; Amiga-mode G sources should be treated as linear. G has its **own** memory slot in both ST3 and Taud, so conversion is straightforward and does not suffer the shared-memory problem of E/F.
|
||||
|
||||
**Implementation.**
|
||||
|
||||
```
|
||||
on row parse:
|
||||
if row has note and G effect:
|
||||
target_pitch = period_for(note)
|
||||
# do NOT re-trigger sample
|
||||
if arg != 0:
|
||||
memory_G = arg
|
||||
speed_this_row = memory_G
|
||||
|
||||
on tick > 0:
|
||||
if target_pitch set:
|
||||
delta = sign(target_pitch - pitch) × speed_this_row
|
||||
pitch += delta
|
||||
if sign crossed target: pitch = target_pitch; target_pitch = None
|
||||
```
|
||||
|
||||
Glissando (S $1x) snaps the output frequency to the nearest semitone ($0155 step approximation) after each advance without changing the internal pitch counter; it affects only what the mixer sees.
|
||||
|
||||
---
|
||||
|
||||
## H $xxyy — Vibrato with speed $xx and depth $yy
|
||||
|
||||
**Plain.** Modulates pitch with a low-frequency oscillator (LFO). `$xx` is the LFO speed (high byte), `$yy` is the depth (low byte). On H rows the LFO accumulator advances at `$xx × 4` per tick through a 256-entry lookup of the selected waveform (see S $3x). The current pitch offset is added to the channel's base pitch for the duration of each tick.
|
||||
|
||||
**Compatibility.** ST3 `Hxy` uses 4-bit nibbles for speed and depth; convert by nibble-repeating each into Taud's bytes: ST3 `H27` → Taud `H $2277`. This preserves the effective LFO rate and peak depth. H and U share memory in Taud (they did in ST3 too).
|
||||
|
||||
Unlike ProTracker, ST3 vibrato fires on tick 0 as well; Taud follows ST3.
|
||||
|
||||
**Implementation.** The reference sine table is OpenMPT's 64-entry 8-bit table, indexed `pos >> 2` through a 256-entry logical LFO (equivalently, a 256-sample 4×-oversampled sine peaking at ±$7F):
|
||||
|
||||
```
|
||||
ModSinusTable[64] =
|
||||
00 0C 19 25 31 3C 47 51 5A 62 6A 70 75 7A 7D 7E
|
||||
7F 7E 7D 7A 75 70 6A 62 5A 51 47 3C 31 25 19 0C
|
||||
00 F4 E7 DB CF C4 B9 AF A6 9E 96 90 8B 86 83 82
|
||||
81 82 83 86 8B 90 96 9E A6 AF B9 C4 CF DB E7 F4
|
||||
```
|
||||
|
||||
Per row/tick:
|
||||
|
||||
```
|
||||
on row parse (H):
|
||||
if (arg >> 8) != 0: memory_HU.speed = arg >> 8
|
||||
if (arg & $FF) != 0: memory_HU.depth = arg & $FF
|
||||
|
||||
on every tick (including tick 0):
|
||||
sine = ModSinusTable[(lfo_pos >> 2) & $3F] # signed -$80..+$7F
|
||||
pitch_delta = (sine × memory_HU.depth) >> 6
|
||||
applied_pitch = base_pitch + pitch_delta
|
||||
lfo_pos = (lfo_pos + memory_HU.speed × 4) & $FF
|
||||
```
|
||||
|
||||
At maximum speed and depth ($FFFF), peak `pitch_delta` is `$7F × $FF >> 6 ≈ $1FA` — about 1.5 semitones. On a fresh note, if the current LFO waveform retrigger bit is clear (S $3x with $x < $4), `lfo_pos` resets to 0. When the waveform is "random", a fresh random value is drawn every tick rather than read from the table.
|
||||
|
||||
---
|
||||
|
||||
## U $xxyy — Fine vibrato with speed $xx and depth $yy
|
||||
|
||||
**Plain.** Same LFO as H but four times finer in pitch — useful for subtle microtonal warbles.
|
||||
|
||||
**Compatibility.** ST3 `Uxy` uses nibbles; nibble-repeat each to convert. U shares memory with H.
|
||||
|
||||
**Implementation.** Identical to H except the shift is 8 instead of 6:
|
||||
|
||||
```
|
||||
pitch_delta = (sine × memory_HU.depth) >> 8
|
||||
```
|
||||
|
||||
Peak at maximum settings: $7F × $FF >> 8 ≈ $7E, about 0.4 semitone — exactly a quarter of H's peak.
|
||||
|
||||
---
|
||||
|
||||
## I $xxyy — Tremor with on-time $xx and off-time $yy
|
||||
|
||||
**Plain.** Rapidly gates the channel on and off. Volume plays normally for `$xx + 1` ticks, then mutes for `$yy + 1` ticks, repeating. Counters persist across rows and only reset on a fresh I row with a new argument.
|
||||
|
||||
**Compatibility.** ST3 `Ixy` uses nibbles (`$xy`) with the same semantics; convert by nibble-repeating each into Taud bytes: ST3 `I47` → Taud `I $4477`. The `+1` behaviour on both counters comes from ProTracker and is preserved throughout. Memory is private.
|
||||
|
||||
**Implementation.**
|
||||
|
||||
```
|
||||
on row parse (I):
|
||||
if arg != 0: memory_I = arg
|
||||
on_time = ((memory_I >> 8) & $FF) + 1
|
||||
off_time = ( memory_I & $FF) + 1
|
||||
|
||||
on every tick:
|
||||
if phase == ON:
|
||||
play at full channel volume
|
||||
tick_in_phase += 1
|
||||
if tick_in_phase >= on_time: phase = OFF; tick_in_phase = 0
|
||||
else:
|
||||
force output volume to 0 (base volume preserved for later effects)
|
||||
tick_in_phase += 1
|
||||
if tick_in_phase >= off_time: phase = ON; tick_in_phase = 0
|
||||
```
|
||||
|
||||
A zero `$xx` or `$yy` input becomes 1 tick after the `+1`, never zero.
|
||||
|
||||
---
|
||||
|
||||
## J $xxyy — Microtonal arpeggio with offsets $xx00 and $yy00
|
||||
|
||||
**Plain.** Cycles the playing pitch through three values across consecutive ticks: the note, the note plus `$xx00` Taud units, and the note plus `$yy00` Taud units, repeating. At the default 50 Hz tick rate (speed 6, 125 BPM), this produces a classic chord-arpeggio effect; because Taud's grid is 4096-TET, the intervals can be microtonal.
|
||||
|
||||
The encoding places each 8-bit offset byte into the **high byte** of a 16-bit pitch delta, giving 256 discrete intervals per arp voice with a resolution of $0100 ≈ 0.75 semitone per step. This is coarser than E/F's 16-bit slides, but adequate for arpeggios and well-suited to non-12-TET intervals.
|
||||
|
||||
**Compatibility.** ST3 `Jxy` uses nibbles as 12-TET semitones; Taud uses bytes as $0100-scaled 4096-TET offsets. The conversion is therefore lossy — 12-TET intervals that are not multiples of 3 semitones incur ±25 cent rounding error. The table below gives the best Taud byte for each 12-TET semitone offset:
|
||||
|
||||
| Semitones | Taud byte | Taud units | Error (cents) |
|
||||
|---|---|---|---|
|
||||
| 0 | $00 | 0 | 0 |
|
||||
| 1 | $01 | 256 | −25 |
|
||||
| 2 | $03 | 768 | +25 |
|
||||
| 3 | $04 | 1024 | 0 |
|
||||
| 4 | $05 | 1280 | −25 |
|
||||
| 5 | $07 | 1792 | +25 |
|
||||
| 6 | $08 | 2048 | 0 |
|
||||
| 7 | $09 | 2304 | −25 |
|
||||
| 8 | $0B | 2816 | +25 |
|
||||
| 9 | $0C | 3072 | 0 |
|
||||
| 10 | $0D | 3328 | −25 |
|
||||
| 11 | $0F | 3840 | +25 |
|
||||
| 12 | $10 | 4096 | 0 |
|
||||
|
||||
For example, ST3 `J37` (minor chord) imports as Taud `J $0409`; ST3 `J47` (major chord) as Taud `J $0509`. Memory is private and stores the full 16-bit argument.
|
||||
|
||||
**Implementation.**
|
||||
|
||||
```
|
||||
on row parse (J):
|
||||
if arg != 0: memory_J = arg
|
||||
off1 = (memory_J >> 8) & $FF # high byte
|
||||
off2 = memory_J & $FF # low byte
|
||||
|
||||
on every tick:
|
||||
selector = tick_within_row mod 3
|
||||
if selector == 0: voice_pitch = base_pitch
|
||||
elif selector == 1: voice_pitch = base_pitch + (off1 << 8)
|
||||
elif selector == 2: voice_pitch = base_pitch + (off2 << 8)
|
||||
```
|
||||
|
||||
The `tick_within_row mod 3` counter resets every row start (so every row begins at `base_pitch`). A subsequent E/F slide after a J row resumes from the last arpeggiated voice's pitch, not from `base_pitch` — this mirrors ST3's `kST3PortaAfterArpeggio` quirk and is deliberately preserved.
|
||||
|
||||
---
|
||||
|
||||
## K $xy00 — Dual: vibrato continuation and volume slide $xy
|
||||
|
||||
**Plain.** **Unimplemented**. On ST3, continues a previously started vibrato (H or U) without retriggering it, while applying a volume slide of `$xy` per non-first tick. Fine volume slides are not available in this form.
|
||||
|
||||
**Compatibility.** ST3 `Kxy` maps directly. Implementations must convert K to an explicit pair of commands: `H $0000` (continue with stored speed/depth) combined with volume-column command `1.$xy` (volume slide), and emit both.
|
||||
|
||||
**Implementation.** Execute the per-tick vibrato update as if an H command were active with argument $0000 (recall), then execute a D $0y00 or $x000 slide using the bytes of the K argument: high nibble as up-slide, low nibble as down-slide. If both nibbles are non-zero, down-slide takes precedence (matching ST3). K has no memory of its own; it uses H/U's stored speed and depth.
|
||||
|
||||
---
|
||||
|
||||
## L $xy00 — Dual: tone portamento continuation and volume slide $xy
|
||||
|
||||
**Plain.** **Unimplemented**. On ST3, continues a previously started tone portamento (G) without retriggering, while applying a volume slide of `$xy` per non-first tick. Fine volume slides are not available here.
|
||||
|
||||
**Compatibility.** ST3 `Lxy` maps directly. Like K, L must be equivalently implemented as `G $0000` plus a volume-column slide.
|
||||
|
||||
**Implementation.** Execute the per-tick G update (recalling G's stored speed), then the D slide as in K. L has no memory of its own.
|
||||
|
||||
---
|
||||
|
||||
## O $xxyy — Set sample offset to $xxyy
|
||||
|
||||
**Plain.** On the row where it appears, jumps the sample playhead to byte $xxyy of the sample data. If the sample is looped and the requested offset exceeds the loop end, the offset wraps around through the loop as if playback had reached that point naturally.
|
||||
|
||||
**Compatibility.** ST3 `Oxx` is 8-bit, addressing offset `xx × $100`. On import, copy the ST3 byte into Taud's high byte and zero the low byte: Taud `O $xx00`. ProTracker `9xx` maps identically. The Taud 16-bit form allows byte-precise seeking within samples larger than $100 bytes. Memory is private.
|
||||
|
||||
**Implementation.** On the row start, set the sample playhead to `arg` (in bytes, relative to the sample's start). Apply the loop-wrap calculation if the sample has loop points and `arg > loop_end`: `arg = loop_start + ((arg - loop_start) mod loop_length)`. The O command does not retrigger the sample; it only relocates the playhead for an already-triggered note.
|
||||
|
||||
---
|
||||
|
||||
## Q $xy00 — Retrigger note every $y ticks with volume modifier $x
|
||||
|
||||
**Plain.** Retriggers the currently playing sample at an interval of `$y` ticks, optionally modifying its volume on each retrigger according to `$x`. The retrigger interval runs across rows until a new Q with a different `$y` or no Q at all.
|
||||
|
||||
**Compatibility.** ST3 `Qxy` maps directly. The **`$y == 0` behaviour is preserved from ST3**: the entire effect is ignored (no retrigger, and memory is not updated). Memory is private.
|
||||
|
||||
ProTracker `E9x` is equivalent to Taud `Q $0x00` (retrigger only, no volume change).
|
||||
|
||||
**Implementation.** A per-channel tick counter advances every tick, including tick 0. When it reaches `$y`, the sample retriggers (keeping current pitch), the counter resets to 0, and the volume modifier `$x` applies. The counter resets only when a row has **no** Q command; successive Q rows share and advance the counter.
|
||||
|
||||
The volume modifier table, **computed with arithmetic (no LUT)**, is:
|
||||
|
||||
| $x | Action | $x | Action |
|
||||
|---|---|---|---|
|
||||
| 0 | no change | 8 | no change |
|
||||
| 1 | vol − $01 | 9 | vol + $01 |
|
||||
| 2 | vol − $02 | A | vol + $02 |
|
||||
| 3 | vol − $04 | B | vol + $04 |
|
||||
| 4 | vol − $08 | C | vol + $08 |
|
||||
| 5 | vol − $10 | D | vol + $10 |
|
||||
| 6 | vol × 2 / 3 | E | vol × 3 / 2 |
|
||||
| 7 | vol × 1 / 2 | F | vol × 2 |
|
||||
|
||||
Multiplicative cases use integer arithmetic: `vol × 2 / 3` is `(vol × 2) / 3` (truncated); `vol × 3 / 2` is `(vol × 3) / 2`; `vol × 1 / 2` is `vol >> 1`; `vol × 2` is `vol << 1`. All results clip to $00..$3F after.
|
||||
|
||||
A note previously silenced by a cut (`^^^` or `SCx` earlier in the row) is not retriggered, matching ST3's `kST3RetrigAfterNoteCut` rule.
|
||||
|
||||
---
|
||||
|
||||
## R $xxyy — Tremolo with speed $xx and depth $yy
|
||||
|
||||
**Plain.** Modulates volume with an LFO, symmetrically with H's pitch modulation. `$xx` is LFO speed, `$yy` depth; the waveform is selected by S $4x.
|
||||
|
||||
**Compatibility.** ST3 `Rxy` uses nibbles; convert by nibble-repeat. ST3's volume cap is $40; Taud's is $3F — very deep tremolo that would have briefly clipped at $40 in ST3 may clip slightly earlier in Taud. R has its own memory slot (not shared with H/U).
|
||||
|
||||
**Implementation.** Identical machinery to H with a larger shift to fit the narrower volume range:
|
||||
|
||||
```
|
||||
on row parse (R):
|
||||
if (arg >> 8) != 0: memory_R.speed = arg >> 8
|
||||
if (arg & $FF) != 0: memory_R.depth = arg & $FF
|
||||
|
||||
on every tick (including tick 0):
|
||||
sine = ModSinusTable[(lfo_pos >> 2) & $3F]
|
||||
vol_delta = (sine × memory_R.depth) >> 9
|
||||
applied_vol = clamp(base_vol + vol_delta, 0, $3F)
|
||||
lfo_pos = (lfo_pos + memory_R.speed × 4) & $FF
|
||||
```
|
||||
|
||||
Peak at maximum settings: $7F × $FF >> 9 = $3F — the full volume range. Retrigger behaviour tracks the S $4x waveform nibble bit 2: cleared means retrigger on new note, set means preserve LFO position.
|
||||
|
||||
---
|
||||
|
||||
## T $xxyy — Tempo set or tempo slide
|
||||
|
||||
Taud splits T by which byte carries the value:
|
||||
|
||||
### T $xx00 (high byte non-zero) — Set tempo
|
||||
|
||||
**Plain.** Sets the Taud tempo byte to `$xx`. The resulting BPM is `$xx + $18`: Taud byte $00 → 24 BPM, $65 → 125 BPM (default), $FF → 279 BPM.
|
||||
|
||||
**Compatibility.** ST3 `Txx` (where `xx ∈ $20..$FF`) stores BPM directly; convert with `taud_byte = xx − $18`. Taud byte $08 corresponds to ST3's minimum BPM of 32; Taud bytes below $08 are inexpressible in ST3 and should round up to $08 (BPM 32) when exporting. OpenMPT's extended tempo slides (`T $0x` down, `T $1x` up) in S3M files map to Taud T $00xx — see below.
|
||||
|
||||
ProTracker `Fxx` with `xx ≥ $20` maps to Taud `T $(xx − $18)00`; `Fxx` with `xx < $20` maps to A (speed) instead.
|
||||
|
||||
**Implementation.** If the high byte is non-zero, set `tempo_byte = arg >> 8`; derive `BPM = tempo_byte + $18`; compute tick duration as `samples_per_tick = 32000 × 5 / (BPM × 2) = 80000 / BPM` (integer truncated) at the fixed 32000 Hz output rate. Example: BPM 125 → 640 samples per tick; BPM 24 → 3333 samples per tick; BPM 279 → 286 samples per tick. There is no memory for set-tempo.
|
||||
|
||||
### T $00xy (high byte zero) — Tempo slide
|
||||
|
||||
**Plain.** Adjusts the tempo continuously during the row. `$00_0y` (low nibble under a zero high nibble within the low byte) slides BPM down by `$y` per non-first tick; `$00_1y` slides up. Out-of-range encodings ($00_20 through $00_FF) are reserved and behave as no-ops.
|
||||
|
||||
**Compatibility.** ST3 itself has only the set form; the slide forms originate in the OpenMPT/Schism extension of S3M. On export to strict ST3, slide forms are unrepresentable and should be approximated as an equivalent set-tempo on a later row.
|
||||
|
||||
**Implementation.**
|
||||
|
||||
```
|
||||
on row parse (T with high byte == 0):
|
||||
low = arg & $FF
|
||||
if (low & $F0) == $00:
|
||||
slide_dir = DOWN
|
||||
slide_amount = low & $0F
|
||||
elif (low & $F0) == $10:
|
||||
slide_dir = UP
|
||||
slide_amount = low & $0F
|
||||
else:
|
||||
ignore row
|
||||
|
||||
on tick > 0 (if slide armed):
|
||||
if slide_dir == DOWN: tempo_byte = max($00, tempo_byte - slide_amount)
|
||||
else: tempo_byte = min($FF, tempo_byte + slide_amount)
|
||||
recompute samples_per_tick for next tick
|
||||
```
|
||||
|
||||
A tempo slide's memory slot is separate from the set-tempo path and is private to T-slide.
|
||||
|
||||
---
|
||||
|
||||
## V $xx00 — Set global volume to $xx
|
||||
|
||||
**Plain.** Sets the global mix bus volume (0..$FF). $00 is silence; $FF is full. The default is $80.
|
||||
|
||||
**Compatibility.** ST3's global volume is 0..$40; convert with `taud_v = st3_v × 4`, clamped at $FF. On export, `st3_v = taud_v >> 2`, clamped at $40.
|
||||
|
||||
**Implementation.** Write the high byte to `global_volume` on the row the command appears. The low byte is reserved. ST3's `kST3NoMutedChannels` rule applies: V on a muted channel is ignored by ST3; for strict-compatible playback Taud follows suit, but new Taud compositions should avoid muting channels that carry global effects.
|
||||
|
||||
---
|
||||
|
||||
## Y $xxyy — Panbrello (panning vibrato) with speed $xx and depth $yy
|
||||
|
||||
**Plain.** Modulates panning with an LFO, symmetrically with H's pitch modulation. `$xx` is LFO speed, `$yy` depth; the waveform is selected by S $5x.
|
||||
|
||||
**Compatibility.** IT `Yxy` uses nibbles; convert by nibble-repeat. IT's panning cap is $40; Taud's is $3F — very deep vibrato that would have briefly clipped at $40 in IT may clip slightly earlier in Taud. Y has its own memory slot.
|
||||
|
||||
**Implementation.** Identical machinery to H with a larger shift to fit the narrower volume range:
|
||||
|
||||
```
|
||||
on row parse (Y):
|
||||
if (arg >> 8) != 0: memory_Y.speed = arg >> 8
|
||||
if (arg & $FF) != 0: memory_Y.depth = arg & $FF
|
||||
|
||||
on every tick (including tick 0):
|
||||
sine = ModSinusTable[(lfo_pos >> 2) & $3F]
|
||||
vol_delta = (sine × memory_Y.depth) >> 9
|
||||
applied_vol = clamp(base_vol + vol_delta, 0, $3F)
|
||||
lfo_pos = (lfo_pos + memory_Y.speed × 4) & $FF
|
||||
```
|
||||
|
||||
Peak at maximum settings: $7F × $FF >> 9 = $3F — the full panning range. Retrigger behaviour tracks the S $5x waveform nibble bit 2: cleared means retrigger on new note, set means preserve LFO position.
|
||||
|
||||
---
|
||||
|
||||
# The S subcommand family
|
||||
|
||||
S is a multiplexing opcode; the **high nibble of the high byte** selects the sub-effect, and the remainder is the sub-argument.
|
||||
|
||||
## S $1x00 — Glissando control
|
||||
|
||||
**Plain.** `$1000` turns glissando off; `$1100` turns it on. When on, tone portamento (G) output is quantised to the nearest semitone ($0155 approximation) before being sent to the mixer. The internal G pitch counter still advances smoothly; only the audible pitch steps. **This command is implemented sorely for ST3 compatibility.**
|
||||
|
||||
**Compatibility.** ST3 `S10`/`S11` maps directly. In Taud, "nearest semitone" uses the best integer approximation: round `pitch / $155` to the nearest integer, multiply by $155; equivalently, `snapped = (pitch + $AB) / $155 × $155`. Because $155 is an approximation of 4096/12, accumulated rounding across many octaves will drift by up to a few cents; this is documented behaviour and intentional given the microtonal grid.
|
||||
|
||||
**Implementation.** Maintain a per-channel boolean `glissando_on`. When G updates `pitch`, if `glissando_on` is set, compute `display_pitch = round(pitch × 12 / 4096) × 4096 / 12` (using integer division with rounding) and send `display_pitch` to the mixer; otherwise send `pitch` directly.
|
||||
|
||||
---
|
||||
|
||||
## S $2x00 — Set fine-tune
|
||||
|
||||
**Plain.** Overrides the current note's fine-tune by applying a fixed 4096-TET offset. The index `$x` selects one of sixteen predefined pitch offsets, following ScreamTracker 3's Hz-based fine-tune table but expressed directly in Taud units. This command is implemented for ST3 compatibility.
|
||||
|
||||
**Compatibility.** The index scheme matches ST3 exactly: `$8` is the baseline (no change), `$0..$7` are progressively flatter, `$9..$F` are progressively sharper. The Hz reference values come from the ST3 User's Manual and are reproduced here for auditability; the Taud offset is `log2(Hz / 8363) × 4096`, rounded to the nearest integer. **Format converters are advised to apply offset to the note value directly.**
|
||||
|
||||
| $x | Reference Hz | Taud offset |
|
||||
|---|---|---|
|
||||
| $0 | 7895 | −$0154 |
|
||||
| $1 | 7941 | −$0132 |
|
||||
| $2 | 7985 | −$0111 |
|
||||
| $3 | 8046 | −$00E4 |
|
||||
| $4 | 8107 | −$00B8 |
|
||||
| $5 | 8169 | −$008B |
|
||||
| $6 | 8232 | −$005D |
|
||||
| $7 | 8280 | −$003B |
|
||||
| $8 | 8363 | $0000 |
|
||||
| $9 | 8413 | +$0023 |
|
||||
| $A | 8463 | +$0046 |
|
||||
| $B | 8529 | +$0074 |
|
||||
| $C | 8581 | +$0098 |
|
||||
| $D | 8651 | +$00C8 |
|
||||
| $E | 8723 | +$00F9 |
|
||||
| $F | 8757 | +$0110 |
|
||||
|
||||
ProTracker `E5x` maps to Taud `S $2x00` with the same index meaning.
|
||||
|
||||
**Implementation.** On the row, look up the offset from the table and add it to the channel's base pitch before any other per-tick effect processes. The offset persists until another S $2x command or a note-reset event.
|
||||
|
||||
---
|
||||
|
||||
## S $3x00 — Vibrato LFO waveform
|
||||
|
||||
**Plain.** Selects the shape of the vibrato (H and U) oscillator.
|
||||
|
||||
| $x | Waveform | Retrigger on new note? |
|
||||
|---|---|---|
|
||||
| $0 | Sine | Yes |
|
||||
| $1 | Ramp down (sawtooth) | Yes |
|
||||
| $2 | Square | Yes |
|
||||
| $3 | Random | Yes |
|
||||
| $4 | Sine | No |
|
||||
| $5 | Ramp down | No |
|
||||
| $6 | Square | No |
|
||||
| $7 | Random | No |
|
||||
|
||||
**Compatibility.** ST3 `S3x` maps directly.
|
||||
|
||||
**Implementation.** Store `vibrato_waveform = $x & $3` and `vibrato_retrigger = (($x & $4) == 0)` for the channel. The ramp-down shape is `$7F − ((pos & $3F) << 2)` across one logical cycle; the square shape is `sign(sine(pos)) × $7F`; random draws a fresh `rand() & $FF − $80` every tick. On a new note, if `vibrato_retrigger` is true, reset `lfo_pos = 0`.
|
||||
|
||||
---
|
||||
|
||||
## S $4x00 — Tremolo LFO waveform
|
||||
|
||||
**Plain.** Selects the shape of the tremolo (R) oscillator; value encoding is identical to S $3x.
|
||||
|
||||
**Compatibility.** ST3 `S4x` maps directly. ProTracker `E7x` maps to Taud `S $4x00`.
|
||||
|
||||
**Implementation.** As for S $3x, but applied to R's separate state (`tremolo_waveform`, `tremolo_retrigger`, and tremolo `lfo_pos`).
|
||||
|
||||
---
|
||||
|
||||
## S $5x00 — Panbrello LFO waveform
|
||||
|
||||
**Plain.** Selects the shape of the panbrello (Y) oscillator; value encoding is identical to S $3x.
|
||||
|
||||
**Compatibility.** IT `S5x` maps directly.
|
||||
|
||||
**Implementation.** As for S $3x, but applied to Y's separate state (`panbrello_waveform`, `panbrello_retrigger`, and panbrello `lfo_pos`).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## S $80xx — Set channel pan position
|
||||
|
||||
**Plain.** Sets the channel pan to `$xx`, with $00 being full left and $FF being full right. $80 is centre.
|
||||
|
||||
**Compatibility.** ST3 `S8x` uses a 4-bit value; convert by nibble-repeat: ST3 `S83` → Taud `S $8033`. Panning column command `0.$xx` has the same semantics and is the preferred form when a pan column is available in the pattern. ProTracker `8xx` (fine pan) and `E8x` (coarse pan) both map into Taud's 8-bit pan — the ProTracker 8-bit form maps directly; the 4-bit form nibble-repeats.
|
||||
|
||||
**Implementation.** Write `channel_pan = arg & $FF`. The pan value is applied at the mixer: `left_gain = (($FF − pan) × $100) >> 8`, `right_gain = (pan × $100) >> 8`, with both applied before the global volume stage.
|
||||
|
||||
---
|
||||
|
||||
## S $Bx00 — Pattern loop
|
||||
|
||||
**Plain.** Sets a loop point and loops within a pattern. `S $B000` marks the current row as the loop start (per channel, not per song); `S $Bx00` with $x > 0 returns playback to the saved row and plays the intervening range `$x` more times (so `$B200` plays the loop twice total beyond the initial pass).
|
||||
|
||||
**Compatibility.** ST3 `SBx` maps directly. ProTracker `E6x` maps to Taud `S $Bx00`.
|
||||
|
||||
ST3 has a long-documented bug where pattern delay (SEx) inside a pattern-loop range causes the loop counter to decrement multiple times per visit, producing unintended behaviour. **Taud fixes this bug.** On import, ST3 songs that relied on the bug will loop fewer times in Taud. Converters that want bit-exact ST3 playback should emit a warning when SBx and SEx appear in the same channel within a loop range, or optionally flatten loops by duplicating rows.
|
||||
|
||||
**Implementation.** State per channel: `loop_start_row` (defaulting to 0 at each pattern entry) and `loop_count` (defaulting to 0).
|
||||
|
||||
```
|
||||
on row event (S $Bx00):
|
||||
x = (arg >> 8) & $0F
|
||||
if x == 0:
|
||||
loop_start_row = current_row
|
||||
else:
|
||||
if loop_count == 0:
|
||||
loop_count = x
|
||||
jump next_row -> loop_start_row
|
||||
else:
|
||||
loop_count -= 1
|
||||
if loop_count > 0:
|
||||
jump next_row -> loop_start_row
|
||||
# else loop_count hits 0 on its own; fall through to next row
|
||||
|
||||
on pattern change: loop_start_row = 0; loop_count = 0
|
||||
```
|
||||
|
||||
The crucial bug fix relative to ST3: the loop-counter decrement happens **once per actual row playback**, not once per tick-0 invocation. When SBx shares a row with SEx (pattern delay), the pattern-delay machinery replays the row as a unit, but the SBx state machine treats the whole delay group as a single visit. Implement this by gating the SBx decrement on `pattern_delay_repetition == 0`.
|
||||
|
||||
---
|
||||
|
||||
## S $Cx00 — Note cut in $x ticks
|
||||
|
||||
**Plain.** Silences the note on tick `$x` of the current row by forcing the channel's output volume to 0. The sample continues running internally, so a later volume-change or retrigger event can resume audio.
|
||||
|
||||
**Compatibility.** ST3 `SCx` maps directly. ProTracker `ECx` also maps directly. ST3 ignores `SC0` (treats it as no cut at all); Taud preserves this.
|
||||
|
||||
**Implementation.** On tick `$x`, set `output_volume = 0` but leave `base_volume` unchanged. If `$x ≥ speed`, the cut never fires. If `$x == 0`, the command is ignored. Set the `note_was_cut` flag so a later Q retrigger on the same row is suppressed.
|
||||
|
||||
---
|
||||
|
||||
## S $Dx00 — Note delay for $x ticks
|
||||
|
||||
**Plain.** Delays the triggering of the note (and any co-row instrument, offset, and volume event) until tick `$x`. Until then, any currently playing note continues.
|
||||
|
||||
**Compatibility.** ST3 `SDx` maps directly. ProTracker `EDx` also maps directly. `SD0` plays the note normally on tick 0. If `$x ≥ speed`, the note never plays on this row and does not carry over to the next row.
|
||||
|
||||
**Implementation.** On row parse, defer the note-trigger event (including sample selection, volume, offset, and any volume-column effect) until tick `$x`. On tick `$x`, execute the deferred trigger. When combined with pattern delay (S $Ex00), the deferred trigger re-fires at the start of each row repetition — matching ST3's `kRowDelayWithNoteDelay` behaviour.
|
||||
|
||||
---
|
||||
|
||||
## S $Ex00 — Pattern delay for $x row-repeats
|
||||
|
||||
**Plain.** Repeats the current row `$x` additional times (so `$x = 0` means no repeat and the row plays once; `$x = 3` means the row plays four times total). Notes do not retrigger across repetitions, but per-tick effects re-run and tick-0 events (fine slides, delayed notes) re-fire on each repetition.
|
||||
|
||||
**Compatibility.** ST3 `SEx` maps directly. ProTracker `EEx` also maps directly. Simultaneous SEx on multiple channels: ST3 uses the first SEx in **pan order** (L1..L8 then R1..R8); **Taud uses the first SEx in ascending channel-index order** for predictability. Converters that encounter ST3 songs relying on the pan-order rule should emit a warning.
|
||||
|
||||
Q retrigger counters do **not** reset between SEx repetitions.
|
||||
|
||||
**Implementation.** Row duration becomes `speed × (1 + arg_x)` ticks. Treat each repetition as a fresh row for tick-0 purposes (so fine slides, delayed notes, and the like re-trigger), but do not reset arpeggio, vibrato, or tremolo LFO positions, and do not decrement SBx's loop counter more than once across the whole delay block.
|
||||
|
||||
---
|
||||
|
||||
## S $Fx00 — Funk repeat with speed $x (non-destructive)
|
||||
|
||||
**Plain.** Produces a hiss-like progressive inversion of the sample loop, toggling individual bytes over time for a gritty textural effect. Setting `$x = 0` turns the effect off; higher `$x` advances the inversion faster.
|
||||
|
||||
**Compatibility.** ProTracker `EFx` is destructive — it XORs bytes directly in the sample data, permanently corrupting the sample. **Taud's implementation is non-destructive**: the XOR is applied at playback time through a per-instrument bit-mask, leaving source samples pristine. ST3 does not implement SFx at all and will parse Taud's S $Fx00 as a no-op; converters targeting ST3 should drop the effect. ProTracker `EFx` imports directly as Taud `S $Fx00`.
|
||||
|
||||
**Implementation.** Each instrument carries a `funk_mask` bit array, one bit per byte of the loop region, all zero at song start. A per-channel counter `funk_accumulator` and a per-channel `funk_write_pos` track progress.
|
||||
|
||||
```
|
||||
funk_table[16] = { 0, 5, 6, 7, 8, $A, $B, $D, $10, $13, $16, $1A, $20, $2B, $40, $80 }
|
||||
|
||||
on every tick (when S $Fx00 is active with x != 0):
|
||||
funk_accumulator += funk_table[x]
|
||||
while funk_accumulator >= $80:
|
||||
funk_accumulator -= $80
|
||||
bit = funk_mask[funk_write_pos]
|
||||
funk_mask[funk_write_pos] = bit XOR 1
|
||||
funk_write_pos = (funk_write_pos + 1) mod loop_length
|
||||
|
||||
on sample byte read during loop playback:
|
||||
raw_byte = sample_data[offset_in_loop]
|
||||
if funk_mask[offset_in_loop] == 1:
|
||||
output_byte = raw_byte XOR $FF
|
||||
else:
|
||||
output_byte = raw_byte
|
||||
```
|
||||
|
||||
`S $F000` clears `funk_accumulator` but leaves `funk_mask` intact (so the accumulated inversion pattern persists until the instrument is reset). On a fresh note or instrument-change event, Taud optionally resets `funk_mask` to all zero; this is a per-implementation choice, but the recommended default is **reset on instrument-change, preserve on pure note retrigger**.
|
||||
|
||||
---
|
||||
|
||||
# Volume column effects
|
||||
|
||||
Each cell carries a 6-bit value field plus a 2-bit selector field for the volume column. The four selectors are:
|
||||
|
||||
- **`0.$xx` — Set volume** to `$xx` (6-bit, $00..$3F). Equivalent to a note's default volume.
|
||||
- **`1.$xx` — Volume slide up** by `$xx` per non-first tick (4-bit). Volume clamps at $3F.
|
||||
- **`2.$xx` — Volume slide down** by `$xx` per non-first tick (4-bit). Volume clamps at $00.
|
||||
- **`3.$Sx` — Fine volume slide** on tick 0 only. The high bit `$S` of the value selects direction (0 = down, 1 = up); the low 4 bits `$x` ($0..$F) are the magnitude. Equivalent in scale to `D $xF00` / `D $Fy00` but with a 5-bit cap. Fires once per row regardless of speed.
|
||||
|
||||
Volume-column effects do not consume the main effect slot; a cell can carry both (for instance, a tone portamento in the effect slot and a volume slide in the volume column).
|
||||
|
||||
When the converter folds an ST3 K, L, M, or N effect into the volume column, the slide-up / slide-down nibbles map to selectors 1 / 2 (clamped to 6 bits — values above $3F clip).
|
||||
|
||||
NOTE: **`3.00` — is No-op**
|
||||
|
||||
---
|
||||
|
||||
# Panning column effects
|
||||
|
||||
The panning column uses the same 6-bit value + 2-bit selector layout:
|
||||
|
||||
- **`0.$xx` — Set pan** (6-bit, $00..$3F mapped onto the channel's 8-bit pan space; $01 = full left, $1F = centre-left, $20 = centre-right, $3F = full right). For 8-bit precision use `S $80xx` instead.
|
||||
- **`1.$xx` — Pan slide right** by `$xx` per non-first tick (4-bit).
|
||||
- **`2.$xx` — Pan slide left** by `$xx` per non-first tick (4-bit).
|
||||
- **`3.$Sx` — Fine pan slide** on tick 0 only, same direction-bit encoding as the volume column's selector 3.
|
||||
|
||||
NOTE: **`3.00` — is No-op**
|
||||
|
||||
---
|
||||
|
||||
# ProTracker to Taud conversion table
|
||||
|
||||
This table maps each PT effect to its Taud equivalent. Arguments follow PT's two-nibble form and expand to Taud's 16-bit form as shown.
|
||||
|
||||
| PT effect | Taud effect | Notes |
|
||||
|---|---|---|
|
||||
| `0 $xy` | `J $xxyy` | Arpeggio; nibble-repeat each byte. See the 12-TET → Taud table above for conversion losses |
|
||||
| `1 $xx` | `F $0xxx × $0015` | Portamento up; ST3 slide unit = 1/16 semitone |
|
||||
| `2 $xx` | `E $0xxx × $0015` | Portamento down |
|
||||
| `5 $xy` | `L $xy00` | Combined portamento + volume slide |
|
||||
| `6 $xy` | `K $xy00` | Combined vibrato + volume slide |
|
||||
| `7 $xy` | `R $xxyy` | Tremolo; nibble-repeat |
|
||||
| `8 $xx` | `S $80xx` or panning column `0.$xx` | Fine pan |
|
||||
| `9 $xx` | `O $xx00` | Sample offset |
|
||||
| `A $xy` | Volume column `1.$xy` | Volume slide |
|
||||
| `B $xx` | `B $00xx` | Position jump |
|
||||
| `C $xx` | Volume column `0.$xx` | Set volume |
|
||||
| `D $xx` | `C $00xx` (after BCD decode) | Pattern break |
|
||||
| `E $3x` | `S $1x00` | Glissando control |
|
||||
| `E $4x` | `S $3x00` | Vibrato waveform |
|
||||
| `E $5x` | `S $2x00` | Set fine-tune |
|
||||
| `E $6x` | `S $Bx00` | Pattern loop |
|
||||
| `E $7x` | `S $4x00` | Tremolo waveform |
|
||||
| `E $8x` | `S $80xx` or panning column `0.$xx` | Coarse pan (nibble-repeat) |
|
||||
| `E $9x` | `Q $0x00` | Retrigger |
|
||||
| `E $Cx` | `S $Cx00` | Note cut |
|
||||
| `E $Dx` | `S $Dx00` | Note delay |
|
||||
| `E $Ex` | `S $Ex00` | Pattern delay |
|
||||
| `E $Fx` | `S $Fx00` | Funk repeat |
|
||||
| `F $xx` (xx < $20) | `A $xx00` | Set speed |
|
||||
| `F $xx` (xx ≥ $20) | `T $(xx−$18)00` | Set tempo |
|
||||
|
||||
---
|
||||
|
||||
# ScreamTracker 3 conversion notes
|
||||
|
||||
These quirks of ST3 are worth preserving or flagging when importing S3M files into Taud:
|
||||
|
||||
**Shared memory across effects.** In ST3, a single memory slot backs D, E, F, I, J, K, L, Q, R, and S. A `$00` argument on any of these recalls whichever effect last wrote a non-zero argument. Taud narrows this to four cohorts (EF / G / HU / R) plus private slots. The converter must **eagerly resolve ST3 recalls** — walking the pattern in playback order, tracking the shared memory value, and emitting explicit Taud arguments wherever an ST3 recall crosses a cohort boundary. Otherwise a Taud player will either recall the wrong value or recall $0000.
|
||||
|
||||
**Cxx BCD encoding.** ST3 stores pattern-break row numbers as BCD on disk (`$10` means decimal 10). Taud uses binary. Decode on import; encode on export. Out-of-range BCD bytes (decimal 64 or higher) clamp to row 0.
|
||||
|
||||
**Tempo range.** ST3 accepts tempos $20..$FF (BPM 32..255); Taud accepts bytes $00..$FF (BPM 24..279). Imported ST3 tempos must be shifted down by $18; Taud tempos below $08 and above $E7 cannot be represented in ST3 and should clamp on export.
|
||||
|
||||
**SBx + SEx interaction.** ST3 miscounts loop iterations when pattern delay is active inside a pattern loop; Taud fixes this. Songs that depended on the bug for their intended playback will loop fewer times in Taud. Flag such songs on import.
|
||||
|
||||
**Simultaneous SEx priority.** ST3 uses pan order (L1..L8, R1..R8); Taud uses ascending channel-index order. Rare; flag on import if multiple channels carry SEx in the same row.
|
||||
|
||||
**Muted channels.** ST3 skips all effect processing on muted channels (no volume change, no tempo change, no jumps); Taud follows this rule for strict compatibility but recommends that new compositions avoid muting channels that carry global effects.
|
||||
|
||||
**Volume cap.** ST3's volume caps at $40; Taud's at $3F. Notes that reached $40 in ST3 (a rare edge) will play marginally quieter in Taud.
|
||||
|
||||
**Global volume scale.** ST3's 0..$40 maps to Taud's 0..$FF with a ×4 scale on import, truncated ÷4 on export.
|
||||
|
||||
**Linear pitch slides.** ST3's slide arithmetic is period-based (Amiga) or linear-table-indexed; Taud's is purely linear in 4096-TET units. ST3 songs in linear mode convert cleanly via the $0015-per-unit coarse and $0005-per-unit extra-fine constants; Amiga-mode slides change character slightly because the non-linearity of period math is not replicated.
|
||||
|
||||
**Default tempo byte.** Taud's default $65 equals 125 BPM under the $18 offset; this is not the same as ST3's `$7D` default, which maps to Taud `$65` after subtracting $18. Converters must remap on both import and export.
|
||||
|
||||
---
|
||||
|
||||
End of reference.
|
||||
@@ -5,4 +5,6 @@ set PATH=\tvdos\installer;\tvdos\tuidev;$PATH
|
||||
set KEYBOARD=us_colemak
|
||||
|
||||
rem this line specifies which shell to be presented after the boot precess:
|
||||
tvdos/i18n/korean
|
||||
zfm
|
||||
command -fancy
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
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.OFF = 0x0000; // key-off
|
||||
t.CUT = 0xFFFE; // note cut (immediate)
|
||||
t.NOP = 0xFFFF; // no-op (empty row)
|
||||
return t;
|
||||
}());
|
||||
|
||||
var PLAYHEAD = 0;
|
||||
|
||||
// -- 1. Sample: triangle wave (256 samples @ C3) --------------------------------
|
||||
var SAMPLE_LEN = 256;
|
||||
var sampleBytes = new Array(SAMPLE_LEN);
|
||||
for (var i = 0; i < SAMPLE_LEN; i++) {
|
||||
var phase = (i / SAMPLE_LEN) * 2.0;
|
||||
var val_ = phase < 1.0 ? phase : 2.0 - phase;
|
||||
sampleBytes[i] = Math.round(val_ * 254) & 0xFF;
|
||||
}
|
||||
var memBase = audio.getMemAddr();
|
||||
for (var i = 0; i < SAMPLE_LEN; i++) {
|
||||
sys.poke(memBase - i, sampleBytes[i]);
|
||||
}
|
||||
|
||||
// -- 2. Instrument 0 -----------------------------------------------------------
|
||||
var instBytes = new Array(64).fill(0);
|
||||
instBytes[2] = 0; instBytes[3] = 1; // sampleLength = 256
|
||||
instBytes[4] = 0x00; instBytes[5] = 0x7D; // samplingRate = 32000
|
||||
instBytes[10] = 0x00; instBytes[11] = 0x01; // sampleLoopEnd = 256 (whole sample)
|
||||
instBytes[12] = 1; // loopMode = 1 (forward)
|
||||
instBytes[16] = 255; instBytes[17] = 0; // envelope: vol=255, hold
|
||||
audio.uploadInstrument(1, instBytes);
|
||||
|
||||
// -- 3. Piano-roll builder -----------------------------------------------------
|
||||
// Source convention: C1=0, C2=12, C3=24, C4=36 (i.e. C3=24, octave every 12).
|
||||
function midiToTsvm(n) {
|
||||
var oct = Math.floor(n / 12) + 1;
|
||||
return Math.round(0x3000 + oct * 4096 + (n % 12) * (4096 / 12)) & 0xFFFF;
|
||||
}
|
||||
|
||||
var noteMap = {}; // absRow → TSVM note value
|
||||
var rowCursor = 0;
|
||||
|
||||
function seq(notes, lens) {
|
||||
for (var i = 0; i < notes.length; i++) {
|
||||
noteMap[rowCursor] = midiToTsvm(notes[i]);
|
||||
rowCursor += lens[i];
|
||||
}
|
||||
}
|
||||
|
||||
var TD = 3; // rows per note step (= source TICK_DIVISOR)
|
||||
|
||||
function prel(n1, n2, n3, n4, n5) {
|
||||
seq([n1, n2, n3, n4, n5, n3, n4, n5, n1, n2, n3, n4, n5, n3, n4, n5],
|
||||
[TD+2, TD, TD, TD-1, TD, TD, TD, TD, TD, TD, TD, TD-1, TD, TD, TD, TD]);
|
||||
}
|
||||
function end1(n1,n2,n3,n4,n5,n6,n7,n8,n9) {
|
||||
seq([n1, n2, n3, n4, n5, n6, n5, n4, n5, n7, n8, n7, n8, n9, n8, n9],
|
||||
[TD+2, TD, TD, TD-1, TD, TD, TD, TD, TD, TD, TD, TD-1, TD, TD, TD, TD]);
|
||||
}
|
||||
function end2(n1,n2,n3,n4,n5,n6,n7,n8,n9) {
|
||||
seq([n1, n2, n3, n4, n5, n6, n5, n4, n5, n4, n3, n4, n7, n8, n9, n7],
|
||||
[TD+2, TD+1, TD+1, TD+1, TD+1, TD+2, TD+2, TD+2,
|
||||
TD+3, TD+3, TD+4, TD+4, TD+6, TD+8, TD+12, TD+24]);
|
||||
}
|
||||
function end3(ns) {
|
||||
for (var i = 0; i < ns.length; i++) {
|
||||
noteMap[rowCursor] = midiToTsvm(ns[i]);
|
||||
rowCursor += 1;
|
||||
}
|
||||
for (var i = 0; i < TD*2; i++) {
|
||||
noteMap[rowCursor] = Note.NOP
|
||||
rowCursor += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// -- 4. Build the piece --------------------------------------------------------
|
||||
rowCursor = 16 * TD; // 160-row intro silence
|
||||
|
||||
prel(24,28,31,36,40);
|
||||
prel(24,26,33,38,41);
|
||||
prel(23,26,31,38,41);
|
||||
prel(24,28,31,36,40);
|
||||
prel(24,28,33,40,45);
|
||||
prel(24,26,30,33,38);
|
||||
prel(23,26,31,38,43);
|
||||
prel(23,24,28,31,36);
|
||||
prel(21,24,28,31,36);
|
||||
prel(14,21,26,30,36);
|
||||
prel(19,23,26,31,35);
|
||||
prel(19,22,28,31,37);
|
||||
prel(17,21,26,33,38);
|
||||
prel(17,20,26,29,35);
|
||||
prel(16,19,24,31,36);
|
||||
prel(16,17,21,24,29);
|
||||
prel(14,17,21,24,29);
|
||||
prel( 7,14,19,23,29);
|
||||
prel(12,16,19,24,28);
|
||||
prel(12,19,22,24,28);
|
||||
prel( 5,17,21,24,28);
|
||||
prel( 6,12,21,24,27);
|
||||
prel( 8,17,23,24,26);
|
||||
prel( 7,17,19,23,26);
|
||||
prel( 7,16,19,24,28);
|
||||
prel( 7,14,19,24,29);
|
||||
prel( 7,14,19,23,29);
|
||||
prel( 7,15,21,24,30);
|
||||
prel( 7,16,19,24,31);
|
||||
prel( 7,14,19,24,29);
|
||||
prel( 7,14,19,23,29);
|
||||
prel( 0,12,19,22,28);
|
||||
end1( 0,12,17,21,24,29,21,17,14);
|
||||
end2( 0,11,31,35,38,41,26,29,28);
|
||||
end3([0,12,28,31,36]);
|
||||
|
||||
noteMap[rowCursor] = Note.OFF; // key-off at start of final silence
|
||||
rowCursor += 16 * TD - 5; // 155 more rows of silence
|
||||
|
||||
var totalRows = rowCursor; // 5836
|
||||
var NUM_ROWS = 64;
|
||||
var numPatterns = Math.ceil(totalRows / NUM_ROWS); // 92
|
||||
|
||||
// -- 5. Build and upload patterns ----------------------------------------------
|
||||
for (var p = 0; p < numPatterns; p++) {
|
||||
var patBytes = new Array(512).fill(0);
|
||||
for (var r = 0; r < NUM_ROWS; r++) {
|
||||
var absRow = p * NUM_ROWS + r;
|
||||
var noteVal = (noteMap[absRow] !== undefined) ? noteMap[absRow] : Note.NOP;
|
||||
var isOn = (noteVal !== Note.NOP && noteVal !== Note.OFF && noteVal !== Note.CUT);
|
||||
var off = r * 8;
|
||||
patBytes[off] = noteVal & 0xFF;
|
||||
patBytes[off + 1] = (noteVal >> 8) & 0xFF;
|
||||
patBytes[off + 2] = 1; // instrument 1
|
||||
patBytes[off + 3] = 63; // volume
|
||||
patBytes[off + 4] = 31; // pan (centre)
|
||||
}
|
||||
audio.uploadPattern(p, patBytes);
|
||||
}
|
||||
|
||||
// -- 6. Cue sheet: one entry per pattern, last halts -------------------------
|
||||
// Cue format: 32 bytes, 20 voices with 12-bit pattern numbers packed as:
|
||||
// bytes 0-9: low nybbles (byte i = voice i*2 in hi-nybble, voice i*2+1 in lo-nybble)
|
||||
// bytes 10-19: mid nybbles (same packing)
|
||||
// bytes 20-29: high nybbles (same packing)
|
||||
// byte 30: instruction (0=NOP, 1=Halt)
|
||||
// Voice 0 plays pattern c; voices 1-19 are disabled (0xFFF).
|
||||
for (var c = 0; c < numPatterns; c++) {
|
||||
var cueBytes = new Array(32).fill(0xFF);
|
||||
// voice 0 = c (12-bit), voice 1 = 0xFFF → byte0=(c&0xF)<<4|0xF
|
||||
cueBytes[0] = ((c & 0xF) << 4) | 0xF; // lo nybbles v0,v1
|
||||
cueBytes[10] = (((c >> 4) & 0xF) << 4) | 0xF; // mid nybbles v0,v1
|
||||
cueBytes[20] = (((c >> 8) & 0xF) << 4) | 0xF; // hi nybbles v0,v1
|
||||
cueBytes[30] = (c === numPatterns - 1) ? 0x01 : 0;
|
||||
audio.uploadCue(c, cueBytes);
|
||||
}
|
||||
|
||||
// -- 7. Playback ---------------------------------------------------------------
|
||||
// BPM=500, tickRate=1: 1 row = 5 ms; 10 rows/step × 16 steps/bar ≈ 75 bars/min.
|
||||
audio.setTrackerMode(PLAYHEAD);
|
||||
audio.setBPM(PLAYHEAD, 250);
|
||||
audio.setTickRate(PLAYHEAD, 6);
|
||||
audio.setMasterVolume(PLAYHEAD, 255);
|
||||
audio.setMasterPan(PLAYHEAD, 128);
|
||||
audio.setCuePosition(PLAYHEAD, 0);
|
||||
audio.play(PLAYHEAD);
|
||||
|
||||
println("Bach's Prelude in C Major -- " + numPatterns + " patterns loaded.");
|
||||
println("Stop: audio.stop(" + PLAYHEAD + ")");
|
||||
@@ -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
|
||||
|
||||
@@ -225,8 +225,9 @@ class TVDOSFileDescriptor {
|
||||
}
|
||||
|
||||
/** reads the file bytewise and puts it to the specified memory address
|
||||
* @param count optional -- how many bytes to read
|
||||
* @param offset optional -- how many bytes to skip initially
|
||||
* @param ptr -- where the bytes should be dumped
|
||||
* @param count -- how many bytes to read
|
||||
* @param offset -- how many bytes to skip initially from the file
|
||||
*/
|
||||
pread(ptr, count, offset) {
|
||||
this.driver.pread(this, ptr, count, offset)
|
||||
@@ -241,7 +242,9 @@ class TVDOSFileDescriptor {
|
||||
}
|
||||
|
||||
/** writes the bytes stored in the memory[ptr .. ptr+count-1] to file[offset .. offset+count-1]
|
||||
* - @param offset is optional
|
||||
* @param ptr -- where the bytes are
|
||||
* @param count -- how many bytes to write
|
||||
* @param offset -- position in the file
|
||||
*/
|
||||
pwrite(ptr, count, offset) {
|
||||
this.driver.pwrite(this, ptr, count, offset)
|
||||
@@ -870,11 +873,21 @@ Object.freeze(_TVDOS.DRV.FS.DEVPT)
|
||||
_TVDOS.DRV.FS.DEVFBIPF = {}
|
||||
|
||||
_TVDOS.DRV.FS.DEVFBIPF.pwrite = (fd, infilePtr, count, _2) => {
|
||||
let decodefun = ([graphics.decodeIpf1, graphics.decodeIpf2])[sys.peek(infilePtr + 13)]
|
||||
let flags = sys.peek(infilePtr+12)
|
||||
let ipfType = sys.peek(infilePtr+13)
|
||||
let isProgressive = (flags & 0x80) != 0
|
||||
let hasAlpha = (flags & 0x01) != 0
|
||||
|
||||
// Select decode function based on type and progressive flag
|
||||
let decodefun
|
||||
if (isProgressive) {
|
||||
decodefun = ([graphics.decodeIpf1Progressive, graphics.decodeIpf2Progressive])[ipfType]
|
||||
} else {
|
||||
decodefun = ([graphics.decodeIpf1, graphics.decodeIpf2])[ipfType]
|
||||
}
|
||||
|
||||
let width = sys.peek(infilePtr+8) | (sys.peek(infilePtr+9) << 8)
|
||||
let height = sys.peek(infilePtr+10) | (sys.peek(infilePtr+11) << 8)
|
||||
let hasAlpha = (sys.peek(infilePtr+12) != 0)
|
||||
let ipfType = sys.peek(infilePtr+13)
|
||||
let imgLen = sys.peek(infilePtr+24) | (sys.peek(infilePtr+25) << 8) | (sys.peek(infilePtr+26) << 16) | (sys.peek(infilePtr+27) << 24)
|
||||
|
||||
let ipfbuf = sys.malloc(imgLen)
|
||||
@@ -1014,136 +1027,6 @@ _TVDOS.DRV.FS.NET.exists = (fd) => {
|
||||
return (0 == com.getStatusCode(port[0]))
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
// Legacy Serial filesystem, !!pending for removal!!
|
||||
|
||||
|
||||
/*const filesystem = {};
|
||||
|
||||
filesystem._toPorts = (driveLetter) => {
|
||||
if (driveLetter.toUpperCase === undefined) {
|
||||
throw Error("'"+driveLetter+"' (type: "+typeof driveLetter+") is not a valid drive letter");
|
||||
}
|
||||
var port = _TVDOS.DRIVES[driveLetter.toUpperCase()];
|
||||
if (port === undefined) {
|
||||
throw Error("Drive letter '" + driveLetter.toUpperCase() + "' does not exist");
|
||||
}
|
||||
return port
|
||||
};
|
||||
filesystem._close = (portNo) => {
|
||||
com.sendMessage(portNo, "CLOSE")
|
||||
}
|
||||
filesystem._flush = (portNo) => {
|
||||
com.sendMessage(portNo, "FLUSH")
|
||||
}
|
||||
|
||||
// @return disk status code (0 for successful operation)
|
||||
// throws if:
|
||||
// - java.lang.NullPointerException if path is null
|
||||
// - Error if operation mode is not "R", "W" nor "A"
|
||||
filesystem.open = (driveLetter, path, operationMode) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
|
||||
filesystem._flush(port[0]); filesystem._close(port[0]);
|
||||
|
||||
var mode = operationMode.toUpperCase();
|
||||
if (mode != "R" && mode != "W" && mode != "A") {
|
||||
throw Error("Unknown file opening mode: " + mode);
|
||||
}
|
||||
|
||||
com.sendMessage(port[0], "OPEN"+mode+'"'+path+'",'+port[1]);
|
||||
return com.getStatusCode(port[0]);
|
||||
};
|
||||
filesystem.getFileLen = (driveLetter) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "GETLEN");
|
||||
var response = com.getStatusCode(port[0]);
|
||||
if (135 == response) {
|
||||
throw Error("File not opened");
|
||||
}
|
||||
if (response < 0 || response >= 128) {
|
||||
throw Error("Reading a file failed with "+response);
|
||||
}
|
||||
return Number(com.pullMessage(port[0]));
|
||||
};
|
||||
// @return the entire contents of the file in String
|
||||
filesystem.readAll = (driveLetter) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "READ");
|
||||
var response = com.getStatusCode(port[0]);
|
||||
if (135 == response) {
|
||||
throw Error("File not opened");
|
||||
}
|
||||
if (response < 0 || response >= 128) {
|
||||
throw Error("Reading a file failed with "+response);
|
||||
}
|
||||
return com.pullMessage(port[0]);
|
||||
};
|
||||
filesystem.readAllBytes = (driveLetter) => {
|
||||
var str = filesystem.readAll(driveLetter);
|
||||
var bytes = new Uint8Array(str.length);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
bytes[i] = str.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
filesystem.write = (driveLetter, string) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "WRITE"+string.length);
|
||||
var response = com.getStatusCode(port[0]);
|
||||
if (135 == response) {
|
||||
throw Error("File not opened");
|
||||
}
|
||||
if (response < 0 || response >= 128) {
|
||||
throw Error("Writing a file failed with "+response);
|
||||
}
|
||||
com.sendMessage(port[0], string);
|
||||
filesystem._flush(port[0]); filesystem._close(port[0]);
|
||||
};
|
||||
filesystem.writeBytes = (driveLetter, bytes) => {
|
||||
var string = btostr(bytes); // no spreading: has length limit
|
||||
filesystem.write(driveLetter, string);
|
||||
};
|
||||
filesystem.isDirectory = (driveLetter) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "LISTFILES");
|
||||
var response = com.getStatusCode(port[0]);
|
||||
return (response === 0);
|
||||
};
|
||||
filesystem.mkDir = (driveLetter) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "MKDIR");
|
||||
var response = com.getStatusCode(port[0]);
|
||||
|
||||
if (response < 0 || response >= 128) {
|
||||
var status = com.getDeviceStatus(port[0]);
|
||||
throw Error("Creating a directory failed with ("+response+"): "+status.message+"\n");
|
||||
}
|
||||
return (response === 0); // possible status codes: 0 (success), 1 (fail)
|
||||
};
|
||||
filesystem.touch = (driveLetter) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "TOUCH");
|
||||
var response = com.getStatusCode(port[0]);
|
||||
return (response === 0);
|
||||
};
|
||||
filesystem.mkFile = (driveLetter) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "MKFILE");
|
||||
var response = com.getStatusCode(port[0]);
|
||||
return (response === 0);
|
||||
};
|
||||
filesystem.remove = (driveLetter) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "DELETE");
|
||||
var response = com.getStatusCode(port[0]);
|
||||
return (response === 0);
|
||||
};
|
||||
Object.freeze(filesystem);*/
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const files = {}
|
||||
@@ -1366,12 +1249,12 @@ unicode.getUniprint = (c) => {
|
||||
return unicode.uniprint[k]
|
||||
}}
|
||||
|
||||
print = function(str) {
|
||||
unicode.print = (str) => {
|
||||
if ((typeof str === 'string' || str instanceof String) && str.length > 0) {
|
||||
|
||||
let cp = unicode.utf8toCodepoints(str)
|
||||
cp.forEach(c => {
|
||||
let q = unicode.getUniprint(c)
|
||||
|
||||
if (q == undefined || !q[0](c)) {
|
||||
con.addch(4)
|
||||
con.curs_right()
|
||||
@@ -1381,6 +1264,34 @@ print = function(str) {
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
sys.print(str)
|
||||
}
|
||||
}
|
||||
|
||||
unicode.println = (str) => {
|
||||
unicode.print(str+'\n\n')
|
||||
}
|
||||
|
||||
unicode.strlen = (str) => {
|
||||
// Convert string to an array of codepoints using spread operator
|
||||
// This correctly handles surrogate pairs and counts each codepoint as one
|
||||
return unicode.utf8toCodepoints(str).length
|
||||
}
|
||||
|
||||
unicode.visualStrlen = (str) => {
|
||||
function isTripleWidth(c) {
|
||||
return (0xAC00 <= c && c <= 0xD7FF) && [1,4,8,10,13].includes(((c - 0xAC00) / 588)|0)
|
||||
}
|
||||
|
||||
function isDoubleWidth(c) {
|
||||
return (0x3000 <= c && c <= 0x303f) || (0x3100 <= c && c <= 0x312f) || (0x3200 <= c && c <= 0x33ff) ||
|
||||
(0xAC00 <= c && c <= 0xD7FF) || (0xFE30 <= c && c <= 0xFE4F) || (0xFF00 <= c && c <= 0xff60)
|
||||
}
|
||||
|
||||
// Convert string to an array of codepoints using spread operator
|
||||
// This correctly handles surrogate pairs and counts each codepoint as one
|
||||
return unicode.utf8toCodepoints(str).reduce((acc, c) => acc + (isTripleWidth(c) ? 3 : isDoubleWidth(c) ? 2 : 1), 0)
|
||||
}
|
||||
|
||||
Object.freeze(unicode);
|
||||
|
||||
@@ -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(' ')
|
||||
|
||||
5
assets/disk0/tvdos/bin/hopper.js
Normal file
5
assets/disk0/tvdos/bin/hopper.js
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Hopper is a package manager for TSVM
|
||||
* Created by CuriousTorvald on 2026-04-16
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
361
assets/disk0/tvdos/bin/playtad.js
Normal file
361
assets/disk0/tvdos/bin/playtad.js
Normal file
@@ -0,0 +1,361 @@
|
||||
const SND_BASE_ADDR = audio.getBaseAddr()
|
||||
const SND_MEM_ADDR = audio.getMemAddr()
|
||||
const TAD_INPUT_ADDR = SND_MEM_ADDR - 262144 // TAD input buffer (matches TAV packet 0x24)
|
||||
const TAD_DECODED_ADDR = SND_MEM_ADDR - 262144 + 65536 // TAD decoded buffer
|
||||
|
||||
if (!SND_BASE_ADDR) return 10
|
||||
|
||||
// Check for help flag or missing arguments
|
||||
if (!exec_args[1] || exec_args[1] == "-h" || exec_args[1] == "--help") {
|
||||
serial.println("Usage: playtad <file.tad> [-i | -d] [quality]")
|
||||
serial.println(" -i Interactive mode (progress bar, press Backspace to exit)")
|
||||
serial.println(" -d Dump mode (show first 3 chunks with payload hex and decoded samples)")
|
||||
serial.println("")
|
||||
serial.println("Examples:")
|
||||
serial.println(" playtad audio.tad -i # Play with progress bar")
|
||||
serial.println(" playtad audio.tad -d # Dump first 3 chunks for debugging")
|
||||
return 0
|
||||
}
|
||||
|
||||
const pcm = require("pcm")
|
||||
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
|
||||
const dumpCoeffs = exec_args[2] && exec_args[2].toLowerCase() == "-d"
|
||||
|
||||
function printdbg(s) { if (0) serial.println(s) }
|
||||
|
||||
|
||||
class SequentialFileBuffer {
|
||||
|
||||
constructor(path, offset, length) {
|
||||
if (Array.isArray(path)) throw Error("arg #1 is path(string), not array")
|
||||
|
||||
this.path = path
|
||||
this.file = files.open(path)
|
||||
|
||||
this.offset = offset || 0
|
||||
this.originalOffset = offset
|
||||
this.length = length || this.file.size
|
||||
|
||||
this.seq = require("seqread")
|
||||
this.seq.prepare(path)
|
||||
}
|
||||
|
||||
readBytes(size, ptr) {
|
||||
return this.seq.readBytes(size, ptr)
|
||||
}
|
||||
|
||||
readByte() {
|
||||
let ptr = this.seq.readBytes(1)
|
||||
let val = sys.peek(ptr)
|
||||
sys.free(ptr)
|
||||
return val
|
||||
}
|
||||
|
||||
readShort() {
|
||||
let ptr = this.seq.readBytes(2)
|
||||
let val = sys.peek(ptr) | (sys.peek(ptr + 1) << 8)
|
||||
sys.free(ptr)
|
||||
return val
|
||||
}
|
||||
|
||||
readInt() {
|
||||
let ptr = this.seq.readBytes(4)
|
||||
let val = sys.peek(ptr) | (sys.peek(ptr + 1) << 8) | (sys.peek(ptr + 2) << 16) | (sys.peek(ptr + 3) << 24)
|
||||
sys.free(ptr)
|
||||
return val
|
||||
}
|
||||
|
||||
readStr(n) {
|
||||
let ptr = this.seq.readBytes(n)
|
||||
let s = ''
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (i >= this.length) break
|
||||
s += String.fromCharCode(sys.peek(ptr + i))
|
||||
}
|
||||
sys.free(ptr)
|
||||
return s
|
||||
}
|
||||
|
||||
unread(diff) {
|
||||
let newSkipLen = this.seq.getReadCount() - diff
|
||||
this.seq.prepare(this.path)
|
||||
this.seq.skip(newSkipLen)
|
||||
}
|
||||
|
||||
rewind() {
|
||||
this.seq.prepare(this.path)
|
||||
}
|
||||
|
||||
seek(p) {
|
||||
this.seq.prepare(this.path)
|
||||
this.seq.skip(p)
|
||||
}
|
||||
|
||||
get byteLength() {
|
||||
return this.length
|
||||
}
|
||||
|
||||
get fileHeader() {
|
||||
return this.seq.fileHeader
|
||||
}
|
||||
|
||||
getReadCount() {
|
||||
return this.seq.getReadCount()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Read TAD chunk header to determine format
|
||||
let filebuf = new SequentialFileBuffer(_G.shell.resolvePathInput(exec_args[1]).full)
|
||||
const FILE_SIZE = filebuf.length
|
||||
|
||||
if (FILE_SIZE < 7) {
|
||||
serial.println(`ERROR: File too small (${FILE_SIZE} bytes). Expected TAD format.`)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Read first chunk header (standalone TAD format: no TAV wrapper)
|
||||
let firstSampleCount = filebuf.readShort()
|
||||
let firstMaxIndex = filebuf.readByte()
|
||||
let firstPayloadSize = filebuf.readInt()
|
||||
|
||||
// Validate first chunk
|
||||
if (firstSampleCount < 0 || firstSampleCount > 65536) {
|
||||
serial.println(`ERROR: Invalid sample count ${firstSampleCount}. File may be corrupted.`)
|
||||
return 1
|
||||
}
|
||||
if (firstMaxIndex < 0 || firstMaxIndex > 255) {
|
||||
serial.println(`ERROR: Invalid max index ${firstMaxIndex}. File may be corrupted.`)
|
||||
return 1
|
||||
}
|
||||
if (firstPayloadSize < 1 || firstPayloadSize > 65536) {
|
||||
serial.println(`ERROR: Invalid payload size ${firstPayloadSize}. File may be corrupted.`)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Rewind to start
|
||||
filebuf.rewind()
|
||||
|
||||
// Calculate approximate frame info
|
||||
const AVG_CHUNK_SIZE = 7 + firstPayloadSize // TAD header (2+1+4) + payload
|
||||
const SAMPLE_RATE = 32000
|
||||
const bufRealTimeLen = Math.floor((firstSampleCount / SAMPLE_RATE) * 1000) // milliseconds per chunk
|
||||
|
||||
if (dumpCoeffs) {
|
||||
serial.println(`TAD Coefficient Dump Mode`)
|
||||
serial.println(`File: ${filebuf.file.name}`)
|
||||
serial.println(`First chunk header:`)
|
||||
serial.println(` Sample Count: ${firstSampleCount}`)
|
||||
serial.println(` Max Index: ${firstMaxIndex}`)
|
||||
serial.println(` Payload Size: ${firstPayloadSize} bytes`)
|
||||
serial.println(`Chunk Duration: ${bufRealTimeLen} ms`)
|
||||
serial.println(``)
|
||||
}
|
||||
|
||||
|
||||
let bytes_left = FILE_SIZE
|
||||
let decodedLength = 0
|
||||
let chunkNumber = 0
|
||||
|
||||
|
||||
con.curs_set(0)
|
||||
let [__, CONSOLE_WIDTH] = con.getmaxyx()
|
||||
if (interactive) {
|
||||
let [cy, cx] = con.getyx()
|
||||
// file name
|
||||
con.mvaddch(cy, 1)
|
||||
con.prnch(0xC9);con.prnch(0xCD);con.prnch(0xB5)
|
||||
print(filebuf.file.name)
|
||||
con.prnch(0xC6);con.prnch(0xCD)
|
||||
print("\x84205u".repeat(CONSOLE_WIDTH - 26 - filebuf.file.name.length))
|
||||
con.prnch(0xB5)
|
||||
print("Hold Bksp to Exit")
|
||||
con.prnch(0xC6);con.prnch(0xCD);con.prnch(0xBB)
|
||||
|
||||
// L R pillar
|
||||
con.prnch(0xBA)
|
||||
con.mvaddch(cy+1, CONSOLE_WIDTH, 0xBA)
|
||||
|
||||
// media info
|
||||
let mediaInfoStr = `TAD Q${firstMaxIndex} ${SAMPLE_RATE/1000}kHz`
|
||||
con.move(cy+2,1)
|
||||
con.prnch(0xC8)
|
||||
print("\x84205u".repeat(CONSOLE_WIDTH - 5 - mediaInfoStr.length))
|
||||
con.prnch(0xB5)
|
||||
print(mediaInfoStr)
|
||||
con.prnch(0xC6);con.prnch(0xCD);con.prnch(0xBC)
|
||||
|
||||
con.move(cy+1, 2)
|
||||
}
|
||||
let [cy, cx] = con.getyx()
|
||||
let paintWidth = CONSOLE_WIDTH - 20
|
||||
|
||||
function bytesToSec(i) {
|
||||
// Approximate: use first chunk's ratio
|
||||
return Math.round((i / FILE_SIZE) * (FILE_SIZE / AVG_CHUNK_SIZE) * (bufRealTimeLen / 1000))
|
||||
}
|
||||
|
||||
function secToReadable(n) {
|
||||
let mins = ''+((n/60)|0)
|
||||
let secs = ''+(n % 60)
|
||||
return `${mins.padStart(2,'0')}:${secs.padStart(2,'0')}`
|
||||
}
|
||||
|
||||
function printPlayBar() {
|
||||
if (interactive) {
|
||||
let currently = decodedLength
|
||||
let total = FILE_SIZE
|
||||
|
||||
let currentlySec = bytesToSec(currently)
|
||||
let totalSec = bytesToSec(total)
|
||||
|
||||
con.move(cy, 3)
|
||||
print(' '.repeat(15))
|
||||
con.move(cy, 3)
|
||||
|
||||
print(`${secToReadable(currentlySec)} / ${secToReadable(totalSec)}`)
|
||||
|
||||
con.move(cy, 17)
|
||||
print(' ')
|
||||
let progressbar = '\x84196u'.repeat(paintWidth + 1)
|
||||
print(progressbar)
|
||||
|
||||
con.mvaddch(cy, 18 + Math.round(paintWidth * (currently / total)), 0xDB)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
audio.resetParams(0)
|
||||
audio.purgeQueue(0)
|
||||
audio.setPcmMode(0)
|
||||
audio.setPcmQueueCapacityIndex(0, 2) // queue size is now 8
|
||||
const QUEUE_MAX = audio.getPcmQueueCapacity(0)
|
||||
audio.setMasterVolume(0, 255)
|
||||
audio.play(0)
|
||||
|
||||
|
||||
let stopPlay = false
|
||||
let errorlevel = 0
|
||||
|
||||
try {
|
||||
while (bytes_left > 0 && !stopPlay) {
|
||||
|
||||
if (interactive) {
|
||||
sys.poke(-40, 1)
|
||||
if (sys.peek(-41) == 67) { // Backspace key
|
||||
stopPlay = true
|
||||
}
|
||||
}
|
||||
|
||||
printPlayBar()
|
||||
|
||||
// Read TAD chunk header (standalone TAD format)
|
||||
// Format: [sample_count][max_index][payload_size][payload]
|
||||
let sampleCount = filebuf.readShort()
|
||||
let maxIndex = filebuf.readByte()
|
||||
let payloadSize = filebuf.readInt()
|
||||
|
||||
// Validate every chunk (not just first one)
|
||||
if (sampleCount < 0 || sampleCount > 65536) {
|
||||
serial.println(`ERROR: Chunk ${chunkNumber}: Invalid sample count ${sampleCount}. File may be corrupted.`)
|
||||
errorlevel = 1
|
||||
break
|
||||
}
|
||||
if (maxIndex < 0 || maxIndex > 255) {
|
||||
serial.println(`ERROR: Chunk ${chunkNumber}: Invalid max index ${maxIndex}. File may be corrupted.`)
|
||||
errorlevel = 1
|
||||
break
|
||||
}
|
||||
if (payloadSize < 1 || payloadSize > 65536) {
|
||||
serial.println(`ERROR: Chunk ${chunkNumber}: Invalid payload size ${payloadSize}. File may be corrupted.`)
|
||||
errorlevel = 1
|
||||
break
|
||||
}
|
||||
if (payloadSize + 7 > bytes_left) {
|
||||
serial.println(`ERROR: Chunk ${chunkNumber}: Chunk size ${payloadSize + 7} exceeds remaining file size ${bytes_left}`)
|
||||
errorlevel = 1
|
||||
break
|
||||
}
|
||||
|
||||
if (dumpCoeffs && chunkNumber < 3) {
|
||||
serial.println(`=== Chunk ${chunkNumber} ===`)
|
||||
serial.println(` Sample Count: ${sampleCount}`)
|
||||
serial.println(` Max Index: ${maxIndex}`)
|
||||
serial.println(` Payload Size: ${payloadSize} bytes`)
|
||||
serial.println(` Bytes remaining in file: ${bytes_left}`)
|
||||
}
|
||||
|
||||
// Rewind 7 bytes to re-read the header along with payload
|
||||
// This allows reading the complete chunk (header + payload) in one call
|
||||
filebuf.unread(7)
|
||||
|
||||
// Read entire chunk (header + payload) to TAD input buffer
|
||||
// This matches TAV's approach for packet 0x24
|
||||
let totalChunkSize = 7 + payloadSize
|
||||
filebuf.readBytes(totalChunkSize, TAD_INPUT_ADDR)
|
||||
|
||||
if (dumpCoeffs && chunkNumber < 3) {
|
||||
// Dump first 32 bytes of compressed payload (skip 7-byte header)
|
||||
serial.print(` Compressed data (first 32 bytes): `)
|
||||
for (let i = 0; i < Math.min(32, payloadSize); i++) {
|
||||
let b = sys.peek(TAD_INPUT_ADDR + 7 + i)
|
||||
serial.print(`${(b & 0xFF).toString(16).padStart(2, '0')} `)
|
||||
}
|
||||
serial.println('')
|
||||
}
|
||||
|
||||
// Decode TAD chunk
|
||||
audio.tadDecode()
|
||||
|
||||
if (dumpCoeffs && chunkNumber < 3) {
|
||||
// After decoding, the decoded PCMu8 samples are in tadDecodedBin
|
||||
serial.println(` Decoded ${sampleCount} samples`)
|
||||
|
||||
// Dump first 16 decoded samples (PCMu8 stereo interleaved)
|
||||
serial.print(` Decoded (first 16 L samples): `)
|
||||
for (let i = 0; i < 16; i++) {
|
||||
serial.print(`${sys.peek(TAD_DECODED_ADDR + i * 2) & 0xFF} `)
|
||||
}
|
||||
serial.println('')
|
||||
serial.print(` Decoded (first 16 R samples): `)
|
||||
for (let i = 0; i < 16; i++) {
|
||||
serial.print(`${sys.peek(TAD_DECODED_ADDR + i * 2 + 1) & 0xFF} `)
|
||||
}
|
||||
serial.println('')
|
||||
serial.println('')
|
||||
}
|
||||
|
||||
// Upload decoded audio to queue
|
||||
audio.tadUploadDecoded(0, sampleCount)
|
||||
|
||||
if (!dumpCoeffs) {
|
||||
// Sleep for the duration of the audio chunk to pace playback
|
||||
// This prevents uploading everything at once
|
||||
sys.sleep(bufRealTimeLen)
|
||||
}
|
||||
|
||||
// Chunk size = header (7 bytes) + payload
|
||||
let chunkSize = 7 + payloadSize
|
||||
bytes_left -= chunkSize
|
||||
decodedLength += chunkSize
|
||||
chunkNumber++
|
||||
|
||||
// Limit coefficient dump to first 3 chunks
|
||||
if (dumpCoeffs && chunkNumber >= 3) {
|
||||
serial.println(`... (remaining chunks omitted)`)
|
||||
// Keep playing but don't dump more
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
printerrln(e)
|
||||
errorlevel = 1
|
||||
}
|
||||
finally {
|
||||
if (interactive) {
|
||||
con.move(cy + 3, 1)
|
||||
con.curs_set(1)
|
||||
}
|
||||
}
|
||||
|
||||
return errorlevel
|
||||
2468
assets/disk0/tvdos/bin/playtav.js
Normal file
2468
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\\bin\\${playerFile}.js`
|
||||
if (files.open(playerPath).exists) {
|
||||
eval(files.readText(playerPath))
|
||||
} else {
|
||||
serial.println(`Warning: Player not found: ${playerFile}`)
|
||||
}
|
||||
} else {
|
||||
serial.println(`Warning: No player found for file type: ${element.name}`)
|
||||
}
|
||||
} catch (e) {
|
||||
serial.println(`Error playing ${element.name}: ${e.message}`)
|
||||
} finally {
|
||||
// Restore original environment
|
||||
files.open = originalFilesOpen
|
||||
require = originalRequire
|
||||
exec_args = originalExecArgs
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (!playerFile) {
|
||||
serial.println(`Warning: No player found for file type: ${element.name}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Execute the appropriate player
|
||||
let playerPath = `A:\\tvdos\\bin\\${playerFile}.js`
|
||||
if (!files.open(playerPath).exists) {
|
||||
serial.println(`Warning: Player script not found: ${playerPath}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Save and modify exec_args for the player
|
||||
let originalExecArgs = [...exec_args]
|
||||
exec_args[1] = targetPath
|
||||
|
||||
try {
|
||||
eval(files.readText(playerPath))
|
||||
} catch (e) {
|
||||
serial.println(`Error playing ${element.name}: ${e.message}`)
|
||||
} finally {
|
||||
// Restore original exec_args
|
||||
exec_args = originalExecArgs
|
||||
}
|
||||
}
|
||||
|
||||
serial.println("\nUCF playback completed")
|
||||
return 0
|
||||
@@ -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)
|
||||
|
||||
|
||||
1062
assets/disk0/tvdos/bin/taut.js
Normal file
1062
assets/disk0/tvdos/bin/taut.js
Normal file
File diff suppressed because it is too large
Load Diff
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.
@@ -26,33 +26,45 @@ const COL_HL_EXT = {
|
||||
"wav": 31,
|
||||
"adpcm": 31,
|
||||
"pcm": 32,
|
||||
"mp3": 33,
|
||||
// "mp3": 33,
|
||||
"tad": 33,
|
||||
"mp2": 34,
|
||||
"mov": 213,
|
||||
"mv2": 214,
|
||||
"mv3": 214,
|
||||
"mv1": 213,
|
||||
"mv2": 213,
|
||||
"mv3": 213,
|
||||
"tav": 213,
|
||||
"ipf": 190,
|
||||
"ipf1": 190,
|
||||
"ipf2": 191,
|
||||
"ipf2": 190,
|
||||
"im3": 190,
|
||||
"tap": 190,
|
||||
"txt": 223,
|
||||
"md": 223,
|
||||
"log": 223
|
||||
"log": 223,
|
||||
"taud":109,
|
||||
}
|
||||
|
||||
const EXEC_FUNS = {
|
||||
"wav": (f) => _G.shell.execute(`playwav "${f}" -i`),
|
||||
"adpcm": (f) => _G.shell.execute(`playwav "${f}" -i`),
|
||||
"mp3": (f) => _G.shell.execute(`playmp3 "${f}" -i`),
|
||||
// "mp3": (f) => _G.shell.execute(`playmp3 "${f}" -i`),
|
||||
"mp2": (f) => _G.shell.execute(`playmp2 "${f}" -i`),
|
||||
"mov": (f) => _G.shell.execute(`playmov "${f}" -i`),
|
||||
"mv1": (f) => _G.shell.execute(`playmv1 "${f}" -i`),
|
||||
"mv2": (f) => _G.shell.execute(`playtev "${f}" -i`),
|
||||
"mv3": (f) => _G.shell.execute(`playtev "${f}" -i`),
|
||||
"mv3": (f) => _G.shell.execute(`playtav "${f}" -i`),
|
||||
"tav": (f) => _G.shell.execute(`playtav "${f}" -i`),
|
||||
"im3": (f) => _G.shell.execute(`playtav "${f}" -i`),
|
||||
"tap": (f) => _G.shell.execute(`playtav "${f}" -i`),
|
||||
"tad": (f) => _G.shell.execute(`playtad "${f}" -i`),
|
||||
"pcm": (f) => _G.shell.execute(`playpcm "${f}" -i`),
|
||||
"ipf": (f) => _G.shell.execute(`decodeipf "${f}" -i`),
|
||||
"ipf1": (f) => _G.shell.execute(`decodeipf "${f}" -i`),
|
||||
"ipf2": (f) => _G.shell.execute(`decodeipf "${f}" -i`),
|
||||
"bas": (f) => _G.shell.execute(`basic "${f}"`),
|
||||
"txt": (f) => _G.shell.execute(`less "${f}"`),
|
||||
"md": (f) => _G.shell.execute(`less "${f}"`),
|
||||
"log": (f) => _G.shell.execute(`less "${f}"`)
|
||||
"log": (f) => _G.shell.execute(`less "${f}"`),
|
||||
"taud": (f) => _G.shell.execute(`taut "${f}"`),
|
||||
}
|
||||
|
||||
let windowMode = 0 // 0 == left, 1 == right
|
||||
@@ -68,6 +80,7 @@ let cursor = [0, 0] // absolute position!
|
||||
|
||||
function bytesToReadable(i) {
|
||||
return ''+ (
|
||||
(i > 999999999) ? (((i / 10000000)|0)/100 + "G") :
|
||||
(i > 999999) ? (((i / 10000)|0)/100 + "M") :
|
||||
(i > 9999) ? (((i / 100)|0)/10 + "K") :
|
||||
i
|
||||
@@ -474,6 +487,7 @@ let filenavOninput = (window, event) => {
|
||||
|
||||
firstRunLatch = true
|
||||
con.curs_set(0);clearScr()
|
||||
refreshFilePanelCache(windowMode)
|
||||
redraw()
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
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
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
261
assets/disk0/tvdos/include/taud.mjs
Normal file
261
assets/disk0/tvdos/include/taud.mjs
Normal file
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* 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) + rsvd(2) + sig(16)
|
||||
const TAUD_SONG_ENTRY = 16 // bytes per song-table row (offset(4)+voices(1)+pats(2)+bpm(1)+tick(1)+pad(7))
|
||||
const SAMPLEINST_SIZE = 786432 // 770047 sample + 16384 instrument
|
||||
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 targetPlaydataSlot Playhead number (0-3) to configure
|
||||
*/
|
||||
function uploadTaudFile(inFile, songIndex, targetPlaydataSlot) {
|
||||
const drive = inFile[0].toUpperCase()
|
||||
const diskPath = inFile.substring(2)
|
||||
|
||||
const memBase = audio.getMemAddr()
|
||||
|
||||
// -- 1. Read whole file into VM memory ------------------------------------
|
||||
const fileHandle = files.open(inFile)
|
||||
|
||||
if (!fileHandle.exists) {
|
||||
throw Error("taud: file not exists")
|
||||
}
|
||||
|
||||
const fileSize = fileHandle.size
|
||||
const filePtr = sys.malloc(fileSize)
|
||||
fileHandle.pread(filePtr, fileSize, 0)
|
||||
|
||||
let pos = 0
|
||||
|
||||
// -- 2. Verify magic ------------------------------------------------------
|
||||
for (let i = 0; i < 8; i++) {
|
||||
let magicc = sys.peek(filePtr + i)
|
||||
if (magicc !== TAUD_MAGIC[i]) {
|
||||
sys.free(filePtr)
|
||||
throw Error("taud: bad magic byte " + magicc.toString(16) + " at index " + i)
|
||||
}
|
||||
}
|
||||
pos = 8
|
||||
|
||||
// -- 3. Parse header ------------------------------------------------------
|
||||
// version(1) + numSongs(1) + compressedSize(4) + rsvd(2) + signature(16) = 24 bytes
|
||||
let version = sys.peek(filePtr + pos) & 0xFF; pos++
|
||||
let numSongs = sys.peek(filePtr + pos) & 0xFF; pos++
|
||||
let compressedSize = _peekU32LE(filePtr, pos); pos += 4
|
||||
pos += 18 // skip reserved(2) + signature(16)
|
||||
// pos == 32 == TAUD_HEADER_SIZE
|
||||
|
||||
if (songIndex < 0 || songIndex >= numSongs) {
|
||||
sys.free(filePtr)
|
||||
throw Error("taud: songIndex " + songIndex + " out of range (numSongs=" + numSongs + ")")
|
||||
}
|
||||
|
||||
// -- 4. Decompress and upload sample+instrument bin -----------------------
|
||||
let decompPtr = sys.malloc(SAMPLEINST_SIZE)
|
||||
gzip.decompFromTo(filePtr + pos, compressedSize, decompPtr)
|
||||
pos += compressedSize
|
||||
|
||||
// Write decompressed data to peripheral memory (backwards addressing:
|
||||
// peripheral byte k lives at memBase - k).
|
||||
for (let i = 0; i < SAMPLEINST_SIZE; i++) {
|
||||
sys.poke(memBase - i, sys.peek(decompPtr + i))
|
||||
}
|
||||
sys.free(decompPtr)
|
||||
|
||||
// -- 5. Parse song-table entry for the requested song --------------------
|
||||
let entryOff = pos + songIndex * TAUD_SONG_ENTRY
|
||||
let songOffset = _peekU32LE(filePtr, entryOff)
|
||||
let numVoices = sys.peek(filePtr + entryOff + 4) & 0xFF
|
||||
let numPatsLo = sys.peek(filePtr + entryOff + 5) & 0xFF
|
||||
let numPatsHi = sys.peek(filePtr + entryOff + 6) & 0xFF
|
||||
let bpmStored = sys.peek(filePtr + entryOff + 7) & 0xFF
|
||||
let tickRate = sys.peek(filePtr + entryOff + 8) & 0xFF
|
||||
|
||||
let bpm = bpmStored + 24
|
||||
let patsToLoad = numPatsLo | (numPatsHi << 8)
|
||||
|
||||
// -- 6. Upload patterns ---------------------------------------------------
|
||||
let songBase = filePtr + songOffset
|
||||
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(songBase + p * PATTERN_SIZE + k) & 0xFF
|
||||
audio.uploadPattern(p, patBytes)
|
||||
}
|
||||
|
||||
// -- 7. Upload cue sheet --------------------------------------------------
|
||||
let cueBase = songBase + patsToLoad * PATTERN_SIZE
|
||||
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(cueBase + c * CUE_SIZE + k) & 0xFF
|
||||
audio.uploadCue(c, cueBytes)
|
||||
}
|
||||
|
||||
// -- 8. Configure playhead ------------------------------------------------
|
||||
audio.setTrackerMode(targetPlaydataSlot)
|
||||
audio.setBPM(targetPlaydataSlot, bpm)
|
||||
audio.setTickRate(targetPlaydataSlot, tickRate > 0 ? tickRate : 6)
|
||||
|
||||
|
||||
fileHandle.close()
|
||||
sys.free(filePtr)
|
||||
}
|
||||
|
||||
// ── captureTrackerDataToFile ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Dump the current tracker hardware state (sample bin, instruments, patterns
|
||||
* in bank 0, cue sheet) to a single-song Taud file. BPM and tick-rate are
|
||||
* taken from playhead 0.
|
||||
*
|
||||
* @param outFile Full path with drive letter, e.g. "A:/music/out.taud"
|
||||
*/
|
||||
function captureTrackerDataToFile(outFile) {
|
||||
const drive = outFile[0].toUpperCase()
|
||||
const diskPath = outFile.substring(2)
|
||||
|
||||
const memBase = audio.getMemAddr()
|
||||
const baseAddr = audio.getBaseAddr()
|
||||
|
||||
// -- 1. Compress sample+instrument bin ------------------------------------
|
||||
// sys.memcpy(negative_src, positive_dst, len) copies peripheral byte k from
|
||||
// (memBase - k) into (sampleInstBuf + k).
|
||||
let sampleInstBuf = sys.malloc(SAMPLEINST_SIZE)
|
||||
sys.memcpy(memBase, sampleInstBuf, SAMPLEINST_SIZE)
|
||||
|
||||
let compBuf = sys.malloc(SAMPLEINST_SIZE + 4096) // headroom for incompressible data
|
||||
let compressedSize = gzip.compFromTo(sampleInstBuf, SAMPLEINST_SIZE, compBuf)
|
||||
sys.free(sampleInstBuf)
|
||||
|
||||
// -- 2. Find last non-empty pattern in bank 0 (all-zero = uninitialized) --
|
||||
let numPatsActual = 0
|
||||
outer: for (let p = NUM_PATTERNS_MAX - 1; p >= 0; p--) {
|
||||
let patBase = 131072 + p * PATTERN_SIZE // offset within peripheral memory space
|
||||
for (let k = 0; k < PATTERN_SIZE; k++) {
|
||||
if ((sys.peek(memBase - (patBase + k)) & 0xFF) !== 0) {
|
||||
numPatsActual = p + 1
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
if (numPatsActual === 0) numPatsActual = 1 // always emit at least one pattern slot
|
||||
|
||||
let numPats = numPatsActual // Uint16, 1-65535
|
||||
let patsToSave = numPatsActual
|
||||
|
||||
// -- 3. BPM / tick-rate from playhead 0 -----------------------------------
|
||||
let bpm = audio.getBPM(0) || 125
|
||||
let tickRate = audio.getTickRate(0) || 6
|
||||
let bpmStored = (bpm - 24) & 0xFF
|
||||
|
||||
// -- 4. 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
|
||||
|
||||
// -- 5. Build header byte array (32 bytes) --------------------------------
|
||||
let sigBytes = new Array(16)
|
||||
for (let i = 0; i < 16; 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)
|
||||
(compressedSize ) & 0xFF,
|
||||
(compressedSize >>> 8) & 0xFF,
|
||||
(compressedSize >>> 16) & 0xFF,
|
||||
(compressedSize >>> 24) & 0xFF,
|
||||
// reserved (4)
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
].concat(sigBytes) // 8 + 2 + 4 + 2 + 16 = 32 bytes
|
||||
|
||||
// -- 6. Build song-table row (16 bytes) -----------------------------------
|
||||
let songTable = [
|
||||
(songOffset ) & 0xFF,
|
||||
(songOffset >>> 8) & 0xFF,
|
||||
(songOffset >>> 16) & 0xFF,
|
||||
(songOffset >>> 24) & 0xFF,
|
||||
20, // numVoices
|
||||
numPats & 0xFF, (numPats >>> 8) & 0xFF, // numPatterns Uint16 LE
|
||||
bpmStored, // BPM with −24 bias
|
||||
tickRate, // initial tick-rate
|
||||
0x00,0x4C, // basenote (0x4C00 -- A3)
|
||||
0x00,0x00,0xDC,0x43, // basefreq (440 Hz)
|
||||
0, // padding
|
||||
]
|
||||
|
||||
// -- 7. Write header (creates / truncates file) ---------------------------
|
||||
const fileHandle = files.open(outFile)
|
||||
fileHandle.bwrite(header)
|
||||
|
||||
// -- 8. Append compressed sample+inst bin ---------------------------------
|
||||
fileHandle.pwrite(compBuf, compressedSize, 32)
|
||||
sys.free(compBuf)
|
||||
|
||||
// -- 9. Write song table --------------------------------------------------
|
||||
fileHandle.bwrite(songTable)
|
||||
|
||||
// -- 10. Append pattern bin -----------------------------------------------
|
||||
let patBuf = sys.malloc(patsToSave * PATTERN_SIZE)
|
||||
sys.memcpy(memBase - 131072, patBuf, patsToSave * PATTERN_SIZE)
|
||||
fileHandle.pwrite(patBuf, patsToSave * PATTERN_SIZE, 32 + compressedSize + songTable.length)
|
||||
sys.free(patBuf)
|
||||
|
||||
// -- 11. Append cue sheet (all 1024 entries from MMIO space) --------------
|
||||
// Cue entry c, byte k is at MMIO address 32768 + c*32 + k,
|
||||
// accessed as sys.peek(baseAddr − (32768 + c*32 + k)).
|
||||
let cueBuf = sys.malloc(NUM_CUES * CUE_SIZE)
|
||||
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)
|
||||
}
|
||||
fileHandle.pwrite(cueBuf, NUM_CUES * CUE_SIZE, 32 + compressedSize + songTable.length + patsToSave * PATTERN_SIZE)
|
||||
sys.free(cueBuf)
|
||||
|
||||
|
||||
fileHandle.flush(); fileHandle.close()
|
||||
}
|
||||
|
||||
exports = { uploadTaudFile, captureTrackerDataToFile }
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* WinTex — TUI window management and renderer
|
||||
* @author CuriousTorvald
|
||||
*/
|
||||
|
||||
class WindowObject {
|
||||
|
||||
constructor(x, y, w, h, inputProcessor, drawContents, title, drawFrame) {
|
||||
|
||||
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
|
||||
|
||||
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;
|
||||
}
|
||||
200
latency_simulator_storage_device.kts
Normal file
200
latency_simulator_storage_device.kts
Normal file
@@ -0,0 +1,200 @@
|
||||
import kotlin.math.ceil
|
||||
|
||||
|
||||
object Random {
|
||||
fun uniformRand(low: Int, high: Int) = (Math.random() * (high + 1)).toInt()
|
||||
|
||||
fun triangularRand(low: Float, high: Float): Float {
|
||||
val a = (Math.random() + Math.random()) / 2.0
|
||||
return ((high - low) * a + low).toFloat()
|
||||
}
|
||||
fun gaussianRand(avg: Float, stddev: Float): Float {
|
||||
// Box-Muller transform to generate random numbers with standard normal distribution
|
||||
// This implementation uses the polar form for better efficiency
|
||||
|
||||
// We need two uniform random values between 0 and 1
|
||||
val random = kotlin.random.Random
|
||||
|
||||
// Using the polar form of the Box-Muller transformation
|
||||
var u: Double
|
||||
var v: Double
|
||||
var s: Double
|
||||
|
||||
do {
|
||||
// Generate two uniform random numbers between -1 and 1
|
||||
u = Math.random() * 2 - 1
|
||||
v = Math.random() * 2 - 1
|
||||
|
||||
// Calculate sum of squares
|
||||
s = u * u + v * v
|
||||
} while (s >= 1 || s == 0.0)
|
||||
|
||||
// Calculate polar transformation
|
||||
val multiplier = kotlin.math.sqrt(-2.0 * kotlin.math.ln(s) / s)
|
||||
|
||||
// Transform to the desired mean and standard deviation
|
||||
// We only use one of the two generated values here
|
||||
return (avg + stddev * u * multiplier).toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class SeekSimulator {
|
||||
|
||||
abstract fun computeSeekTime(currentSector: Int, targetSector: Int): Float
|
||||
|
||||
class Tape(
|
||||
val totalSectors: Int,
|
||||
val tapeLengthMeters: Float = 200f,
|
||||
val baseSeekTime: Float = 0.5f, // seconds base inertia
|
||||
val tapeSpeedMetersPerSec: Float = 2.0f, // normal speed
|
||||
) : SeekSimulator() {
|
||||
override fun computeSeekTime(currentSector: Int, targetSector: Int): Float {
|
||||
val posCurrent = (currentSector.toFloat() / totalSectors) * tapeLengthMeters
|
||||
val posTarget = (targetSector.toFloat() / totalSectors) * tapeLengthMeters
|
||||
val distance = kotlin.math.abs(posTarget - posCurrent)
|
||||
|
||||
// Inject random tape jitter
|
||||
val effectiveSpeed = tapeSpeedMetersPerSec * Random.triangularRand(0.9f, 1.1f)
|
||||
|
||||
return baseSeekTime + (distance / effectiveSpeed)
|
||||
}
|
||||
}
|
||||
|
||||
class Disc(
|
||||
val totalTracks: Int,
|
||||
val armSeekBaseTime: Float = 0.005f, // fast seek, seconds
|
||||
val armSeekMultiplier: Float = 0.002f, // slower for bigger jumps
|
||||
val rotationLatencyAvg: Float = 0.008f, // seconds (half-rotation average)
|
||||
) : SeekSimulator() {
|
||||
override fun computeSeekTime(currentSector: Int, targetSector: Int): Float {
|
||||
val cylCurrent = sectorToTrack(currentSector)
|
||||
val cylTarget = sectorToTrack(targetSector)
|
||||
val deltaTracks = kotlin.math.abs(cylTarget - cylCurrent)
|
||||
|
||||
val armSeek = armSeekBaseTime + (armSeekMultiplier * kotlin.math.sqrt(deltaTracks.toFloat()))
|
||||
val rotationLatency = Random.gaussianRand(rotationLatencyAvg, rotationLatencyAvg * 0.2f)
|
||||
|
||||
return armSeek + rotationLatency
|
||||
}
|
||||
|
||||
private fun sectorToTrack(sector: Int): Int {
|
||||
// Simplistic assumption: sector layout maps 1:1 to track at this level
|
||||
return sector % totalTracks
|
||||
}
|
||||
}
|
||||
|
||||
class Drum(
|
||||
val rpm: Float = 3000f
|
||||
) : SeekSimulator() {
|
||||
override fun computeSeekTime(currentSector: Int, targetSector: Int): Float {
|
||||
val degreesPerSector = 360.0f / 10000.0f // Assume 10k sectors per drum circumference
|
||||
val angleCurrent = currentSector * degreesPerSector
|
||||
val angleTarget = targetSector * degreesPerSector
|
||||
val deltaAngle = kotlin.math.abs(angleTarget - angleCurrent) % 360f
|
||||
|
||||
val rotationLatencySeconds = (deltaAngle / 360f) * (60f / rpm)
|
||||
|
||||
// Add a little mechanical jitter
|
||||
val jitteredLatency = rotationLatencySeconds * Random.triangularRand(0.95f, 1.05f)
|
||||
|
||||
return jitteredLatency
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SeekLatencySampler(
|
||||
val simulator: SeekSimulator,
|
||||
val totalSectors: Int,
|
||||
val sampleCount: Int = 10000
|
||||
) {
|
||||
data class Sample(val fromSector: Int, val toSector: Int, val latency: Float)
|
||||
|
||||
val samples = mutableListOf<Sample>()
|
||||
|
||||
fun runSampling() {
|
||||
samples.clear()
|
||||
var lastSector = Random.uniformRand(0, totalSectors - 1)
|
||||
|
||||
repeat(sampleCount) {
|
||||
val nextSector = Random.uniformRand(0, totalSectors - 1)
|
||||
val latency = simulator.computeSeekTime(lastSector, nextSector)
|
||||
samples.add(Sample(lastSector, nextSector, latency))
|
||||
lastSector = nextSector
|
||||
}
|
||||
}
|
||||
|
||||
fun analyzeAndPrint() {
|
||||
if (samples.isEmpty()) {
|
||||
println("No samples generated. Run runSampling() first.")
|
||||
return
|
||||
}
|
||||
|
||||
val latencies = samples.map { it.latency }
|
||||
val minLatency = latencies.minOrNull() ?: 0f
|
||||
val maxLatency = latencies.maxOrNull() ?: 0f
|
||||
val avgLatency = latencies.average().toFloat()
|
||||
val stddevLatency = kotlin.math.sqrt(latencies.map { (it - avgLatency).let { diff -> diff * diff } }.average()).toFloat()
|
||||
|
||||
println("=== Seek Latency Stats ===")
|
||||
println("Samples: $sampleCount")
|
||||
println("Min: ${"%.4f".format(minLatency)} s")
|
||||
println("Max: ${"%.4f".format(maxLatency)} s")
|
||||
println("Avg: ${"%.4f".format(avgLatency)} s")
|
||||
println("Stddev: ${"%.4f".format(stddevLatency)} s")
|
||||
|
||||
printSimpleHistogram(latencies)
|
||||
}
|
||||
|
||||
private fun printSimpleHistogram(latencies: List<Float>, bins: Int = 30) {
|
||||
val min = latencies.minOrNull() ?: return
|
||||
val max = latencies.maxOrNull() ?: return
|
||||
val binSize = (max - min) / bins
|
||||
|
||||
val histogram = IntArray(bins) { 0 }
|
||||
|
||||
latencies.forEach { latency ->
|
||||
val bin = kotlin.math.min(((latency - min) / binSize).toInt(), bins - 1)
|
||||
histogram[bin]++
|
||||
}
|
||||
|
||||
println("--- Latency Distribution ---")
|
||||
histogram.forEachIndexed { index, count ->
|
||||
val lower = min + binSize * index
|
||||
val upper = lower + binSize
|
||||
val bar = "#".repeat(count / (sampleCount / 200)) // Scale bar length
|
||||
println("${"%.4f".format(lower)} - ${"%.4f".format(upper)} s: $bar")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val tapeSimulator = SeekSimulator.Tape(
|
||||
totalSectors = 100000,
|
||||
tapeLengthMeters = 200f,
|
||||
baseSeekTime = 0.2f,
|
||||
tapeSpeedMetersPerSec = 5.0f
|
||||
)
|
||||
|
||||
val discSimulator = SeekSimulator.Disc(
|
||||
totalTracks = 3810,
|
||||
armSeekBaseTime = 0.005f,
|
||||
armSeekMultiplier = 0.002f,
|
||||
rotationLatencyAvg = 0.008f
|
||||
)
|
||||
|
||||
val drumSimulator = SeekSimulator.Drum(
|
||||
rpm = 3000f
|
||||
)
|
||||
|
||||
listOf(tapeSimulator, discSimulator, drumSimulator).forEach { sim ->
|
||||
SeekLatencySampler(
|
||||
simulator = sim,
|
||||
totalSectors = 100000,
|
||||
sampleCount = 5000
|
||||
).also {
|
||||
it.runSampling()
|
||||
it.analyzeAndPrint()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,8 @@
|
||||
## How To Edit the Graaljs Jars
|
||||
## GraalJS JAR Editing (OBSOLETE)
|
||||
|
||||
0. Download following from Maven:
|
||||
The META-INF/services cross-registration hack was needed for GraalJS 22.3.1 where
|
||||
`js` and `regex` JARs each needed the other's `TruffleLanguage$Provider` registered.
|
||||
|
||||
org.graalvm.js:js:00.0.0
|
||||
org.graalvm.js:js-scriptengine:00.0.0
|
||||
|
||||
1. grab `js-00.0.0.jar`
|
||||
2. on `META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider`, edit as shown:
|
||||
|
||||
com.oracle.truffle.js.lang.JavaScriptLanguageProvider (existing line)
|
||||
com.oracle.truffle.regex.RegexLanguageProvider (<< add this line)
|
||||
|
||||
3. grab `regex-00.0.0.jar`
|
||||
4. on `META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider`, edit as shown:
|
||||
|
||||
com.oracle.truffle.regex.RegexLanguageProvider (existing line)
|
||||
com.oracle.truffle.js.lang.JavaScriptLanguageProvider (<< add this line)
|
||||
|
||||
5. Re-zip two files
|
||||
As of GraalJS 24.1.2, the service discovery mechanism changed to
|
||||
`TruffleLanguageProvider` and each JAR registers its own provider independently.
|
||||
No JAR editing is required.
|
||||
|
||||
BIN
lib/collections-23.1.10-javadoc.jar
Normal file
BIN
lib/collections-23.1.10-javadoc.jar
Normal file
Binary file not shown.
BIN
lib/collections-23.1.10-sources.jar
Normal file
BIN
lib/collections-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/collections-23.1.10.jar
Normal file
BIN
lib/collections-23.1.10.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
lib/compiler-23.1.10-sources.jar
Normal file
BIN
lib/compiler-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/compiler-23.1.10.jar
Normal file
BIN
lib/compiler-23.1.10.jar
Normal file
Binary file not shown.
BIN
lib/compiler-management-23.1.10-javadoc.jar
Normal file
BIN
lib/compiler-management-23.1.10-javadoc.jar
Normal file
Binary file not shown.
BIN
lib/compiler-management-23.1.10-sources.jar
Normal file
BIN
lib/compiler-management-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/compiler-management-23.1.10.jar
Normal file
BIN
lib/compiler-management-23.1.10.jar
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
lib/icu4j-23.1.10-javadoc.jar
Normal file
BIN
lib/icu4j-23.1.10-javadoc.jar
Normal file
Binary file not shown.
BIN
lib/icu4j-23.1.10-sources.jar
Normal file
BIN
lib/icu4j-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/icu4j-23.1.10.jar
Normal file
BIN
lib/icu4j-23.1.10.jar
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
lib/jniutils-23.1.10-javadoc.jar
Normal file
BIN
lib/jniutils-23.1.10-javadoc.jar
Normal file
Binary file not shown.
BIN
lib/jniutils-23.1.10-sources.jar
Normal file
BIN
lib/jniutils-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/jniutils-23.1.10.jar
Normal file
BIN
lib/jniutils-23.1.10.jar
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
lib/js-language-23.1.10-sources.jar
Normal file
BIN
lib/js-language-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/js-language-23.1.10.jar
Normal file
BIN
lib/js-language-23.1.10.jar
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user