83 Commits

Author SHA1 Message Date
minjaesong
65d89db9c6 minor colour change(2) 2026-04-28 09:16:48 +09:00
minjaesong
53173a359c minor colour change 2026-04-28 09:14:02 +09:00
minjaesong
8d7d534bc8 taud logo; command.js supporting .alias file 2026-04-28 01:20:48 +09:00
minjaesong
dc3c73252e taut: colour scheme change 2026-04-27 23:44:51 +09:00
minjaesong
2053526dfa taut: minor bugfixes 2026-04-27 18:19:58 +09:00
minjaesong
6bc49e3f0b fix: taut voice mute status persisting on other tabs 2026-04-27 13:29:19 +09:00
minjaesong
02c4f9590c taut: popups 2026-04-27 07:31:47 +09:00
minjaesong
34afa95137 more simulateRowState fix 2026-04-27 03:06:53 +09:00
minjaesong
284108f07f more ui changes 2026-04-27 02:58:12 +09:00
minjaesong
76011d4fa9 taut: better channel sim; s3m converter S8x->PanEff 2026-04-27 02:25:23 +09:00
minjaesong
b44d9c6b68 taut: fancier UI 2026-04-26 23:36:29 +09:00
minjaesong
e47e9e1259 taut: transport control 2026-04-26 22:36:39 +09:00
minjaesong
c5789ec28b taud: panning law toggle 2026-04-26 20:08:02 +09:00
minjaesong
93f7f436a3 taud note eff more PT conv table 2026-04-26 19:24:01 +09:00
minjaesong
3ca31e57a1 taut: fully column navigatable 2026-04-26 14:14:40 +09:00
minjaesong
1f630aee62 taut: more ui improvements 2026-04-26 13:31:54 +09:00
minjaesong
3f3644d165 taut: minor ui change 2026-04-26 02:19:24 +09:00
minjaesong
e29f9c3032 taut: patterns tab 2026-04-26 02:05:10 +09:00
minjaesong
85b8586a3a taut: panelised view 2026-04-25 15:29:02 +09:00
minjaesong
92b9984ef8 taut: fx colour change 2026-04-25 15:03:56 +09:00
minjaesong
3f98d25828 taut control and font changes 2026-04-25 11:33:43 +09:00
minjaesong
d4ea9b2d29 more correct pitch slide conversion rule 2026-04-24 20:17:20 +09:00
minjaesong
a1b62f3155 taud-related changes (docs, converter supports Y eff) 2026-04-24 18:35:24 +09:00
minjaesong
4802e10dfc better tracker font, IT eff for Taud 2026-04-24 12:18:34 +09:00
minjaesong
6d70960e5c Panbrello is 'Y' not 'W', oops 2026-04-24 09:21:10 +09:00
minjaesong
3d99568359 taud: implemented eff W (panbrello) 2026-04-24 09:15:24 +09:00
minjaesong
1fe966ca09 more taut font change 2026-04-24 02:42:09 +09:00
minjaesong
037b2c1a16 taut font change 2026-04-24 02:33:59 +09:00
minjaesong
ea09065802 more taut gui 2026-04-24 02:17:41 +09:00
minjaesong
8d28bde119 taud base note def changed (A3@440Hz) 2026-04-23 23:11:09 +09:00
minjaesong
755afb7df4 reatable fx names; horz scroll fix 2026-04-23 22:23:15 +09:00
minjaesong
539df453ec taut: fix voleff 'v1' rendering as 'v.1' 2026-04-23 21:51:47 +09:00
minjaesong
5e3ffea6d3 taut: better scrolling behav(2) 2026-04-23 21:18:30 +09:00
minjaesong
4bda55d511 taut: better scrolling behav 2026-04-23 21:12:11 +09:00
minjaesong
852c0e6e80 taut: displaying note symbol 2026-04-23 21:03:20 +09:00
minjaesong
25309cf5b6 format revision and tracker GUI 2026-04-23 20:48:40 +09:00
minjaesong
44f11120d8 tightening formats 2026-04-23 14:47:53 +09:00
minjaesong
bc16ffabb4 minor colour change 2026-04-23 14:04:49 +09:00
minjaesong
ad5e5b62bc more tracker gui 2026-04-23 13:59:48 +09:00
minjaesong
887c2fbfba s3m to taud fix (not emitting volcmd on note retrigger) 2026-04-23 13:35:38 +09:00
minjaesong
e58eb2c12b more tracker stuff 2026-04-23 12:43:56 +09:00
minjaesong
3a91edb379 playback ctrl 2026-04-23 09:59:48 +09:00
minjaesong
74d94b350c taut: always scroll to centre 2026-04-23 01:32:44 +09:00
minjaesong
4559e4f3f6 better tauty 2026-04-23 00:16:32 +09:00
minjaesong
c5bc0d6526 tauty 2026-04-22 23:03:33 +09:00
minjaesong
a92727862e taut: detailed custom notation def 2026-04-22 17:38:47 +09:00
minjaesong
31d25d940b taut: note symbols(2) 2026-04-22 17:23:28 +09:00
minjaesong
460046c4ed taut: note symbols 2026-04-22 16:01:57 +09:00
minjaesong
6eeccb4baa manually entering syms 2026-04-21 23:18:21 +09:00
minjaesong
a7c44bd05f taut font update 2026-04-21 13:28:13 +09:00
minjaesong
4e647f9fe1 tracker wip 2026-04-20 17:38:02 +09:00
minjaesong
b2faab9377 s3m converter: not emitting skip order (0xFE) 2026-04-20 03:20:55 +09:00
minjaesong
e833d75b2c tracker engine bugfixes 2026-04-20 03:10:08 +09:00
minjaesong
f84ea5e68a tracker effects definition 2026-04-20 01:35:23 +09:00
minjaesong
5374ca43c3 proper tracker effects documentation 2026-04-19 19:18:52 +09:00
minjaesong
bef85f6e2f tracker impl, s3m converter, larger tracker sample bin 2026-04-19 02:52:12 +09:00
minjaesong
f02ad1de79 minor spec change for Taud 2026-04-17 17:28:03 +09:00
minjaesong
7f0ff3e653 Taud tracker: explicit nop and key-off behaviour 2026-04-17 16:42:17 +09:00
minjaesong
8702104bfe tracker impl 2026-04-17 12:03:43 +09:00
minjaesong
7d899936e2 audio changes 2026-04-16 21:58:06 +09:00
minjaesong
6aa2542bb8 audio device changes 2026-04-16 15:04:44 +09:00
minjaesong
2ac084acd7 psg.mjs 2026-04-16 02:05:21 +09:00
minjaesong
1208690c4f graal update with graal compiler 2026-04-13 22:20:50 +09:00
minjaesong
ca977b074d vm update 2026-04-10 20:36:55 +09:00
minjaesong
102801d8b0 tav: vendor string update 2026-01-21 22:00:47 +09:00
minjaesong
10351bafb1 tav fix: fractional framerate breaking audio encoding 2026-01-21 21:41:03 +09:00
minjaesong
9310885260 tav fix: webm being recognised as still image 2026-01-21 21:21:11 +09:00
minjaesong
b10d5d3a34 tav: vendor string update 2025-12-30 09:36:19 +09:00
minjaesong
86b44565e0 tav: revived adaptive gop alloc 2025-12-30 09:27:14 +09:00
minjaesong
54b61fb436 tav: support for fractional framerate 2025-12-25 18:26:56 +09:00
minjaesong
4afe3816c7 tav: extended header XFPS 2025-12-25 13:26:38 +09:00
minjaesong
f09dd66185 playtav: fixed using wrong flag 2025-12-25 11:20:21 +09:00
minjaesong
b590415231 playtav: still picture playback 2025-12-25 11:13:34 +09:00
minjaesong
237d3d6fd2 playtav: playing next file must not work with still images 2025-12-25 03:21:07 +09:00
minjaesong
3421d71012 TAV: still picture impl 2025-12-23 04:00:53 +09:00
minjaesong
5d5576c077 fix: mmio-based readKey() having delayed event because of race condition 2025-12-20 11:17:50 +09:00
minjaesong
64026be133 disabling broken code until fixed 2025-12-19 23:18:16 +09:00
minjaesong
96d697e158 iPF progressive mode decoder 2025-12-19 21:41:51 +09:00
minjaesong
1680137b7d external iPF encoder 2025-12-19 21:36:48 +09:00
minjaesong
c71920b95d TAV fix 13/7 wavelet not decoding correctly 2025-12-18 20:09:06 +09:00
minjaesong
629ed5fb12 tsvm "compiler" update 2025-12-18 10:29:46 +09:00
minjaesong
4f6efbe000 Update terranmon.txt 2025-12-18 10:28:56 +09:00
minjaesong
4362610c70 rm readme 2025-12-18 10:07:08 +09:00
145 changed files with 11626 additions and 1008 deletions

