28 Commits

Author SHA1 Message Date
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
126 changed files with 6541 additions and 984 deletions

2
.gitignore vendored
View File

@@ -66,3 +66,5 @@ assets/disk0/home/basic/*
assets/disk0/movtestimg/*.jpg
assets/disk0/*.mov
assets/diskMediabin/*
video_encoder/*

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

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,17 @@
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)
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(0, 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] = 0; // instrument 0
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

@@ -225,8 +225,9 @@ class TVDOSFileDescriptor {
}
/** reads the file bytewise and puts it to the specified memory address
* @param count optional -- how many bytes to read
* @param offset optional -- how many bytes to skip initially
* @param ptr -- where the bytes should be dumped
* @param count -- how many bytes to read
* @param offset -- how many bytes to skip initially from the file
*/
pread(ptr, count, offset) {
this.driver.pread(this, ptr, count, offset)
@@ -241,7 +242,9 @@ class TVDOSFileDescriptor {
}
/** writes the bytes stored in the memory[ptr .. ptr+count-1] to file[offset .. offset+count-1]
* - @param offset is optional
* @param ptr -- where the bytes are
* @param count -- how many bytes to write
* @param offset -- position in the file
*/
pwrite(ptr, count, offset) {
this.driver.pwrite(this, ptr, count, offset)
@@ -870,11 +873,21 @@ Object.freeze(_TVDOS.DRV.FS.DEVPT)
_TVDOS.DRV.FS.DEVFBIPF = {}
_TVDOS.DRV.FS.DEVFBIPF.pwrite = (fd, infilePtr, count, _2) => {
let decodefun = ([graphics.decodeIpf1, graphics.decodeIpf2])[sys.peek(infilePtr + 13)]
let flags = sys.peek(infilePtr+12)
let ipfType = sys.peek(infilePtr+13)
let isProgressive = (flags & 0x80) != 0
let hasAlpha = (flags & 0x01) != 0
// Select decode function based on type and progressive flag
let decodefun
if (isProgressive) {
decodefun = ([graphics.decodeIpf1Progressive, graphics.decodeIpf2Progressive])[ipfType]
} else {
decodefun = ([graphics.decodeIpf1, graphics.decodeIpf2])[ipfType]
}
let width = sys.peek(infilePtr+8) | (sys.peek(infilePtr+9) << 8)
let height = sys.peek(infilePtr+10) | (sys.peek(infilePtr+11) << 8)
let hasAlpha = (sys.peek(infilePtr+12) != 0)
let ipfType = sys.peek(infilePtr+13)
let imgLen = sys.peek(infilePtr+24) | (sys.peek(infilePtr+25) << 8) | (sys.peek(infilePtr+26) << 16) | (sys.peek(infilePtr+27) << 24)
let ipfbuf = sys.malloc(imgLen)
@@ -1014,136 +1027,6 @@ _TVDOS.DRV.FS.NET.exists = (fd) => {
return (0 == com.getStatusCode(port[0]))
}
///////////////////////////////////////////////////////////////////////////////
// Legacy Serial filesystem, !!pending for removal!!
/*const filesystem = {};
filesystem._toPorts = (driveLetter) => {
if (driveLetter.toUpperCase === undefined) {
throw Error("'"+driveLetter+"' (type: "+typeof driveLetter+") is not a valid drive letter");
}
var port = _TVDOS.DRIVES[driveLetter.toUpperCase()];
if (port === undefined) {
throw Error("Drive letter '" + driveLetter.toUpperCase() + "' does not exist");
}
return port
};
filesystem._close = (portNo) => {
com.sendMessage(portNo, "CLOSE")
}
filesystem._flush = (portNo) => {
com.sendMessage(portNo, "FLUSH")
}
// @return disk status code (0 for successful operation)
// throws if:
// - java.lang.NullPointerException if path is null
// - Error if operation mode is not "R", "W" nor "A"
filesystem.open = (driveLetter, path, operationMode) => {
var port = filesystem._toPorts(driveLetter);
filesystem._flush(port[0]); filesystem._close(port[0]);
var mode = operationMode.toUpperCase();
if (mode != "R" && mode != "W" && mode != "A") {
throw Error("Unknown file opening mode: " + mode);
}
com.sendMessage(port[0], "OPEN"+mode+'"'+path+'",'+port[1]);
return com.getStatusCode(port[0]);
};
filesystem.getFileLen = (driveLetter) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "GETLEN");
var response = com.getStatusCode(port[0]);
if (135 == response) {
throw Error("File not opened");
}
if (response < 0 || response >= 128) {
throw Error("Reading a file failed with "+response);
}
return Number(com.pullMessage(port[0]));
};
// @return the entire contents of the file in String
filesystem.readAll = (driveLetter) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "READ");
var response = com.getStatusCode(port[0]);
if (135 == response) {
throw Error("File not opened");
}
if (response < 0 || response >= 128) {
throw Error("Reading a file failed with "+response);
}
return com.pullMessage(port[0]);
};
filesystem.readAllBytes = (driveLetter) => {
var str = filesystem.readAll(driveLetter);
var bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes;
};
filesystem.write = (driveLetter, string) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "WRITE"+string.length);
var response = com.getStatusCode(port[0]);
if (135 == response) {
throw Error("File not opened");
}
if (response < 0 || response >= 128) {
throw Error("Writing a file failed with "+response);
}
com.sendMessage(port[0], string);
filesystem._flush(port[0]); filesystem._close(port[0]);
};
filesystem.writeBytes = (driveLetter, bytes) => {
var string = btostr(bytes); // no spreading: has length limit
filesystem.write(driveLetter, string);
};
filesystem.isDirectory = (driveLetter) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "LISTFILES");
var response = com.getStatusCode(port[0]);
return (response === 0);
};
filesystem.mkDir = (driveLetter) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "MKDIR");
var response = com.getStatusCode(port[0]);
if (response < 0 || response >= 128) {
var status = com.getDeviceStatus(port[0]);
throw Error("Creating a directory failed with ("+response+"): "+status.message+"\n");
}
return (response === 0); // possible status codes: 0 (success), 1 (fail)
};
filesystem.touch = (driveLetter) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "TOUCH");
var response = com.getStatusCode(port[0]);
return (response === 0);
};
filesystem.mkFile = (driveLetter) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "MKFILE");
var response = com.getStatusCode(port[0]);
return (response === 0);
};
filesystem.remove = (driveLetter) => {
var port = filesystem._toPorts(driveLetter);
com.sendMessage(port[0], "DELETE");
var response = com.getStatusCode(port[0]);
return (response === 0);
};
Object.freeze(filesystem);*/
///////////////////////////////////////////////////////////////////////////////
const files = {}

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(' ')

View File

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

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)

View File

@@ -26,15 +26,18 @@ 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
@@ -43,14 +46,17 @@ const COL_HL_EXT = {
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}"`),

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
*/

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,259 @@
/*
* 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 (16 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 (2)
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
0,0,0,0,0,0,0, // 7 bytes 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

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

BIN
lib/polyglot-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.

BIN
lib/regex-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.

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