mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
Compare commits
146 Commits
libtav
...
0124b062d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0124b062d0 | ||
|
|
18881a6d16 | ||
|
|
5a4d200fdc | ||
|
|
75ddfcde0f | ||
|
|
d058f11329 | ||
|
|
60b07a325a | ||
|
|
1e482e32a8 | ||
|
|
4ff48bba1c | ||
|
|
2dcdff83c8 | ||
|
|
89d3c5d776 | ||
|
|
517d0ad9a7 | ||
|
|
9524bf36e0 | ||
|
|
8e17256224 | ||
|
|
ac409bf961 | ||
|
|
94e3ce55ce | ||
|
|
9a9893b9a3 | ||
|
|
789c78f1e7 | ||
|
|
c7e7ee650d | ||
|
|
5d968fecf5 | ||
|
|
aaf3cc28b2 | ||
|
|
24375727db | ||
|
|
6a7ef670d9 | ||
|
|
1bbf0de381 | ||
|
|
5e6ac17146 | ||
|
|
d2b1e792b9 | ||
|
|
219ca1e475 | ||
|
|
902ab00132 | ||
|
|
5dc87a80be | ||
|
|
2b91251d6e | ||
|
|
f84d317f95 | ||
|
|
f295223f15 | ||
|
|
6de9476c4f | ||
|
|
e317d79a21 | ||
|
|
fe59df18f7 | ||
|
|
a4adc428d0 | ||
|
|
31e46b78ce | ||
|
|
ac94a52329 | ||
|
|
01ff4b1d47 | ||
|
|
50802186ce | ||
|
|
7184392521 | ||
|
|
018b9f5eb3 | ||
|
|
bb0810798d | ||
|
|
909f970d60 | ||
|
|
80c26c6b35 | ||
|
|
515e0268e6 | ||
|
|
606fa736af | ||
|
|
89effb5b24 | ||
|
|
376c3c4766 | ||
|
|
0a247897e4 | ||
|
|
b838b35525 | ||
|
|
1148454fb3 | ||
|
|
cfb7b97bf0 | ||
|
|
176aa824fe | ||
|
|
d33484c3c8 | ||
|
|
737b1cebe7 | ||
|
|
176766b793 | ||
|
|
191c733913 | ||
|
|
895f1b27ef | ||
|
|
538d718568 | ||
|
|
b3c5719e3a | ||
|
|
27e4bc1ae5 | ||
|
|
2282e0c10b | ||
|
|
e7287fae37 | ||
|
|
65d89db9c6 | ||
|
|
53173a359c | ||
|
|
8d7d534bc8 | ||
|
|
dc3c73252e | ||
|
|
2053526dfa | ||
|
|
6bc49e3f0b | ||
|
|
02c4f9590c | ||
|
|
34afa95137 | ||
|
|
284108f07f | ||
|
|
76011d4fa9 | ||
|
|
b44d9c6b68 | ||
|
|
e47e9e1259 | ||
|
|
c5789ec28b | ||
|
|
93f7f436a3 | ||
|
|
3ca31e57a1 | ||
|
|
1f630aee62 | ||
|
|
3f3644d165 | ||
|
|
e29f9c3032 | ||
|
|
85b8586a3a | ||
|
|
92b9984ef8 | ||
|
|
3f98d25828 | ||
|
|
d4ea9b2d29 | ||
|
|
a1b62f3155 | ||
|
|
4802e10dfc | ||
|
|
6d70960e5c | ||
|
|
3d99568359 | ||
|
|
1fe966ca09 | ||
|
|
037b2c1a16 | ||
|
|
ea09065802 | ||
|
|
8d28bde119 | ||
|
|
755afb7df4 | ||
|
|
539df453ec | ||
|
|
5e3ffea6d3 | ||
|
|
4bda55d511 | ||
|
|
852c0e6e80 | ||
|
|
25309cf5b6 | ||
|
|
44f11120d8 | ||
|
|
bc16ffabb4 | ||
|
|
ad5e5b62bc | ||
|
|
887c2fbfba | ||
|
|
e58eb2c12b | ||
|
|
3a91edb379 | ||
|
|
74d94b350c | ||
|
|
4559e4f3f6 | ||
|
|
c5bc0d6526 | ||
|
|
a92727862e | ||
|
|
31d25d940b | ||
|
|
460046c4ed | ||
|
|
6eeccb4baa | ||
|
|
a7c44bd05f | ||
|
|
4e647f9fe1 | ||
|
|
b2faab9377 | ||
|
|
e833d75b2c | ||
|
|
f84ea5e68a | ||
|
|
5374ca43c3 | ||
|
|
bef85f6e2f | ||
|
|
f02ad1de79 | ||
|
|
7f0ff3e653 | ||
|
|
8702104bfe | ||
|
|
7d899936e2 | ||
|
|
6aa2542bb8 | ||
|
|
2ac084acd7 | ||
|
|
1208690c4f | ||
|
|
ca977b074d | ||
|
|
102801d8b0 | ||
|
|
10351bafb1 | ||
|
|
9310885260 | ||
|
|
b10d5d3a34 | ||
|
|
86b44565e0 | ||
|
|
54b61fb436 | ||
|
|
4afe3816c7 | ||
|
|
f09dd66185 | ||
|
|
b590415231 | ||
|
|
237d3d6fd2 | ||
|
|
3421d71012 | ||
|
|
5d5576c077 | ||
|
|
64026be133 | ||
|
|
96d697e158 | ||
|
|
1680137b7d | ||
|
|
c71920b95d | ||
|
|
629ed5fb12 | ||
|
|
4f6efbe000 | ||
|
|
4362610c70 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -66,3 +66,7 @@ assets/disk0/home/basic/*
|
||||
assets/disk0/movtestimg/*.jpg
|
||||
assets/disk0/*.mov
|
||||
assets/diskMediabin/*
|
||||
|
||||
video_encoder/*
|
||||
|
||||
assets/disk0/tvdos/bin/tautfont.png
|
||||
|
||||
54
.idea/artifacts/TerranBASIC.xml
generated
54
.idea/artifacts/TerranBASIC.xml
generated
@@ -21,36 +21,54 @@
|
||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-reflect/1.8.21/kotlin-reflect-1.8.21.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-test/1.8.21/kotlin-test-1.8.21.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.21/kotlin-stdlib-common-1.8.21.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/graal-sdk-22.3.1.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-71.1.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-22.3.1-edit.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-22.3.1.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-22.3.1-edit.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-22.3.1.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/polyglot-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-language-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-runtime-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-compiler-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/compiler-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/compiler-management-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/word-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/collections-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/nativeimage-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jniutils-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-1.12.1.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-22.3.1-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-22.3.1-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-language-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-language-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-1.12.1-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-1.12.1-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-71.1-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-71.1-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-openal-3.3.3.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-opengl-3.3.3.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-jemalloc-3.3.3.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-22.3.1-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-22.3.1-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/TerranVirtualDisk-src.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jorbis-0.0.17-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jorbis-0.0.17-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/graal-sdk-22.3.1-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/graal-sdk-22.3.1-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/polyglot-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/polyglot-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-runtime-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-runtime-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/collections-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/collections-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/nativeimage-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/nativeimage-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jniutils-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jniutils-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jlayer-1.0.1-gdx-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jlayer-1.0.1-gdx-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3-javadoc.jar" path-in-jar="/" />
|
||||
@@ -62,15 +80,15 @@
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-openal-3.3.3-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-opengl-3.3.3-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-opengl-3.3.3-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-22.3.1-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-22.3.1-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3-natives-windows.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-jemalloc-3.3.3-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-jemalloc-3.3.3-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-natives-linux.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-natives-macos.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-22.3.1-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-22.3.1-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3-natives-linux.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3-natives-macos.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-jnigen-loader-2.3.1-javadoc.jar" path-in-jar="/" />
|
||||
|
||||
133
.idea/cody_history.xml
generated
Normal file
133
.idea/cody_history.xml
generated
Normal file
@@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ChatHistory">
|
||||
<accountData>
|
||||
<list>
|
||||
<AccountData>
|
||||
<accountId value="VXNlcjo1NTUxMzY=" />
|
||||
<chats>
|
||||
<list>
|
||||
<chat>
|
||||
<internalId value="8a32c880-d8ec-4d84-b5f7-a62b2edf63c3" />
|
||||
<llm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-5-sonnet-20240620" />
|
||||
<provider value="Anthropic" />
|
||||
<title value="Claude 3.5 Sonnet" />
|
||||
<usage>
|
||||
<list>
|
||||
<option value="chat" />
|
||||
<option value="edit" />
|
||||
</list>
|
||||
</usage>
|
||||
</llm>
|
||||
</llm>
|
||||
</chat>
|
||||
<chat>
|
||||
<internalId value="f4e7338b-57d3-44e0-b1d9-04aa2dfb4bae" />
|
||||
<llm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-5-sonnet-20240620" />
|
||||
<provider value="Anthropic" />
|
||||
<title value="Claude 3.5 Sonnet" />
|
||||
<usage>
|
||||
<list>
|
||||
<option value="chat" />
|
||||
<option value="edit" />
|
||||
</list>
|
||||
</usage>
|
||||
</llm>
|
||||
</llm>
|
||||
</chat>
|
||||
<chat>
|
||||
<internalId value="26ac815b-5db9-488b-be0b-ab174fbc5646" />
|
||||
<llm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-5-sonnet-20240620" />
|
||||
<provider value="Anthropic" />
|
||||
<title value="Claude 3.5 Sonnet" />
|
||||
<usage>
|
||||
<list>
|
||||
<option value="chat" />
|
||||
<option value="edit" />
|
||||
</list>
|
||||
</usage>
|
||||
</llm>
|
||||
</llm>
|
||||
</chat>
|
||||
<chat>
|
||||
<internalId value="8ab39b26-cdda-4256-878f-e0416e66bbea" />
|
||||
<llm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-5-sonnet-20240620" />
|
||||
<provider value="Anthropic" />
|
||||
<title value="Claude 3.5 Sonnet" />
|
||||
<usage>
|
||||
<list>
|
||||
<option value="chat" />
|
||||
<option value="edit" />
|
||||
</list>
|
||||
</usage>
|
||||
</llm>
|
||||
</llm>
|
||||
</chat>
|
||||
<chat>
|
||||
<internalId value="f79a288a-adc5-4d45-a069-4bf024d90236" />
|
||||
<llm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-5-sonnet-20240620" />
|
||||
<provider value="Anthropic" />
|
||||
<title value="Claude 3.5 Sonnet" />
|
||||
<usage>
|
||||
<list>
|
||||
<option value="chat" />
|
||||
<option value="edit" />
|
||||
</list>
|
||||
</usage>
|
||||
</llm>
|
||||
</llm>
|
||||
</chat>
|
||||
<chat>
|
||||
<internalId value="20bc02fd-c6b5-4590-a00b-b7012a630ef4" />
|
||||
<llm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-5-sonnet-20240620" />
|
||||
<provider value="Anthropic" />
|
||||
<title value="Claude 3.5 Sonnet" />
|
||||
<usage>
|
||||
<list>
|
||||
<option value="chat" />
|
||||
<option value="edit" />
|
||||
</list>
|
||||
</usage>
|
||||
</llm>
|
||||
</llm>
|
||||
</chat>
|
||||
</list>
|
||||
</chats>
|
||||
<defaultLlm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-5-sonnet-20240620" />
|
||||
<provider value="Anthropic" />
|
||||
<tags>
|
||||
<list>
|
||||
<option value="gateway" />
|
||||
<option value="accuracy" />
|
||||
<option value="recommended" />
|
||||
<option value="free" />
|
||||
</list>
|
||||
</tags>
|
||||
<title value="Claude 3.5 Sonnet" />
|
||||
<usage>
|
||||
<list>
|
||||
<option value="chat" />
|
||||
<option value="edit" />
|
||||
</list>
|
||||
</usage>
|
||||
</llm>
|
||||
</defaultLlm>
|
||||
</AccountData>
|
||||
</list>
|
||||
</accountData>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/markdown.xml
generated
Normal file
9
.idea/markdown.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<option name="previewPanelProviderInfo">
|
||||
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||
</option>
|
||||
<option name="splitLayout" value="SHOW_EDITOR" />
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
17
.idea/runConfigurations/AppLoader.xml
generated
Normal file
17
.idea/runConfigurations/AppLoader.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="AppLoader" type="Application" factoryName="Application" nameIsGenerated="true">
|
||||
<option name="ALTERNATIVE_JRE_PATH" value="21" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.torvald.tsvm.AppLoader" />
|
||||
<module name="tsvm_executable" />
|
||||
<option name="VM_PARAMETERS" value="-ea --upgrade-module-path=lib/compiler-23.1.10.jar:lib/compiler-management-23.1.10.jar:lib/truffle-compiler-23.1.10.jar:lib/truffle-api-23.1.10.jar:lib/truffle-runtime-23.1.10.jar:lib/polyglot-23.1.10.jar:lib/collections-23.1.10.jar:lib/word-23.1.10.jar:lib/nativeimage-23.1.10.jar:lib/jniutils-23.1.10.jar -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseZGC -XX:+UseDynamicNumberOfGCThreads --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="net.torvald.tsvm.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
16
.idea/runConfigurations/TerranBASIC.xml
generated
Normal file
16
.idea/runConfigurations/TerranBASIC.xml
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="TerranBASIC" type="Application" factoryName="Application" nameIsGenerated="true">
|
||||
<option name="MAIN_CLASS_NAME" value="net.torvald.tsvm.TerranBASIC" />
|
||||
<module name="TerranBASICexecutable" />
|
||||
<option name="VM_PARAMETERS" value="--upgrade-module-path=lib/compiler-23.1.10.jar:lib/compiler-management-23.1.10.jar:lib/truffle-compiler-23.1.10.jar:lib/truffle-api-23.1.10.jar:lib/truffle-runtime-23.1.10.jar:lib/polyglot-23.1.10.jar:lib/collections-23.1.10.jar:lib/word-23.1.10.jar:lib/nativeimage-23.1.10.jar:lib/jniutils-23.1.10.jar -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="net.torvald.tsvm.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
16
.idea/runConfigurations/TsvmEmulator.xml
generated
Normal file
16
.idea/runConfigurations/TsvmEmulator.xml
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="TsvmEmulator" type="Application" factoryName="Application" nameIsGenerated="true">
|
||||
<option name="MAIN_CLASS_NAME" value="net.torvald.tsvm.TsvmEmulator" />
|
||||
<module name="tsvm_executable" />
|
||||
<option name="VM_PARAMETERS" value="-ea --upgrade-module-path=lib/compiler-23.1.10.jar:lib/compiler-management-23.1.10.jar:lib/truffle-compiler-23.1.10.jar:lib/truffle-api-23.1.10.jar:lib/truffle-runtime-23.1.10.jar:lib/polyglot-23.1.10.jar:lib/collections-23.1.10.jar:lib/word-23.1.10.jar:lib/nativeimage-23.1.10.jar:lib/jniutils-23.1.10.jar -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseZGC -XX:+UseDynamicNumberOfGCThreads --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="net.torvald.tsvm.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@@ -2,5 +2,7 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/assets/disk0/home/tetrino" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/assets/disk0/home/tvnes" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
8
2taud.sh
Executable file
8
2taud.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env fish
|
||||
|
||||
for f in *.mod; python3 mod2taud.py $f assets/disk0/(basename $f .mod).taud; end
|
||||
for f in *.s3m; python3 s3m2taud.py $f assets/disk0/(basename $f .s3m).taud; end
|
||||
for f in *.it; python3 it2taud.py $f assets/disk0/(basename $f .it).taud; end
|
||||
for f in *.xm; python3 xm2taud.py $f assets/disk0/(basename $f .xm).taud; end
|
||||
for f in *.mon; python3 mon2taud.py $f assets/disk0/(basename $f .mon).taud; end
|
||||
for f in *.MON; python3 mon2taud.py $f assets/disk0/(basename $f .MON).taud; end
|
||||
42
CLAUDE.md
42
CLAUDE.md
@@ -12,6 +12,36 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
- TerranBASIC integration
|
||||
- Multiple platform build system
|
||||
|
||||
## Documentations
|
||||
|
||||
Documentation for TSVM and TVDOS are available on `./doc/*.tex` as machine-readable format.
|
||||
|
||||
Documentatino for TSVM architecture is available on `terranmon.txt`
|
||||
|
||||
## Reference Materials
|
||||
|
||||
Third-party source-code references that inform TSVM implementations live in
|
||||
`reference_materials/<topic>/`. Each topic folder has a `README.md` that
|
||||
summarises the takeaway and points back into the verbatim source files.
|
||||
**Consult these before reimplementing tracker / codec / DSP behaviour from
|
||||
memory** — TSVM aims to match the audible behaviour of the originals.
|
||||
|
||||
Current topics:
|
||||
|
||||
- `reference_materials/tracker_filter/` — Impulse Tracker / OpenMPT / Schism
|
||||
Tracker resonant low-pass filter source. Defines the cutoff formula, the
|
||||
resonance damping curve, and the **IIR-only 2-pole topology** (NOT a
|
||||
biquad — no feedforward x[n−1] / x[n−2] terms) that `AudioAdapter.kt` uses
|
||||
for Taud playback.
|
||||
- `reference_materials/ft2-clone` — Modernised clone for the original FastTracker 2
|
||||
- `reference_materials/impulse-tracker` — The original source code for ImpulseTracker
|
||||
- `reference_materials/MilkyTracker` — FastTracker 2 compatible tracker
|
||||
- `reference_materials/schismtracker` — Open-source re-implementation of ImpulseTracker
|
||||
|
||||
When fetching new references, copy the relevant upstream files verbatim into
|
||||
a topic folder, write a `README.md` summarising the relevant maths /
|
||||
algorithms with file:line citations, and add an entry here.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
@@ -62,12 +92,12 @@ Use the build scripts in `buildapp/`:
|
||||
|
||||
### 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)
|
||||
1. Download JDK 21 runtimes to `~/Documents/openjdk/*` with specific naming:
|
||||
- `jdk-21.0.1-x86` (Linux AMD64)
|
||||
- `jdk-21.0.1-arm` (Linux Aarch64)
|
||||
- `jdk-21.0.1-windows` (Windows AMD64)
|
||||
- `jdk-21.0.1.jdk-arm` (macOS Apple Silicon)
|
||||
- `jdk-21.0.1.jdk-x86` (macOS Intel)
|
||||
|
||||
2. Run `jlink` commands to create custom Java runtimes in `out/runtime-*` directories
|
||||
|
||||
|
||||
224
README.md
224
README.md
@@ -1,8 +1,222 @@
|
||||

|
||||
|
||||
**tsvm** /tiː.ɛs.viː.ɛm/ is a virtual machine with the architecture that mimics the 8-bit era of
|
||||
computers, and runs programs written in Javascript.
|
||||
# tsvm
|
||||
|
||||
**tsvm** repository includes the virtual machine itself, the reference BIOS
|
||||
implementation and a DOS; BASIC is provided by the [TerranBASIC](https://github.com/curioustorvald/TerranBASIC)
|
||||
repository.
|
||||
**tsvm** /tiː.ɛs.viː.ɛm/ is a fantasy computer platform: a virtual machine whose
|
||||
architecture is inspired by the 8-bit and early 16-bit home computers, built
|
||||
from the ground up around running JavaScript as its native machine code.
|
||||
|
||||
What started as "an 8-bit-flavoured VM that runs JS" has grown into a complete,
|
||||
self-hosted retro computing ecosystem — with its own BIOS, operating system,
|
||||
filesystem, video and audio codecs, video display coprocessor with its own
|
||||
assembly language, tracker music format, and a stack of userland tools that
|
||||
together come closer to a small alternate-history computer line than a
|
||||
single-binary emulator.
|
||||
|
||||
This repository contains the virtual machine core, the reference BIOS
|
||||
implementations, the **TVDOS** operating system, the **Videotron2K** video
|
||||
display controller, hardware-accelerated codec backends for the **TEV / TAV /
|
||||
TAD** media formats, and the multi-platform packaging scripts. The
|
||||
[TerranBASIC](https://github.com/curioustorvald/TerranBASIC) repository
|
||||
provides the matching BASIC dialect that ships on the system disk.
|
||||
|
||||
## What's actually in here
|
||||
|
||||
### The virtual machine
|
||||
|
||||
- **VM core** (`tsvm_core/`) — memory model, peripheral bus, MMIO, JS
|
||||
sandboxing through GraalVM, watchdog, DMA engine, and cooperative scheduling.
|
||||
Up to 8 hot-pluggable peripheral slots, each with a dedicated MMIO window
|
||||
and memory-space window mapped into the VM's negative address range.
|
||||
- **Multiple BIOS implementations** (`assets/bios/`) — including the reference
|
||||
`tsvmbios.js`, an OpenBIOS variant, the TBM-BIOS for TerranBASIC machines,
|
||||
and the Pip-Boy-style `pipboot.rom`. BIOSes are first-class swappable
|
||||
components, not a fixed boot blob.
|
||||
- **Reference monitor / debugger** (`mon.js`) for poking at memory and
|
||||
peripherals from a running machine.
|
||||
- **Multi-platform packaging** (`buildapp/`) — scripts to produce Linux x86_64
|
||||
/ ARM64 AppImages, macOS Intel / Apple Silicon bundles, and Windows builds,
|
||||
each with its own `jlink`-trimmed JDK 21 runtime.
|
||||
|
||||
### Peripherals (the "hardware")
|
||||
|
||||
Living under `tsvm_core/src/net/torvald/tsvm/peripheral/`:
|
||||
|
||||
- **Graphics adapters** — the standard `GraphicsAdapter`, plus `TexticsAdapter`
|
||||
for text-mode framebuffers, `ExtDisp` for external displays, and a
|
||||
`RemoteGraphicsAdapter` for networked rendering.
|
||||
- **Audio devices** — `AudioAdapter` (the main programmable sound chip with
|
||||
PCM channels, an Impulse Tracker-style resonant low-pass filter, and a
|
||||
hardware-accelerated **TAD** decoder), `OpenALBufferedAudioDevice`, and the
|
||||
`MP2Env` MPEG audio environment.
|
||||
- **Disk drives** — `TevdDiskDrive` (TEVD custom filesystem),
|
||||
`ClusteredDiskDrive`, `TestDiskDrive`, and a latency-simulator script for
|
||||
testing slow-storage behaviour.
|
||||
- **Networking and serial** — `HttpModem`, `HSDPA` / `HostFileHSDPA` for
|
||||
high-speed packet I/O, `SerialStdioHost`, `BlockTransferInterface` /
|
||||
`BlockTransferPort`.
|
||||
- **Terminals and displays** — `TTY`, `GlassTty`, `TermSim`, and a
|
||||
`CharacterLCDdisplay` for HD44780-flavoured projects.
|
||||
- **Memory expansion** — `RamBank` for bank-switched memory, plus a
|
||||
programmable `TestFunctionGenerator`.
|
||||
|
||||
### Videotron2K — the video coprocessor
|
||||
|
||||
Videotron2K is a programmable video display controller with its **own
|
||||
assembly-like language**, six general registers (`r1`–`r6`), special registers
|
||||
(`tmr`, `frm`, `px`, `py`, `c1`–`c6`), a scene-based programming model, and
|
||||
conditional postfixes (`zr`, `nz`, `gt`, `ls`, `ge`, `le`). Programs declare
|
||||
`SCENE` blocks and dispatch them with `perform`. Drawing primitives include
|
||||
`plot`, `fillin`, `fillscr`, and `goto`. See `Videotron2K.md` and the VDC
|
||||
implementation under `tsvm_core/.../vdc/`.
|
||||
|
||||
### TVDOS — the operating system
|
||||
|
||||
`assets/disk0/tvdos/` is a complete DOS-style userland:
|
||||
|
||||
- **Kernel and drivers** — `TVDOS.SYS`, `HSDPADRV.SYS`, `hyve.SYS`,
|
||||
installable drivers under `moviedev/` and `tuidev/`.
|
||||
- **Custom filesystem** — TEVD, with the on-disk format documented in
|
||||
`tvdos/filesystem.md`.
|
||||
- **Internationalisation** — Colemak / Dvorak / QWERTY keymaps and an `i18n/`
|
||||
resource tree.
|
||||
- **Userland binaries** (`tvdos/bin/`) — a shell (`command.js`), file tools
|
||||
(`hexdump`, `less`, `tee`, `touch`, `printfile`, `writeto`, `defrag`,
|
||||
`lfs`, `drives`), an editor (`edit.js`), a file manager (`zfm.js`), a
|
||||
network fetcher (`geturl`), gzip/Zstd helpers, palette tools, and a battery
|
||||
of media players (`playmp2`, `playpcm`, `playwav`, `playmv1`, `playtev`,
|
||||
`playtav`, `playtad`, `playucf`).
|
||||
- **Taut tracker** — a full in-VM tracker (`taut.js`,
|
||||
`taut_instredit.js`, `taut_sampleedit.js`, `taut_notationedit.js`,
|
||||
`taut_fileop.js`) with its own font and chrome assets.
|
||||
|
||||
### Codecs and media formats
|
||||
|
||||
tsvm ships a small but serious codec lab. Encoders are written in C and live
|
||||
in `video_encoder/`; decoders are split between JavaScript players in TVDOS
|
||||
and hardware-accelerated Kotlin backends in the VM core.
|
||||
|
||||
- **iPF (Type 1 / 2 / 1-delta)** — picture and legacy movie format. Encoders:
|
||||
`encodeipf.js`, `encodemov.js`, `encodemov2.js`. Documented in
|
||||
`terranmon.txt`.
|
||||
- **TEV (TSVM Enhanced Video)** — modern DCT codec with motion compensation,
|
||||
16×16 blocks, YCoCg-R 4:2:0, and either quality-mode or bitrate-mode rate
|
||||
control. Encoder: `video_encoder/encoder_tev.c`. Decoder: `playtev.js`,
|
||||
with `tevDecode` / `tevIdct8x8` / `tevMotionCopy8x8` accelerated in
|
||||
`GraphicsJSR223Delegate.kt`.
|
||||
- **TAV (TSVM Advanced Video)** — successor to TEV based on the Discrete
|
||||
Wavelet Transform. Five wavelet types (5/3 reversible, 9/7 irreversible,
|
||||
CDF 13/7, DD-4, Haar), 6-level decomposition, EZBC sparsity coding,
|
||||
perceptual quantisation, and an optional **3D temporal DWT** that encodes
|
||||
whole groups of pictures as one unified wavelet tree. Includes a packet
|
||||
inspector (`tav_inspector.c`) and coefficient visualiser
|
||||
(`tav_visualise_coefficients.c`).
|
||||
- **TAD (TSVM Advanced Audio)** — perceptual audio codec at 32 kHz stereo,
|
||||
using CDF 9/7 wavelets, M/S decorrelation, gamma compression, pre-emphasis,
|
||||
EZBC, and Zstd. Achieves ~2.5:1 compression vs. PCMu8 at quality 3 while
|
||||
preserving the full 0–16 kHz band. Designed to be embeddable inside TAV so
|
||||
audio chunks can align with video GOP boundaries.
|
||||
- **Taud** — tracker module format with conversion tools from
|
||||
the major formats: `it2taud.py` (Impulse Tracker), `mod2taud.py`
|
||||
(ProTracker / FastTracker), `s3m2taud.py` (Scream Tracker 3), plus
|
||||
`2taud.sh` and shared helpers in `taud_common.py`. Note effects are
|
||||
documented in `TAUD_NOTE_EFFECTS.md`. The `AudioAdapter` runs the same
|
||||
IIR-only 2-pole resonant low-pass topology used by Impulse Tracker /
|
||||
OpenMPT / Schism.
|
||||
- **MP2** — reference MPEG-1 Layer II environment via `MP2Env.kt` and
|
||||
`playmp2.js`.
|
||||
|
||||
### Languages and runtimes
|
||||
|
||||
- **JavaScript** is the VM's native code, executed by GraalVM in a sandboxed
|
||||
context with a curated set of host bindings (graphics, audio, filesystem,
|
||||
DMA, compression, networking, low-level peek/poke).
|
||||
- **TerranBASIC** is provided by the
|
||||
[TerranBASIC](https://github.com/curioustorvald/TerranBASIC) repository and
|
||||
shipped as `tbas` on the system disk. The `TerranBASICexecutable/` subproject
|
||||
packages a BASIC-only flavour of the machine.
|
||||
- **Videotron2K assembly** for VDC programs.
|
||||
|
||||
### Documentation
|
||||
|
||||
- `terranmon.txt` — the architecture reference (memory map, peripheral
|
||||
protocol, codec bitstreams).
|
||||
- `doc/*.tex` — machine-readable LaTeX sources for the TSVM and TVDOS manuals,
|
||||
built with `doc/makepdf.sh`.
|
||||
- `Videotron2K.md` — VDC programming guide.
|
||||
- `TAUD_NOTE_EFFECTS.md` — tracker effect reference.
|
||||
- `CLAUDE.md` — a condensed map of the project for collaborators (and
|
||||
language-model assistants) working in the tree.
|
||||
|
||||
## Building and running
|
||||
|
||||
### Prerequisites
|
||||
|
||||
JDK 21 runtimes laid out under `~/Documents/openjdk/` with platform-specific
|
||||
names:
|
||||
|
||||
- `jdk-21.0.1-x86` — Linux AMD64
|
||||
- `jdk-21.0.1-arm` — Linux Aarch64
|
||||
- `jdk-21.0.1-windows` — Windows AMD64
|
||||
- `jdk-21.0.1.jdk-x86` — macOS Intel
|
||||
- `jdk-21.0.1.jdk-arm` — macOS Apple Silicon
|
||||
|
||||
`jlink` is then used to produce trimmed runtimes under `out/runtime-*`.
|
||||
|
||||
### Common entry points
|
||||
|
||||
- **Run the emulator** — `TsvmEmulator.java` (in `tsvm_executable/`).
|
||||
- **Run TerranBASIC-only build** — `TerranBASIC.java` (in
|
||||
`TerranBASICexecutable/`).
|
||||
- **Package an installable bundle** — pick the right script in `buildapp/`:
|
||||
- `build_app_linux_x86.sh`
|
||||
- `build_app_linux_arm.sh`
|
||||
- `build_app_mac_x86.sh`
|
||||
- `build_app_mac_arm.sh`
|
||||
- `build_app_windows_x86.sh`
|
||||
- **Build C encoders** — in `video_encoder/`: `make` (TEV), `make tav`,
|
||||
`make tad`.
|
||||
|
||||
### Encoding sample media
|
||||
|
||||
```bash
|
||||
# Quality-mode TEV encode
|
||||
./encoder_tev -i input.mp4 -o clip.tev -q 3
|
||||
|
||||
# TAV with 9/7 wavelet, quality 4
|
||||
./encoder_tav -i input.mp4 -w 1 -q 4 -o clip.tav
|
||||
|
||||
# TAV with 3D temporal DWT (GOP-unified encoding)
|
||||
./encoder_tav -i input.mp4 --temporal-dwt -o clip.tav
|
||||
|
||||
# TAD audio at the highest quality
|
||||
./encoder_tad -i input.mp4 -o track.tad -q 5
|
||||
```
|
||||
|
||||
Then, inside TVDOS:
|
||||
|
||||
```
|
||||
A:\> playtev clip.tev
|
||||
A:\> playtav clip.tav
|
||||
A:\> playtad track.tad
|
||||
```
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
tsvm_core/ VM core, peripherals, VDC, JS bindings (Kotlin)
|
||||
tsvm_executable/ Main emulator GUI (LibGDX)
|
||||
TerranBASICexecutable/ For creatingTerranBASIC executable
|
||||
assets/bios/ BIOS ROMs and source
|
||||
assets/disk0/ Boot disk image, including all of TVDOS
|
||||
video_encoder/ C encoders, decoder libs, inspectors (TEV / TAV / TAD)
|
||||
ipf_encoder/ Reference iPF encoder
|
||||
doc/ LaTeX sources for the TSVM / TVDOS manuals
|
||||
buildapp/ Per-platform packaging scripts
|
||||
My_BASIC_Programs/ Example BASIC programs
|
||||
*.py, *.sh, *.kts Conversion tools and ad-hoc utilities
|
||||
```
|
||||
|
||||
## Licence
|
||||
|
||||
See `COPYING`.
|
||||
|
||||
1100
TAUD_NOTE_EFFECTS.md
Normal file
1100
TAUD_NOTE_EFFECTS.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -122,8 +122,39 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
|
||||
private var rebootRequested = false
|
||||
|
||||
private fun reboot() {
|
||||
vmRunner.close()
|
||||
coroutineJob.interrupt()
|
||||
// Order is critical: stop ALL execution first, then dispose peripherals
|
||||
// before re-initialising. Without this, the old JS thread races the new
|
||||
// one on shared VM memory / IO state and can SIGSEGV on disposed peripherals.
|
||||
|
||||
// 1. Stop parallel/child contexts. park() interrupts and joins them.
|
||||
vm.park()
|
||||
vm.poke(-90L, -128)
|
||||
|
||||
// 2. Interrupt the main runner thread and cancel the GraalVM context.
|
||||
if (::coroutineJob.isInitialized) coroutineJob.interrupt()
|
||||
try { if (::vmRunner.isInitialized) vmRunner.close() } catch (_: Throwable) {}
|
||||
|
||||
// 3. Wait for the main runner thread to actually finish.
|
||||
if (::coroutineJob.isInitialized && coroutineJob !== Thread.currentThread()) {
|
||||
try {
|
||||
coroutineJob.join(2000L)
|
||||
if (coroutineJob.isAlive) {
|
||||
System.err.println("[VMGUI] runner ${vm.id} did not exit within 2s; proceeding anyway")
|
||||
coroutineJob.interrupt()
|
||||
}
|
||||
}
|
||||
catch (_: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Now it's safe to release native resources held by peripherals.
|
||||
for (i in 1 until vm.peripheralTable.size) {
|
||||
try {
|
||||
vm.peripheralTable[i].peripheral?.dispose()
|
||||
}
|
||||
catch (_: Throwable) {}
|
||||
}
|
||||
|
||||
vm.init()
|
||||
init()
|
||||
|
||||
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
con.reset_graphics();con.curs_set(0);con.clear();
|
||||
graphics.resetPalette();graphics.setBackground(0,0,0);
|
||||
graphics.resetPalette();graphics.setPalette(0, 0, 0, 0, 15);graphics.setBackground(0,0,0);
|
||||
|
||||
let logo = gzip.decomp(base64.atob("H4sICJoBTGECA3Rzdm1sb2dvLnJhdwDtneu2nCoQhPf7v6xLEMUL5lxyVk6yhxm7mmZGpfqnK7uC+gkN1TA/fhTFF+Ni8eOjwedPXsgLeSEvDPLCIC8M8sIgL+SFvJAX8kJeGOSFQV4Y5IVBXsgLeSEv5IW8MMgLow1e1i4XfH/kJR8deSEvcl48eSEvAC+RvJAXgJedvJAXOS9DR17Ii5yXSF7IC8DLTl7Ii5yX0JEX8iLnZSUv5EXOy7Nsl7yQF6h7IS/kBcheyAt5eYx+Jy/kRc7L0pEX8iLmZezIC3kR8zJ05IW8iHnxO3khL2JeDnAhL+Tlj8HoABfyQl6kqS55IS9/rrssHXkhL1Jewt6RF/Ii5GVYO4vYctouxGVLe2cXXvHg3TeN3eeu6rR9lRafl5ewGr3I6RHEOXXmMSse/PeSwTV7Vac9V2nxSXkZotmnv/ffvulYAZZ//h8HP/f+e0tC9qpK2+01WnxSXtZq372bu1oxwc/9u+mesld12lOVFp+Ul65SXtHHrl5s8HNfs+9vNdHeqrT4/rz8/kxC6mrGUJiR/hwfvIn2UKXFDfAyIhlgWSyFGenyopWo9lKlxffn5f9s122VcUHzx4casCF7VaXt9hotboCX+OsJpq56ROipj9mRczTRjlVa3AAvTmhym0QqykjHl3kqpp2qtPj+vKxY/1waoSAj/TlyDibaoUqLG+AlvG8w+h1PTUY6H+SpiPZapcX35yX18sWIN5tIDz2eP+oH5dq+Sosb4GV6z0RaY8lM2Q99MtGeq7S4AV4cOJqbm1XyjDQc5qli7X6v0uL787J8PfHv6sVobh3h2mOVFjfAi4fWIt5qIq3ZhZDVRHur0uL787J95auPTmAiPSwHOckikUx7qNLiBngZ35zsApZMzP5VNNFeqrT4/rz8zOTe3L3ILBnIOgK14aVJ3ES6Jy/z+7OX3+bwmHXUy/JUifZUpcUN8OIhJ+WtJhJmHWHaqUqL78/Lqkr+3mIi+ezI6U20Q5UWN8BL+ES2K7Nk5uzIOZtor1VafH9e/rOO0vt56RyakXp5nnqoXaXFDfAyfWLx5fe1N3lGugF5agQn6jYtboCXt1tHj664NCMdgZ7wQFvpfaS+dV6Wr8/MpgWWzJB9WYOJ9lilxQ3wMujWOt9hIi3ZwWAx0d6qtPj+vGyFz89k6UeY7TpsVdYbFUrJVS+wfxrBp2DxalIUf0gwXMytI5n2Ujp+t87LbrsQLk0TXlkye3adSG76vNAuqGqHTKT78vL6L3stL4cvZpIXSvXoPG4ytI503w55QeNoLTaJh7IJzrOSoXWkM5E4HqFxmFgO5tbRsXaZVzaQl2r57rFNswo7pkXhcq2G1pHKRLovL2Xz6T1tSwxOZQM7WaGUhwv6n2qXeh+OvNis16V5wBfeo6xQSrUqGw2tI42JdF9erPyAFB2onLdkZIVSq0b7kOBN1eK2eDH0G2eH9f5BkJHm99jvXqN9eKuDRrUxXkzrGWKPDHWr2jqKKu2jTmlRqTbGi229VArI7NVrC6W8Rlsww1eoNseLcT3mDKA4H2ZT69OruLZkBRFXbY4X63rvzYlX3x93ssv22AeNdi9xKPAWN8eLeQFvcmoTSWYd/XsV1j5EwZXZXs3wYl5ht3vpELAdZKTTi6uo9iYaalDVBnmxr/j+Zf2DJpLPLqjmr6LawlRWbXu1w0uFHUi/hiSsbEpWKLWotBdhx1FS6NUILxW2lGzS6mr3KiMdnl9FtQ/vcdSotslLjT0CMzApwayjDZrwwFO13iTjvTcvNc4jC7iJJLOORo1BBZifOturKV5qbFr777ECRo/QOurlC7ZBfoNeo9osLzU23Ue0bEp2PPOsKslCire0hV4t8VJjG5LDvmyxdfSF9xpQnwH0Re3yUuE8+BkzkWTHM6/Q0vSsKj43MJFuz0uN35tw0MxEbh3Bsx5wzmNgIt2flwq/ZxNlII7ZbDe/x/7b5ESoDW6eE6o2zov9kJSQlVXZ8cwRrD7eVGu20rXgtnmx/z2+QebcDLn1V/f19CriCg3SfwSrkpdatVOSzxuzjuTzukXVXRSbSI3wYvx7wklmyfydPz6svw7ZVdnhcPtJThtPRwSq5OXnVMLUS3LS6cmYJW18Oe2VaiumO8UmUjO8/J0zGA5KQbj80cv22E+KITT1muWUY1Xy8j8x0WpUisLl1Sk7wfWvp71C7cMO02tUA3n5Y4YwmyCzCC2ZlP3kZ9G66pH20dCymp4W0Cgv//QyIS5bKlvE25T+t3++897cWw86VUde8OgnoS+TFJhNwlWysp4wKVUjedHEa2B2XQXfUaGUZXVgVKq+znjJy7MeRvY/O/wHWQfpmkeRU/r0FMMyE+navPQf5wU6ZubZHvtnUXKEzaJWXa/MS61T6KzGI2jXrc9aR77Kjt5Br+ovzEu1U+iM8l2kgO/5Hnv74sCtQHW+MC8fOtUdeB3yk29D1joK6k5O2/OWlE2dnZflnLwsgCXzZ58UhNNeTBvyDUtMpLPzEs/JS1TUSrzaY29dhzEXqW7X5SWck5eAWDKwdQRrQylr0d77s/PizsmLw3Os/PHMS5X8bStUXS7Ly0d+tRNca5edoft6j/2z0P1q2lio+rzXOz0v8xl5mfGs9GCPvWnGe1gld6gaL8vLcEZeBjwpx6yjsoQ/Fqumy/JyxgEp4UkWaB2VJXCuXDVclpcTzqgjWoQk2WP/LPCfHlkNVNfL8nLCGZLDZ/2odVSyohAMVHd/VV7Ol/E+9gqHpdcpuxAvOoUdPvNIdO5Pr9x7fwFe3Om7F6ElA1lHehNpMlF9klpdgJezZTBRw/SIWkf678XZqI6X5aU/1RQp391LtqauAvDKPdfFSHW7LC/nMpGC1pIBrSOtieStVIfL8nKmlHdWWzJR2RFgJtJmprpcl5fzlE1takvGJ8n3W2wijWaq2f7vIry4k6QwyaktmUXdESAm0t7bqU7X5aXGKXQaI8/ZjZnyjgDRng1V04V5qXAKnQIXb1fatCOV6nJtb6kaLszLCYak5AyNHqQjkGuvpqrrlXmxP4UOTXWd5azfQ/cu1Q6mqpnh90K8fHhafdghQMuKG3bnQu3U26rGa/NifAodNBYJvlzE6Angncu0J2PVxyTrWrwYn0IHeEaSDxcwenZ0X6ZM21mrjhfnxfYUOvFQJHwPcqMnwvct0V7MVbfL82J5Cp1sJIrir1Zca7w7+K4l2oO9qr8+L19mp9AJYJmhdyCdwa2Kez7W3iqozrfg5cvmFLpXPUDalhjQbkBq9ATFDR9rjxVUv/eEl+WF8ZEgLwzywiAvDPLC509eyAt5IS8M8sIgLwzywiAv5IW8kBfyQl4Y5IVBXhjkhUFeyAt5IS/khbwwyAuDvDDIC+OWvPwFgd7gz8BmAQA="));
|
||||
|
||||
@@ -77,7 +77,7 @@ tmr = sys.nanoTime();
|
||||
while (sys.nanoTime() - tmr < 2147483648) sys.spin();
|
||||
// clear screen
|
||||
graphics.clearPixels(255);con.color_pair(239,255);
|
||||
con.clear();con.move(1,1);
|
||||
con.clear();con.move(1,1);graphics.resetPalette();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -5,5 +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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 + ")");
|
||||
@@ -147,7 +147,7 @@ _TVDOS.variables = {
|
||||
LANG: "EN",
|
||||
KEYBOARD: "us_qwerty",
|
||||
PATH: "\\tvdos\\bin;\\home",
|
||||
PATHEXT: ".com;.bat;.app;.js",
|
||||
PATHEXT: ".com;.bat;.app;.js;.alias",
|
||||
HELPPATH: "\\tvdos\\help",
|
||||
OS_NAME: "TSVM Disk Operating System",
|
||||
OS_VERSION: _TVDOS.VERSION
|
||||
@@ -225,8 +225,9 @@ class TVDOSFileDescriptor {
|
||||
}
|
||||
|
||||
/** reads the file bytewise and puts it to the specified memory address
|
||||
* @param count optional -- how many bytes to read
|
||||
* @param offset optional -- how many bytes to skip initially
|
||||
* @param ptr -- where the bytes should be dumped
|
||||
* @param count -- how many bytes to read
|
||||
* @param offset -- how many bytes to skip initially from the file
|
||||
*/
|
||||
pread(ptr, count, offset) {
|
||||
this.driver.pread(this, ptr, count, offset)
|
||||
@@ -241,7 +242,9 @@ class TVDOSFileDescriptor {
|
||||
}
|
||||
|
||||
/** writes the bytes stored in the memory[ptr .. ptr+count-1] to file[offset .. offset+count-1]
|
||||
* - @param offset is optional
|
||||
* @param ptr -- where the bytes are
|
||||
* @param count -- how many bytes to write
|
||||
* @param offset -- position in the file
|
||||
*/
|
||||
pwrite(ptr, count, offset) {
|
||||
this.driver.pwrite(this, ptr, count, offset)
|
||||
@@ -420,11 +423,11 @@ _TVDOS.DRV.FS.SERIAL.sread = (fd) => {
|
||||
}
|
||||
_TVDOS.DRV.FS.SERIAL.bread = (fd) => {
|
||||
let str = _TVDOS.DRV.FS.SERIAL.sread(fd)
|
||||
let bytes = new Int8Array(str.length)
|
||||
let bytes = []//new Int8Array(str.length)
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
// let p = str.charCodeAt(i)
|
||||
// bytes[i] = (p > 127) ? p - 255 : p
|
||||
bytes[i] = str.charCodeAt(i)
|
||||
bytes.push(str.charCodeAt(i))
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
@@ -870,11 +873,21 @@ Object.freeze(_TVDOS.DRV.FS.DEVPT)
|
||||
_TVDOS.DRV.FS.DEVFBIPF = {}
|
||||
|
||||
_TVDOS.DRV.FS.DEVFBIPF.pwrite = (fd, infilePtr, count, _2) => {
|
||||
let decodefun = ([graphics.decodeIpf1, graphics.decodeIpf2])[sys.peek(infilePtr + 13)]
|
||||
let flags = sys.peek(infilePtr+12)
|
||||
let ipfType = sys.peek(infilePtr+13)
|
||||
let isProgressive = (flags & 0x80) != 0
|
||||
let hasAlpha = (flags & 0x01) != 0
|
||||
|
||||
// Select decode function based on type and progressive flag
|
||||
let decodefun
|
||||
if (isProgressive) {
|
||||
decodefun = ([graphics.decodeIpf1Progressive, graphics.decodeIpf2Progressive])[ipfType]
|
||||
} else {
|
||||
decodefun = ([graphics.decodeIpf1, graphics.decodeIpf2])[ipfType]
|
||||
}
|
||||
|
||||
let width = sys.peek(infilePtr+8) | (sys.peek(infilePtr+9) << 8)
|
||||
let height = sys.peek(infilePtr+10) | (sys.peek(infilePtr+11) << 8)
|
||||
let hasAlpha = (sys.peek(infilePtr+12) != 0)
|
||||
let ipfType = sys.peek(infilePtr+13)
|
||||
let imgLen = sys.peek(infilePtr+24) | (sys.peek(infilePtr+25) << 8) | (sys.peek(infilePtr+26) << 16) | (sys.peek(infilePtr+27) << 24)
|
||||
|
||||
let ipfbuf = sys.malloc(imgLen)
|
||||
@@ -1014,136 +1027,6 @@ _TVDOS.DRV.FS.NET.exists = (fd) => {
|
||||
return (0 == com.getStatusCode(port[0]))
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
// Legacy Serial filesystem, !!pending for removal!!
|
||||
|
||||
|
||||
/*const filesystem = {};
|
||||
|
||||
filesystem._toPorts = (driveLetter) => {
|
||||
if (driveLetter.toUpperCase === undefined) {
|
||||
throw Error("'"+driveLetter+"' (type: "+typeof driveLetter+") is not a valid drive letter");
|
||||
}
|
||||
var port = _TVDOS.DRIVES[driveLetter.toUpperCase()];
|
||||
if (port === undefined) {
|
||||
throw Error("Drive letter '" + driveLetter.toUpperCase() + "' does not exist");
|
||||
}
|
||||
return port
|
||||
};
|
||||
filesystem._close = (portNo) => {
|
||||
com.sendMessage(portNo, "CLOSE")
|
||||
}
|
||||
filesystem._flush = (portNo) => {
|
||||
com.sendMessage(portNo, "FLUSH")
|
||||
}
|
||||
|
||||
// @return disk status code (0 for successful operation)
|
||||
// throws if:
|
||||
// - java.lang.NullPointerException if path is null
|
||||
// - Error if operation mode is not "R", "W" nor "A"
|
||||
filesystem.open = (driveLetter, path, operationMode) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
|
||||
filesystem._flush(port[0]); filesystem._close(port[0]);
|
||||
|
||||
var mode = operationMode.toUpperCase();
|
||||
if (mode != "R" && mode != "W" && mode != "A") {
|
||||
throw Error("Unknown file opening mode: " + mode);
|
||||
}
|
||||
|
||||
com.sendMessage(port[0], "OPEN"+mode+'"'+path+'",'+port[1]);
|
||||
return com.getStatusCode(port[0]);
|
||||
};
|
||||
filesystem.getFileLen = (driveLetter) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "GETLEN");
|
||||
var response = com.getStatusCode(port[0]);
|
||||
if (135 == response) {
|
||||
throw Error("File not opened");
|
||||
}
|
||||
if (response < 0 || response >= 128) {
|
||||
throw Error("Reading a file failed with "+response);
|
||||
}
|
||||
return Number(com.pullMessage(port[0]));
|
||||
};
|
||||
// @return the entire contents of the file in String
|
||||
filesystem.readAll = (driveLetter) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "READ");
|
||||
var response = com.getStatusCode(port[0]);
|
||||
if (135 == response) {
|
||||
throw Error("File not opened");
|
||||
}
|
||||
if (response < 0 || response >= 128) {
|
||||
throw Error("Reading a file failed with "+response);
|
||||
}
|
||||
return com.pullMessage(port[0]);
|
||||
};
|
||||
filesystem.readAllBytes = (driveLetter) => {
|
||||
var str = filesystem.readAll(driveLetter);
|
||||
var bytes = new Uint8Array(str.length);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
bytes[i] = str.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
filesystem.write = (driveLetter, string) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "WRITE"+string.length);
|
||||
var response = com.getStatusCode(port[0]);
|
||||
if (135 == response) {
|
||||
throw Error("File not opened");
|
||||
}
|
||||
if (response < 0 || response >= 128) {
|
||||
throw Error("Writing a file failed with "+response);
|
||||
}
|
||||
com.sendMessage(port[0], string);
|
||||
filesystem._flush(port[0]); filesystem._close(port[0]);
|
||||
};
|
||||
filesystem.writeBytes = (driveLetter, bytes) => {
|
||||
var string = btostr(bytes); // no spreading: has length limit
|
||||
filesystem.write(driveLetter, string);
|
||||
};
|
||||
filesystem.isDirectory = (driveLetter) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "LISTFILES");
|
||||
var response = com.getStatusCode(port[0]);
|
||||
return (response === 0);
|
||||
};
|
||||
filesystem.mkDir = (driveLetter) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "MKDIR");
|
||||
var response = com.getStatusCode(port[0]);
|
||||
|
||||
if (response < 0 || response >= 128) {
|
||||
var status = com.getDeviceStatus(port[0]);
|
||||
throw Error("Creating a directory failed with ("+response+"): "+status.message+"\n");
|
||||
}
|
||||
return (response === 0); // possible status codes: 0 (success), 1 (fail)
|
||||
};
|
||||
filesystem.touch = (driveLetter) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "TOUCH");
|
||||
var response = com.getStatusCode(port[0]);
|
||||
return (response === 0);
|
||||
};
|
||||
filesystem.mkFile = (driveLetter) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "MKFILE");
|
||||
var response = com.getStatusCode(port[0]);
|
||||
return (response === 0);
|
||||
};
|
||||
filesystem.remove = (driveLetter) => {
|
||||
var port = filesystem._toPorts(driveLetter);
|
||||
com.sendMessage(port[0], "DELETE");
|
||||
var response = com.getStatusCode(port[0]);
|
||||
return (response === 0);
|
||||
};
|
||||
Object.freeze(filesystem);*/
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const files = {}
|
||||
|
||||
@@ -35,7 +35,7 @@ function print_prompt_text() {
|
||||
print(" "+CURRENT_DRIVE+":")
|
||||
con.color_pair(161,253)
|
||||
con.addch(16);con.curs_right()
|
||||
con.color_pair(0,253)
|
||||
con.color_pair(240,253)
|
||||
print(" \\"+shell_pwd.join("\\").substring(1)+" ")
|
||||
if (errorlevel != 0 && errorlevel != "undefined" && errorlevel != undefined) {
|
||||
con.color_pair(166,253)
|
||||
@@ -61,7 +61,7 @@ function greet() {
|
||||
con.clear()
|
||||
con.color_pair(253,255)
|
||||
print(' ');con.addch(17);con.curs_right()
|
||||
con.color_pair(0,253)
|
||||
con.color_pair(240,253)
|
||||
print(" ".repeat(greetLeftPad)+welcome_text+" ".repeat(greetRightPad))
|
||||
con.color_pair(253,255)
|
||||
con.addch(16);con.curs_right();print(' ')
|
||||
@@ -753,6 +753,25 @@ shell.execute = function(line) {
|
||||
shell.execute(line)
|
||||
})
|
||||
}
|
||||
else if ("ALIAS" == extension) {
|
||||
// parse alias
|
||||
// $0: all arguments
|
||||
// $1..9: specific arguments
|
||||
var lines = programCode.split('\n').filter(function(it) { return it.length > 0 }) // this return is not shell's return!
|
||||
lines.forEach(function(line) {
|
||||
var newLine = line
|
||||
|
||||
// replace $1..$9
|
||||
for (let j = 1; j < 9; j++) {
|
||||
newLine = newLine.replaceAll('$'+j, tokens[j])
|
||||
}
|
||||
|
||||
// replace $0
|
||||
newLine = newLine.replaceAll('$0', tokens.slice(1).join(' '))
|
||||
|
||||
shell.execute(newLine)
|
||||
})
|
||||
}
|
||||
else if ("APP" == extension) {
|
||||
let appexec = `A:${_TVDOS.variables.DOSDIR}\\sbin\\appexec.js`
|
||||
let foundFile = searchFile.fullPath
|
||||
|
||||
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
assets/disk0/tvdos/bin/microtone.alias
Normal file
1
assets/disk0/tvdos/bin/microtone.alias
Normal file
@@ -0,0 +1 @@
|
||||
taut $0
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ const MAXMEM = sys.maxmem()
|
||||
const WIDTH = 560
|
||||
const HEIGHT = 448
|
||||
const TAV_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x41, 0x56] // "\x1FTSVM TAV"
|
||||
const TAP_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x41, 0x50] // "\x1FTSVM TAP"
|
||||
const UCF_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x55, 0x43, 0x46] // "\x1FTSVM UCF"
|
||||
const TAV_VERSION = 1 // Initial DWT version
|
||||
const UCF_VERSION = 1
|
||||
@@ -17,6 +18,7 @@ const ADDRESSING_INTERNAL = 0x02
|
||||
const SND_BASE_ADDR = audio.getBaseAddr()
|
||||
const SND_MEM_ADDR = audio.getMemAddr()
|
||||
const pcm = require("pcm")
|
||||
const AUDIO_DEVICE = 0
|
||||
const MP2_FRAME_SIZE = [144,216,252,288,360,432,504,576,720,864,1008,1152,1440,1728]
|
||||
const TAV_TEMPORAL_LEVELS = 2
|
||||
|
||||
@@ -151,13 +153,10 @@ graphics.clearPixels4(0)
|
||||
const gpuGraphicsMode = graphics.getGraphicsMode()
|
||||
|
||||
// Initialize audio
|
||||
audio.resetParams(0)
|
||||
audio.purgeQueue(0)
|
||||
audio.setPcmMode(0)
|
||||
audio.setMasterVolume(0, 255)
|
||||
|
||||
// set colour zero as half-opaque black
|
||||
graphics.setPalette(0, 0, 0, 0, 7)
|
||||
audio.resetParams(AUDIO_DEVICE)
|
||||
audio.purgeQueue(AUDIO_DEVICE)
|
||||
audio.setPcmMode(AUDIO_DEVICE)
|
||||
audio.setMasterVolume(AUDIO_DEVICE, 255)
|
||||
|
||||
// Parse SSF-TC subtitle packet and add to event buffer (0x31)
|
||||
function parseSubtitlePacketTC(packetSize) {
|
||||
@@ -367,6 +366,8 @@ let header = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
fps: 0,
|
||||
fps_num: 0, // Fractional FPS numerator (from XFPS or derived from fps)
|
||||
fps_den: 1, // Fractional FPS denominator (from XFPS, default 1)
|
||||
totalFrames: 0,
|
||||
waveletFilter: 0, // TAV-specific: wavelet filter type
|
||||
decompLevels: 0, // TAV-specific: decomposition levels
|
||||
@@ -381,6 +382,22 @@ let header = {
|
||||
fileRole: 0
|
||||
}
|
||||
|
||||
// Helper function to parse XFPS string ("num/den" format) and update header
|
||||
function parseXFPS(xfpsStr) {
|
||||
let parts = xfpsStr.split("/")
|
||||
if (parts.length === 2) {
|
||||
let num = parseInt(parts[0], 10)
|
||||
let den = parseInt(parts[1], 10)
|
||||
if (!isNaN(num) && !isNaN(den) && den > 0) {
|
||||
header.fps_num = num
|
||||
header.fps_den = den
|
||||
header.fps = num / den
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Read and validate header
|
||||
for (let i = 0; i < 8; i++) {
|
||||
header.magic[i] = seqread.readOneByte()
|
||||
@@ -389,7 +406,7 @@ for (let i = 0; i < 8; i++) {
|
||||
// Validate magic number
|
||||
let magicValid = true
|
||||
for (let i = 0; i < 8; i++) {
|
||||
if (header.magic[i] !== TAV_MAGIC[i]) {
|
||||
if (header.magic[i] !== TAV_MAGIC[i] &&header.magic[i] !== TAP_MAGIC[i] ) {
|
||||
magicValid = false
|
||||
break
|
||||
}
|
||||
@@ -401,10 +418,16 @@ if (!magicValid) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is a TAP still image file (magic ends with 'P' instead of 'V')
|
||||
const isTapFile = (header.magic[7] === TAP_MAGIC[7])
|
||||
|
||||
header.version = seqread.readOneByte()
|
||||
header.width = seqread.readShort()
|
||||
header.height = seqread.readShort()
|
||||
header.fps = seqread.readOneByte()
|
||||
// Set default fractional fps (will be overridden by XFPS if present)
|
||||
header.fps_num = header.fps
|
||||
header.fps_den = 1
|
||||
header.totalFrames = seqread.readInt()
|
||||
header.waveletFilter = seqread.readOneByte()
|
||||
header.decompLevels = seqread.readOneByte()
|
||||
@@ -457,7 +480,7 @@ const isLossless = (header.videoFlags & 0x04) !== 0
|
||||
|
||||
console.log(`TAV Decoder`)
|
||||
console.log(`Resolution: ${header.width}x${header.height}`)
|
||||
console.log(`FPS: ${header.fps}`)
|
||||
console.log(`FPS: ${header.fps === 255 ? "(see XFPS)" : header.fps}`)
|
||||
console.log(`Total frames: ${header.totalFrames}`)
|
||||
console.log(`Wavelet filter: ${header.waveletFilter === WAVELET_5_3_REVERSIBLE ? "5/3 reversible" : header.waveletFilter === WAVELET_9_7_IRREVERSIBLE ? "9/7 irreversible" : header.waveletFilter === WAVELET_BIORTHOGONAL_13_7 ? "Biorthogonal 13/7" : header.waveletFilter === WAVELET_DD4 ? "DD-4" : header.waveletFilter === WAVELET_HAAR ? "Haar" : "unknown"}`)
|
||||
console.log(`Decomposition levels: ${header.decompLevels}`)
|
||||
@@ -469,6 +492,135 @@ console.log(`Features: ${hasAudio ? "Audio " : ""}${hasSubtitles ? "Subtitles "
|
||||
console.log(`Video flags raw: 0x${header.videoFlags.toString(16)}`)
|
||||
console.log(`Scan type: ${isInterlaced ? "Interlaced" : "Progressive"}`)
|
||||
|
||||
// Handle TAP still image file
|
||||
if (isTapFile) {
|
||||
console.log("TAP still image detected")
|
||||
|
||||
// Allocate single frame buffer for still image
|
||||
const FRAME_PIXELS = header.width * header.height
|
||||
const FRAME_SIZE = FRAME_PIXELS * 3
|
||||
|
||||
const RGB_BUFFER = sys.malloc(FRAME_SIZE)
|
||||
const PREV_RGB_BUFFER = sys.malloc(FRAME_SIZE)
|
||||
sys.memset(RGB_BUFFER, 0, FRAME_SIZE)
|
||||
sys.memset(PREV_RGB_BUFFER, 0, FRAME_SIZE)
|
||||
|
||||
// Read the image packet (should be I-frame)
|
||||
let packetType = seqread.readOneByte()
|
||||
|
||||
// Skip non-video packets until we find the image data
|
||||
while (packetType !== TAV_PACKET_IFRAME) {
|
||||
if (packetType === TAV_PACKET_EXTENDED_HDR) {
|
||||
// Parse extended header - look for XFPS
|
||||
let numPairs = seqread.readShort()
|
||||
for (let i = 0; i < numPairs; i++) {
|
||||
// Read key (4 bytes)
|
||||
let keyBytes = seqread.readBytes(4)
|
||||
let key = ""
|
||||
for (let j = 0; j < 4; j++) {
|
||||
key += String.fromCharCode(sys.peek(keyBytes + j))
|
||||
}
|
||||
sys.free(keyBytes)
|
||||
|
||||
// Read value type and value
|
||||
let valueType = seqread.readOneByte()
|
||||
if (valueType === 0x04) { // Uint64 - 8 bytes
|
||||
seqread.skip(8)
|
||||
} else if (valueType === 0x10) { // Bytes - length-prefixed
|
||||
let length = seqread.readShort()
|
||||
let dataBytes = seqread.readBytes(length)
|
||||
// Check for XFPS key
|
||||
if (key === "XFPS") {
|
||||
let xfpsStr = ""
|
||||
for (let j = 0; j < length; j++) {
|
||||
xfpsStr += String.fromCharCode(sys.peek(dataBytes + j))
|
||||
}
|
||||
parseXFPS(xfpsStr)
|
||||
}
|
||||
sys.free(dataBytes)
|
||||
}
|
||||
}
|
||||
} else if (packetType === TAV_PACKET_SCREEN_MASK) {
|
||||
// Skip screen mask packet - single entry: frame_num(4) + top(2) + right(2) + bottom(2) + left(2)
|
||||
seqread.skip(12)
|
||||
} else if (packetType === TAV_PACKET_TIMECODE) {
|
||||
seqread.skip(8)
|
||||
} else {
|
||||
console.log(`got unknown packet type 0x${packetType.toString(16)}`)
|
||||
let size = seqread.readInt()
|
||||
seqread.skip(size)
|
||||
}
|
||||
packetType = seqread.readOneByte()
|
||||
}
|
||||
|
||||
if (packetType === TAV_PACKET_IFRAME) {
|
||||
// Read and decode I-frame
|
||||
const compressedSize = seqread.readInt()
|
||||
const compressedPtr = seqread.readBytes(compressedSize)
|
||||
|
||||
// Decode using TAV hardware decoder
|
||||
graphics.tavDecodeCompressed(
|
||||
compressedPtr, compressedSize,
|
||||
RGB_BUFFER, PREV_RGB_BUFFER,
|
||||
header.width, header.height,
|
||||
header.qualityLevel,
|
||||
QLUT[header.qualityY], QLUT[header.qualityCo], QLUT[header.qualityCg],
|
||||
header.channelLayout, 0, header.waveletFilter, header.decompLevels,
|
||||
isLossless, header.version, header.entropyCoder, 2
|
||||
)
|
||||
sys.free(compressedPtr)
|
||||
|
||||
// Upload to framebuffer
|
||||
graphics.uploadRGBToFramebuffer(RGB_BUFFER, header.width, header.height, 0, false)
|
||||
|
||||
}
|
||||
|
||||
// Free buffers
|
||||
sys.free(RGB_BUFFER)
|
||||
sys.free(PREV_RGB_BUFFER)
|
||||
|
||||
// Show "backspace to exit" message
|
||||
con.clear()
|
||||
con.curs_set(0)
|
||||
con.move(1, 1)
|
||||
println("Push and hold Backspace to exit")
|
||||
|
||||
// Wait loop for still image viewing (similar to decodeipf.js)
|
||||
let wait = true
|
||||
let t1 = sys.nanoTime()
|
||||
let tapNotifHideTimer = 0
|
||||
const TAP_NOTIF_SHOWUPTIME = 3000000000 // 3 seconds
|
||||
|
||||
while (wait) {
|
||||
sys.poke(-40, 1)
|
||||
if (sys.peek(-41) == 67) { // Backspace
|
||||
wait = false
|
||||
con.curs_set(1)
|
||||
}
|
||||
|
||||
sys.sleep(50)
|
||||
|
||||
let t2 = sys.nanoTime()
|
||||
tapNotifHideTimer += (t2 - t1)
|
||||
if (tapNotifHideTimer > TAP_NOTIF_SHOWUPTIME) {
|
||||
con.clear()
|
||||
}
|
||||
t1 = t2
|
||||
}
|
||||
|
||||
// Clean up and exit (matching normal video playback cleanup)
|
||||
con.clear()
|
||||
con.curs_set(1)
|
||||
|
||||
// Reset font ROM
|
||||
sys.poke(-1299460, 20)
|
||||
sys.poke(-1299460, 21)
|
||||
|
||||
graphics.setPalette(0, 0, 0, 0, 0)
|
||||
con.move(cy, cx) // restore cursor
|
||||
return errorlevel
|
||||
}
|
||||
|
||||
// Adjust decode height for interlaced content
|
||||
// For interlaced: header.height is display height (448)
|
||||
// Each field is half of display height (448/2 = 224)
|
||||
@@ -998,10 +1150,10 @@ try {
|
||||
else if (keyCode == 62) { // SPACE - pause/resume
|
||||
paused = !paused
|
||||
if (paused) {
|
||||
audio.stop(0)
|
||||
audio.stop(AUDIO_DEVICE)
|
||||
serial.println(`Paused at frame ${frameCount}`)
|
||||
} else {
|
||||
audio.play(0)
|
||||
audio.play(AUDIO_DEVICE)
|
||||
serial.println(`Resumed`)
|
||||
}
|
||||
}
|
||||
@@ -1022,10 +1174,10 @@ try {
|
||||
baseTimecodeFrameCount = 0
|
||||
currentTimecodeNs = 0
|
||||
nextSubtitleEventIndex = 0 // Reset subtitle event processing
|
||||
audio.purgeQueue(0)
|
||||
audio.purgeQueue(AUDIO_DEVICE)
|
||||
if (paused) {
|
||||
audio.play(0)
|
||||
audio.stop(0)
|
||||
audio.play(AUDIO_DEVICE)
|
||||
audio.stop(AUDIO_DEVICE)
|
||||
}
|
||||
skipped = true
|
||||
}
|
||||
@@ -1047,10 +1199,10 @@ try {
|
||||
baseTimecodeFrameCount = 0
|
||||
currentTimecodeNs = 0
|
||||
nextSubtitleEventIndex = 0 // Reset subtitle event processing
|
||||
audio.purgeQueue(0)
|
||||
audio.purgeQueue(AUDIO_DEVICE)
|
||||
if (paused) {
|
||||
audio.play(0)
|
||||
audio.stop(0)
|
||||
audio.play(AUDIO_DEVICE)
|
||||
audio.stop(AUDIO_DEVICE)
|
||||
}
|
||||
skipped = true
|
||||
}
|
||||
@@ -1078,10 +1230,10 @@ try {
|
||||
break
|
||||
}
|
||||
}
|
||||
audio.purgeQueue(0)
|
||||
audio.purgeQueue(AUDIO_DEVICE)
|
||||
if (paused) {
|
||||
audio.play(0)
|
||||
audio.stop(0)
|
||||
audio.play(AUDIO_DEVICE)
|
||||
audio.stop(AUDIO_DEVICE)
|
||||
}
|
||||
skipped = true
|
||||
}
|
||||
@@ -1117,10 +1269,10 @@ try {
|
||||
break
|
||||
}
|
||||
}
|
||||
audio.purgeQueue(0)
|
||||
audio.purgeQueue(AUDIO_DEVICE)
|
||||
if (paused) {
|
||||
audio.play(0)
|
||||
audio.stop(0)
|
||||
audio.play(AUDIO_DEVICE)
|
||||
audio.stop(AUDIO_DEVICE)
|
||||
}
|
||||
skipped = true
|
||||
} else if (!seekTarget) {
|
||||
@@ -1159,7 +1311,7 @@ try {
|
||||
baseTimecodeFrameCount = 0
|
||||
currentTimecodeNs = 0
|
||||
nextSubtitleEventIndex = 0 // Reset subtitle event processing
|
||||
audio.purgeQueue(0)
|
||||
audio.purgeQueue(AUDIO_DEVICE)
|
||||
currentFileIndex++
|
||||
if (skipped) {
|
||||
skipped = false
|
||||
@@ -1170,7 +1322,7 @@ try {
|
||||
|
||||
console.log(`\nStarting file ${currentFileIndex}:`)
|
||||
console.log(`Resolution: ${header.width}x${header.height}`)
|
||||
console.log(`FPS: ${header.fps}`)
|
||||
console.log(`FPS: ${header.fps === 255 ? "(see XFPS)" : header.fps}`)
|
||||
console.log(`Total frames: ${header.totalFrames}`)
|
||||
console.log(`Wavelet filter: ${header.waveletFilter === WAVELET_5_3_REVERSIBLE ? "5/3 reversible" : header.waveletFilter === WAVELET_9_7_IRREVERSIBLE ? "9/7 irreversible" : header.waveletFilter === WAVELET_BIORTHOGONAL_13_7 ? "Biorthogonal 13/7" : header.waveletFilter === WAVELET_DD4 ? "DD-4" : header.waveletFilter === WAVELET_HAAR ? "Haar" : "unknown"}`)
|
||||
console.log(`Quality: Y=${header.qualityY}, Co=${header.qualityCo}, Cg=${header.qualityCg}`)
|
||||
@@ -1583,7 +1735,7 @@ try {
|
||||
|
||||
seqread.readBytes(audioLen, SND_BASE_ADDR - 2368)
|
||||
audio.mp2Decode()
|
||||
audio.mp2UploadDecoded(0)
|
||||
audio.mp2UploadDecoded(AUDIO_DEVICE)
|
||||
|
||||
}
|
||||
else if (packetType === TAV_PACKET_AUDIO_TAD) {
|
||||
@@ -1596,7 +1748,7 @@ try {
|
||||
|
||||
seqread.readBytes(payloadLen, SND_MEM_ADDR - 262144)
|
||||
audio.tadDecode()
|
||||
audio.tadUploadDecoded(0, sampleLen)
|
||||
audio.tadUploadDecoded(AUDIO_DEVICE, sampleLen)
|
||||
}
|
||||
else if (packetType === TAV_PACKET_AUDIO_NATIVE) {
|
||||
// PCM length must not exceed 65536 bytes!
|
||||
@@ -1608,10 +1760,10 @@ try {
|
||||
let pcmLen = gzip.decompFromTo(zstdPtr, zstdLen, pcmPtr) // <- segfaults!
|
||||
if (pcmLen > 65536) throw Error(`PCM data too long -- got ${pcmLen} bytes`)
|
||||
|
||||
audio.putPcmDataByPtr(pcmPtr, pcmLen, 0)
|
||||
audio.putPcmDataByPtr(AUDIO_DEVICE, pcmPtr, pcmLen, 0)
|
||||
|
||||
audio.setSampleUploadLength(0, pcmLen)
|
||||
audio.startSampleUpload(0)
|
||||
audio.setSampleUploadLength(AUDIO_DEVICE, pcmLen)
|
||||
audio.startSampleUpload(AUDIO_DEVICE)
|
||||
sys.free(zstdPtr)
|
||||
|
||||
sys.free(pcmPtr)
|
||||
@@ -1704,7 +1856,19 @@ try {
|
||||
}
|
||||
sys.free(dataBytes)
|
||||
|
||||
if (interactive) {
|
||||
// Parse XFPS if present (always try, not just when fps=255)
|
||||
if (key === "XFPS") {
|
||||
if (parseXFPS(dataStr)) {
|
||||
// Update frame timing with new fps
|
||||
frametime = 1000000000.0 / header.fps
|
||||
FRAME_TIME = 1.0 / header.fps
|
||||
if (interactive) {
|
||||
serial.println(` ${key}: ${dataStr} -> ${header.fps.toFixed(3)} fps`)
|
||||
}
|
||||
} else if (interactive) {
|
||||
serial.println(` ${key}: "${dataStr}" (parse failed)`)
|
||||
}
|
||||
} else if (interactive) {
|
||||
serial.println(` ${key}: "${dataStr}"`)
|
||||
}
|
||||
} else {
|
||||
@@ -1883,7 +2047,7 @@ try {
|
||||
|
||||
// Fire audio on first frame
|
||||
if (!audioFired) {
|
||||
audio.play(0)
|
||||
audio.play(AUDIO_DEVICE)
|
||||
audioFired = true
|
||||
}
|
||||
|
||||
@@ -1971,7 +2135,7 @@ try {
|
||||
|
||||
// Fire audio on first frame
|
||||
if (!audioFired) {
|
||||
audio.play(0)
|
||||
audio.play(AUDIO_DEVICE)
|
||||
audioFired = true
|
||||
}
|
||||
|
||||
@@ -2007,8 +2171,8 @@ try {
|
||||
sys.memcpy(predecodedPcmBuffer + predecodedPcmOffset, SND_BASE_ADDR, uploadSize)
|
||||
|
||||
// Set upload parameters and trigger upload to queue
|
||||
audio.setSampleUploadLength(0, uploadSize)
|
||||
audio.startSampleUpload(0)
|
||||
audio.setSampleUploadLength(AUDIO_DEVICE, uploadSize)
|
||||
audio.startSampleUpload(AUDIO_DEVICE)
|
||||
|
||||
predecodedPcmOffset += uploadSize
|
||||
}
|
||||
@@ -2292,10 +2456,10 @@ finally {
|
||||
sys.poke(-1299460, 20)
|
||||
sys.poke(-1299460, 21)
|
||||
|
||||
audio.stop(0)
|
||||
audio.purgeQueue(0)
|
||||
audio.stop(AUDIO_DEVICE)
|
||||
audio.purgeQueue(AUDIO_DEVICE)
|
||||
}
|
||||
|
||||
graphics.setPalette(0, 0, 0, 0, 0)
|
||||
graphics.resetPalette()
|
||||
con.move(cy, cx) // restore cursor
|
||||
return errorlevel
|
||||
@@ -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)
|
||||
|
||||
|
||||
2649
assets/disk0/tvdos/bin/taut.js
Normal file
2649
assets/disk0/tvdos/bin/taut.js
Normal file
File diff suppressed because it is too large
Load Diff
77
assets/disk0/tvdos/bin/taut_fileop.js
Normal file
77
assets/disk0/tvdos/bin/taut_fileop.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* TAUT File Operations
|
||||
* Sub-program launched by taut.js when the File tab is active.
|
||||
* Rows 1-3 are owned by the parent; this program draws rows 4+.
|
||||
*
|
||||
* exec_args[1] = path to .taud file
|
||||
* Sets _G.TAUT.UI.NEXTPANEL before returning to request a panel switch.
|
||||
*
|
||||
* Created by minjaesong on 2026-04-27
|
||||
*/
|
||||
|
||||
const win = require("wintex")
|
||||
|
||||
const PANEL_COUNT = 7
|
||||
const MY_PANEL = 6 // VIEW_FILE
|
||||
|
||||
const [SCRH, SCRW] = con.getmaxyx()
|
||||
const PANEL_Y = 4
|
||||
const PANEL_H = SCRH - PANEL_Y
|
||||
|
||||
const colStatus = 253
|
||||
const colContent = 240
|
||||
const colHdr = 230
|
||||
|
||||
function drawFileOpContents(wo) {
|
||||
for (let y = PANEL_Y; y < SCRH; y++) {
|
||||
con.move(y, 1)
|
||||
con.color_pair(colContent, 255)
|
||||
print(' '.repeat(SCRW))
|
||||
}
|
||||
con.move(PANEL_Y + 1, 3)
|
||||
con.color_pair(colHdr, 255)
|
||||
print('[ File ]')
|
||||
con.move(PANEL_Y + 3, 3)
|
||||
con.color_pair(colStatus, 255)
|
||||
print('placeholder — not yet implemented')
|
||||
}
|
||||
|
||||
function drawHints() {
|
||||
con.move(SCRH, 1)
|
||||
con.color_pair(colStatus, 255)
|
||||
print(' '.repeat(SCRW - 1))
|
||||
con.move(SCRH, 1)
|
||||
con.color_pair(colHdr, 255); print('Tab ')
|
||||
con.color_pair(colStatus, 255); print('Panel')
|
||||
}
|
||||
|
||||
function fileOpInput(wo, event) {
|
||||
// placeholder — no interaction yet
|
||||
}
|
||||
|
||||
const panel = new win.WindowObject(1, PANEL_Y, SCRW, PANEL_H, fileOpInput, drawFileOpContents, undefined, ()=>{})
|
||||
|
||||
panel.drawContents()
|
||||
drawHints()
|
||||
|
||||
let done = false
|
||||
while (!done) {
|
||||
input.withEvent(event => {
|
||||
if (event[0] !== 'key_down') return
|
||||
const keysym = event[1]
|
||||
const keyJustHit = (1 == event[2])
|
||||
const shiftDown = (event.includes(59) || event.includes(60))
|
||||
|
||||
if (!keyJustHit) return
|
||||
|
||||
if (keysym === '<TAB>') {
|
||||
_G.TAUT.UI.NEXTPANEL = (MY_PANEL + (shiftDown ? -1 : 1) + PANEL_COUNT) % PANEL_COUNT
|
||||
done = true
|
||||
return
|
||||
}
|
||||
|
||||
panel.processInput(event)
|
||||
})
|
||||
}
|
||||
|
||||
return 0
|
||||
84
assets/disk0/tvdos/bin/taut_helpmsg.js
Normal file
84
assets/disk0/tvdos/bin/taut_helpmsg.js
Normal file
@@ -0,0 +1,84 @@
|
||||
if (!_G.TAUT) _G.TAUT = {};
|
||||
let help = {}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/*
|
||||
Tags:
|
||||
<b> - print the text in emphasis colour (colVoiceHdr aka 230)
|
||||
<c> - centre the line. If the line spans multiple lines, centre each line
|
||||
<r> - align right
|
||||
<l> - align left
|
||||
µtone; - replace with the brand string
|
||||
&bul; - replace with bullet (\u00847u)
|
||||
&ddot; - replace with double-dot (\u008419u)
|
||||
&mdot; - replace with BIGDOT (\u00F9)
|
||||
&updn; - up-down arrow (\u008418u)
|
||||
&udlr; - four direction arrow (\u008428u\u008429u)
|
||||
&keyoffsym; - pattern view key-off symbol (\u00A0\u00CD\u00CD\u00A1)
|
||||
¬ecutsym; - pattern view note-cut symbol (\u00A4\u00A4\u00A4\u00A4)
|
||||
- nonbreakable space (only meaningful for typesetters)
|
||||
­ - soft hyphen (only meaningful for typesetters)
|
||||
default alignment: fully justified
|
||||
*/
|
||||
|
||||
help.notation = `<c>CONTROL NOTATON</c>
|
||||
|
||||
µtone; shortcuts differentiate normal and shifted shortcuts.
|
||||
&bul;a&ddot;z : alphabet without shift-in
|
||||
&bul;A&ddot;Z : alphabet with shift-in
|
||||
&bul;^ : control key`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
help.jam = `<c>NOTE JAMMING</c>
|
||||
|
||||
Push keys to play or insert notes.
|
||||
w e t y u i
|
||||
a s d f g h j k`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
help.common = `<c>COMMON CONTROLS</c>
|
||||
|
||||
&bul;Y : play the entire song from the current cue
|
||||
&bul;U : play the current cue then stop
|
||||
&bul;I : play the current row
|
||||
&bul;O : stop the playback
|
||||
&bul;tab : switch forward a tab
|
||||
&bul;TAB : switch backward a tab
|
||||
&bul;q : close µtone;`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
help.timeline = `<c>TIMELINE VIEW</c>
|
||||
|
||||
Timeline has two distinct modes: view and edit mode. Two modes are toggled using the space bar.
|
||||
|
||||
<b>View mode</b>
|
||||
&bul;Note jamming : plays the note
|
||||
&bul;&udlr; : move the viewing cursor by voices and rows
|
||||
&bul;pg&updn; : go to previous/next cue
|
||||
&bul;W&mdot;E&mdot;R : toggle timeline view mode. W-most detailed, R-most abridged
|
||||
&bul;n : toggle soloing of the selected voice
|
||||
&bul;m : toggle muting of the selected voice
|
||||
|
||||
<b>Edit mode</b>
|
||||
&bul;Note jammping : (note column) inserts the note
|
||||
&bul;{&mdot;} : (note column) lower/raise a note by one octave (or period)
|
||||
&bul;[&mdot;] : (note column) lower/raise a note by one unit
|
||||
&bul;= : (note column) insert a key-off &keyoffsym;
|
||||
&bul;^ : (note column) insert a note-cut ¬ecutsym;
|
||||
&bul;. : remove a symbol on the selected column
|
||||
&bul;bksp : delete one character on the selected column
|
||||
&bul;0&ddot;9 a&ddot;f : inserts a (hexa)decimal number
|
||||
&bul;^&mdot;v : (volume column) slide up/down
|
||||
&bul;<&mdot;> : (panning column) slide left/right
|
||||
&bul;-&mdot;= : (vol/pan col) fine slide down/up
|
||||
&bul;&udlr; : move the viewing cursor by columns and rows
|
||||
&bul;pg&updn; : go to previous/next cue`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (!_G.TAUT.HELPMSG) _G.TAUT.HELPMSG=help;
|
||||
|
||||
77
assets/disk0/tvdos/bin/taut_instredit.js
Normal file
77
assets/disk0/tvdos/bin/taut_instredit.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* TAUT Instrument Editor
|
||||
* Sub-program launched by taut.js when the Instrmnt tab is active.
|
||||
* Rows 1-3 are owned by the parent; this program draws rows 4+.
|
||||
*
|
||||
* exec_args[1] = path to .taud file
|
||||
* Sets _G.TAUT.UI.NEXTPANEL before returning to request a panel switch.
|
||||
*
|
||||
* Created by minjaesong on 2026-04-27
|
||||
*/
|
||||
|
||||
const win = require("wintex")
|
||||
|
||||
const PANEL_COUNT = 7
|
||||
const MY_PANEL = 4 // VIEW_INSTRMNT
|
||||
|
||||
const [SCRH, SCRW] = con.getmaxyx()
|
||||
const PANEL_Y = 4
|
||||
const PANEL_H = SCRH - PANEL_Y
|
||||
|
||||
const colStatus = 253
|
||||
const colContent = 240
|
||||
const colHdr = 230
|
||||
|
||||
function drawInstEditContents(wo) {
|
||||
for (let y = PANEL_Y; y < SCRH; y++) {
|
||||
con.move(y, 1)
|
||||
con.color_pair(colContent, 255)
|
||||
print(' '.repeat(SCRW))
|
||||
}
|
||||
con.move(PANEL_Y + 1, 3)
|
||||
con.color_pair(colHdr, 255)
|
||||
print('[ Instrument Editor ]')
|
||||
con.move(PANEL_Y + 3, 3)
|
||||
con.color_pair(colStatus, 255)
|
||||
print('placeholder — not yet implemented')
|
||||
}
|
||||
|
||||
function drawHints() {
|
||||
con.move(SCRH, 1)
|
||||
con.color_pair(colStatus, 255)
|
||||
print(' '.repeat(SCRW - 1))
|
||||
con.move(SCRH, 1)
|
||||
con.color_pair(colHdr, 255); print('Tab ')
|
||||
con.color_pair(colStatus, 255); print('Panel')
|
||||
}
|
||||
|
||||
function instEditInput(wo, event) {
|
||||
// placeholder — no interaction yet
|
||||
}
|
||||
|
||||
const panel = new win.WindowObject(1, PANEL_Y, SCRW, PANEL_H, instEditInput, drawInstEditContents, undefined, ()=>{})
|
||||
|
||||
panel.drawContents()
|
||||
drawHints()
|
||||
|
||||
let done = false
|
||||
while (!done) {
|
||||
input.withEvent(event => {
|
||||
if (event[0] !== 'key_down') return
|
||||
const keysym = event[1]
|
||||
const keyJustHit = (1 == event[2])
|
||||
const shiftDown = (event.includes(59) || event.includes(60))
|
||||
|
||||
if (!keyJustHit) return
|
||||
|
||||
if (keysym === '<TAB>') {
|
||||
_G.TAUT.UI.NEXTPANEL = (MY_PANEL + (shiftDown ? -1 : 1) + PANEL_COUNT) % PANEL_COUNT
|
||||
done = true
|
||||
return
|
||||
}
|
||||
|
||||
panel.processInput(event)
|
||||
})
|
||||
}
|
||||
|
||||
return 0
|
||||
0
assets/disk0/tvdos/bin/taut_notationedit.js
Normal file
0
assets/disk0/tvdos/bin/taut_notationedit.js
Normal file
77
assets/disk0/tvdos/bin/taut_sampleedit.js
Normal file
77
assets/disk0/tvdos/bin/taut_sampleedit.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* TAUT Sample Editor
|
||||
* Sub-program launched by taut.js when the Samples tab is active.
|
||||
* Rows 1-3 are owned by the parent; this program draws rows 4+.
|
||||
*
|
||||
* exec_args[1] = path to .taud file
|
||||
* Sets _G.TAUT.UI.NEXTPANEL before returning to request a panel switch.
|
||||
*
|
||||
* Created by minjaesong on 2026-04-27
|
||||
*/
|
||||
|
||||
const win = require("wintex")
|
||||
|
||||
const PANEL_COUNT = 7
|
||||
const MY_PANEL = 3 // VIEW_SAMPLES
|
||||
|
||||
const [SCRH, SCRW] = con.getmaxyx()
|
||||
const PANEL_Y = 4
|
||||
const PANEL_H = SCRH - PANEL_Y
|
||||
|
||||
const colStatus = 253
|
||||
const colContent = 240
|
||||
const colHdr = 230
|
||||
|
||||
function drawSampleEditContents(wo) {
|
||||
for (let y = PANEL_Y; y < SCRH; y++) {
|
||||
con.move(y, 1)
|
||||
con.color_pair(colContent, 255)
|
||||
print(' '.repeat(SCRW))
|
||||
}
|
||||
con.move(PANEL_Y + 1, 3)
|
||||
con.color_pair(colHdr, 255)
|
||||
print('[ Sample Editor ]')
|
||||
con.move(PANEL_Y + 3, 3)
|
||||
con.color_pair(colStatus, 255)
|
||||
print('placeholder — not yet implemented')
|
||||
}
|
||||
|
||||
function drawHints() {
|
||||
con.move(SCRH, 1)
|
||||
con.color_pair(colStatus, 255)
|
||||
print(' '.repeat(SCRW - 1))
|
||||
con.move(SCRH, 1)
|
||||
con.color_pair(colHdr, 255); print('Tab ')
|
||||
con.color_pair(colStatus, 255); print('Panel')
|
||||
}
|
||||
|
||||
function sampleEditInput(wo, event) {
|
||||
// placeholder — no interaction yet
|
||||
}
|
||||
|
||||
const panel = new win.WindowObject(1, PANEL_Y, SCRW, PANEL_H, sampleEditInput, drawSampleEditContents, undefined, ()=>{})
|
||||
|
||||
panel.drawContents()
|
||||
drawHints()
|
||||
|
||||
let done = false
|
||||
while (!done) {
|
||||
input.withEvent(event => {
|
||||
if (event[0] !== 'key_down') return
|
||||
const keysym = event[1]
|
||||
const keyJustHit = (1 == event[2])
|
||||
const shiftDown = (event.includes(59) || event.includes(60))
|
||||
|
||||
if (!keyJustHit) return
|
||||
|
||||
if (keysym === '<TAB>') {
|
||||
_G.TAUT.UI.NEXTPANEL = (MY_PANEL + (shiftDown ? -1 : 1) + PANEL_COUNT) % PANEL_COUNT
|
||||
done = true
|
||||
return
|
||||
}
|
||||
|
||||
panel.processInput(event)
|
||||
})
|
||||
}
|
||||
|
||||
return 0
|
||||
BIN
assets/disk0/tvdos/bin/tautbtn.png
Normal file
BIN
assets/disk0/tvdos/bin/tautbtn.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 518 B |
1
assets/disk0/tvdos/bin/tautbtn.r8
Normal file
1
assets/disk0/tvdos/bin/tautbtn.r8
Normal file
@@ -0,0 +1 @@
|
||||
<EFBFBD><EFBFBD>洄洄洄洄洄泚<EFBFBD>泚<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD>_
|
||||
1
assets/disk0/tvdos/bin/tautbtn0.r8
Normal file
1
assets/disk0/tvdos/bin/tautbtn0.r8
Normal file
@@ -0,0 +1 @@
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
BIN
assets/disk0/tvdos/bin/tautfont.kra
LFS
Normal file
BIN
assets/disk0/tvdos/bin/tautfont.kra
LFS
Normal file
Binary file not shown.
BIN
assets/disk0/tvdos/bin/tautfont_high.chr
Normal file
BIN
assets/disk0/tvdos/bin/tautfont_high.chr
Normal file
Binary file not shown.
BIN
assets/disk0/tvdos/bin/tautfont_low.chr
Normal file
BIN
assets/disk0/tvdos/bin/tautfont_low.chr
Normal file
Binary file not shown.
BIN
assets/disk0/tvdos/bin/tauthdr.png
Normal file
BIN
assets/disk0/tvdos/bin/tauthdr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 490 B |
BIN
assets/disk0/tvdos/bin/tauthdr.r8
Normal file
BIN
assets/disk0/tvdos/bin/tauthdr.r8
Normal file
Binary file not shown.
@@ -1,4 +1,6 @@
|
||||
const win = require("wintex")
|
||||
const keys = require("keysym")
|
||||
|
||||
const COL_TEXT = 253
|
||||
const COL_BACK = 255
|
||||
const COL_BACK_SEL = 81
|
||||
@@ -26,37 +28,45 @@ const COL_HL_EXT = {
|
||||
"wav": 31,
|
||||
"adpcm": 31,
|
||||
"pcm": 32,
|
||||
"mp3": 33,
|
||||
// "mp3": 33,
|
||||
"tad": 33,
|
||||
"mp2": 34,
|
||||
"mv1": 213,
|
||||
"mv2": 213,
|
||||
"mv3": 213,
|
||||
"tav": 213,
|
||||
"ipf": 190,
|
||||
"ipf1": 190,
|
||||
"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`),
|
||||
"mv1": (f) => _G.shell.execute(`playmv1 "${f}" -i`),
|
||||
"mv2": (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
|
||||
@@ -412,6 +422,8 @@ let filenavOninput = (window, event) => {
|
||||
let keycodes = [event[3],event[4],event[5],event[6],event[7],event[8],event[9],event[10]]
|
||||
let keycode = keycodes[0]
|
||||
|
||||
let scrollPeek = (LIST_HEIGHT / 3)|0
|
||||
|
||||
if (keyJustHit && keysym == "q") {
|
||||
exit = true
|
||||
}
|
||||
@@ -420,19 +432,19 @@ let filenavOninput = (window, event) => {
|
||||
redraw() // this would double-redraw (hence no panel switching) or something if redraw() is not merely a request to do so
|
||||
}
|
||||
else if (keysym == "<UP>") {
|
||||
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(-1, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], 1)
|
||||
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(-1, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], scrollPeek)
|
||||
drawFilePanel()
|
||||
}
|
||||
else if (keysym == "<DOWN>") {
|
||||
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(+1, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], 1)
|
||||
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(+1, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], scrollPeek)
|
||||
drawFilePanel()
|
||||
}
|
||||
else if (keysym == "<PAGE_UP>") {
|
||||
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(-LIST_HEIGHT, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], 1)
|
||||
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(-LIST_HEIGHT, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], scrollPeek)
|
||||
drawFilePanel()
|
||||
}
|
||||
else if (keysym == "<PAGE_DOWN>") {
|
||||
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(+LIST_HEIGHT, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], 1)
|
||||
[cursor[windowMode], scroll[windowMode]] = win.scrollVert(+LIST_HEIGHT, dirFileList[windowMode].length, LIST_HEIGHT, cursor[windowMode], scroll[windowMode], scrollPeek)
|
||||
drawFilePanel()
|
||||
}
|
||||
else if (keyJustHit && keycode == 66) { // enter
|
||||
@@ -665,7 +677,7 @@ while (!exit) {
|
||||
let keysym = event[1]
|
||||
let keyJustHit = (1 == event[2])
|
||||
|
||||
if (keyJustHit && event[3] != 66) { // release the latch right away if the key is not Return
|
||||
if (keyJustHit && event[3] != keys.ENTER) { // release the latch right away if the key is not Return
|
||||
firstRunLatch = false
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ exports.SpriteSheet = function(tilew, tileh, tex) {
|
||||
return ty;
|
||||
};
|
||||
};
|
||||
exports.drawTexPattern = function(texture, x, y, width, height, framebuffer, fgcol, bgcol) {
|
||||
exports.drawTexPattern = function(texture, x, y, width = texture.width, height = texture.height, framebuffer = false, fgcol, bgcol) {
|
||||
if (!(texture instanceof exports.Texture) && !(texture instanceof exports.MonoTex)) throw Error("Texture is not a GL Texture types");
|
||||
|
||||
let paint = (!framebuffer) ? graphics.plotPixel : graphics.plotPixel2
|
||||
|
||||
80
assets/disk0/tvdos/include/keysym.mjs
Normal file
80
assets/disk0/tvdos/include/keysym.mjs
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* These are key symbols returned by `input.withEvent`, NOT `con.getch()`
|
||||
*/
|
||||
|
||||
exports = {
|
||||
NUM_0:7,
|
||||
NUM_1:8,
|
||||
NUM_2:9,
|
||||
NUM_3:10,
|
||||
NUM_4:11,
|
||||
NUM_5:12,
|
||||
NUM_6:13,
|
||||
NUM_7:14,
|
||||
NUM_8:15,
|
||||
NUM_9:16,
|
||||
A:29,
|
||||
ALT_LEFT:57,
|
||||
ALT_RIGHT:58,
|
||||
APOSTROPHE:75,
|
||||
AT:77,
|
||||
B:30,
|
||||
BACK:4,
|
||||
BACKSLASH:73,
|
||||
C:31,
|
||||
CAPS_LOCK:115,
|
||||
COMMA:55,
|
||||
D:32,
|
||||
DEL:67,
|
||||
BACKSPACE:67,
|
||||
FORWARD_DEL:112,
|
||||
DOWN:20,
|
||||
LEFT:21,
|
||||
RIGHT:22,
|
||||
UP:19,
|
||||
E:33,
|
||||
ENTER:66,
|
||||
EQUALS:70,
|
||||
F:34,
|
||||
G:35,
|
||||
GRAVE:68,
|
||||
H:36,
|
||||
HOME:3,
|
||||
I:37,
|
||||
J:38,
|
||||
K:39,
|
||||
L:40,
|
||||
LEFT_BRACKET:71,
|
||||
M:41,
|
||||
MINUS:69,
|
||||
N:42,
|
||||
O:43,
|
||||
P:44,
|
||||
PERIOD:56,
|
||||
PLUS:81,
|
||||
Q:45,
|
||||
R:46,
|
||||
RIGHT_BRACKET:72,
|
||||
S:47,
|
||||
SEMICOLON:74,
|
||||
SHIFT_LEFT:59,
|
||||
SHIFT_RIGHT:60,
|
||||
SLASH:76,
|
||||
SPACE:62,
|
||||
SYM:63, // on MacOS, this is Command (⌘)
|
||||
T:48,
|
||||
TAB:61,
|
||||
U:49,
|
||||
V:50,
|
||||
W:51,
|
||||
X:52,
|
||||
Y:53,
|
||||
Z:54,
|
||||
CONTROL_LEFT:129,
|
||||
CONTROL_RIGHT:130,
|
||||
ESCAPE:111,
|
||||
END:123,
|
||||
INSERT:124,
|
||||
PAGE_UP:92,
|
||||
PAGE_DOWN:93,
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* LibPCM — PCM decoder for TSVM
|
||||
* @author CuriousTorvald
|
||||
*/
|
||||
|
||||
const HW_SAMPLING_RATE = 32000
|
||||
function printdbg(s) { if (0) serial.println(s) }
|
||||
function printvis(s) { if (0) println(s) }
|
||||
@@ -29,7 +34,7 @@ function s16Tou8(i) {
|
||||
}
|
||||
function u16Tos16(i) { return (i > 32767) ? i - 65536 : i }
|
||||
function randomRound(k) {
|
||||
let rnd = (Math.random() + Math.random()) / 2.0 // this produces triangular distribution
|
||||
let rnd = Math.random() // note to self: no triangular here
|
||||
return (rnd < (k - (k|0))) ? Math.ceil(k) : Math.floor(k)
|
||||
}
|
||||
function lerp(start, end, x) {
|
||||
|
||||
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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
312
assets/disk0/tvdos/include/taud.mjs
Normal file
312
assets/disk0/tvdos/include/taud.mjs
Normal file
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
* LibTaud — Helper functions for interaction between Taud format and TSVM Tracker
|
||||
* Requires TVDOS to function.
|
||||
* @author CuriousTorvald
|
||||
*/
|
||||
|
||||
// ── Format constants ────────────────────────────────────────────────────────
|
||||
|
||||
const TAUD_MAGIC = [0x1F,0x54,0x53,0x56,0x4D,0x61,0x75,0x64] // \x1F TSVMaud
|
||||
const TAUD_VERSION = 1
|
||||
const TAUD_HEADER_SIZE = 32 // magic(8) + version(1) + numSongs(1) + compSize(4) + projOff(4) + sig(14)
|
||||
const TAUD_SONG_ENTRY = 32 // see encodeSongEntry / decodeSongEntry below
|
||||
const SAMPLEINST_SIZE = 786432 // 737280 sample + 49152 instrument (256 × 192)
|
||||
const PATTERN_SIZE = 512 // bytes per pattern (64 rows × 8 bytes)
|
||||
const NUM_PATTERNS_MAX = 256
|
||||
const NUM_CUES = 1024
|
||||
const CUE_SIZE = 32 // bytes per cue entry (packed 12-bit×20 voices + instruction + pad)
|
||||
|
||||
// Signature written into the file (14 bytes, space-padded)
|
||||
const CAPTURE_SIGNATURE = "LibTaud/TSVM "
|
||||
|
||||
// ── Internal helpers ────────────────────────────────────────────────────────
|
||||
|
||||
function _peekU32LE(ptr, off) {
|
||||
return ((sys.peek(ptr+off) & 0xFF) ) |
|
||||
((sys.peek(ptr+off+1) & 0xFF) << 8 ) |
|
||||
((sys.peek(ptr+off+2) & 0xFF) << 16 ) |
|
||||
((sys.peek(ptr+off+3) & 0xFF) * 0x1000000) // avoid sign-extend
|
||||
}
|
||||
|
||||
function _pokeU32LE(ptr, off, v) {
|
||||
sys.poke(ptr+off, (v ) & 0xFF)
|
||||
sys.poke(ptr+off+1, (v >>> 8) & 0xFF)
|
||||
sys.poke(ptr+off+2, (v >>> 16) & 0xFF)
|
||||
sys.poke(ptr+off+3, (v >>> 24) & 0xFF)
|
||||
}
|
||||
|
||||
// ── uploadTaudFile ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Load one song from a Taud file into the tracker hardware and configure the
|
||||
* given playhead ready to play.
|
||||
*
|
||||
* @param inFile Full path with drive letter, e.g. "A:/music/song.taud"
|
||||
* @param songIndex 0-based index of the song in the SONG TABLE
|
||||
* @param playhead Playhead number (0-3) to configure
|
||||
*/
|
||||
function uploadTaudFile(inFile, songIndex, playhead) {
|
||||
const drive = inFile[0].toUpperCase()
|
||||
const diskPath = inFile.substring(2)
|
||||
|
||||
const memBase = audio.getMemAddr()
|
||||
|
||||
// -- 1. Read whole file into VM memory ------------------------------------
|
||||
const fileHandle = files.open(inFile)
|
||||
|
||||
if (!fileHandle.exists) {
|
||||
throw Error("taud: file not exists")
|
||||
}
|
||||
|
||||
const fileSize = fileHandle.size
|
||||
const filePtr = sys.malloc(fileSize)
|
||||
fileHandle.pread(filePtr, fileSize, 0)
|
||||
|
||||
let pos = 0
|
||||
|
||||
// -- 2. Verify magic ------------------------------------------------------
|
||||
for (let i = 0; i < 8; i++) {
|
||||
let magicc = sys.peek(filePtr + i)
|
||||
if (magicc !== TAUD_MAGIC[i]) {
|
||||
sys.free(filePtr)
|
||||
throw Error("taud: bad magic byte " + magicc.toString(16) + " at index " + i)
|
||||
}
|
||||
}
|
||||
pos = 8
|
||||
|
||||
// -- 3. Parse header ------------------------------------------------------
|
||||
// version(1) + numSongs(1) + compressedSize(4) + rsvd(2) + signature(16) = 24 bytes
|
||||
let version = sys.peek(filePtr + pos) & 0xFF; pos++
|
||||
let numSongs = sys.peek(filePtr + pos) & 0xFF; pos++
|
||||
let compressedSize = _peekU32LE(filePtr, pos); pos += 4
|
||||
pos += 18 // skip reserved(2) + signature(16)
|
||||
// pos == 32 == TAUD_HEADER_SIZE
|
||||
|
||||
if (songIndex < 0 || songIndex >= numSongs) {
|
||||
sys.free(filePtr)
|
||||
throw Error("taud: songIndex " + songIndex + " out of range (numSongs=" + numSongs + ")")
|
||||
}
|
||||
|
||||
// -- 4. Decompress and upload sample+instrument bin -----------------------
|
||||
let decompPtr = sys.malloc(SAMPLEINST_SIZE)
|
||||
gzip.decompFromTo(filePtr + pos, compressedSize, decompPtr)
|
||||
pos += compressedSize
|
||||
|
||||
// Write decompressed data to peripheral memory (backwards addressing:
|
||||
// peripheral byte k lives at memBase - k).
|
||||
for (let i = 0; i < SAMPLEINST_SIZE; i++) {
|
||||
// TODO use sys.memcpy
|
||||
sys.poke(memBase - i, sys.peek(decompPtr + i))
|
||||
}
|
||||
sys.free(decompPtr)
|
||||
|
||||
// -- 5. Parse song-table entry for the requested song --------------------
|
||||
let entryOff = pos + songIndex * TAUD_SONG_ENTRY
|
||||
let songOffset = _peekU32LE(filePtr, entryOff)
|
||||
let numVoices = sys.peek(filePtr + entryOff + 4) & 0xFF
|
||||
let numPatsLo = sys.peek(filePtr + entryOff + 5) & 0xFF
|
||||
let numPatsHi = sys.peek(filePtr + entryOff + 6) & 0xFF
|
||||
let bpmStored = sys.peek(filePtr + entryOff + 7) & 0xFF
|
||||
let tickRate = sys.peek(filePtr + entryOff + 8) & 0xFF
|
||||
let mixerflags = sys.peek(filePtr + entryOff + 15) & 0xFF
|
||||
let songGlobalVolume = sys.peek(filePtr + entryOff + 16) & 0xFF
|
||||
let songMixingVolume = sys.peek(filePtr + entryOff + 17) & 0xFF
|
||||
let patBinCompSize = _peekU32LE(filePtr, entryOff + 18)
|
||||
let cueSheetCompSize = _peekU32LE(filePtr, entryOff + 22)
|
||||
|
||||
let bpm = bpmStored + 24
|
||||
let patsToLoad = numPatsLo | (numPatsHi << 8)
|
||||
|
||||
// -- 6. Decompress + upload patterns --------------------------------------
|
||||
let patBinSize = patsToLoad * PATTERN_SIZE
|
||||
let patBinPtr = sys.malloc(patBinSize)
|
||||
gzip.decompFromTo(filePtr + songOffset, patBinCompSize, patBinPtr)
|
||||
|
||||
let patBytes = new Array(PATTERN_SIZE)
|
||||
for (let p = 0; p < patsToLoad; p++) {
|
||||
for (let k = 0; k < PATTERN_SIZE; k++)
|
||||
patBytes[k] = sys.peek(patBinPtr + p * PATTERN_SIZE + k) & 0xFF
|
||||
audio.uploadPattern(p, patBytes)
|
||||
}
|
||||
sys.free(patBinPtr)
|
||||
|
||||
// -- 7. Decompress + upload cue sheet -------------------------------------
|
||||
let cueSheetSize = NUM_CUES * CUE_SIZE
|
||||
let cueSheetPtr = sys.malloc(cueSheetSize)
|
||||
gzip.decompFromTo(filePtr + songOffset + patBinCompSize, cueSheetCompSize, cueSheetPtr)
|
||||
|
||||
let cueBytes = new Array(CUE_SIZE)
|
||||
for (let c = 0; c < NUM_CUES; c++) {
|
||||
for (let k = 0; k < CUE_SIZE; k++)
|
||||
cueBytes[k] = sys.peek(cueSheetPtr + c * CUE_SIZE + k) & 0xFF
|
||||
audio.uploadCue(c, cueBytes)
|
||||
}
|
||||
sys.free(cueSheetPtr)
|
||||
|
||||
// -- 8. Configure playhead ------------------------------------------------
|
||||
audio.setTrackerMode(playhead)
|
||||
audio.setBPM(playhead, bpm)
|
||||
audio.setTickRate(playhead, tickRate > 0 ? tickRate : 6)
|
||||
audio.setTrackerMixerFlags(playhead, mixerflags)
|
||||
audio.setSongGlobalVolume(playhead, songGlobalVolume)
|
||||
audio.setSongMixingVolume(playhead, songMixingVolume)
|
||||
|
||||
|
||||
fileHandle.close()
|
||||
sys.free(filePtr)
|
||||
}
|
||||
|
||||
// ── captureTrackerDataToFile ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Dump the current tracker hardware state (sample bin, instruments, patterns
|
||||
* in bank 0, cue sheet) to a single-song Taud file. BPM and tick-rate are
|
||||
* taken from playhead 0.
|
||||
*
|
||||
* @param outFile Full path with drive letter, e.g. "A:/music/out.taud"
|
||||
*/
|
||||
function captureTrackerDataToFile(outFile) {
|
||||
const drive = outFile[0].toUpperCase()
|
||||
const diskPath = outFile.substring(2)
|
||||
|
||||
const memBase = audio.getMemAddr()
|
||||
const baseAddr = audio.getBaseAddr()
|
||||
|
||||
// -- 1. Compress sample+instrument bin ------------------------------------
|
||||
// sys.memcpy(negative_src, positive_dst, len) copies peripheral byte k from
|
||||
// (memBase - k) into (sampleInstBuf + k).
|
||||
let sampleInstBuf = sys.malloc(SAMPLEINST_SIZE)
|
||||
sys.memcpy(memBase, sampleInstBuf, SAMPLEINST_SIZE)
|
||||
|
||||
let compBuf = sys.malloc(SAMPLEINST_SIZE + 4096) // headroom for incompressible data
|
||||
let compressedSize = gzip.compFromTo(sampleInstBuf, SAMPLEINST_SIZE, compBuf)
|
||||
sys.free(sampleInstBuf)
|
||||
|
||||
// -- 2. Find last non-empty pattern in bank 0 (all-zero = uninitialized) --
|
||||
let numPatsActual = 0
|
||||
outer: for (let p = NUM_PATTERNS_MAX - 1; p >= 0; p--) {
|
||||
let patBase = 131072 + p * PATTERN_SIZE // offset within peripheral memory space
|
||||
for (let k = 0; k < PATTERN_SIZE; k++) {
|
||||
if ((sys.peek(memBase - (patBase + k)) & 0xFF) !== 0) {
|
||||
numPatsActual = p + 1
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
if (numPatsActual === 0) numPatsActual = 1 // always emit at least one pattern slot
|
||||
|
||||
let numPats = numPatsActual // Uint16, 1-65535
|
||||
let patsToSave = numPatsActual
|
||||
|
||||
// -- 3. BPM / tick-rate / volumes from playhead 0 -------------------------
|
||||
let bpm = audio.getBPM(0) || 125
|
||||
let tickRate = audio.getTickRate(0) || 6
|
||||
let bpmStored = (bpm - 24) & 0xFF
|
||||
let songGlobalVolume = audio.getSongGlobalVolume(0)
|
||||
let songMixingVolume = audio.getSongMixingVolume(0)
|
||||
if (songGlobalVolume === undefined || songGlobalVolume === null) songGlobalVolume = 0x80
|
||||
if (songMixingVolume === undefined || songMixingVolume === null) songMixingVolume = 0x80
|
||||
|
||||
// -- 4. Compress pattern bin ----------------------------------------------
|
||||
let patBinSize = patsToSave * PATTERN_SIZE
|
||||
let patBuf = sys.malloc(patBinSize)
|
||||
sys.memcpy(memBase - 131072, patBuf, patBinSize)
|
||||
|
||||
let patCompBuf = sys.malloc(patBinSize + 4096)
|
||||
let patCompSize = gzip.compFromTo(patBuf, patBinSize, patCompBuf)
|
||||
sys.free(patBuf)
|
||||
|
||||
// -- 5. Compress cue sheet ------------------------------------------------
|
||||
// Cue entry c, byte k is at MMIO address 32768 + c*32 + k,
|
||||
// accessed as sys.peek(baseAddr − (32768 + c*32 + k)).
|
||||
let cueSheetSize = NUM_CUES * CUE_SIZE
|
||||
let cueBuf = sys.malloc(cueSheetSize)
|
||||
for (let c = 0; c < NUM_CUES; c++) {
|
||||
let cueOff = 32768 + c * CUE_SIZE
|
||||
for (let k = 0; k < CUE_SIZE; k++)
|
||||
sys.poke(cueBuf + c * CUE_SIZE + k,
|
||||
sys.peek(baseAddr - (cueOff + k)) & 0xFF)
|
||||
}
|
||||
|
||||
let cueCompBuf = sys.malloc(cueSheetSize + 4096)
|
||||
let cueCompSize = gzip.compFromTo(cueBuf, cueSheetSize, cueCompBuf)
|
||||
sys.free(cueBuf)
|
||||
|
||||
// -- 6. Compute song offset (absolute from file start) --------------------
|
||||
// Layout: header(32) + compressed(compressedSize) + songTable(1 × TAUD_SONG_ENTRY)
|
||||
let songOffset = TAUD_HEADER_SIZE + compressedSize + 1 * TAUD_SONG_ENTRY
|
||||
|
||||
// -- 7. Build header byte array (32 bytes) --------------------------------
|
||||
let sigBytes = new Array(14)
|
||||
for (let i = 0; i < 14; i++)
|
||||
sigBytes[i] = i < CAPTURE_SIGNATURE.length ? CAPTURE_SIGNATURE.charCodeAt(i) : 0
|
||||
|
||||
let header = [
|
||||
// Magic (8)
|
||||
0x1F, 0x54, 0x53, 0x56, 0x4D, 0x61, 0x75, 0x64,
|
||||
// version, numSongs
|
||||
TAUD_VERSION, 1,
|
||||
// compressedSize uint32 LE (4) -- sample+inst bin
|
||||
(compressedSize ) & 0xFF,
|
||||
(compressedSize >>> 8) & 0xFF,
|
||||
(compressedSize >>> 16) & 0xFF,
|
||||
(compressedSize >>> 24) & 0xFF,
|
||||
// project data offset (4) -- not emitted
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
].concat(sigBytes) // 8 + 2 + 4 + 4 + 14 = 32 bytes
|
||||
|
||||
// -- 8. Build song-table row (32 bytes) -----------------------------------
|
||||
let songTable = [
|
||||
(songOffset ) & 0xFF,
|
||||
(songOffset >>> 8) & 0xFF,
|
||||
(songOffset >>> 16) & 0xFF,
|
||||
(songOffset >>> 24) & 0xFF,
|
||||
20, // numVoices
|
||||
numPats & 0xFF, (numPats >>> 8) & 0xFF, // numPatterns Uint16 LE
|
||||
bpmStored, // BPM with −24 bias
|
||||
tickRate, // initial tick-rate
|
||||
0x00,0xA0, // basenote (0xA000 -- C9)
|
||||
0x00,0xAC,0x02,0x46, // basefreq (8363 Hz)
|
||||
sys.peek(baseAddr - 7), // mixer flags
|
||||
songGlobalVolume & 0xFF, // global volume
|
||||
songMixingVolume & 0xFF, // mixing volume
|
||||
// pattern bin compressed size (4)
|
||||
(patCompSize ) & 0xFF,
|
||||
(patCompSize >>> 8) & 0xFF,
|
||||
(patCompSize >>> 16) & 0xFF,
|
||||
(patCompSize >>> 24) & 0xFF,
|
||||
// cue sheet compressed size (4)
|
||||
(cueCompSize ) & 0xFF,
|
||||
(cueCompSize >>> 8) & 0xFF,
|
||||
(cueCompSize >>> 16) & 0xFF,
|
||||
(cueCompSize >>> 24) & 0xFF,
|
||||
// reserved (6)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]
|
||||
|
||||
// -- 9. Write header (creates / truncates file) ---------------------------
|
||||
const fileHandle = files.open(outFile)
|
||||
fileHandle.bwrite(header)
|
||||
|
||||
// -- 10. Append compressed sample+inst bin --------------------------------
|
||||
fileHandle.pwrite(compBuf, compressedSize, TAUD_HEADER_SIZE)
|
||||
sys.free(compBuf)
|
||||
|
||||
// -- 11. Write song table -------------------------------------------------
|
||||
fileHandle.bwrite(songTable)
|
||||
|
||||
// -- 12. Append compressed pattern bin ------------------------------------
|
||||
fileHandle.pwrite(patCompBuf, patCompSize,
|
||||
TAUD_HEADER_SIZE + compressedSize + songTable.length)
|
||||
sys.free(patCompBuf)
|
||||
|
||||
// -- 13. Append compressed cue sheet --------------------------------------
|
||||
fileHandle.pwrite(cueCompBuf, cueCompSize,
|
||||
TAUD_HEADER_SIZE + compressedSize + songTable.length + patCompSize)
|
||||
sys.free(cueCompBuf)
|
||||
|
||||
|
||||
fileHandle.flush(); fileHandle.close()
|
||||
}
|
||||
|
||||
exports = { uploadTaudFile, captureTrackerDataToFile }
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* WinTex — TUI window management and renderer
|
||||
* @author CuriousTorvald
|
||||
*/
|
||||
|
||||
class WindowObject {
|
||||
|
||||
constructor(x, y, w, h, inputProcessor, drawContents, title, drawFrame) {
|
||||
@@ -91,8 +96,6 @@ class WindowObject {
|
||||
* @return [new cursor pos, new scroll pos]
|
||||
*/
|
||||
function scrollVert(dy, listSize, listHeight, currentCursorPos, currentScrollPos, scrollPeek) {
|
||||
let peek = 1
|
||||
|
||||
// clamp dy
|
||||
if (currentCursorPos + dy > listSize - 1)
|
||||
dy = (listSize - 1) - currentCursorPos
|
||||
@@ -103,13 +106,13 @@ function scrollVert(dy, listSize, listHeight, currentCursorPos, currentScrollPos
|
||||
|
||||
// update vertical scroll stats
|
||||
if (dy != 0) {
|
||||
let visible = listHeight - 1 - peek
|
||||
let visible = listHeight - 1 - scrollPeek
|
||||
|
||||
if (nextRow - currentScrollPos > visible) {
|
||||
currentScrollPos = nextRow - visible
|
||||
}
|
||||
else if (nextRow - currentScrollPos < 0 + peek) {
|
||||
currentScrollPos = nextRow - peek // nextRow is less than zero
|
||||
else if (nextRow - currentScrollPos < 0 + scrollPeek) {
|
||||
currentScrollPos = nextRow - scrollPeek // nextRow is less than zero
|
||||
}
|
||||
|
||||
// NOTE: future-proofing here -- scroll clamping is moved outside of go-up/go-down
|
||||
@@ -140,8 +143,6 @@ function scrollVert(dy, listSize, listHeight, currentCursorPos, currentScrollPos
|
||||
* @return [new cursor pos, new scroll pos]
|
||||
*/
|
||||
function scrollHorz(dx, stringSize, stringViewSize, currentCursorPos, currentScrollPos, scrollPeek) {
|
||||
let peek = 1
|
||||
|
||||
// clamp dx
|
||||
if (currentCursorPos + dx > stringSize - 1)
|
||||
dx = (stringSize - 1) - currentCursorPos
|
||||
@@ -152,13 +153,13 @@ function scrollHorz(dx, stringSize, stringViewSize, currentCursorPos, currentScr
|
||||
|
||||
// update vertical scroll stats
|
||||
if (dx != 0) {
|
||||
let visible = stringViewSize - 1 - peek
|
||||
let visible = stringViewSize - 1 - scrollPeek
|
||||
|
||||
if (nextCol - currentScrollPos > visible) {
|
||||
currentScrollPos = nextCol - visible
|
||||
}
|
||||
else if (nextCol - currentScrollPos < 0 + peek) {
|
||||
currentScrollPos = nextCol - peek // nextCol is less than zero
|
||||
else if (nextCol - currentScrollPos < 0 + scrollPeek) {
|
||||
currentScrollPos = nextCol - scrollPeek // nextCol is less than zero
|
||||
}
|
||||
|
||||
// NOTE: future-proofing here -- scroll clamping is moved outside of go-up/go-down
|
||||
|
||||
6
assets/disk0/tvdos/tuidev/Makefile
Normal file
6
assets/disk0/tvdos/tuidev/Makefile
Normal file
@@ -0,0 +1,6 @@
|
||||
CC = gcc
|
||||
CFLAGS = -std=c99 -O3 -Wall -Wextra -Ofast -D_GNU_SOURCE
|
||||
|
||||
font_rom_builder:
|
||||
rm -f font_rom_builder
|
||||
$(CC) $(CFLAGS) font_rom_builder.c -o font_rom_builder
|
||||
202
assets/disk0/tvdos/tuidev/font_rom_builder.c
Normal file
202
assets/disk0/tvdos/tuidev/font_rom_builder.c
Normal file
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* font_rom_builder.c
|
||||
* Build TSVM 7x14 font ROM from human-readable images (.png, .tga)
|
||||
*
|
||||
* Input: Image with no gaps between characters (7x14 pixels per glyph)
|
||||
* Output: TSVM-compatible font ROM file(s) padded to 1920 bytes
|
||||
*
|
||||
* Usage:
|
||||
* gcc -O2 -std=c99 -Wall font_rom_builder.c -o font_rom_builder
|
||||
* ./font_rom_builder <input.png|tga> <output_prefix>
|
||||
*
|
||||
* For 128-char images: outputs <output_prefix>_high.chr
|
||||
* For 256-char images: outputs <output_prefix>_low.chr and <output_prefix>_high.chr
|
||||
*
|
||||
* Image layout:
|
||||
* - 128 chars: 16 columns × 8 rows = 112×112 pixels
|
||||
* - 256 chars: 16 columns × 16 rows = 112×224 pixels
|
||||
* or 32 columns × 8 rows = 224×112 pixels
|
||||
*
|
||||
* ROM format:
|
||||
* - Each glyph: 14 bytes (one byte per row)
|
||||
* - Bit 6 = leftmost pixel, Bit 0 = rightmost pixel
|
||||
* - Each ROM padded to 1920 bytes
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define GLYPH_W 7
|
||||
#define GLYPH_H 14
|
||||
#define GLYPH_BYTES 14
|
||||
#define ROM_PADDED_SIZE 1920
|
||||
|
||||
static void die(const char *msg) {
|
||||
fprintf(stderr, "Error: %s\n", msg);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
static void write_rom(const char *filename, const uint8_t *glyphs, int glyph_count) {
|
||||
FILE *out = fopen(filename, "wb");
|
||||
if (!out) {
|
||||
fprintf(stderr, "Failed to open output file: %s\n", filename);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Write glyph data
|
||||
size_t data_size = glyph_count * GLYPH_BYTES;
|
||||
fwrite(glyphs, 1, data_size, out);
|
||||
|
||||
// Pad to 1920 bytes
|
||||
if (data_size < ROM_PADDED_SIZE) {
|
||||
size_t padding = ROM_PADDED_SIZE - data_size;
|
||||
uint8_t *pad = calloc(padding, 1);
|
||||
fwrite(pad, 1, padding, out);
|
||||
free(pad);
|
||||
fprintf(stderr, " Wrote %zu bytes + %zu bytes padding = %d bytes total\n",
|
||||
data_size, padding, ROM_PADDED_SIZE);
|
||||
} else {
|
||||
fprintf(stderr, " Wrote %zu bytes (no padding needed)\n", data_size);
|
||||
}
|
||||
|
||||
fclose(out);
|
||||
fprintf(stderr, " Output: %s\n", filename);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 3) {
|
||||
fprintf(stderr, "Usage: %s <input.png|tga> <output_prefix>\n", argv[0]);
|
||||
fprintf(stderr, "\n");
|
||||
fprintf(stderr, "Converts human-readable font images to TSVM font ROM format.\n");
|
||||
fprintf(stderr, "\n");
|
||||
fprintf(stderr, "Input requirements:\n");
|
||||
fprintf(stderr, " - Image with no gaps between characters\n");
|
||||
fprintf(stderr, " - Each character is 7x14 pixels\n");
|
||||
fprintf(stderr, " - 128 chars: typically 112x112 (16 cols × 8 rows)\n");
|
||||
fprintf(stderr, " - 256 chars: typically 112x224 (16 cols × 16 rows)\n");
|
||||
fprintf(stderr, "\n");
|
||||
fprintf(stderr, "Output:\n");
|
||||
fprintf(stderr, " - 128 chars: <prefix>_high.chr (high ROM only)\n");
|
||||
fprintf(stderr, " - 256 chars: <prefix>_low.chr + <prefix>_high.chr\n");
|
||||
fprintf(stderr, " - Each ROM padded to 1920 bytes\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *input_path = argv[1];
|
||||
const char *output_prefix = argv[2];
|
||||
|
||||
// Get image dimensions using ImageMagick identify
|
||||
char cmd[1024];
|
||||
snprintf(cmd, sizeof(cmd), "identify -format '%%w %%h' \"%s\" 2>/dev/null", input_path);
|
||||
|
||||
FILE *pipe = popen(cmd, "r");
|
||||
if (!pipe) die("Failed to run 'identify' command (ImageMagick required)");
|
||||
|
||||
int img_w = 0, img_h = 0;
|
||||
if (fscanf(pipe, "%d %d", &img_w, &img_h) != 2) {
|
||||
pclose(pipe);
|
||||
die("Failed to read image dimensions (is ImageMagick installed?)");
|
||||
}
|
||||
pclose(pipe);
|
||||
|
||||
fprintf(stderr, "Input: %s (%dx%d)\n", input_path, img_w, img_h);
|
||||
|
||||
// Calculate grid dimensions
|
||||
int cols = img_w / GLYPH_W;
|
||||
int rows = img_h / GLYPH_H;
|
||||
int total_chars = cols * rows;
|
||||
|
||||
if (img_w % GLYPH_W != 0 || img_h % GLYPH_H != 0) {
|
||||
fprintf(stderr, "Warning: Image dimensions not evenly divisible by %dx%d\n",
|
||||
GLYPH_W, GLYPH_H);
|
||||
}
|
||||
|
||||
fprintf(stderr, "Grid: %d columns × %d rows = %d characters\n", cols, rows, total_chars);
|
||||
|
||||
// Validate character count
|
||||
if (total_chars != 128 && total_chars != 256) {
|
||||
fprintf(stderr, "Error: Expected 128 or 256 characters, got %d\n", total_chars);
|
||||
fprintf(stderr, " For 128 chars: use 112x112 (16×8) or similar layout\n");
|
||||
fprintf(stderr, " For 256 chars: use 112x224 (16×16) or 224x112 (32×8)\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Read image as grayscale using ImageMagick convert
|
||||
// IMPORTANT: Flatten alpha onto black background first, so transparent pixels become black
|
||||
size_t img_size = img_w * img_h;
|
||||
uint8_t *img_data = malloc(img_size);
|
||||
if (!img_data) die("Memory allocation failed");
|
||||
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"convert \"%s\" -background black -alpha remove -colorspace Gray -depth 8 gray:- 2>/dev/null",
|
||||
input_path);
|
||||
|
||||
pipe = popen(cmd, "r");
|
||||
if (!pipe) die("Failed to run 'convert' command (ImageMagick required)");
|
||||
|
||||
if (fread(img_data, 1, img_size, pipe) != img_size) {
|
||||
pclose(pipe);
|
||||
die("Failed to read image data from ImageMagick");
|
||||
}
|
||||
pclose(pipe);
|
||||
|
||||
fprintf(stderr, "Read %zu bytes of grayscale data\n", img_size);
|
||||
|
||||
// Extract glyphs
|
||||
uint8_t *glyphs = calloc(total_chars, GLYPH_BYTES);
|
||||
if (!glyphs) die("Memory allocation failed");
|
||||
|
||||
for (int gy = 0; gy < rows; gy++) {
|
||||
for (int gx = 0; gx < cols; gx++) {
|
||||
int glyph_idx = gy * cols + gx;
|
||||
uint8_t *glyph = &glyphs[glyph_idx * GLYPH_BYTES];
|
||||
|
||||
for (int row = 0; row < GLYPH_H; row++) {
|
||||
uint8_t byte = 0;
|
||||
for (int col = 0; col < GLYPH_W; col++) {
|
||||
int px = gx * GLYPH_W + col;
|
||||
int py = gy * GLYPH_H + row;
|
||||
uint8_t pixel = img_data[py * img_w + px];
|
||||
|
||||
// Threshold: >= 128 is foreground (white/lit)
|
||||
int is_set = (pixel >= 128) ? 1 : 0;
|
||||
|
||||
// Pack: bit 6 = leftmost, bit 0 = rightmost
|
||||
if (is_set) {
|
||||
byte |= (1u << (6 - col));
|
||||
}
|
||||
}
|
||||
glyph[row] = byte;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(img_data);
|
||||
fprintf(stderr, "Extracted %d glyphs\n", total_chars);
|
||||
|
||||
// Write output ROM file(s)
|
||||
char out_path[1024];
|
||||
|
||||
if (total_chars == 128) {
|
||||
// High ROM only (chars 128-255)
|
||||
snprintf(out_path, sizeof(out_path), "%s.chr", output_prefix);
|
||||
fprintf(stderr, "\nWriting high ROM (128 chars):\n");
|
||||
write_rom(out_path, glyphs, 128);
|
||||
} else {
|
||||
// 256 chars: low ROM (0-127) and high ROM (128-255)
|
||||
snprintf(out_path, sizeof(out_path), "%s_low.chr", output_prefix);
|
||||
fprintf(stderr, "\nWriting low ROM (chars 0-127):\n");
|
||||
write_rom(out_path, glyphs, 128);
|
||||
|
||||
snprintf(out_path, sizeof(out_path), "%s_high.chr", output_prefix);
|
||||
fprintf(stderr, "\nWriting high ROM (chars 128-255):\n");
|
||||
write_rom(out_path, &glyphs[128 * GLYPH_BYTES], 128);
|
||||
}
|
||||
|
||||
free(glyphs);
|
||||
fprintf(stderr, "\nDone.\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -22,6 +22,7 @@ cp -r "../out/$RUNTIME" $DESTDIR/
|
||||
|
||||
# Copy over all the assets and a jarfile
|
||||
cp -r "../out/TerranBASIC.jar" $DESTDIR/
|
||||
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/
|
||||
|
||||
# Pack everything to AppImage
|
||||
ARCH=arm_aarch64 "./$APPIMAGETOOL" $DESTDIR "out/$DESTDIR.AppImage" || { echo 'Building AppImage failed' >&2; exit 1; }
|
||||
|
||||
@@ -22,6 +22,7 @@ cp -r "../out/$RUNTIME" $DESTDIR/
|
||||
|
||||
# Copy over all the assets and a jarfile
|
||||
cp -r "../out/TerranBASIC.jar" $DESTDIR/
|
||||
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/
|
||||
|
||||
# Pack everything to AppImage
|
||||
"./$APPIMAGETOOL" $DESTDIR "out/$DESTDIR.AppImage" || { echo 'Building AppImage failed' >&2; exit 1; }
|
||||
|
||||
@@ -23,5 +23,6 @@ cp -r "../out/$RUNTIME" $DESTDIR/Contents/MacOS/
|
||||
|
||||
# Copy over all the assets and a jarfile
|
||||
cp -r "../out/TerranBASIC.jar" $DESTDIR/Contents/MacOS/
|
||||
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/Contents/MacOS/
|
||||
|
||||
echo "Build successful: $DESTDIR"
|
||||
|
||||
@@ -23,5 +23,6 @@ cp -r "../out/$RUNTIME" $DESTDIR/Contents/MacOS/
|
||||
|
||||
# Copy over all the assets and a jarfile
|
||||
cp -r "../out/TerranBASIC.jar" $DESTDIR/Contents/MacOS/
|
||||
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/Contents/MacOS/
|
||||
|
||||
echo "Build successful: $DESTDIR"
|
||||
|
||||
@@ -18,6 +18,7 @@ cp -r "../out/$RUNTIME" $DESTDIR/
|
||||
|
||||
# Copy over all the assets and a jarfile
|
||||
cp -r "../out/TerranBASIC.jar" $DESTDIR/
|
||||
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/
|
||||
|
||||
# Temporary solution: zip everything
|
||||
zip -r -9 -l "out/$DESTDIR.zip" $DESTDIR
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
cd "${0%/*}"
|
||||
./runtime-linux-arm/bin/java -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar
|
||||
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
|
||||
./runtime-linux-arm/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
cd "${0%/*}"
|
||||
./runtime-linux-x86/bin/java -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar
|
||||
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
|
||||
./runtime-linux-x86/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
cd "${0%/*}"
|
||||
./runtime-osx-arm/bin/java -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar
|
||||
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
|
||||
./runtime-osx-arm/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
cd "${0%/*}"
|
||||
./runtime-osx-x86/bin/java -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar
|
||||
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
|
||||
./runtime-osx-x86/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
cd /D "%~dp0"
|
||||
.\runtime-windows-x86\bin\java -Xms128M -Xmx2G -jar .\TerranBASIC.jar
|
||||
set GRAAL_MODULE_PATH=compiler-23.1.10.jar;compiler-management-23.1.10.jar;truffle-compiler-23.1.10.jar;truffle-api-23.1.10.jar;truffle-runtime-23.1.10.jar;polyglot-23.1.10.jar;collections-23.1.10.jar;word-23.1.10.jar;nativeimage-23.1.10.jar;jniutils-23.1.10.jar
|
||||
.\runtime-windows-x86\bin\java --upgrade-module-path=%GRAAL_MODULE_PATH% -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -Xms128M -Xmx2G -jar .\TerranBASIC.jar
|
||||
|
||||
@@ -662,7 +662,7 @@ TODO
|
||||
\endlastfoot
|
||||
\centering
|
||||
\begin{tabulary}{\textwidth}{rl}
|
||||
{\ttfamily 0} & {\ttfamily \#000F} \\
|
||||
{\ttfamily 0} & {\ttfamily \#0007} \\
|
||||
{\ttfamily 1} & {\ttfamily \#004F} \\
|
||||
{\ttfamily 2} & {\ttfamily \#008F} \\
|
||||
{\ttfamily 3} & {\ttfamily \#00BF} \\
|
||||
|
||||
BIN
doc/tsvmpal.png
BIN
doc/tsvmpal.png
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
81
ipf_encoder/Makefile
Normal file
81
ipf_encoder/Makefile
Normal file
@@ -0,0 +1,81 @@
|
||||
# Makefile for iPF (TSVM Interchangeable Picture Format) Encoder
|
||||
# Created by CuriousTorvald and Claude on 2025-12-19.
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -std=c99 -Wall -Wextra -O2 -D_GNU_SOURCE
|
||||
DBGFLAGS =
|
||||
PREFIX = /usr/local
|
||||
|
||||
# Zstd flags (use pkg-config if available, fallback for cross-platform compatibility)
|
||||
ZSTD_CFLAGS = $(shell pkg-config --cflags libzstd 2>/dev/null || echo "")
|
||||
ZSTD_LIBS = $(shell pkg-config --libs libzstd 2>/dev/null || echo "-lzstd")
|
||||
LIBS = -lm $(ZSTD_LIBS)
|
||||
|
||||
# Targets
|
||||
TARGETS = encoder_ipf decoder_ipf
|
||||
|
||||
# Build all (default)
|
||||
all: $(TARGETS)
|
||||
|
||||
encoder_ipf: encoder_ipf.c
|
||||
rm -f encoder_ipf
|
||||
$(CC) $(CFLAGS) $(ZSTD_CFLAGS) -o encoder_ipf encoder_ipf.c $(LIBS)
|
||||
@echo "iPF encoder built: encoder_ipf"
|
||||
|
||||
decoder_ipf: decoder_ipf.c
|
||||
rm -f decoder_ipf
|
||||
$(CC) $(CFLAGS) $(ZSTD_CFLAGS) -o decoder_ipf decoder_ipf.c $(LIBS)
|
||||
@echo "iPF decoder built: decoder_ipf"
|
||||
|
||||
# Build with debug symbols
|
||||
debug: CFLAGS += -g -DDEBUG -fsanitize=address -fno-omit-frame-pointer
|
||||
debug: DBGFLAGS += -fsanitize=address -fno-omit-frame-pointer
|
||||
debug: clean $(TARGETS)
|
||||
|
||||
# Build with optimizations
|
||||
release: CFLAGS = -std=c99 -Wall -Wextra -O3 -D_GNU_SOURCE -march=native
|
||||
release: clean $(TARGETS)
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -f $(TARGETS) *.o
|
||||
|
||||
# Install
|
||||
install: $(TARGETS)
|
||||
cp encoder_ipf $(PREFIX)/bin/
|
||||
cp decoder_ipf $(PREFIX)/bin/
|
||||
|
||||
# Check for required dependencies
|
||||
check-deps:
|
||||
@echo "Checking dependencies..."
|
||||
@pkg-config --exists libzstd || (echo "Error: libzstd-dev not found. Install libzstd-dev or equivalent" && exit 1)
|
||||
@which ffmpeg >/dev/null 2>&1 || (echo "Error: ffmpeg not found in PATH" && exit 1)
|
||||
@which ffprobe >/dev/null 2>&1 || (echo "Error: ffprobe not found in PATH" && exit 1)
|
||||
@echo "All dependencies found."
|
||||
|
||||
# Help
|
||||
help:
|
||||
@echo "iPF (TSVM Interchangeable Picture Format) Tools"
|
||||
@echo ""
|
||||
@echo "Targets:"
|
||||
@echo " all - Build encoder and decoder (default)"
|
||||
@echo " encoder_ipf - Build encoder only"
|
||||
@echo " decoder_ipf - Build decoder only"
|
||||
@echo " debug - Build with debug symbols and AddressSanitizer"
|
||||
@echo " release - Build with full optimizations"
|
||||
@echo " clean - Remove build artifacts"
|
||||
@echo " install - Install to /usr/local/bin"
|
||||
@echo " check-deps - Check for required dependencies"
|
||||
@echo " help - Show this help"
|
||||
@echo ""
|
||||
@echo "Requirements:"
|
||||
@echo " - GCC with C99 support"
|
||||
@echo " - libzstd-dev (Zstd compression library)"
|
||||
@echo " - FFmpeg (for image encoding/decoding)"
|
||||
@echo ""
|
||||
@echo "Usage:"
|
||||
@echo " make # Build all"
|
||||
@echo " ./encoder_ipf -i input.png -o output.ipf # Encode"
|
||||
@echo " ./decoder_ipf -i output.ipf -o decoded.png # Decode"
|
||||
|
||||
.PHONY: all clean install check-deps help debug release
|
||||
592
ipf_encoder/decoder_ipf.c
Normal file
592
ipf_encoder/decoder_ipf.c
Normal file
@@ -0,0 +1,592 @@
|
||||
/**
|
||||
* iPF Decoder - TSVM Interchangeable Picture Format Decoder
|
||||
*
|
||||
* Decodes iPF format (Type 1 or Type 2) images to standard formats via FFmpeg.
|
||||
*
|
||||
* Created by CuriousTorvald and Claude on 2025-12-19.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <getopt.h>
|
||||
#include <zstd.h>
|
||||
|
||||
// =============================================================================
|
||||
// Constants
|
||||
// =============================================================================
|
||||
|
||||
#define IPF_MAGIC "\x1F\x54\x53\x56\x4D\x69\x50\x46" // "\x1FTSVMiPF"
|
||||
#define IPF_HEADER_SIZE 28 // 8 magic + 2 width + 2 height + 1 flags + 1 type + 10 reserved + 4 uncompressed
|
||||
|
||||
#define IPF_TYPE_1 0 // 4:2:0 chroma subsampling
|
||||
#define IPF_TYPE_2 1 // 4:2:2 chroma subsampling
|
||||
|
||||
#define IPF_FLAG_ALPHA 0x01
|
||||
#define IPF_FLAG_ZSTD 0x10
|
||||
#define IPF_FLAG_PROGRESSIVE 0x80
|
||||
|
||||
#define MAX_PATH 4096
|
||||
|
||||
// =============================================================================
|
||||
// Structures
|
||||
// =============================================================================
|
||||
|
||||
typedef struct {
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
uint8_t flags;
|
||||
uint8_t type;
|
||||
uint32_t uncompressed_size;
|
||||
} ipf_header_t;
|
||||
|
||||
typedef struct {
|
||||
char *input_file;
|
||||
char *output_file;
|
||||
int verbose;
|
||||
int raw_output; // Output raw RGB instead of using FFmpeg
|
||||
} decoder_config_t;
|
||||
|
||||
// =============================================================================
|
||||
// Utility Functions
|
||||
// =============================================================================
|
||||
|
||||
static void print_usage(const char *program) {
|
||||
printf("iPF Decoder - TSVM Interchangeable Picture Format\n");
|
||||
printf("\nUsage: %s -i input.ipf -o output.png [options]\n\n", program);
|
||||
printf("Required:\n");
|
||||
printf(" -i, --input FILE Input iPF file\n");
|
||||
printf(" -o, --output FILE Output image file (any format FFmpeg supports)\n");
|
||||
printf("\nOptions:\n");
|
||||
printf(" --raw Output raw RGB24/RGBA data instead of image file\n");
|
||||
printf(" -v, --verbose Verbose output\n");
|
||||
printf(" -h, --help Show this help\n");
|
||||
printf("\nExamples:\n");
|
||||
printf(" %s -i photo.ipf -o photo.png\n", program);
|
||||
printf(" %s -i logo.ipf -o logo.jpg -v\n", program);
|
||||
}
|
||||
|
||||
static float clampf(float v, float lo, float hi) {
|
||||
return v < lo ? lo : (v > hi ? hi : v);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// iPF File Reading
|
||||
// =============================================================================
|
||||
|
||||
static int read_ipf_header(FILE *fp, ipf_header_t *header) {
|
||||
uint8_t magic[8];
|
||||
|
||||
if (fread(magic, 1, 8, fp) != 8) {
|
||||
fprintf(stderr, "Error: Failed to read magic\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (memcmp(magic, IPF_MAGIC, 8) != 0) {
|
||||
fprintf(stderr, "Error: Invalid iPF magic\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read width (uint16 LE)
|
||||
if (fread(&header->width, 2, 1, fp) != 1) return -1;
|
||||
|
||||
// Read height (uint16 LE)
|
||||
if (fread(&header->height, 2, 1, fp) != 1) return -1;
|
||||
|
||||
// Read flags
|
||||
if (fread(&header->flags, 1, 1, fp) != 1) return -1;
|
||||
|
||||
// Read type
|
||||
if (fread(&header->type, 1, 1, fp) != 1) return -1;
|
||||
|
||||
// Skip reserved (10 bytes)
|
||||
fseek(fp, 10, SEEK_CUR);
|
||||
|
||||
// Read uncompressed size (uint32 LE)
|
||||
if (fread(&header->uncompressed_size, 4, 1, fp) != 1) return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// YCoCg to RGB Conversion
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Convert YCoCg to RGB for 4 pixels sharing the same chroma.
|
||||
* y_values: 4 Y values packed as nibbles (Y0|Y1 in low byte, Y2|Y3 in high byte style)
|
||||
* a_values: 4 alpha values packed similarly
|
||||
* co, cg: 4-bit chroma values [0..15]
|
||||
*
|
||||
* Output: fills rgb array with R,G,B[,A] values for 4 pixels
|
||||
*/
|
||||
static void ycocg_to_rgb_quad(int co, int cg, int y0, int y1, int y2, int y3,
|
||||
int a0, int a1, int a2, int a3,
|
||||
int has_alpha, uint8_t *rgb) {
|
||||
// Convert chroma from [0..15] to [-1..1]
|
||||
float co_f = (co - 7) / 8.0f;
|
||||
float cg_f = (cg - 7) / 8.0f;
|
||||
|
||||
int ys[4] = {y0, y1, y2, y3};
|
||||
int as[4] = {a0, a1, a2, a3};
|
||||
|
||||
int stride = has_alpha ? 4 : 3;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
float y = ys[i] / 15.0f;
|
||||
|
||||
// YCoCg to RGB conversion
|
||||
float tmp = y - cg_f / 2.0f;
|
||||
float g = clampf(cg_f + tmp, 0.0f, 1.0f);
|
||||
float b = clampf(tmp - co_f / 2.0f, 0.0f, 1.0f);
|
||||
float r = clampf(b + co_f, 0.0f, 1.0f);
|
||||
|
||||
rgb[i * stride + 0] = (uint8_t)(r * 255.0f + 0.5f);
|
||||
rgb[i * stride + 1] = (uint8_t)(g * 255.0f + 0.5f);
|
||||
rgb[i * stride + 2] = (uint8_t)(b * 255.0f + 0.5f);
|
||||
|
||||
if (has_alpha) {
|
||||
rgb[i * stride + 3] = (uint8_t)(as[i] * 17); // Scale 0-15 to 0-255
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode iPF1 block (4:2:0 chroma subsampling).
|
||||
* Input: 12 bytes (or 20 with alpha)
|
||||
* Output: 16 pixels in RGB24/RGBA format
|
||||
*/
|
||||
static void decode_ipf1_block(const uint8_t *block, int has_alpha, uint8_t *pixels, int stride) {
|
||||
// Read chroma (4 values for 2x2 regions)
|
||||
int co1 = block[0] & 0x0F;
|
||||
int co2 = (block[0] >> 4) & 0x0F;
|
||||
int co3 = block[1] & 0x0F;
|
||||
int co4 = (block[1] >> 4) & 0x0F;
|
||||
|
||||
int cg1 = block[2] & 0x0F;
|
||||
int cg2 = (block[2] >> 4) & 0x0F;
|
||||
int cg3 = block[3] & 0x0F;
|
||||
int cg4 = (block[3] >> 4) & 0x0F;
|
||||
|
||||
// Read Y values (16 values)
|
||||
// Layout: [Y1|Y0|Y5|Y4], [Y3|Y2|Y7|Y6], [Y9|Y8|YD|YC], [YB|YA|YF|YE]
|
||||
int Y[16];
|
||||
Y[0] = block[4] & 0x0F;
|
||||
Y[1] = (block[4] >> 4) & 0x0F;
|
||||
Y[4] = block[5] & 0x0F;
|
||||
Y[5] = (block[5] >> 4) & 0x0F;
|
||||
Y[2] = block[6] & 0x0F;
|
||||
Y[3] = (block[6] >> 4) & 0x0F;
|
||||
Y[6] = block[7] & 0x0F;
|
||||
Y[7] = (block[7] >> 4) & 0x0F;
|
||||
Y[8] = block[8] & 0x0F;
|
||||
Y[9] = (block[8] >> 4) & 0x0F;
|
||||
Y[12] = block[9] & 0x0F;
|
||||
Y[13] = (block[9] >> 4) & 0x0F;
|
||||
Y[10] = block[10] & 0x0F;
|
||||
Y[11] = (block[10] >> 4) & 0x0F;
|
||||
Y[14] = block[11] & 0x0F;
|
||||
Y[15] = (block[11] >> 4) & 0x0F;
|
||||
|
||||
// Read alpha values if present
|
||||
int A[16];
|
||||
if (has_alpha) {
|
||||
A[0] = block[12] & 0x0F;
|
||||
A[1] = (block[12] >> 4) & 0x0F;
|
||||
A[4] = block[13] & 0x0F;
|
||||
A[5] = (block[13] >> 4) & 0x0F;
|
||||
A[2] = block[14] & 0x0F;
|
||||
A[3] = (block[14] >> 4) & 0x0F;
|
||||
A[6] = block[15] & 0x0F;
|
||||
A[7] = (block[15] >> 4) & 0x0F;
|
||||
A[8] = block[16] & 0x0F;
|
||||
A[9] = (block[16] >> 4) & 0x0F;
|
||||
A[12] = block[17] & 0x0F;
|
||||
A[13] = (block[17] >> 4) & 0x0F;
|
||||
A[10] = block[18] & 0x0F;
|
||||
A[11] = (block[18] >> 4) & 0x0F;
|
||||
A[14] = block[19] & 0x0F;
|
||||
A[15] = (block[19] >> 4) & 0x0F;
|
||||
} else {
|
||||
for (int i = 0; i < 16; i++) A[i] = 15;
|
||||
}
|
||||
|
||||
int channels = has_alpha ? 4 : 3;
|
||||
uint8_t quad[16]; // 4 pixels max
|
||||
|
||||
// Decode 4 quads (2x2 regions), each sharing one chroma pair
|
||||
// Top-left quad (pixels 0,1,4,5) uses co1/cg1
|
||||
ycocg_to_rgb_quad(co1, cg1, Y[0], Y[1], Y[4], Y[5], A[0], A[1], A[4], A[5], has_alpha, quad);
|
||||
memcpy(pixels + 0 * stride + 0 * channels, quad + 0 * channels, channels);
|
||||
memcpy(pixels + 0 * stride + 1 * channels, quad + 1 * channels, channels);
|
||||
memcpy(pixels + 1 * stride + 0 * channels, quad + 2 * channels, channels);
|
||||
memcpy(pixels + 1 * stride + 1 * channels, quad + 3 * channels, channels);
|
||||
|
||||
// Top-right quad (pixels 2,3,6,7) uses co2/cg2
|
||||
ycocg_to_rgb_quad(co2, cg2, Y[2], Y[3], Y[6], Y[7], A[2], A[3], A[6], A[7], has_alpha, quad);
|
||||
memcpy(pixels + 0 * stride + 2 * channels, quad + 0 * channels, channels);
|
||||
memcpy(pixels + 0 * stride + 3 * channels, quad + 1 * channels, channels);
|
||||
memcpy(pixels + 1 * stride + 2 * channels, quad + 2 * channels, channels);
|
||||
memcpy(pixels + 1 * stride + 3 * channels, quad + 3 * channels, channels);
|
||||
|
||||
// Bottom-left quad (pixels 8,9,12,13) uses co3/cg3
|
||||
ycocg_to_rgb_quad(co3, cg3, Y[8], Y[9], Y[12], Y[13], A[8], A[9], A[12], A[13], has_alpha, quad);
|
||||
memcpy(pixels + 2 * stride + 0 * channels, quad + 0 * channels, channels);
|
||||
memcpy(pixels + 2 * stride + 1 * channels, quad + 1 * channels, channels);
|
||||
memcpy(pixels + 3 * stride + 0 * channels, quad + 2 * channels, channels);
|
||||
memcpy(pixels + 3 * stride + 1 * channels, quad + 3 * channels, channels);
|
||||
|
||||
// Bottom-right quad (pixels 10,11,14,15) uses co4/cg4
|
||||
ycocg_to_rgb_quad(co4, cg4, Y[10], Y[11], Y[14], Y[15], A[10], A[11], A[14], A[15], has_alpha, quad);
|
||||
memcpy(pixels + 2 * stride + 2 * channels, quad + 0 * channels, channels);
|
||||
memcpy(pixels + 2 * stride + 3 * channels, quad + 1 * channels, channels);
|
||||
memcpy(pixels + 3 * stride + 2 * channels, quad + 2 * channels, channels);
|
||||
memcpy(pixels + 3 * stride + 3 * channels, quad + 3 * channels, channels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode iPF2 block (4:2:2 chroma subsampling).
|
||||
* Input: 16 bytes (or 24 with alpha)
|
||||
* Output: 16 pixels in RGB24/RGBA format
|
||||
*/
|
||||
static void decode_ipf2_block(const uint8_t *block, int has_alpha, uint8_t *pixels, int stride) {
|
||||
// Read chroma (8 values for horizontal pairs)
|
||||
int co[8], cg[8];
|
||||
co[0] = block[0] & 0x0F;
|
||||
co[1] = (block[0] >> 4) & 0x0F;
|
||||
co[2] = block[1] & 0x0F;
|
||||
co[3] = (block[1] >> 4) & 0x0F;
|
||||
co[4] = block[2] & 0x0F;
|
||||
co[5] = (block[2] >> 4) & 0x0F;
|
||||
co[6] = block[3] & 0x0F;
|
||||
co[7] = (block[3] >> 4) & 0x0F;
|
||||
|
||||
cg[0] = block[4] & 0x0F;
|
||||
cg[1] = (block[4] >> 4) & 0x0F;
|
||||
cg[2] = block[5] & 0x0F;
|
||||
cg[3] = (block[5] >> 4) & 0x0F;
|
||||
cg[4] = block[6] & 0x0F;
|
||||
cg[5] = (block[6] >> 4) & 0x0F;
|
||||
cg[6] = block[7] & 0x0F;
|
||||
cg[7] = (block[7] >> 4) & 0x0F;
|
||||
|
||||
// Read Y values (16 values) - same layout as iPF1
|
||||
int Y[16];
|
||||
Y[0] = block[8] & 0x0F;
|
||||
Y[1] = (block[8] >> 4) & 0x0F;
|
||||
Y[4] = block[9] & 0x0F;
|
||||
Y[5] = (block[9] >> 4) & 0x0F;
|
||||
Y[2] = block[10] & 0x0F;
|
||||
Y[3] = (block[10] >> 4) & 0x0F;
|
||||
Y[6] = block[11] & 0x0F;
|
||||
Y[7] = (block[11] >> 4) & 0x0F;
|
||||
Y[8] = block[12] & 0x0F;
|
||||
Y[9] = (block[12] >> 4) & 0x0F;
|
||||
Y[12] = block[13] & 0x0F;
|
||||
Y[13] = (block[13] >> 4) & 0x0F;
|
||||
Y[10] = block[14] & 0x0F;
|
||||
Y[11] = (block[14] >> 4) & 0x0F;
|
||||
Y[14] = block[15] & 0x0F;
|
||||
Y[15] = (block[15] >> 4) & 0x0F;
|
||||
|
||||
// Read alpha values if present
|
||||
int A[16];
|
||||
if (has_alpha) {
|
||||
A[0] = block[16] & 0x0F;
|
||||
A[1] = (block[16] >> 4) & 0x0F;
|
||||
A[4] = block[17] & 0x0F;
|
||||
A[5] = (block[17] >> 4) & 0x0F;
|
||||
A[2] = block[18] & 0x0F;
|
||||
A[3] = (block[18] >> 4) & 0x0F;
|
||||
A[6] = block[19] & 0x0F;
|
||||
A[7] = (block[19] >> 4) & 0x0F;
|
||||
A[8] = block[20] & 0x0F;
|
||||
A[9] = (block[20] >> 4) & 0x0F;
|
||||
A[12] = block[21] & 0x0F;
|
||||
A[13] = (block[21] >> 4) & 0x0F;
|
||||
A[10] = block[22] & 0x0F;
|
||||
A[11] = (block[22] >> 4) & 0x0F;
|
||||
A[14] = block[23] & 0x0F;
|
||||
A[15] = (block[23] >> 4) & 0x0F;
|
||||
} else {
|
||||
for (int i = 0; i < 16; i++) A[i] = 15;
|
||||
}
|
||||
|
||||
int channels = has_alpha ? 4 : 3;
|
||||
|
||||
// iPF2: 4:2:2 - each horizontal pair shares chroma
|
||||
// Row 0: pixels 0,1 share co[0]/cg[0], pixels 2,3 share co[1]/cg[1]
|
||||
// Row 1: pixels 4,5 share co[2]/cg[2], pixels 6,7 share co[3]/cg[3]
|
||||
// Row 2: pixels 8,9 share co[4]/cg[4], pixels 10,11 share co[5]/cg[5]
|
||||
// Row 3: pixels 12,13 share co[6]/cg[6], pixels 14,15 share co[7]/cg[7]
|
||||
|
||||
int pixel_map[8][4] = {
|
||||
{0, 1, 0, 1}, // co/cg index 0: pixels 0,1
|
||||
{2, 3, 2, 3}, // co/cg index 1: pixels 2,3
|
||||
{4, 5, 4, 5}, // co/cg index 2: pixels 4,5
|
||||
{6, 7, 6, 7}, // co/cg index 3: pixels 6,7
|
||||
{8, 9, 8, 9}, // co/cg index 4: pixels 8,9
|
||||
{10, 11, 10, 11}, // co/cg index 5: pixels 10,11
|
||||
{12, 13, 12, 13}, // co/cg index 6: pixels 12,13
|
||||
{14, 15, 14, 15} // co/cg index 7: pixels 14,15
|
||||
};
|
||||
|
||||
for (int ci = 0; ci < 8; ci++) {
|
||||
int p0 = pixel_map[ci][0];
|
||||
int p1 = pixel_map[ci][1];
|
||||
|
||||
uint8_t quad[16]; // 4 pixels max (ycocg_to_rgb_quad writes 4 pixels)
|
||||
ycocg_to_rgb_quad(co[ci], cg[ci], Y[p0], Y[p1], Y[p0], Y[p1],
|
||||
A[p0], A[p1], A[p0], A[p1], has_alpha, quad);
|
||||
|
||||
int row = p0 / 4;
|
||||
int col0 = p0 % 4;
|
||||
int col1 = p1 % 4;
|
||||
|
||||
memcpy(pixels + row * stride + col0 * channels, quad + 0 * channels, channels);
|
||||
memcpy(pixels + row * stride + col1 * channels, quad + 1 * channels, channels);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main Decoding
|
||||
// =============================================================================
|
||||
|
||||
static int decode_ipf(const decoder_config_t *cfg) {
|
||||
FILE *fp = fopen(cfg->input_file, "rb");
|
||||
if (!fp) {
|
||||
fprintf(stderr, "Error: Failed to open input file: %s\n", cfg->input_file);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read header
|
||||
ipf_header_t header;
|
||||
if (read_ipf_header(fp, &header) < 0) {
|
||||
fclose(fp);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int has_alpha = (header.flags & IPF_FLAG_ALPHA) != 0;
|
||||
int use_zstd = (header.flags & IPF_FLAG_ZSTD) != 0;
|
||||
int progressive = (header.flags & IPF_FLAG_PROGRESSIVE) != 0;
|
||||
|
||||
if (cfg->verbose) {
|
||||
printf("iPF Header:\n");
|
||||
printf(" Size: %dx%d\n", header.width, header.height);
|
||||
printf(" Type: iPF%d (%s)\n", header.type + 1,
|
||||
header.type == 0 ? "4:2:0" : "4:2:2");
|
||||
printf(" Flags: %s%s%s\n",
|
||||
has_alpha ? "alpha " : "",
|
||||
use_zstd ? "zstd " : "",
|
||||
progressive ? "progressive " : "");
|
||||
printf(" Uncompressed size: %u bytes\n", header.uncompressed_size);
|
||||
}
|
||||
|
||||
if (progressive) {
|
||||
fprintf(stderr, "Warning: Progressive mode not implemented, decoding as sequential\n");
|
||||
}
|
||||
|
||||
// Read compressed/raw block data
|
||||
fseek(fp, 0, SEEK_END);
|
||||
long file_size = ftell(fp);
|
||||
fseek(fp, IPF_HEADER_SIZE, SEEK_SET);
|
||||
|
||||
size_t compressed_size = file_size - IPF_HEADER_SIZE;
|
||||
uint8_t *compressed_data = malloc(compressed_size);
|
||||
if (!compressed_data) {
|
||||
fclose(fp);
|
||||
fprintf(stderr, "Error: Failed to allocate memory\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (fread(compressed_data, 1, compressed_size, fp) != compressed_size) {
|
||||
free(compressed_data);
|
||||
fclose(fp);
|
||||
fprintf(stderr, "Error: Failed to read block data\n");
|
||||
return -1;
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
// Decompress if needed
|
||||
uint8_t *block_data;
|
||||
size_t block_data_size;
|
||||
|
||||
if (use_zstd) {
|
||||
block_data_size = header.uncompressed_size;
|
||||
block_data = malloc(block_data_size);
|
||||
if (!block_data) {
|
||||
free(compressed_data);
|
||||
fprintf(stderr, "Error: Failed to allocate decompression buffer\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t result = ZSTD_decompress(block_data, block_data_size,
|
||||
compressed_data, compressed_size);
|
||||
if (ZSTD_isError(result)) {
|
||||
fprintf(stderr, "Error: Zstd decompression failed: %s\n",
|
||||
ZSTD_getErrorName(result));
|
||||
free(block_data);
|
||||
free(compressed_data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (cfg->verbose) {
|
||||
printf("Decompressed: %zu -> %zu bytes\n", compressed_size, block_data_size);
|
||||
}
|
||||
|
||||
free(compressed_data);
|
||||
} else {
|
||||
block_data = compressed_data;
|
||||
block_data_size = compressed_size;
|
||||
}
|
||||
|
||||
// Allocate output image
|
||||
int channels = has_alpha ? 4 : 3;
|
||||
size_t image_size = (size_t)header.width * header.height * channels;
|
||||
uint8_t *image = malloc(image_size);
|
||||
if (!image) {
|
||||
free(block_data);
|
||||
fprintf(stderr, "Error: Failed to allocate image buffer\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Decode blocks
|
||||
int blocks_x = (header.width + 3) / 4;
|
||||
int blocks_y = (header.height + 3) / 4;
|
||||
int block_size = (header.type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16);
|
||||
int row_stride = header.width * channels;
|
||||
int block_stride = 4 * channels; // 4 pixels per block row
|
||||
|
||||
size_t block_offset = 0;
|
||||
for (int by = 0; by < blocks_y; by++) {
|
||||
for (int bx = 0; bx < blocks_x; bx++) {
|
||||
// Calculate output position
|
||||
uint8_t *block_pixels = image + by * 4 * row_stride + bx * block_stride;
|
||||
|
||||
if (header.type == IPF_TYPE_1) {
|
||||
decode_ipf1_block(block_data + block_offset, has_alpha, block_pixels, row_stride);
|
||||
} else {
|
||||
decode_ipf2_block(block_data + block_offset, has_alpha, block_pixels, row_stride);
|
||||
}
|
||||
|
||||
block_offset += block_size;
|
||||
}
|
||||
}
|
||||
|
||||
free(block_data);
|
||||
|
||||
if (cfg->verbose) {
|
||||
printf("Decoded %d blocks (%dx%d)\n", blocks_x * blocks_y, blocks_x, blocks_y);
|
||||
}
|
||||
|
||||
// Output image
|
||||
int result = 0;
|
||||
|
||||
if (cfg->raw_output) {
|
||||
// Write raw RGB/RGBA data
|
||||
FILE *out = fopen(cfg->output_file, "wb");
|
||||
if (!out) {
|
||||
fprintf(stderr, "Error: Failed to open output file: %s\n", cfg->output_file);
|
||||
result = -1;
|
||||
} else {
|
||||
fwrite(image, 1, image_size, out);
|
||||
fclose(out);
|
||||
if (cfg->verbose) {
|
||||
printf("Wrote %zu bytes raw %s data\n", image_size, has_alpha ? "RGBA" : "RGB24");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use FFmpeg to write output image
|
||||
char cmd[MAX_PATH * 2];
|
||||
const char *pix_fmt = has_alpha ? "rgba" : "rgb24";
|
||||
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"ffmpeg -hide_banner -v quiet -y -f rawvideo -pix_fmt %s -s %dx%d "
|
||||
"-i - \"%s\"",
|
||||
pix_fmt, header.width, header.height, cfg->output_file);
|
||||
|
||||
if (cfg->verbose) {
|
||||
printf("FFmpeg command: %s\n", cmd);
|
||||
}
|
||||
|
||||
FILE *pipe = popen(cmd, "w");
|
||||
if (!pipe) {
|
||||
fprintf(stderr, "Error: Failed to start FFmpeg\n");
|
||||
result = -1;
|
||||
} else {
|
||||
fwrite(image, 1, image_size, pipe);
|
||||
int status = pclose(pipe);
|
||||
if (status != 0) {
|
||||
fprintf(stderr, "Error: FFmpeg failed with status %d\n", status);
|
||||
result = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(image);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main Entry Point
|
||||
// =============================================================================
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
decoder_config_t cfg = {
|
||||
.input_file = NULL,
|
||||
.output_file = NULL,
|
||||
.verbose = 0,
|
||||
.raw_output = 0
|
||||
};
|
||||
|
||||
static struct option long_options[] = {
|
||||
{"input", required_argument, 0, 'i'},
|
||||
{"output", required_argument, 0, 'o'},
|
||||
{"raw", no_argument, 0, 'R'},
|
||||
{"verbose", no_argument, 0, 'v'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{0, 0, 0, 0}
|
||||
};
|
||||
|
||||
int opt;
|
||||
while ((opt = getopt_long(argc, argv, "i:o:vh", long_options, NULL)) != -1) {
|
||||
switch (opt) {
|
||||
case 'i':
|
||||
cfg.input_file = optarg;
|
||||
break;
|
||||
case 'o':
|
||||
cfg.output_file = optarg;
|
||||
break;
|
||||
case 'R':
|
||||
cfg.raw_output = 1;
|
||||
break;
|
||||
case 'v':
|
||||
cfg.verbose = 1;
|
||||
break;
|
||||
case 'h':
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
default:
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required arguments
|
||||
if (!cfg.input_file || !cfg.output_file) {
|
||||
fprintf(stderr, "Error: Input and output files are required\n\n");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int result = decode_ipf(&cfg);
|
||||
|
||||
if (result == 0) {
|
||||
printf("Successfully decoded: %s\n", cfg.output_file);
|
||||
}
|
||||
|
||||
return result == 0 ? 0 : 1;
|
||||
}
|
||||
787
ipf_encoder/encoder_ipf.c
Normal file
787
ipf_encoder/encoder_ipf.c
Normal file
@@ -0,0 +1,787 @@
|
||||
/**
|
||||
* iPF Encoder - TSVM Interchangeable Picture Format Encoder
|
||||
*
|
||||
* Encodes images to iPF format (Type 1 or Type 2) with:
|
||||
* - YCoCg colour space with chroma subsampling
|
||||
* - 4x4 block encoding
|
||||
* - Optional Zstd compression
|
||||
* - Optional alpha channel
|
||||
* - Optional Adam7 progressive ordering
|
||||
*
|
||||
* Created by CuriousTorvald and Claude on 2025-12-19.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <getopt.h>
|
||||
#include <zstd.h>
|
||||
|
||||
// =============================================================================
|
||||
// Constants
|
||||
// =============================================================================
|
||||
|
||||
#define IPF_MAGIC "\x1F\x54\x53\x56\x4D\x69\x50\x46" // "\x1FTSVMiPF"
|
||||
#define IPF_HEADER_SIZE 28 // 8 magic + 2 width + 2 height + 1 flags + 1 type + 10 reserved + 4 uncompressed size
|
||||
|
||||
#define DEFAULT_WIDTH 560
|
||||
#define DEFAULT_HEIGHT 448
|
||||
|
||||
#define IPF_TYPE_1 0 // 4:2:0 chroma subsampling (12 bytes per block, +8 with alpha)
|
||||
#define IPF_TYPE_2 1 // 4:2:2 chroma subsampling (16 bytes per block, +8 with alpha)
|
||||
|
||||
#define IPF_FLAG_ALPHA 0x01 // Has alpha channel
|
||||
#define IPF_FLAG_ZSTD 0x10 // Zstd compressed
|
||||
#define IPF_FLAG_PROGRESSIVE 0x80 // Adam7 progressive ordering
|
||||
|
||||
#define MAX_PATH 4096
|
||||
|
||||
// Bayer dithering kernel (4x4)
|
||||
static const float BAYER_4X4[16] = {
|
||||
0.0f/16.0f, 8.0f/16.0f, 2.0f/16.0f, 10.0f/16.0f,
|
||||
12.0f/16.0f, 4.0f/16.0f, 14.0f/16.0f, 6.0f/16.0f,
|
||||
3.0f/16.0f, 11.0f/16.0f, 1.0f/16.0f, 9.0f/16.0f,
|
||||
15.0f/16.0f, 7.0f/16.0f, 13.0f/16.0f, 5.0f/16.0f
|
||||
};
|
||||
|
||||
// Adam7 interlace pattern - pass number (1-7) for each pixel in 8x8 block
|
||||
// 0 = not in this standard pattern, we'll adapt for 4x4 blocks
|
||||
static const int ADAM7_PASS[8][8] = {
|
||||
{1, 6, 4, 6, 2, 6, 4, 6},
|
||||
{7, 7, 7, 7, 7, 7, 7, 7},
|
||||
{5, 6, 5, 6, 5, 6, 5, 6},
|
||||
{7, 7, 7, 7, 7, 7, 7, 7},
|
||||
{3, 6, 4, 6, 3, 6, 4, 6},
|
||||
{7, 7, 7, 7, 7, 7, 7, 7},
|
||||
{5, 6, 5, 6, 5, 6, 5, 6},
|
||||
{7, 7, 7, 7, 7, 7, 7, 7}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Structures
|
||||
// =============================================================================
|
||||
|
||||
typedef struct {
|
||||
char *input_file;
|
||||
char *output_file;
|
||||
int width;
|
||||
int height;
|
||||
int ipf_type; // 0 = iPF1, 1 = iPF2
|
||||
int use_zstd; // 1 = compress with Zstd
|
||||
int force_alpha; // 1 = force alpha channel in output
|
||||
int no_alpha; // 1 = strip alpha even if present in input
|
||||
int progressive; // 1 = Adam7 progressive ordering
|
||||
int dither; // Bayer dither pattern index (-1 = no dithering)
|
||||
int verbose;
|
||||
} encoder_config_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t *data; // RGB or RGBA data
|
||||
int width;
|
||||
int height;
|
||||
int channels; // 3 = RGB, 4 = RGBA
|
||||
int has_alpha; // 1 if input image has meaningful alpha
|
||||
} image_t;
|
||||
|
||||
// =============================================================================
|
||||
// Utility Functions
|
||||
// =============================================================================
|
||||
|
||||
static void print_usage(const char *program) {
|
||||
printf("iPF Encoder - TSVM Interchangeable Picture Format\n");
|
||||
printf("\nUsage: %s -i input.png -o output.ipf [options]\n\n", program);
|
||||
printf("Required:\n");
|
||||
printf(" -i, --input FILE Input image file (any format FFmpeg supports)\n");
|
||||
printf(" -o, --output FILE Output iPF file\n");
|
||||
printf("\nOptions:\n");
|
||||
printf(" -s, --size WxH Output size (default: %dx%d)\n", DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
printf(" -t, --type N iPF type: 1 (4:2:0, default) or 2 (4:2:2)\n");
|
||||
printf(" --no-zstd Disable Zstd compression (default: enabled)\n");
|
||||
printf(" --alpha Force alpha channel in output\n");
|
||||
printf(" --no-alpha Strip alpha channel from input\n");
|
||||
printf(" -p, --progressive Use Adam7 progressive ordering\n");
|
||||
printf(" -d, --dither N Bayer dither pattern (0=4x4, -1=none, default: 0)\n");
|
||||
printf(" -v, --verbose Verbose output\n");
|
||||
printf(" -h, --help Show this help\n");
|
||||
printf("\nExamples:\n");
|
||||
printf(" %s -i photo.jpg -o photo.ipf\n", program);
|
||||
printf(" %s -i logo.png -o logo.ipf --alpha\n", program);
|
||||
printf(" %s -i image.png -o image.ipf -s 280x224 -t 2\n", program);
|
||||
}
|
||||
|
||||
static int clampi(int v, int lo, int hi) {
|
||||
return v < lo ? lo : (v > hi ? hi : v);
|
||||
}
|
||||
|
||||
// Convert chroma value [-1..1] to 4-bit [0..15]
|
||||
static int chroma_to_four_bits(float f) {
|
||||
return clampi((int)roundf(f * 8.0f) + 7, 0, 15);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Image Loading via FFmpeg
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Probe input image dimensions using FFmpeg.
|
||||
* Returns 0 on success, -1 on error.
|
||||
*/
|
||||
static int probe_image_dimensions(const char *input_file, int *width, int *height, int *has_alpha) {
|
||||
char cmd[MAX_PATH * 2];
|
||||
|
||||
// Use ffprobe to get dimensions and pixel format
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"ffprobe -v quiet -select_streams v:0 -show_entries stream=width,height,pix_fmt "
|
||||
"-of csv=p=0:s=x \"%s\" 2>/dev/null",
|
||||
input_file);
|
||||
|
||||
FILE *fp = popen(cmd, "r");
|
||||
if (!fp) {
|
||||
fprintf(stderr, "Error: Failed to run ffprobe\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
char buffer[256];
|
||||
if (fgets(buffer, sizeof(buffer), fp) == NULL) {
|
||||
pclose(fp);
|
||||
fprintf(stderr, "Error: Failed to read image info\n");
|
||||
return -1;
|
||||
}
|
||||
pclose(fp);
|
||||
|
||||
// Parse "width x height x pix_fmt"
|
||||
char pix_fmt[64] = "";
|
||||
if (sscanf(buffer, "%dx%dx%63s", width, height, pix_fmt) < 2) {
|
||||
// Try alternate format without pix_fmt
|
||||
if (sscanf(buffer, "%dx%d", width, height) != 2) {
|
||||
fprintf(stderr, "Error: Failed to parse image dimensions\n");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if pixel format indicates alpha
|
||||
*has_alpha = (strstr(pix_fmt, "rgba") != NULL ||
|
||||
strstr(pix_fmt, "argb") != NULL ||
|
||||
strstr(pix_fmt, "bgra") != NULL ||
|
||||
strstr(pix_fmt, "abgr") != NULL ||
|
||||
strstr(pix_fmt, "ya") != NULL ||
|
||||
strstr(pix_fmt, "pal8") != NULL || // palette may have alpha
|
||||
strstr(pix_fmt, "yuva") != NULL);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and resize image using FFmpeg.
|
||||
* Maintains aspect ratio and crops to target size.
|
||||
* Returns image data or NULL on error.
|
||||
*/
|
||||
static image_t* load_image(const char *input_file, int target_width, int target_height,
|
||||
int want_alpha, int verbose) {
|
||||
int src_width, src_height, src_has_alpha;
|
||||
|
||||
// Probe source dimensions
|
||||
if (probe_image_dimensions(input_file, &src_width, &src_height, &src_has_alpha) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
printf("Source image: %dx%d, alpha: %s\n",
|
||||
src_width, src_height, src_has_alpha ? "yes" : "no");
|
||||
}
|
||||
|
||||
// Determine if we need alpha channel
|
||||
int use_alpha = want_alpha || src_has_alpha;
|
||||
int channels = use_alpha ? 4 : 3;
|
||||
const char *pix_fmt = use_alpha ? "rgba" : "rgb24";
|
||||
|
||||
// Build FFmpeg command with scale and crop filter
|
||||
char cmd[MAX_PATH * 2];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"ffmpeg -hide_banner -v quiet -i \"%s\" -f rawvideo -pix_fmt %s -vf "
|
||||
"\"scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d\" -frames:v 1 -",
|
||||
input_file, pix_fmt, target_width, target_height, target_width, target_height);
|
||||
|
||||
if (verbose) {
|
||||
printf("FFmpeg command: %s\n", cmd);
|
||||
}
|
||||
|
||||
FILE *fp = popen(cmd, "r");
|
||||
if (!fp) {
|
||||
fprintf(stderr, "Error: Failed to start FFmpeg\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Allocate image
|
||||
image_t *img = malloc(sizeof(image_t));
|
||||
if (!img) {
|
||||
pclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t data_size = (size_t)target_width * target_height * channels;
|
||||
img->data = malloc(data_size);
|
||||
if (!img->data) {
|
||||
free(img);
|
||||
pclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
img->width = target_width;
|
||||
img->height = target_height;
|
||||
img->channels = channels;
|
||||
img->has_alpha = use_alpha;
|
||||
|
||||
// Read image data
|
||||
size_t bytes_read = fread(img->data, 1, data_size, fp);
|
||||
pclose(fp);
|
||||
|
||||
if (bytes_read != data_size) {
|
||||
fprintf(stderr, "Error: Expected %zu bytes, got %zu\n", data_size, bytes_read);
|
||||
free(img->data);
|
||||
free(img);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
printf("Loaded %dx%d image, %d channels, %zu bytes\n",
|
||||
img->width, img->height, img->channels, data_size);
|
||||
}
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
static void free_image(image_t *img) {
|
||||
if (img) {
|
||||
free(img->data);
|
||||
free(img);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// iPF Block Encoding
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Encode a 4x4 block to YCoCg with dithering.
|
||||
* Returns arrays of Y (16 values), A (16 values), Co (16 values), Cg (16 values).
|
||||
*/
|
||||
static void encode_block_to_ycocg(const image_t *img, int block_x, int block_y,
|
||||
int dither_pattern,
|
||||
int *Y_out, int *A_out, float *Co_out, float *Cg_out) {
|
||||
for (int py = 0; py < 4; py++) {
|
||||
for (int px = 0; px < 4; px++) {
|
||||
int ox = block_x * 4 + px;
|
||||
int oy = block_y * 4 + py;
|
||||
|
||||
// Handle out-of-bounds (extend edge pixels)
|
||||
ox = clampi(ox, 0, img->width - 1);
|
||||
oy = clampi(oy, 0, img->height - 1);
|
||||
|
||||
// Get dither threshold
|
||||
float t = 0.0f;
|
||||
if (dither_pattern >= 0) {
|
||||
t = BAYER_4X4[(py % 4) * 4 + (px % 4)];
|
||||
}
|
||||
|
||||
// Read pixel
|
||||
int offset = (oy * img->width + ox) * img->channels;
|
||||
float r0 = img->data[offset + 0] / 255.0f;
|
||||
float g0 = (img->channels >= 3) ? img->data[offset + 1] / 255.0f : r0;
|
||||
float b0 = (img->channels >= 3) ? img->data[offset + 2] / 255.0f : r0;
|
||||
float a0 = (img->channels == 4) ? img->data[offset + 3] / 255.0f : 1.0f;
|
||||
|
||||
// Apply dithering
|
||||
float r = floorf((t / 15.0f + r0) * 15.0f) / 15.0f;
|
||||
float g = floorf((t / 15.0f + g0) * 15.0f) / 15.0f;
|
||||
float b = floorf((t / 15.0f + b0) * 15.0f) / 15.0f;
|
||||
float a = floorf((t / 15.0f + a0) * 15.0f) / 15.0f;
|
||||
|
||||
// Convert to YCoCg
|
||||
float co = r - b; // [-1..1]
|
||||
float tmp = b + co / 2.0f;
|
||||
float cg = g - tmp; // [-1..1]
|
||||
float y = tmp + cg / 2.0f; // [0..1]
|
||||
|
||||
int index = py * 4 + px;
|
||||
Y_out[index] = (int)roundf(y * 15.0f);
|
||||
A_out[index] = (int)roundf(a * 15.0f);
|
||||
Co_out[index] = co;
|
||||
Cg_out[index] = cg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode iPF1 block (4:2:0 chroma subsampling).
|
||||
* Returns 12 bytes (or 20 with alpha).
|
||||
*/
|
||||
static int encode_ipf1_block(const int *Ys, const int *As, const float *COs, const float *CGs,
|
||||
int has_alpha, uint8_t *out) {
|
||||
// Subsample Co/Cg by averaging 2x2 regions (4:2:0)
|
||||
int cos1 = chroma_to_four_bits((COs[0] + COs[1] + COs[4] + COs[5]) / 4.0f);
|
||||
int cos2 = chroma_to_four_bits((COs[2] + COs[3] + COs[6] + COs[7]) / 4.0f);
|
||||
int cos3 = chroma_to_four_bits((COs[8] + COs[9] + COs[12] + COs[13]) / 4.0f);
|
||||
int cos4 = chroma_to_four_bits((COs[10] + COs[11] + COs[14] + COs[15]) / 4.0f);
|
||||
|
||||
int cgs1 = chroma_to_four_bits((CGs[0] + CGs[1] + CGs[4] + CGs[5]) / 4.0f);
|
||||
int cgs2 = chroma_to_four_bits((CGs[2] + CGs[3] + CGs[6] + CGs[7]) / 4.0f);
|
||||
int cgs3 = chroma_to_four_bits((CGs[8] + CGs[9] + CGs[12] + CGs[13]) / 4.0f);
|
||||
int cgs4 = chroma_to_four_bits((CGs[10] + CGs[11] + CGs[14] + CGs[15]) / 4.0f);
|
||||
|
||||
// Pack according to iPF1 format
|
||||
// uint16 [Co4 | Co3 | Co2 | Co1]
|
||||
out[0] = (cos2 << 4) | cos1;
|
||||
out[1] = (cos4 << 4) | cos3;
|
||||
// uint16 [Cg4 | Cg3 | Cg2 | Cg1]
|
||||
out[2] = (cgs2 << 4) | cgs1;
|
||||
out[3] = (cgs4 << 4) | cgs3;
|
||||
// Y values: [Y1|Y0|Y5|Y4], [Y3|Y2|Y7|Y6], [Y9|Y8|YD|YC], [YB|YA|YF|YE]
|
||||
out[4] = (Ys[1] << 4) | Ys[0];
|
||||
out[5] = (Ys[5] << 4) | Ys[4];
|
||||
out[6] = (Ys[3] << 4) | Ys[2];
|
||||
out[7] = (Ys[7] << 4) | Ys[6];
|
||||
out[8] = (Ys[9] << 4) | Ys[8];
|
||||
out[9] = (Ys[13] << 4) | Ys[12];
|
||||
out[10] = (Ys[11] << 4) | Ys[10];
|
||||
out[11] = (Ys[15] << 4) | Ys[14];
|
||||
|
||||
int block_size = 12;
|
||||
|
||||
if (has_alpha) {
|
||||
// Alpha values: same layout as Y
|
||||
out[12] = (As[1] << 4) | As[0];
|
||||
out[13] = (As[5] << 4) | As[4];
|
||||
out[14] = (As[3] << 4) | As[2];
|
||||
out[15] = (As[7] << 4) | As[6];
|
||||
out[16] = (As[9] << 4) | As[8];
|
||||
out[17] = (As[13] << 4) | As[12];
|
||||
out[18] = (As[11] << 4) | As[10];
|
||||
out[19] = (As[15] << 4) | As[14];
|
||||
block_size = 20;
|
||||
}
|
||||
|
||||
return block_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode iPF2 block (4:2:2 chroma subsampling).
|
||||
* Returns 16 bytes (or 24 with alpha).
|
||||
*/
|
||||
static int encode_ipf2_block(const int *Ys, const int *As, const float *COs, const float *CGs,
|
||||
int has_alpha, uint8_t *out) {
|
||||
// Subsample Co/Cg horizontally only (4:2:2) - 8 values each
|
||||
int cos1 = chroma_to_four_bits((COs[0] + COs[1]) / 2.0f);
|
||||
int cos2 = chroma_to_four_bits((COs[2] + COs[3]) / 2.0f);
|
||||
int cos3 = chroma_to_four_bits((COs[4] + COs[5]) / 2.0f);
|
||||
int cos4 = chroma_to_four_bits((COs[6] + COs[7]) / 2.0f);
|
||||
int cos5 = chroma_to_four_bits((COs[8] + COs[9]) / 2.0f);
|
||||
int cos6 = chroma_to_four_bits((COs[10] + COs[11]) / 2.0f);
|
||||
int cos7 = chroma_to_four_bits((COs[12] + COs[13]) / 2.0f);
|
||||
int cos8 = chroma_to_four_bits((COs[14] + COs[15]) / 2.0f);
|
||||
|
||||
int cgs1 = chroma_to_four_bits((CGs[0] + CGs[1]) / 2.0f);
|
||||
int cgs2 = chroma_to_four_bits((CGs[2] + CGs[3]) / 2.0f);
|
||||
int cgs3 = chroma_to_four_bits((CGs[4] + CGs[5]) / 2.0f);
|
||||
int cgs4 = chroma_to_four_bits((CGs[6] + CGs[7]) / 2.0f);
|
||||
int cgs5 = chroma_to_four_bits((CGs[8] + CGs[9]) / 2.0f);
|
||||
int cgs6 = chroma_to_four_bits((CGs[10] + CGs[11]) / 2.0f);
|
||||
int cgs7 = chroma_to_four_bits((CGs[12] + CGs[13]) / 2.0f);
|
||||
int cgs8 = chroma_to_four_bits((CGs[14] + CGs[15]) / 2.0f);
|
||||
|
||||
// Pack according to iPF2 format
|
||||
// uint32 [Co8 | Co7 | Co6 | Co5 | Co4 | Co3 | Co2 | Co1]
|
||||
out[0] = (cos2 << 4) | cos1;
|
||||
out[1] = (cos4 << 4) | cos3;
|
||||
out[2] = (cos6 << 4) | cos5;
|
||||
out[3] = (cos8 << 4) | cos7;
|
||||
// uint32 [Cg8 | Cg7 | Cg6 | Cg5 | Cg4 | Cg3 | Cg2 | Cg1]
|
||||
out[4] = (cgs2 << 4) | cgs1;
|
||||
out[5] = (cgs4 << 4) | cgs3;
|
||||
out[6] = (cgs6 << 4) | cgs5;
|
||||
out[7] = (cgs8 << 4) | cgs7;
|
||||
// Y values: same as iPF1
|
||||
out[8] = (Ys[1] << 4) | Ys[0];
|
||||
out[9] = (Ys[5] << 4) | Ys[4];
|
||||
out[10] = (Ys[3] << 4) | Ys[2];
|
||||
out[11] = (Ys[7] << 4) | Ys[6];
|
||||
out[12] = (Ys[9] << 4) | Ys[8];
|
||||
out[13] = (Ys[13] << 4) | Ys[12];
|
||||
out[14] = (Ys[11] << 4) | Ys[10];
|
||||
out[15] = (Ys[15] << 4) | Ys[14];
|
||||
|
||||
int block_size = 16;
|
||||
|
||||
if (has_alpha) {
|
||||
// Alpha values: same layout as Y
|
||||
out[16] = (As[1] << 4) | As[0];
|
||||
out[17] = (As[5] << 4) | As[4];
|
||||
out[18] = (As[3] << 4) | As[2];
|
||||
out[19] = (As[7] << 4) | As[6];
|
||||
out[20] = (As[9] << 4) | As[8];
|
||||
out[21] = (As[13] << 4) | As[12];
|
||||
out[22] = (As[11] << 4) | As[10];
|
||||
out[23] = (As[15] << 4) | As[14];
|
||||
block_size = 24;
|
||||
}
|
||||
|
||||
return block_size;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Adam7 Progressive Ordering
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get Adam7 pass number for a block at (block_x, block_y).
|
||||
* For blocks, we use a simplified version based on block position.
|
||||
*/
|
||||
static int get_adam7_pass(int block_x, int block_y) {
|
||||
// Use Adam7 pattern for 8x8 blocks, but adapt for 4x4 block indices
|
||||
int px = (block_x * 4) % 8;
|
||||
int py = (block_y * 4) % 8;
|
||||
return ADAM7_PASS[py][px];
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode blocks in Adam7 progressive order.
|
||||
* Returns the encoded block data in progressive order.
|
||||
*/
|
||||
static uint8_t* encode_progressive(const image_t *img, const encoder_config_t *cfg,
|
||||
int has_alpha, size_t *out_size) {
|
||||
int blocks_x = (img->width + 3) / 4;
|
||||
int blocks_y = (img->height + 3) / 4;
|
||||
int total_blocks = blocks_x * blocks_y;
|
||||
|
||||
int block_size = (cfg->ipf_type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16);
|
||||
size_t max_size = (size_t)total_blocks * block_size;
|
||||
|
||||
uint8_t *output = malloc(max_size);
|
||||
if (!output) return NULL;
|
||||
|
||||
// Temporary storage for all encoded blocks
|
||||
uint8_t *all_blocks = malloc(max_size);
|
||||
if (!all_blocks) {
|
||||
free(output);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Encode all blocks first
|
||||
size_t offset = 0;
|
||||
for (int by = 0; by < blocks_y; by++) {
|
||||
for (int bx = 0; bx < blocks_x; bx++) {
|
||||
int Ys[16], As[16];
|
||||
float COs[16], CGs[16];
|
||||
|
||||
encode_block_to_ycocg(img, bx, by, cfg->dither, Ys, As, COs, CGs);
|
||||
|
||||
if (cfg->ipf_type == IPF_TYPE_1) {
|
||||
encode_ipf1_block(Ys, As, COs, CGs, has_alpha, all_blocks + offset);
|
||||
} else {
|
||||
encode_ipf2_block(Ys, As, COs, CGs, has_alpha, all_blocks + offset);
|
||||
}
|
||||
offset += block_size;
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder blocks according to Adam7 progressive order (7 passes)
|
||||
size_t out_offset = 0;
|
||||
for (int pass = 1; pass <= 7; pass++) {
|
||||
for (int by = 0; by < blocks_y; by++) {
|
||||
for (int bx = 0; bx < blocks_x; bx++) {
|
||||
if (get_adam7_pass(bx, by) == pass) {
|
||||
int block_idx = by * blocks_x + bx;
|
||||
memcpy(output + out_offset, all_blocks + block_idx * block_size, block_size);
|
||||
out_offset += block_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(all_blocks);
|
||||
*out_size = out_offset;
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode blocks in sequential (raster) order.
|
||||
*/
|
||||
static uint8_t* encode_sequential(const image_t *img, const encoder_config_t *cfg,
|
||||
int has_alpha, size_t *out_size) {
|
||||
int blocks_x = (img->width + 3) / 4;
|
||||
int blocks_y = (img->height + 3) / 4;
|
||||
int total_blocks = blocks_x * blocks_y;
|
||||
|
||||
int block_size = (cfg->ipf_type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16);
|
||||
size_t max_size = (size_t)total_blocks * block_size;
|
||||
|
||||
uint8_t *output = malloc(max_size);
|
||||
if (!output) return NULL;
|
||||
|
||||
size_t offset = 0;
|
||||
for (int by = 0; by < blocks_y; by++) {
|
||||
for (int bx = 0; bx < blocks_x; bx++) {
|
||||
int Ys[16], As[16];
|
||||
float COs[16], CGs[16];
|
||||
|
||||
encode_block_to_ycocg(img, bx, by, cfg->dither, Ys, As, COs, CGs);
|
||||
|
||||
if (cfg->ipf_type == IPF_TYPE_1) {
|
||||
offset += encode_ipf1_block(Ys, As, COs, CGs, has_alpha, output + offset);
|
||||
} else {
|
||||
offset += encode_ipf2_block(Ys, As, COs, CGs, has_alpha, output + offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*out_size = offset;
|
||||
return output;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// iPF File Writing
|
||||
// =============================================================================
|
||||
|
||||
static int write_ipf_file(const char *output_file, const encoder_config_t *cfg,
|
||||
const image_t *img, int verbose) {
|
||||
// Determine if we use alpha
|
||||
int has_alpha = 0;
|
||||
if (cfg->force_alpha) {
|
||||
has_alpha = 1;
|
||||
} else if (!cfg->no_alpha && img->has_alpha) {
|
||||
has_alpha = 1;
|
||||
}
|
||||
|
||||
// Encode blocks
|
||||
size_t block_data_size;
|
||||
uint8_t *block_data;
|
||||
|
||||
if (cfg->progressive) {
|
||||
block_data = encode_progressive(img, cfg, has_alpha, &block_data_size);
|
||||
} else {
|
||||
block_data = encode_sequential(img, cfg, has_alpha, &block_data_size);
|
||||
}
|
||||
|
||||
if (!block_data) {
|
||||
fprintf(stderr, "Error: Failed to encode image blocks\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
printf("Encoded %zu bytes of block data\n", block_data_size);
|
||||
}
|
||||
|
||||
// Prepare output data (may be compressed)
|
||||
uint8_t *output_data = block_data;
|
||||
size_t output_size = block_data_size;
|
||||
uint8_t *compressed_data = NULL;
|
||||
|
||||
if (cfg->use_zstd) {
|
||||
size_t max_compressed = ZSTD_compressBound(block_data_size);
|
||||
compressed_data = malloc(max_compressed);
|
||||
if (!compressed_data) {
|
||||
free(block_data);
|
||||
fprintf(stderr, "Error: Failed to allocate compression buffer\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
output_size = ZSTD_compress(compressed_data, max_compressed,
|
||||
block_data, block_data_size, 7);
|
||||
if (ZSTD_isError(output_size)) {
|
||||
fprintf(stderr, "Error: Zstd compression failed: %s\n",
|
||||
ZSTD_getErrorName(output_size));
|
||||
free(block_data);
|
||||
free(compressed_data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
output_data = compressed_data;
|
||||
|
||||
if (verbose) {
|
||||
printf("Compressed: %zu -> %zu bytes (%.1f%%)\n",
|
||||
block_data_size, output_size,
|
||||
100.0 * output_size / block_data_size);
|
||||
}
|
||||
}
|
||||
|
||||
// Open output file
|
||||
FILE *fp = fopen(output_file, "wb");
|
||||
if (!fp) {
|
||||
fprintf(stderr, "Error: Failed to open output file: %s\n", output_file);
|
||||
free(block_data);
|
||||
if (compressed_data) free(compressed_data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Build flags byte
|
||||
uint8_t flags = 0;
|
||||
if (has_alpha) flags |= IPF_FLAG_ALPHA;
|
||||
if (cfg->use_zstd) flags |= IPF_FLAG_ZSTD;
|
||||
if (cfg->progressive) flags |= IPF_FLAG_PROGRESSIVE | IPF_FLAG_ZSTD; // Progressive always sets zstd flag
|
||||
|
||||
// Write header
|
||||
// Magic: "\x1FTSVMiPF" (8 bytes)
|
||||
fwrite(IPF_MAGIC, 1, 8, fp);
|
||||
|
||||
// Width (uint16 LE)
|
||||
uint16_t width_le = (uint16_t)cfg->width;
|
||||
fwrite(&width_le, 2, 1, fp);
|
||||
|
||||
// Height (uint16 LE)
|
||||
uint16_t height_le = (uint16_t)cfg->height;
|
||||
fwrite(&height_le, 2, 1, fp);
|
||||
|
||||
// Flags (uint8)
|
||||
fwrite(&flags, 1, 1, fp);
|
||||
|
||||
// Type (uint8)
|
||||
uint8_t type_byte = (uint8_t)cfg->ipf_type;
|
||||
fwrite(&type_byte, 1, 1, fp);
|
||||
|
||||
// Reserved (10 bytes)
|
||||
uint8_t reserved[10] = {0};
|
||||
fwrite(reserved, 1, 10, fp);
|
||||
|
||||
// Uncompressed size (uint32 LE)
|
||||
uint32_t uncompressed_size_le = (uint32_t)block_data_size;
|
||||
fwrite(&uncompressed_size_le, 4, 1, fp);
|
||||
|
||||
// Write block data
|
||||
fwrite(output_data, 1, output_size, fp);
|
||||
|
||||
fclose(fp);
|
||||
|
||||
if (verbose) {
|
||||
printf("Wrote %zu bytes to %s\n", IPF_HEADER_SIZE + output_size, output_file);
|
||||
printf(" Format: iPF%d, %dx%d\n", cfg->ipf_type + 1, cfg->width, cfg->height);
|
||||
printf(" Flags: %s%s%s\n",
|
||||
has_alpha ? "alpha " : "",
|
||||
cfg->use_zstd ? "zstd " : "",
|
||||
cfg->progressive ? "progressive " : "");
|
||||
}
|
||||
|
||||
free(block_data);
|
||||
if (compressed_data) free(compressed_data);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main Entry Point
|
||||
// =============================================================================
|
||||
|
||||
static int parse_size(const char *arg, int *width, int *height) {
|
||||
return sscanf(arg, "%dx%d", width, height) == 2 ? 0 : -1;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
encoder_config_t cfg = {
|
||||
.input_file = NULL,
|
||||
.output_file = NULL,
|
||||
.width = DEFAULT_WIDTH,
|
||||
.height = DEFAULT_HEIGHT,
|
||||
.ipf_type = IPF_TYPE_1,
|
||||
.use_zstd = 1,
|
||||
.force_alpha = 0,
|
||||
.no_alpha = 0,
|
||||
.progressive = 0,
|
||||
.dither = 0,
|
||||
.verbose = 0
|
||||
};
|
||||
|
||||
static struct option long_options[] = {
|
||||
{"input", required_argument, 0, 'i'},
|
||||
{"output", required_argument, 0, 'o'},
|
||||
{"size", required_argument, 0, 's'},
|
||||
{"type", required_argument, 0, 't'},
|
||||
{"no-zstd", no_argument, 0, 'Z'},
|
||||
{"alpha", no_argument, 0, 'A'},
|
||||
{"no-alpha", no_argument, 0, 'N'},
|
||||
{"progressive", no_argument, 0, 'p'},
|
||||
{"dither", required_argument, 0, 'd'},
|
||||
{"verbose", no_argument, 0, 'v'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{0, 0, 0, 0}
|
||||
};
|
||||
|
||||
int opt;
|
||||
while ((opt = getopt_long(argc, argv, "i:o:s:t:pd:vh", long_options, NULL)) != -1) {
|
||||
switch (opt) {
|
||||
case 'i':
|
||||
cfg.input_file = optarg;
|
||||
break;
|
||||
case 'o':
|
||||
cfg.output_file = optarg;
|
||||
break;
|
||||
case 's':
|
||||
if (parse_size(optarg, &cfg.width, &cfg.height) != 0) {
|
||||
fprintf(stderr, "Error: Invalid size format (use WxH)\n");
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
case 't':
|
||||
cfg.ipf_type = atoi(optarg) - 1; // User specifies 1 or 2
|
||||
if (cfg.ipf_type < 0 || cfg.ipf_type > 1) {
|
||||
fprintf(stderr, "Error: Invalid iPF type (use 1 or 2)\n");
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
case 'Z':
|
||||
cfg.use_zstd = 0;
|
||||
break;
|
||||
case 'A':
|
||||
cfg.force_alpha = 1;
|
||||
break;
|
||||
case 'N':
|
||||
cfg.no_alpha = 1;
|
||||
break;
|
||||
case 'p':
|
||||
cfg.progressive = 1;
|
||||
break;
|
||||
case 'd':
|
||||
cfg.dither = atoi(optarg);
|
||||
break;
|
||||
case 'v':
|
||||
cfg.verbose = 1;
|
||||
break;
|
||||
case 'h':
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
default:
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required arguments
|
||||
if (!cfg.input_file || !cfg.output_file) {
|
||||
fprintf(stderr, "Error: Input and output files are required\n\n");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Load image
|
||||
if (cfg.verbose) {
|
||||
printf("Loading image: %s\n", cfg.input_file);
|
||||
}
|
||||
|
||||
image_t *img = load_image(cfg.input_file, cfg.width, cfg.height,
|
||||
cfg.force_alpha, cfg.verbose);
|
||||
if (!img) {
|
||||
fprintf(stderr, "Error: Failed to load image\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Encode and write iPF file
|
||||
int result = write_ipf_file(cfg.output_file, &cfg, img, cfg.verbose);
|
||||
|
||||
free_image(img);
|
||||
|
||||
if (result == 0) {
|
||||
printf("Successfully encoded: %s\n", cfg.output_file);
|
||||
}
|
||||
|
||||
return result == 0 ? 0 : 1;
|
||||
}
|
||||
1822
it2taud.py
Normal file
1822
it2taud.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user