4
.gitignore vendored
View File

@@ -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

View File

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

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

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

2
.idea/misc.xml generated
View File

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

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

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

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

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

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

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

2
.idea/vcs.xml generated
View File

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

View File

@@ -12,6 +12,12 @@ 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`
## Architecture
### Core Components

842
TAUD_NOTE_EFFECTS.md Normal file
View File

@@ -0,0 +1,842 @@
# Taud Tracker Effect Command Reference
Taud is a tracker-style music format derived from ScreamTracker 3's pattern command set, extended to 16-bit effect arguments and a 4096-tone equal-temperament pitch grid. This document defines every effect command a Taud engine must implement. Each command entry has three parts: a plain explanation for composers, compatibility notes for converting patterns from ScreamTracker 3 (ST3), ImpulseTracker (IT) or ProTracker (PT), and implementation details for engine writers.
---
## 1. Sound device
- **Bit depth:** 8-bit unsigned throughout, including the final mixdown.
- **Sample rate:** fixed at 32000 Hz.
- **Output channels:** strictly stereo; the mix bus always produces a two-channel frame even for mono-source samples.
Internal accumulators may widen to 16 or 32 bits during mixing and effect computation, but stored samples and final output are 8-bit.
## 2. Pitch system — 4096-TET
One octave spans **4096 pitch units** ($1000 exactly). A 12-TET semitone therefore equals **4096 ÷ 12 ≈ 341.333 units** (≈ $0155.55), which is not an integer; this irrationality is a deliberate consequence of choosing a microtonal native grid. Implementations store channel pitch as a signed integer in Taud units, and convert to playback rate using
```
playback_rate = reference_rate × 2 ^ (pitch_units / 4096)
```
Commonly used intervals in Taud units are listed below; all are rounded to the nearest integer.
| Interval | Units (exact) | Hex (rounded) |
|---|---|---|
| Octave | 4096 | $1000 |
| Perfect fifth (7 ST) | 2389.33 | $0955 |
| Tritone (6 ST) | 2048 | $0800 |
| Major third (4 ST) | 1365.33 | $0555 |
| Minor third (3 ST) | 1024 | $0400 |
| 1 semitone | 341.33 | $0155 |
| 1/8 semitone (1 finetune) | 42.67 | $002B |
| 1/16 semitone | 21.33 | $0015 |
| 1/64 semitone | 5.33 | $0005 |
| 1 cent (1/100 semitone) | 3.41 | $0003 |
## 3. Volume system
Per-note and per-channel volume runs from **$00 (silent) to $3F (full)**, a 6-bit range narrower than ST3's 0..$40. Global volume (effect V) runs 0..$FF; this wider range lets the mix bus scale the summed channel output without disturbing individual note volumes. The per-frame mix chain per channel is
```
mix = sample × note_vol × channel_vol × global_vol >> normalisation_shift
```
with saturation applied before the 8-bit stereo output.
## 4. Rows, ticks, patterns, orders
A pattern is a rectangular grid of rows and channels; each cell holds one note event. Playback divides each row into `speed` ticks (effect A); tempo (effect T) sets the duration of one tick. At 125 BPM and speed 6, one row takes 120 ms and one tick 20 ms. Songs play patterns in an order sequence; effects B and C navigate this sequence.
## 5. Default parameters at song start
| Parameter | Value |
|---|---|
| Speed | $06 (6 ticks/row) |
| Tempo byte | $65 (125 BPM; see effect T for the $18 offset) |
| Global volume | $80 (mid-scale) |
| Channel volume | $3F (full) |
| Pan (all channels) | $80 (centre) |
| Order index | $0000 |
## 6. Effect memory groups
Most effects recall their last non-zero argument when re-issued with $0000. Unlike ST3, which shares one memory slot across most effects, Taud groups memories into four cohorts plus private slots:
- **E and F share one slot** (pitch slide down and up). Issuing E $0000 recalls the last E-or-F argument and re-applies it as a down-slide; F $0000 does the same as an up-slide.
- **G has its own slot** (tone portamento).
- **H and U share one slot** (vibrato speed and depth are jointly recalled; the last-written values persist across both commands).
- **R has its own slot** (tremolo).
Every other memory-carrying effect (D, I, J, K, L, O, Q, and others) has a private slot.
## 7. Opcode and argument format
Opcodes are single base-36 digits (0-9, then A-Z); arguments are 16-bit hexadecimal values prefixed with `$`. A cell is notated `OPCODE $HHLL` where HH is the high byte and LL is the low byte. Where an effect partitions its argument into sub-fields (for instance, H's speed and depth), the split is spelled out in the command description.
---
# The effects
## A $xx00 — Set tick speed to $xx
**Plain.** Sets how many ticks each row contains. Lower values make rows shorter and per-tick effects (slides, vibrato) develop faster; higher values stretch the row and give effects more iterations.
**Compatibility.** ST3 `Axx` maps one-to-one: Taud `A $xx00`. ST3 `A00` is a no-op; Taud `A $0000` is likewise ignored. ProTracker `Fxx` with `xx < $20` maps to Taud `A $xx00`; `Fxx` with `xx ≥ $20` maps to T instead (see T).
**Implementation.** If the high byte is non-zero, write it to `ticks_per_row`; the low byte is reserved and must be zero. The change takes effect from the row on which the A command appears. There is no memory for A.
---
## B $xxyy — Jump to order $xxyy
**Plain.** Finishes the current row, then continues playback at row 0 of the pattern at order position $xxyy. Use this to create song-level jumps, loops, or branching structures.
**Compatibility.** ST3 `Bxx` jumps to an 8-bit order and maps to Taud `B $00xx`. The extended 16-bit range means Taud songs may have up to $10000 order entries.
**Implementation.** On the last tick of the current row, set the next order index to the argument and the next row to 0. If the argument exceeds the song length, wrap to the song's defined restart position (order $0000 by default). Jumps are detected by a visited `(order, row)` set so that pathological loops do not prevent song-length computation, though they do not interrupt actual playback. There is no memory for B.
**Simultaneous B and C on the same row.** If a B command appears in the same row as a C command (on any channel), both fire: B chooses the order, C chooses the row within that order. If the two commands appear on different channels, channel priority is **ascending channel index** — the lowest-numbered channel carrying either effect wins its parameter. If both appear on the same channel row (only possible if one is a volume-column equivalent), the effect column takes precedence.
---
## C $xxyy — Break pattern to row $xxyy
**Plain.** Finishes the current row, then skips ahead to row $xxyy of the **next** pattern in the order sequence.
**Compatibility.** ST3 stores `Cxx` as **BCD** (so on-disk `$10` means decimal row 10); Taud stores the argument as plain binary. When converting from ST3, decode with `row = (byte >> 4) × 10 + (byte & $0F)`. Valid ST3 source bytes are those representing decimal 0..63; out-of-range BCD bytes should clamp to row 0 on import. When exporting back to ST3, encode with `byte = ((row / 10) << 4) | (row % 10)`, clamped at row 63.
**Implementation.** On the last tick of the current row, advance the order index by 1 (or honour a co-occurring B), then set the next row to the argument. If the argument exceeds the destination pattern's row count, start the destination pattern at row 0. There is no memory for C.
---
## D — Volume slide (multiple forms)
D's 16-bit argument encodes four mutually exclusive modes using the top nibble and the following byte. All forms operate on the channel's current volume and clip to $00..$3F after each step.
### D $0y00 — Volume slide down by $y per non-first tick
**Plain.** Each tick after tick 0, volume decreases by $y. A D $0400 at speed 8 reduces volume by $1C over the row.
**Compatibility.** ST3 `Dx0` (volume slide down) maps to Taud `D $0x00`. The ST3 volume cap was $40; Taud's is $3F — a very high-volume sample reaching $40 in ST3 will snap to $3F in Taud.
**Implementation.** On ticks > 0, subtract the low nibble of the high byte from `channel_volume`; clamp at $00. Memory is private to D and is keyed on the full original byte (so D $0000 recalls whatever form last ran).
### D $x000 — Volume slide up by $x per non-first tick
**Plain.** Each tick after tick 0, volume increases by $x. Capped at $3F.
**Compatibility.** ST3 `D0y` (volume slide up) maps to Taud `D $y000`.
**Implementation.** On ticks > 0, add the high nibble of the high byte to `channel_volume`; clamp at $3F.
### D $Fy00 — Fine volume slide down by $y on tick 0
**Plain.** Applies a one-shot volume reduction of $y on tick 0 only. Independent of speed. A D $FF00 behaves as a fine slide up by $F (so a request for "down by F" is reinterpreted; see below).
**Compatibility.** ST3 `DFy` maps directly. The $FF edge case is preserved: ST3 treats `DFF` as fine slide up by $F rather than fine slide down by $F, and Taud follows suit.
**Implementation.** On tick 0 only, subtract the low nibble of the high byte from `channel_volume`. If the low nibble is $0, treat as fine-slide-up by $F. If the high byte is $FF, treat as fine-slide-up by $F.
### D $xF00 — Fine volume slide up by $x on tick 0
**Plain.** One-shot volume increase of $x on tick 0 only.
**Compatibility.** ST3 `DxF` maps directly. Volume cap is $3F, lower than ST3's $40.
**Implementation.** On tick 0 only, add the high nibble to `channel_volume`; clamp at $3F.
---
## E $xxxx — Pitch slide down by $xxxx (linear)
**Plain.** Lowers the channel's pitch by the argument per tick. Taud's pitch slides are **linear in the 4096-TET grid** — the slide value is subtracted directly from the stored pitch, without any period-table indirection. A coarse slide uses the full value range; a fine slide applies only once per row; an extra-fine slide is not provided (the 16-bit argument already gives microtonal precision below 1/64 semitone).
Coarse and fine modes are distinguished by the high nibble of the argument:
- `E $0001..$EFFF` — coarse slide: subtracts the full argument from pitch each tick after tick 0. A slide of $0155 drops pitch by one semitone per tick.
- `E $F000..$FFFF` — fine slide: on tick 0 only, subtracts `arg & $0FFF` from pitch.
- `E $0000` — recalls the last E-or-F argument and applies it as a down-slide, preserving the original form (coarse or fine).
**Compatibility.** This is **the single intentionally ST3-incompatible command in Taud**. ST3 pitch slides operate on Amiga periods or linear slide units; Taud operates directly on 4096-TET pitch units. Coarse and fine forms use different unit sizes:
- ST3 `Exx` coarse (where `xx < $E0`) → Taud `E round($00xx × 64/3)` (1 ST3 coarse unit = 1/16 semitone = 64/3 ≈ 21.33 Taud units, rounded).
- ST3 `EFx` fine → Taud `E $F0 round(x × 16/3)` (1 ST3 fine unit = 1/64 semitone = 16/3 ≈ 5.33 Taud units, applied once per row).
- ST3 `EEx` extra-fine → Taud `E $F0 round(x × 16/3)` (same unit as fine, applied once per row).
ST3 Amiga-mode slides do not have a clean conversion and should be treated as linear-mode equivalents during import.
Because E and F share memory in Taud (narrower than ST3's broad shared memory), an ST3 song that used `E00` or `F00` to recall a D, G, or Q argument will break on import; the converter must eagerly resolve ST3 recalls into explicit Taud arguments rather than relying on memory.
**Implementation.** Per-tick processing:
```
on row start:
raw = arg
if raw == 0: raw = memory_EF
else: memory_EF = raw
if (raw & $F000) == $F000: # fine
pitch -= (raw & $0FFF)
mode_this_row = FINE
else: # coarse
slide_amount_this_row = raw
mode_this_row = COARSE
on tick > 0:
if mode_this_row == COARSE:
pitch -= slide_amount_this_row
```
Glissando control (S $1x) snaps the output pitch to the nearest semitone after every slide application; see S $1x.
---
## F $xxxx — Pitch slide up by $xxxx (linear)
**Plain.** Raises the channel's pitch by the argument per tick, with the same mode-selection scheme as E. Coarse, fine, and memory behaviour are identical in form but inverted in direction.
**Compatibility.** Same as E. ST3 `Fxx` coarse converts using `round(x × 64/3)`; `FFx` fine and `FEx` extra-fine convert using `round(x × 16/3)`. F and E share one memory slot in Taud.
**Implementation.** As for E, but add instead of subtract. No upper pitch cap is defined by the effect itself, but the sample-rate conversion at the mixer will saturate well before arithmetic overflow at reasonable playing ranges.
---
## G $xxxx — Tone portamento with speed $xxxx
**Plain.** Slides the channel's current pitch toward the note specified in the same row, at $xxxx Taud units per tick (after tick 0), stopping when the target is reached. A row with G and a note does **not** re-trigger the sample — the note's pitch becomes the portamento target and the already-sounding sample continues at its current pitch.
**Compatibility.** ST3 `Gxx` uses an 8-bit value in period-table units; convert to Taud using the same `round(× 64/3)` scale as E/F coarse (1/16 semitone per ST3 slide unit). ST3 linear mode is the expected import source; Amiga-mode G sources should be treated as linear. G has its **own** memory slot in both ST3 and Taud, so conversion is straightforward and does not suffer the shared-memory problem of E/F.
**Implementation.**
```
on row parse:
if row has note and G effect:
target_pitch = period_for(note)
# do NOT re-trigger sample
if arg != 0:
memory_G = arg
speed_this_row = memory_G
on tick > 0:
if target_pitch set:
delta = sign(target_pitch - pitch) × speed_this_row
pitch += delta
if sign crossed target: pitch = target_pitch; target_pitch = None
```
Glissando (S $1x) snaps the output frequency to the nearest semitone ($0155 step approximation) after each advance without changing the internal pitch counter; it affects only what the mixer sees.
---
## H $xxyy — Vibrato with speed $xx and depth $yy
**Plain.** Modulates pitch with a low-frequency oscillator (LFO). `$xx` is the LFO speed (high byte), `$yy` is the depth (low byte). On H rows the LFO accumulator advances at `$xx × 4` per tick through a 256-entry lookup of the selected waveform (see S $3x). The current pitch offset is added to the channel's base pitch for the duration of each tick.
**Compatibility.** ST3 `Hxy` uses 4-bit nibbles for speed and depth; convert by nibble-repeating each into Taud's bytes: ST3 `H27` → Taud `H $2277`. This preserves the effective LFO rate and peak depth. H and U share memory in Taud (they did in ST3 too).
Unlike ProTracker, ST3 vibrato fires on tick 0 as well; Taud follows ST3.
**Implementation.** The reference sine table is OpenMPT's 64-entry 8-bit table, indexed `pos >> 2` through a 256-entry logical LFO (equivalently, a 256-sample 4×-oversampled sine peaking at ±$7F):
```
ModSinusTable[64] =
00 0C 19 25 31 3C 47 51 5A 62 6A 70 75 7A 7D 7E
7F 7E 7D 7A 75 70 6A 62 5A 51 47 3C 31 25 19 0C
00 F4 E7 DB CF C4 B9 AF A6 9E 96 90 8B 86 83 82
81 82 83 86 8B 90 96 9E A6 AF B9 C4 CF DB E7 F4
```
Per row/tick:
```
on row parse (H):
if (arg >> 8) != 0: memory_HU.speed = arg >> 8
if (arg & $FF) != 0: memory_HU.depth = arg & $FF
on every tick (including tick 0):
sine = ModSinusTable[(lfo_pos >> 2) & $3F] # signed -$80..+$7F
pitch_delta = (sine × memory_HU.depth) >> 6
applied_pitch = base_pitch + pitch_delta
lfo_pos = (lfo_pos + memory_HU.speed × 4) & $FF
```
At maximum speed and depth ($FFFF), peak `pitch_delta` is `$7F × $FF >> 6 ≈ $1FA` — about 1.5 semitones. On a fresh note, if the current LFO waveform retrigger bit is clear (S $3x with $x < $4), `lfo_pos` resets to 0. When the waveform is "random", a fresh random value is drawn every tick rather than read from the table.
---
## U $xxyy — Fine vibrato with speed $xx and depth $yy
**Plain.** Same LFO as H but four times finer in pitch — useful for subtle microtonal warbles.
**Compatibility.** ST3 `Uxy` uses nibbles; nibble-repeat each to convert. U shares memory with H.
**Implementation.** Identical to H except the shift is 8 instead of 6:
```
pitch_delta = (sine × memory_HU.depth) >> 8
```
Peak at maximum settings: $7F × $FF >> 8 ≈ $7E, about 0.4 semitone — exactly a quarter of H's peak.
---
## I $xxyy — Tremor with on-time $xx and off-time $yy
**Plain.** Rapidly gates the channel on and off. Volume plays normally for `$xx + 1` ticks, then mutes for `$yy + 1` ticks, repeating. Counters persist across rows and only reset on a fresh I row with a new argument.
**Compatibility.** ST3 `Ixy` uses nibbles (`$xy`) with the same semantics; convert by nibble-repeating each into Taud bytes: ST3 `I47` → Taud `I $4477`. The `+1` behaviour on both counters comes from ProTracker and is preserved throughout. Memory is private.
**Implementation.**
```
on row parse (I):
if arg != 0: memory_I = arg
on_time = ((memory_I >> 8) & $FF) + 1
off_time = ( memory_I & $FF) + 1
on every tick:
if phase == ON:
play at full channel volume
tick_in_phase += 1
if tick_in_phase >= on_time: phase = OFF; tick_in_phase = 0
else:
force output volume to 0 (base volume preserved for later effects)
tick_in_phase += 1
if tick_in_phase >= off_time: phase = ON; tick_in_phase = 0
```
A zero `$xx` or `$yy` input becomes 1 tick after the `+1`, never zero.
---
## J $xxyy — Microtonal arpeggio with offsets $xx00 and $yy00
**Plain.** Cycles the playing pitch through three values across consecutive ticks: the note, the note plus `$xx00` Taud units, and the note plus `$yy00` Taud units, repeating. At the default 50 Hz tick rate (speed 6, 125 BPM), this produces a classic chord-arpeggio effect; because Taud's grid is 4096-TET, the intervals can be microtonal.
The encoding places each 8-bit offset byte into the **high byte** of a 16-bit pitch delta, giving 256 discrete intervals per arp voice with a resolution of $0100 ≈ 0.75 semitone per step. This is coarser than E/F's 16-bit slides, but adequate for arpeggios and well-suited to non-12-TET intervals.
**Compatibility.** ST3 `Jxy` uses nibbles as 12-TET semitones; Taud uses bytes as $0100-scaled 4096-TET offsets. The conversion is therefore lossy — 12-TET intervals that are not multiples of 3 semitones incur ±25 cent rounding error. The table below gives the best Taud byte for each 12-TET semitone offset:
| Semitones | Taud byte | Taud units | Error (cents) |
|---|---|---|---|
| 0 | $00 | 0 | 0 |
| 1 | $01 | 256 | 25 |
| 2 | $03 | 768 | +25 |
| 3 | $04 | 1024 | 0 |
| 4 | $05 | 1280 | 25 |
| 5 | $07 | 1792 | +25 |
| 6 | $08 | 2048 | 0 |
| 7 | $09 | 2304 | 25 |
| 8 | $0B | 2816 | +25 |
| 9 | $0C | 3072 | 0 |
| 10 | $0D | 3328 | 25 |
| 11 | $0F | 3840 | +25 |
| 12 | $10 | 4096 | 0 |
For example, ST3 `J37` (minor chord) imports as Taud `J $0409`; ST3 `J47` (major chord) as Taud `J $0509`. Memory is private and stores the full 16-bit argument.
**Implementation.**
```
on row parse (J):
if arg != 0: memory_J = arg
off1 = (memory_J >> 8) & $FF # high byte
off2 = memory_J & $FF # low byte
on every tick:
selector = tick_within_row mod 3
if selector == 0: voice_pitch = base_pitch
elif selector == 1: voice_pitch = base_pitch + (off1 << 8)
elif selector == 2: voice_pitch = base_pitch + (off2 << 8)
```
The `tick_within_row mod 3` counter resets every row start (so every row begins at `base_pitch`). A subsequent E/F slide after a J row resumes from the last arpeggiated voice's pitch, not from `base_pitch` — this mirrors ST3's `kST3PortaAfterArpeggio` quirk and is deliberately preserved.
---
## K $xy00 — Dual: vibrato continuation and volume slide $xy
**Plain.** **Unimplemented**. On ST3, continues a previously started vibrato (H or U) without retriggering it, while applying a volume slide of `$xy` per non-first tick. Fine volume slides are not available in this form.
**Compatibility.** ST3 `Kxy` maps directly. Implementations must convert K to an explicit pair of commands: `H $0000` (continue with stored speed/depth) combined with volume-column command `1.$xy` (volume slide), and emit both.
**Implementation.** Execute the per-tick vibrato update as if an H command were active with argument $0000 (recall), then execute a D $0y00 or $x000 slide using the bytes of the K argument: high nibble as up-slide, low nibble as down-slide. If both nibbles are non-zero, down-slide takes precedence (matching ST3). K has no memory of its own; it uses H/U's stored speed and depth.
---
## L $xy00 — Dual: tone portamento continuation and volume slide $xy
**Plain.** **Unimplemented**. On ST3, continues a previously started tone portamento (G) without retriggering, while applying a volume slide of `$xy` per non-first tick. Fine volume slides are not available here.
**Compatibility.** ST3 `Lxy` maps directly. Like K, L must be equivalently implemented as `G $0000` plus a volume-column slide.
**Implementation.** Execute the per-tick G update (recalling G's stored speed), then the D slide as in K. L has no memory of its own.
---
## O $xxyy — Set sample offset to $xxyy
**Plain.** On the row where it appears, jumps the sample playhead to byte $xxyy of the sample data. If the sample is looped and the requested offset exceeds the loop end, the offset wraps around through the loop as if playback had reached that point naturally.
**Compatibility.** ST3 `Oxx` is 8-bit, addressing offset `xx × $100`. On import, copy the ST3 byte into Taud's high byte and zero the low byte: Taud `O $xx00`. ProTracker `9xx` maps identically. The Taud 16-bit form allows byte-precise seeking within samples larger than $100 bytes. Memory is private.
**Implementation.** On the row start, set the sample playhead to `arg` (in bytes, relative to the sample's start). Apply the loop-wrap calculation if the sample has loop points and `arg > loop_end`: `arg = loop_start + ((arg - loop_start) mod loop_length)`. The O command does not retrigger the sample; it only relocates the playhead for an already-triggered note.
---
## Q $xy00 — Retrigger note every $y ticks with volume modifier $x
**Plain.** Retriggers the currently playing sample at an interval of `$y` ticks, optionally modifying its volume on each retrigger according to `$x`. The retrigger interval runs across rows until a new Q with a different `$y` or no Q at all.
**Compatibility.** ST3 `Qxy` maps directly. The **`$y == 0` behaviour is preserved from ST3**: the entire effect is ignored (no retrigger, and memory is not updated). Memory is private.
ProTracker `E9x` is equivalent to Taud `Q $0x00` (retrigger only, no volume change).
**Implementation.** A per-channel tick counter advances every tick, including tick 0. When it reaches `$y`, the sample retriggers (keeping current pitch), the counter resets to 0, and the volume modifier `$x` applies. The counter resets only when a row has **no** Q command; successive Q rows share and advance the counter.
The volume modifier table, **computed with arithmetic (no LUT)**, is:
| $x | Action | $x | Action |
|---|---|---|---|
| 0 | no change | 8 | no change |
| 1 | vol $01 | 9 | vol + $01 |
| 2 | vol $02 | A | vol + $02 |
| 3 | vol $04 | B | vol + $04 |
| 4 | vol $08 | C | vol + $08 |
| 5 | vol $10 | D | vol + $10 |
| 6 | vol × 2 / 3 | E | vol × 3 / 2 |
| 7 | vol × 1 / 2 | F | vol × 2 |
Multiplicative cases use integer arithmetic: `vol × 2 / 3` is `(vol × 2) / 3` (truncated); `vol × 3 / 2` is `(vol × 3) / 2`; `vol × 1 / 2` is `vol >> 1`; `vol × 2` is `vol << 1`. All results clip to $00..$3F after.
A note previously silenced by a cut (`^^^` or `SCx` earlier in the row) is not retriggered, matching ST3's `kST3RetrigAfterNoteCut` rule.
---
## R $xxyy — Tremolo with speed $xx and depth $yy
**Plain.** Modulates volume with an LFO, symmetrically with H's pitch modulation. `$xx` is LFO speed, `$yy` depth; the waveform is selected by S $4x.
**Compatibility.** ST3 `Rxy` uses nibbles; convert by nibble-repeat. ST3's volume cap is $40; Taud's is $3F — very deep tremolo that would have briefly clipped at $40 in ST3 may clip slightly earlier in Taud. R has its own memory slot (not shared with H/U).
**Implementation.** Identical machinery to H with a larger shift to fit the narrower volume range:
```
on row parse (R):
if (arg >> 8) != 0: memory_R.speed = arg >> 8
if (arg & $FF) != 0: memory_R.depth = arg & $FF
on every tick (including tick 0):
sine = ModSinusTable[(lfo_pos >> 2) & $3F]
vol_delta = (sine × memory_R.depth) >> 9
applied_vol = clamp(base_vol + vol_delta, 0, $3F)
lfo_pos = (lfo_pos + memory_R.speed × 4) & $FF
```
Peak at maximum settings: $7F × $FF >> 9 = $3F — the full volume range. Retrigger behaviour tracks the S $4x waveform nibble bit 2: cleared means retrigger on new note, set means preserve LFO position.
---
## T $xxyy — Tempo set or tempo slide
Taud splits T by which byte carries the value:
### T $xx00 (high byte non-zero) — Set tempo
**Plain.** Sets the Taud tempo byte to `$xx`. The resulting BPM is `$xx + $18`: Taud byte $00 → 24 BPM, $65 → 125 BPM (default), $FF → 279 BPM.
**Compatibility.** ST3 `Txx` (where `xx ∈ $20..$FF`) stores BPM directly; convert with `taud_byte = xx $18`. Taud byte $08 corresponds to ST3's minimum BPM of 32; Taud bytes below $08 are inexpressible in ST3 and should round up to $08 (BPM 32) when exporting. OpenMPT's extended tempo slides (`T $0x` down, `T $1x` up) in S3M files map to Taud T $00xx — see below.
ProTracker `Fxx` with `xx ≥ $20` maps to Taud `T $(xx $18)00`; `Fxx` with `xx < $20` maps to A (speed) instead.
**Implementation.** If the high byte is non-zero, set `tempo_byte = arg >> 8`; derive `BPM = tempo_byte + $18`; compute tick duration as `samples_per_tick = 32000 × 5 / (BPM × 2) = 80000 / BPM` (integer truncated) at the fixed 32000 Hz output rate. Example: BPM 125 → 640 samples per tick; BPM 24 → 3333 samples per tick; BPM 279 → 286 samples per tick. There is no memory for set-tempo.
### T $00xy (high byte zero) — Tempo slide
**Plain.** Adjusts the tempo continuously during the row. `$00_0y` (low nibble under a zero high nibble within the low byte) slides BPM down by `$y` per non-first tick; `$00_1y` slides up. Out-of-range encodings ($00_20 through $00_FF) are reserved and behave as no-ops.
**Compatibility.** ST3 itself has only the set form; the slide forms originate in the OpenMPT/Schism extension of S3M. On export to strict ST3, slide forms are unrepresentable and should be approximated as an equivalent set-tempo on a later row.
**Implementation.**
```
on row parse (T with high byte == 0):
low = arg & $FF
if (low & $F0) == $00:
slide_dir = DOWN
slide_amount = low & $0F
elif (low & $F0) == $10:
slide_dir = UP
slide_amount = low & $0F
else:
ignore row
on tick > 0 (if slide armed):
if slide_dir == DOWN: tempo_byte = max($00, tempo_byte - slide_amount)
else: tempo_byte = min($FF, tempo_byte + slide_amount)
recompute samples_per_tick for next tick
```
A tempo slide's memory slot is separate from the set-tempo path and is private to T-slide.
---
## V $xx00 — Set global volume to $xx
**Plain.** Sets the global mix bus volume (0..$FF). $00 is silence; $FF is full. The default is $80.
**Compatibility.** ST3's global volume is 0..$40; convert with `taud_v = st3_v × 4`, clamped at $FF. On export, `st3_v = taud_v >> 2`, clamped at $40. IT's global volume is 0..$80; convert with `taud_v = it_v × 2`, clamped at $FF.
**Implementation.** Write the high byte to `global_volume` on the row the command appears. The low byte is reserved. ST3's `kST3NoMutedChannels` rule applies: V on a muted channel is ignored by ST3; for strict-compatible playback Taud follows suit, but new Taud compositions should avoid muting channels that carry global effects.
---
## Y $xxyy — Panbrello (panning vibrato) with speed $xx and depth $yy
**Plain.** Modulates panning with an LFO, symmetrically with H's pitch modulation. `$xx` is LFO speed, `$yy` depth; the waveform is selected by S $5x.
**Compatibility.** IT `Yxy` uses nibbles; convert by nibble-repeat. IT's panning cap is $40; Taud's is $3F — very deep vibrato that would have briefly clipped at $40 in IT may clip slightly earlier in Taud. Y has its own memory slot.
**Implementation.** Identical machinery to H with a larger shift to fit the narrower volume range:
```
on row parse (Y):
if (arg >> 8) != 0: memory_Y.speed = arg >> 8
if (arg & $FF) != 0: memory_Y.depth = arg & $FF
on every tick (including tick 0):
sine = ModSinusTable[(lfo_pos >> 2) & $3F]
vol_delta = (sine × memory_Y.depth) >> 9
applied_vol = clamp(base_vol + vol_delta, 0, $3F)
lfo_pos = (lfo_pos + memory_Y.speed × 4) & $FF
```
Peak at maximum settings: $7F × $FF >> 9 = $3F — the full panning range. Retrigger behaviour tracks the S $5x waveform nibble bit 2: cleared means retrigger on new note, set means preserve LFO position.
---
## X $xx00 — Fine Set Panning
**Plain.** **Unimplemented**. On IT, sets the panning position of the current channel, $00 being full-left and $FF being full-right.
**Compatibility.** Convert directly into panning effect `0.$xx`, rounded down to nearest 6-bit value.
**Implementation.** Not applicable.
---
# The S subcommand family
S is a multiplexing opcode; the **high nibble of the high byte** selects the sub-effect, and the remainder is the sub-argument.
## S $1x00 — Glissando control
**Plain.** `$1000` turns glissando off; `$1100` turns it on. When on, tone portamento (G) output is quantised to the nearest semitone ($0155 approximation) before being sent to the mixer. The internal G pitch counter still advances smoothly; only the audible pitch steps. **This command is implemented sorely for ST3 compatibility.**
**Compatibility.** ST3 `S10`/`S11` maps directly. In Taud, "nearest semitone" uses the best integer approximation: round `pitch / $155` to the nearest integer, multiply by $155; equivalently, `snapped = (pitch + $AB) / $155 × $155`. Because $155 is an approximation of 4096/12, accumulated rounding across many octaves will drift by up to a few cents; this is documented behaviour and intentional given the microtonal grid.
**Implementation.** Maintain a per-channel boolean `glissando_on`. When G updates `pitch`, if `glissando_on` is set, compute `display_pitch = round(pitch × 12 / 4096) × 4096 / 12` (using integer division with rounding) and send `display_pitch` to the mixer; otherwise send `pitch` directly.
---
## S $2x00 — Set fine-tune
**Plain.** Overrides the current note's fine-tune by applying a fixed 4096-TET offset. The index `$x` selects one of sixteen predefined pitch offsets, following ScreamTracker 3's Hz-based fine-tune table but expressed directly in Taud units. This command is implemented for ST3 compatibility.
**Compatibility.** The index scheme matches ST3 exactly: `$8` is the baseline (no change), `$0..$7` are progressively flatter, `$9..$F` are progressively sharper. The Hz reference values come from the ST3 User's Manual and are reproduced here for auditability; the Taud offset is `log2(Hz / 8363) × 4096`, rounded to the nearest integer. **Format converters are advised to apply offset to the note value directly.**
| $x | Reference Hz | Taud offset |
|---|---|---|
| $0 | 7895 | $0154 |
| $1 | 7941 | $0132 |
| $2 | 7985 | $0111 |
| $3 | 8046 | $00E4 |
| $4 | 8107 | $00B8 |
| $5 | 8169 | $008B |
| $6 | 8232 | $005D |
| $7 | 8280 | $003B |
| $8 | 8363 | $0000 |
| $9 | 8413 | +$0023 |
| $A | 8463 | +$0046 |
| $B | 8529 | +$0074 |
| $C | 8581 | +$0098 |
| $D | 8651 | +$00C8 |
| $E | 8723 | +$00F9 |
| $F | 8757 | +$0110 |
ProTracker `E5x` maps to Taud `S $2x00` with the same index meaning.
**Implementation.** On the row, look up the offset from the table and add it to the channel's base pitch before any other per-tick effect processes. The offset persists until another S $2x command or a note-reset event.
---
## S $3x00 — Vibrato LFO waveform
**Plain.** Selects the shape of the vibrato (H and U) oscillator.
| $x | Waveform | Retrigger on new note? |
|---|---|---|
| $0 | Sine | Yes |
| $1 | Ramp down (sawtooth) | Yes |
| $2 | Square | Yes |
| $3 | Random | Yes |
| $4 | Sine | No |
| $5 | Ramp down | No |
| $6 | Square | No |
| $7 | Random | No |
**Compatibility.** ST3 `S3x` maps directly.
**Implementation.** Store `vibrato_waveform = $x & $3` and `vibrato_retrigger = (($x & $4) == 0)` for the channel. The ramp-down shape is `$7F ((pos & $3F) << 2)` across one logical cycle; the square shape is `sign(sine(pos)) × $7F`; random draws a fresh `rand() & $FF $80` every tick. On a new note, if `vibrato_retrigger` is true, reset `lfo_pos = 0`.
---
## S $4x00 — Tremolo LFO waveform
**Plain.** Selects the shape of the tremolo (R) oscillator; value encoding is identical to S $3x.
**Compatibility.** ST3 `S4x` maps directly. ProTracker `E7x` maps to Taud `S $4x00`.
**Implementation.** As for S $3x, but applied to R's separate state (`tremolo_waveform`, `tremolo_retrigger`, and tremolo `lfo_pos`).
---
## S $5x00 — Panbrello LFO waveform
**Plain.** Selects the shape of the panbrello (Y) oscillator; value encoding is identical to S $3x.
**Compatibility.** IT `S5x` maps directly.
**Implementation.** As for S $3x, but applied to Y's separate state (`panbrello_waveform`, `panbrello_retrigger`, and panbrello `lfo_pos`).
---
## S $80xx — Set channel pan position
**Plain.** Sets the channel pan to `$xx`, with $00 being full left and $FF being full right. $80 is centre.
**Compatibility.** ST3 `S8x` uses a 4-bit value.
1. convert by nibble-repeat: ST3 `S83` → Taud `S $8033`. Panning column command `0.$xx` has the same semantics and is the preferred form when a pan column is available in the pattern. ProTracker `8xx` (fine pan) and `E8x` (coarse pan) both map into Taud's 8-bit pan — the ProTracker 8-bit form maps directly; the 4-bit form nibble-repeats.
2. convert to PanEff: ST3 `S8x` → PanEff `0.yy`, where `yy = round(4.2 * x)`
**Implementation.** Write `channel_pan = arg & $FF`. The pan value is applied at the mixer: `left_gain = (($FF pan) × $100) >> 8`, `right_gain = (pan × $100) >> 8`, with both applied before the global volume stage.
---
## S $Bx00 — Pattern loop
**Plain.** Sets a loop point and loops within a pattern. `S $B000` marks the current row as the loop start (per channel, not per song); `S $Bx00` with $x > 0 returns playback to the saved row and plays the intervening range `$x` more times (so `$B200` plays the loop twice total beyond the initial pass).
**Compatibility.** ST3 `SBx` maps directly. ProTracker `E6x` maps to Taud `S $Bx00`.
ST3 has a long-documented bug where pattern delay (SEx) inside a pattern-loop range causes the loop counter to decrement multiple times per visit, producing unintended behaviour. **Taud fixes this bug.** On import, ST3 songs that relied on the bug will loop fewer times in Taud. Converters that want bit-exact ST3 playback should emit a warning when SBx and SEx appear in the same channel within a loop range, or optionally flatten loops by duplicating rows.
**Implementation.** State per channel: `loop_start_row` (defaulting to 0 at each pattern entry) and `loop_count` (defaulting to 0).
```
on row event (S $Bx00):
x = (arg >> 8) & $0F
if x == 0:
loop_start_row = current_row
else:
if loop_count == 0:
loop_count = x
jump next_row -> loop_start_row
else:
loop_count -= 1
if loop_count > 0:
jump next_row -> loop_start_row
# else loop_count hits 0 on its own; fall through to next row
on pattern change: loop_start_row = 0; loop_count = 0
```
The crucial bug fix relative to ST3: the loop-counter decrement happens **once per actual row playback**, not once per tick-0 invocation. When SBx shares a row with SEx (pattern delay), the pattern-delay machinery replays the row as a unit, but the SBx state machine treats the whole delay group as a single visit. Implement this by gating the SBx decrement on `pattern_delay_repetition == 0`.
---
## S $Cx00 — Note cut in $x ticks
**Plain.** Silences the note on tick `$x` of the current row by forcing the channel's output volume to 0. The sample continues running internally, so a later volume-change or retrigger event can resume audio.
**Compatibility.** ST3 `SCx` maps directly. ProTracker `ECx` also maps directly. ST3 ignores `SC0` (treats it as no cut at all); Taud preserves this.
**Implementation.** On tick `$x`, set `output_volume = 0` but leave `base_volume` unchanged. If `$x ≥ speed`, the cut never fires. If `$x == 0`, the command is ignored. Set the `note_was_cut` flag so a later Q retrigger on the same row is suppressed.
---
## S $Dx00 — Note delay for $x ticks
**Plain.** Delays the triggering of the note (and any co-row instrument, offset, and volume event) until tick `$x`. Until then, any currently playing note continues.
**Compatibility.** ST3 `SDx` maps directly. ProTracker `EDx` also maps directly. `SD0` plays the note normally on tick 0. If `$x ≥ speed`, the note never plays on this row and does not carry over to the next row.
**Implementation.** On row parse, defer the note-trigger event (including sample selection, volume, offset, and any volume-column effect) until tick `$x`. On tick `$x`, execute the deferred trigger. When combined with pattern delay (S $Ex00), the deferred trigger re-fires at the start of each row repetition — matching ST3's `kRowDelayWithNoteDelay` behaviour.
---
## S $Ex00 — Pattern delay for $x row-repeats
**Plain.** Repeats the current row `$x` additional times (so `$x = 0` means no repeat and the row plays once; `$x = 3` means the row plays four times total). Notes do not retrigger across repetitions, but per-tick effects re-run and tick-0 events (fine slides, delayed notes) re-fire on each repetition.
**Compatibility.** ST3 `SEx` maps directly. ProTracker `EEx` also maps directly. Simultaneous SEx on multiple channels: ST3 uses the first SEx in **pan order** (L1..L8 then R1..R8); **Taud uses the first SEx in ascending channel-index order** for predictability. Converters that encounter ST3 songs relying on the pan-order rule should emit a warning.
Q retrigger counters do **not** reset between SEx repetitions.
**Implementation.** Row duration becomes `speed × (1 + arg_x)` ticks. Treat each repetition as a fresh row for tick-0 purposes (so fine slides, delayed notes, and the like re-trigger), but do not reset arpeggio, vibrato, or tremolo LFO positions, and do not decrement SBx's loop counter more than once across the whole delay block.
---
## S $Fx00 — Funk repeat with speed $x (non-destructive)
**Plain.** Produces a hiss-like progressive inversion of the sample loop, toggling individual bytes over time for a gritty textural effect. Setting `$x = 0` turns the effect off; higher `$x` advances the inversion faster.
**Compatibility.** ProTracker `EFx` is destructive — it XORs bytes directly in the sample data, permanently corrupting the sample. **Taud's implementation is non-destructive**: the XOR is applied at playback time through a per-instrument bit-mask, leaving source samples pristine. ST3 does not implement SFx at all and will parse Taud's S $Fx00 as a no-op; converters targeting ST3 should drop the effect. ProTracker `EFx` imports directly as Taud `S $Fx00`.
**Implementation.** Each instrument carries a `funk_mask` bit array, one bit per byte of the loop region, all zero at song start. A per-channel counter `funk_accumulator` and a per-channel `funk_write_pos` track progress.
```
funk_table[16] = { 0, 5, 6, 7, 8, $A, $B, $D, $10, $13, $16, $1A, $20, $2B, $40, $80 }
on every tick (when S $Fx00 is active with x != 0):
funk_accumulator += funk_table[x]
while funk_accumulator >= $80:
funk_accumulator -= $80
bit = funk_mask[funk_write_pos]
funk_mask[funk_write_pos] = bit XOR 1
funk_write_pos = (funk_write_pos + 1) mod loop_length
on sample byte read during loop playback:
raw_byte = sample_data[offset_in_loop]
if funk_mask[offset_in_loop] == 1:
output_byte = raw_byte XOR $FF
else:
output_byte = raw_byte
```
`S $F000` clears `funk_accumulator` but leaves `funk_mask` intact (so the accumulated inversion pattern persists until the instrument is reset). On a fresh note or instrument-change event, Taud optionally resets `funk_mask` to all zero; this is a per-implementation choice, but the recommended default is **reset on instrument-change, preserve on pure note retrigger**.
---
# Volume column effects
Each cell carries a 6-bit value field plus a 2-bit selector field for the volume column. The four selectors are:
- **`0.$xx` — Set volume** to `$xx` (6-bit, $00..$3F). Equivalent to a note's default volume.
- **`1.$xx` — Volume slide up** by `$xx` per non-first tick (4-bit). Volume clamps at $3F.
- **`2.$xx` — Volume slide down** by `$xx` per non-first tick (4-bit). Volume clamps at $00.
- **`3.$Sx` — Fine volume slide** on tick 0 only. The high bit `$S` of the value selects direction (0 = down, 1 = up); the low 4 bits `$x` ($0..$F) are the magnitude. Equivalent in scale to `D $xF00` / `D $Fy00` but with a 5-bit cap. Fires once per row regardless of speed.
Volume-column effects do not consume the main effect slot; a cell can carry both (for instance, a tone portamento in the effect slot and a volume slide in the volume column).
When the converter folds an ST3 K, L, M, or N effect into the volume column, the slide-up / slide-down nibbles map to selectors 1 / 2 (clamped to 6 bits — values above $3F clip).
NOTE: **`3.00` — is No-op**
---
# Panning column effects
The panning column uses the same 6-bit value + 2-bit selector layout:
- **`0.$xx` — Set pan** (6-bit, $00..$3F mapped onto the channel's 8-bit pan space; $01 = full left, $1F = centre-left, $20 = centre-right, $3F = full right). For 8-bit precision use `S $80xx` instead.
- **`1.$xx` — Pan slide right** by `$xx` per non-first tick (4-bit).
- **`2.$xx` — Pan slide left** by `$xx` per non-first tick (4-bit).
- **`3.$Sx` — Fine pan slide** on tick 0 only, same direction-bit encoding as the volume column's selector 3.
NOTE: **`3.00` — is No-op**
---
# Effects That Modifies Global Behaviour
Effects in this section modifies the behaviour of the mixer. Primary intention of the commands is to provide switches for legacy tracker and modern DAW behaviours.
## 1 $01xx — Set stereo panning law
**Plain.** Sets how the mixer should treat the panning. Available modes are:
- 0: Linear panning mode (tracker-accurate). Centre panning gets 3 dB boost. Default setting.
- 1: Equal-power panning mode. L/R amplitude is at 0.707 when centre-panned.
**Implementation.**
- Mode 0:
- L_gain = if (pan < 0x80) 1.0 else 1.0 - (pan - 128.0) / 128.0
- R_gain = if (pan < 0x80) pan / 128.0 else 1.0
- Mode 1:
- L_gain = cos(pi*x / 512.0)
- R_gain = sin(pi*x / 512.0)
---
# ProTracker to Taud conversion table
This table maps each PT effect to its Taud equivalent. Arguments follow PT's two-nibble form and expand to Taud's 16-bit form as shown.
| PT effect | Taud effect | Notes |
|---------|-----------|-------|
| `0 $xy` | `J $xxyy` | Arpeggio; nibble-repeat each byte. See the 12-TET → Taud table above for conversion losses |
| `1 $xx` | `F round($0xxx × 64/3)` | Portamento up; ST3 coarse slide unit = 1/16 semitone |
| `2 $xx` | `E round($0xxx × 64/3)` | Portamento down |
| `3 $xx` | `G round($0xxx × 64/3)` | Portamento to note |
| `4 $xy` | `H $xxyy` | Vibrato; nibble-repeat each byte. |
| `5 $xy` | `L $xy00` | Combined portamento + volume slide (see compatibility note) |
| `6 $xy` | `K $xy00` | Combined vibrato + volume slide (see compatibility note) |
| `7 $xy` | `R $xxyy` | Tremolo; nibble-repeat |
| `8 $xx` | `S $80xx` or panning column `0.$xx` | Fine pan |
| `9 $xx` | `O $xx00` | Sample offset |
| `A $xy` | Volume column `1.$xy` | Volume slide |
| `B $xx` | `B $00xx` | Position jump |
| `C $xx` | Volume column `0.$xx` | Set volume |
| `D $xx` | `C $00xx` (after BCD decode) | Pattern break |
| `E $0x` | `S $000x` | (UNIMPLEMENTED) Set filter |
| `E $1x` | `E $F000 + round($0xxx × 16/3)` | Fine pitch slide up |
| `E $2x` | `E $F000 + round($0xxx × 16/3)` | Fine pitch slide down |
| `E $3x` | `S $1x00` | Glissando control |
| `E $4x` | `S $3x00` | Vibrato waveform |
| `E $5x` | `S $2x00` | Set fine-tune |
| `E $6x` | `S $Bx00` | Pattern loop |
| `E $7x` | `S $4x00` | Tremolo waveform |
| `E $8x` | `S $80xx` or panning column `0.$xx` | Coarse pan (nibble-repeat) |
| `E $9x` | `Q $0x00` | Retrigger |
| `E $Ax` | Volume column `3.$1x` | Fine volume slide up |
| `E $Bx` | Volume column `3.$0x` | Fine volume slide down |
| `E $Cx` | `S $Cx00` | Note cut |
| `E $Dx` | `S $Dx00` | Note delay |
| `E $Ex` | `S $Ex00` | Pattern delay |
| `E $Fx` | `S $Fx00` | Funk repeat |
| `F $xx` (xx < $20) | `A $xx00` | Set speed |
| `F $xx` (xx ≥ $20) | `T $(xx$18)00` | Set tempo |
---
# ScreamTracker 3 conversion notes
These quirks of ST3 are worth preserving or flagging when importing S3M files into Taud:
**Shared memory across effects.** In ST3, a single memory slot backs D, E, F, I, J, K, L, Q, R, and S. A `$00` argument on any of these recalls whichever effect last wrote a non-zero argument. Taud narrows this to four cohorts (EF / G / HU / R) plus private slots. The converter must **eagerly resolve ST3 recalls** — walking the pattern in playback order, tracking the shared memory value, and emitting explicit Taud arguments wherever an ST3 recall crosses a cohort boundary. Otherwise a Taud player will either recall the wrong value or recall $0000.
**Cxx BCD encoding.** ST3 stores pattern-break row numbers as BCD on disk (`$10` means decimal 10). Taud uses binary. Decode on import; encode on export. Out-of-range BCD bytes (decimal 64 or higher) clamp to row 0.
**Tempo range.** ST3 accepts tempos $20..$FF (BPM 32..255); Taud accepts bytes $00..$FF (BPM 24..279). Imported ST3 tempos must be shifted down by $18; Taud tempos below $08 and above $E7 cannot be represented in ST3 and should clamp on export.
**SBx + SEx interaction.** ST3 miscounts loop iterations when pattern delay is active inside a pattern loop; Taud fixes this. Songs that depended on the bug for their intended playback will loop fewer times in Taud. Flag such songs on import.
**Simultaneous SEx priority.** ST3 uses pan order (L1..L8, R1..R8); Taud uses ascending channel-index order. Rare; flag on import if multiple channels carry SEx in the same row.
**Muted channels.** ST3 skips all effect processing on muted channels (no volume change, no tempo change, no jumps); Taud follows this rule for strict compatibility but recommends that new compositions avoid muting channels that carry global effects.
**Volume cap.** ST3's volume caps at $40; Taud's at $3F. Notes that reached $40 in ST3 (a rare edge) will play marginally quieter in Taud.
**Global volume scale.** ST3's 0..$40 maps to Taud's 0..$FF with a ×4 scale on import, truncated ÷4 on export.
**Linear pitch slides.** ST3's slide arithmetic is period-based (Amiga) or linear-table-indexed; Taud's is purely linear in 4096-TET units. ST3 songs in linear mode convert cleanly: coarse forms (Exx/Fxx/Gxx) use `round(× 64/3)` (1/16 semitone per unit), fine/extra-fine forms (EFx/EEx/FFx/FEx) use `round(× 16/3)` (1/64 semitone per unit). Amiga-mode slides change character slightly because the non-linearity of period math is not replicated.
**Default tempo byte.** Taud's default $65 equals 125 BPM under the $18 offset; this is not the same as ST3's `$7D` default, which maps to Taud `$65` after subtracting $18. Converters must remap on both import and export.
---
End of reference.

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = {}

View File

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

View File

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

View File

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

View File

@@ -326,7 +326,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_LENGTH) {
// RAW PCM packets (decode on the fly)
else if (packetType == 0x1000 || packetType == 0x1001) {
let frame = seqread.readBytes(readLength)
audio.putPcmDataByPtr(frame, readLength, 0)
audio.putPcmDataByPtr(0, frame, readLength, 0)
audio.setSampleUploadLength(0, readLength)
audio.startSampleUpload(0)
sys.free(frame)

View File

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

View File

@@ -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,10 +153,10 @@ graphics.clearPixels4(0)
const gpuGraphicsMode = graphics.getGraphicsMode()
// Initialize audio
audio.resetParams(0)
audio.purgeQueue(0)
audio.setPcmMode(0)
audio.setMasterVolume(0, 255)
audio.resetParams(AUDIO_DEVICE)
audio.purgeQueue(AUDIO_DEVICE)
audio.setPcmMode(AUDIO_DEVICE)
audio.setMasterVolume(AUDIO_DEVICE, 255)
// set colour zero as half-opaque black
graphics.setPalette(0, 0, 0, 0, 7)
@@ -367,6 +369,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 +385,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 +409,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 +421,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 +483,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 +495,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 +1153,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 +1177,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 +1202,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 +1233,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 +1272,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 +1314,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 +1325,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 +1738,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 +1751,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 +1763,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 +1859,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 +2050,7 @@ try {
// Fire audio on first frame
if (!audioFired) {
audio.play(0)
audio.play(AUDIO_DEVICE)
audioFired = true
}
@@ -1971,7 +2138,7 @@ try {
// Fire audio on first frame
if (!audioFired) {
audio.play(0)
audio.play(AUDIO_DEVICE)
audioFired = true
}
@@ -2007,8 +2174,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,8 +2459,8 @@ 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)

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

View File

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

View File

@@ -1,4 +1,6 @@
const win = require("wintex")
const keys = require("keysym")
const COL_TEXT = 253
const COL_BACK = 255
const COL_BACK_SEL = 81
@@ -26,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
@@ -665,7 +675,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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,7 @@
/**
* LibSeqread — sequentially read files from disk drive
* @author CuriousTorvald
*/
let readCount = 0
let port = undefined
let fileHeader = new Uint8Array(4096)

View File

@@ -1,3 +1,8 @@
/**
* LibSeqread extension for Tape Drive — sequentially read tape
* @author CuriousTorvald
*/
// Sequential reader for HSDPA TAPE devices
// Unlike seqread.mjs which is limited to 4096 bytes per read due to serial communication,
// this module can read larger chunks efficiently from HSDPA devices.

View File

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

View File

@@ -1,3 +1,8 @@
/**
* WinTex — TUI window management and renderer
* @author CuriousTorvald
*/
class WindowObject {
constructor(x, y, w, h, inputProcessor, drawContents, title, drawFrame) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

81
ipf_encoder/Makefile Normal file
View File

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

592
ipf_encoder/decoder_ipf.c Normal file
View File

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

787
ipf_encoder/encoder_ipf.c Normal file
View File

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

View File

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

Binary file not shown.

Binary file not shown.

BIN
lib/collections-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

BIN
lib/compiler-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
lib/icu4j-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
lib/jniutils-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
lib/js-language-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
lib/nativeimage-23.1.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

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