mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
Compare commits
28 Commits
libtav
...
bef85f6e2f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bef85f6e2f | ||
|
|
f02ad1de79 | ||
|
|
7f0ff3e653 | ||
|
|
8702104bfe | ||
|
|
7d899936e2 | ||
|
|
6aa2542bb8 | ||
|
|
2ac084acd7 | ||
|
|
1208690c4f | ||
|
|
ca977b074d | ||
|
|
102801d8b0 | ||
|
|
10351bafb1 | ||
|
|
9310885260 | ||
|
|
b10d5d3a34 | ||
|
|
86b44565e0 | ||
|
|
54b61fb436 | ||
|
|
4afe3816c7 | ||
|
|
f09dd66185 | ||
|
|
b590415231 | ||
|
|
237d3d6fd2 | ||
|
|
3421d71012 | ||
|
|
5d5576c077 | ||
|
|
64026be133 | ||
|
|
96d697e158 | ||
|
|
1680137b7d | ||
|
|
c71920b95d | ||
|
|
629ed5fb12 | ||
|
|
4f6efbe000 | ||
|
|
4362610c70 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -66,3 +66,5 @@ assets/disk0/home/basic/*
|
||||
assets/disk0/movtestimg/*.jpg
|
||||
assets/disk0/*.mov
|
||||
assets/diskMediabin/*
|
||||
|
||||
video_encoder/*
|
||||
|
||||
54
.idea/artifacts/TerranBASIC.xml
generated
54
.idea/artifacts/TerranBASIC.xml
generated
@@ -21,36 +21,54 @@
|
||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-reflect/1.8.21/kotlin-reflect-1.8.21.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-test/1.8.21/kotlin-test-1.8.21.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.21/kotlin-stdlib-common-1.8.21.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/graal-sdk-22.3.1.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-71.1.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-22.3.1-edit.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-22.3.1.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-22.3.1-edit.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-22.3.1.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/polyglot-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-language-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-runtime-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-compiler-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/compiler-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/compiler-management-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/word-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/collections-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/nativeimage-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jniutils-23.1.10.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-1.12.1.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-22.3.1-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-22.3.1-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-language-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-language-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-1.12.1-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-1.12.1-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-71.1-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-71.1-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/icu4j-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-openal-3.3.3.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-opengl-3.3.3.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-jemalloc-3.3.3.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-22.3.1-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-22.3.1-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/TerranVirtualDisk-src.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jorbis-0.0.17-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jorbis-0.0.17-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/graal-sdk-22.3.1-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/graal-sdk-22.3.1-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/polyglot-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/polyglot-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-runtime-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-runtime-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/collections-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/collections-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/nativeimage-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/nativeimage-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jniutils-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jniutils-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jlayer-1.0.1-gdx-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/jlayer-1.0.1-gdx-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3-javadoc.jar" path-in-jar="/" />
|
||||
@@ -62,15 +80,15 @@
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-openal-3.3.3-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-opengl-3.3.3-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-opengl-3.3.3-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-22.3.1-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/truffle-api-22.3.1-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/regex-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-3.3.3-natives-windows.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-jemalloc-3.3.3-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-jemalloc-3.3.3-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-natives-linux.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-stb-3.3.3-natives-macos.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-22.3.1-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-22.3.1-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-23.1.10-javadoc.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/js-scriptengine-23.1.10-sources.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3-natives-linux.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/lwjgl-glfw-3.3.3-natives-macos.jar" path-in-jar="/" />
|
||||
<element id="extracted-dir" path="$PROJECT_DIR$/lib/gdx-jnigen-loader-2.3.1-javadoc.jar" path-in-jar="/" />
|
||||
|
||||
133
.idea/cody_history.xml
generated
Normal file
133
.idea/cody_history.xml
generated
Normal file
@@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ChatHistory">
|
||||
<accountData>
|
||||
<list>
|
||||
<AccountData>
|
||||
<accountId value="VXNlcjo1NTUxMzY=" />
|
||||
<chats>
|
||||
<list>
|
||||
<chat>
|
||||
<internalId value="8a32c880-d8ec-4d84-b5f7-a62b2edf63c3" />
|
||||
<llm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-5-sonnet-20240620" />
|
||||
<provider value="Anthropic" />
|
||||
<title value="Claude 3.5 Sonnet" />
|
||||
<usage>
|
||||
<list>
|
||||
<option value="chat" />
|
||||
<option value="edit" />
|
||||
</list>
|
||||
</usage>
|
||||
</llm>
|
||||
</llm>
|
||||
</chat>
|
||||
<chat>
|
||||
<internalId value="f4e7338b-57d3-44e0-b1d9-04aa2dfb4bae" />
|
||||
<llm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-5-sonnet-20240620" />
|
||||
<provider value="Anthropic" />
|
||||
<title value="Claude 3.5 Sonnet" />
|
||||
<usage>
|
||||
<list>
|
||||
<option value="chat" />
|
||||
<option value="edit" />
|
||||
</list>
|
||||
</usage>
|
||||
</llm>
|
||||
</llm>
|
||||
</chat>
|
||||
<chat>
|
||||
<internalId value="26ac815b-5db9-488b-be0b-ab174fbc5646" />
|
||||
<llm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-5-sonnet-20240620" />
|
||||
<provider value="Anthropic" />
|
||||
<title value="Claude 3.5 Sonnet" />
|
||||
<usage>
|
||||
<list>
|
||||
<option value="chat" />
|
||||
<option value="edit" />
|
||||
</list>
|
||||
</usage>
|
||||
</llm>
|
||||
</llm>
|
||||
</chat>
|
||||
<chat>
|
||||
<internalId value="8ab39b26-cdda-4256-878f-e0416e66bbea" />
|
||||
<llm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-5-sonnet-20240620" />
|
||||
<provider value="Anthropic" />
|
||||
<title value="Claude 3.5 Sonnet" />
|
||||
<usage>
|
||||
<list>
|
||||
<option value="chat" />
|
||||
<option value="edit" />
|
||||
</list>
|
||||
</usage>
|
||||
</llm>
|
||||
</llm>
|
||||
</chat>
|
||||
<chat>
|
||||
<internalId value="f79a288a-adc5-4d45-a069-4bf024d90236" />
|
||||
<llm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-5-sonnet-20240620" />
|
||||
<provider value="Anthropic" />
|
||||
<title value="Claude 3.5 Sonnet" />
|
||||
<usage>
|
||||
<list>
|
||||
<option value="chat" />
|
||||
<option value="edit" />
|
||||
</list>
|
||||
</usage>
|
||||
</llm>
|
||||
</llm>
|
||||
</chat>
|
||||
<chat>
|
||||
<internalId value="20bc02fd-c6b5-4590-a00b-b7012a630ef4" />
|
||||
<llm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-5-sonnet-20240620" />
|
||||
<provider value="Anthropic" />
|
||||
<title value="Claude 3.5 Sonnet" />
|
||||
<usage>
|
||||
<list>
|
||||
<option value="chat" />
|
||||
<option value="edit" />
|
||||
</list>
|
||||
</usage>
|
||||
</llm>
|
||||
</llm>
|
||||
</chat>
|
||||
</list>
|
||||
</chats>
|
||||
<defaultLlm>
|
||||
<llm>
|
||||
<model value="anthropic/claude-3-5-sonnet-20240620" />
|
||||
<provider value="Anthropic" />
|
||||
<tags>
|
||||
<list>
|
||||
<option value="gateway" />
|
||||
<option value="accuracy" />
|
||||
<option value="recommended" />
|
||||
<option value="free" />
|
||||
</list>
|
||||
</tags>
|
||||
<title value="Claude 3.5 Sonnet" />
|
||||
<usage>
|
||||
<list>
|
||||
<option value="chat" />
|
||||
<option value="edit" />
|
||||
</list>
|
||||
</usage>
|
||||
</llm>
|
||||
</defaultLlm>
|
||||
</AccountData>
|
||||
</list>
|
||||
</accountData>
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
17
.idea/runConfigurations/AppLoader.xml
generated
Normal file
17
.idea/runConfigurations/AppLoader.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="AppLoader" type="Application" factoryName="Application" nameIsGenerated="true">
|
||||
<option name="ALTERNATIVE_JRE_PATH" value="21" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.torvald.tsvm.AppLoader" />
|
||||
<module name="tsvm_executable" />
|
||||
<option name="VM_PARAMETERS" value="-ea --upgrade-module-path=lib/compiler-23.1.10.jar:lib/compiler-management-23.1.10.jar:lib/truffle-compiler-23.1.10.jar:lib/truffle-api-23.1.10.jar:lib/truffle-runtime-23.1.10.jar:lib/polyglot-23.1.10.jar:lib/collections-23.1.10.jar:lib/word-23.1.10.jar:lib/nativeimage-23.1.10.jar:lib/jniutils-23.1.10.jar -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="net.torvald.tsvm.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
16
.idea/runConfigurations/TerranBASIC.xml
generated
Normal file
16
.idea/runConfigurations/TerranBASIC.xml
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="TerranBASIC" type="Application" factoryName="Application" nameIsGenerated="true">
|
||||
<option name="MAIN_CLASS_NAME" value="net.torvald.tsvm.TerranBASIC" />
|
||||
<module name="TerranBASICexecutable" />
|
||||
<option name="VM_PARAMETERS" value="--upgrade-module-path=lib/compiler-23.1.10.jar:lib/compiler-management-23.1.10.jar:lib/truffle-compiler-23.1.10.jar:lib/truffle-api-23.1.10.jar:lib/truffle-runtime-23.1.10.jar:lib/polyglot-23.1.10.jar:lib/collections-23.1.10.jar:lib/word-23.1.10.jar:lib/nativeimage-23.1.10.jar:lib/jniutils-23.1.10.jar -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="net.torvald.tsvm.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
16
.idea/runConfigurations/TsvmEmulator.xml
generated
Normal file
16
.idea/runConfigurations/TsvmEmulator.xml
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="TsvmEmulator" type="Application" factoryName="Application" nameIsGenerated="true">
|
||||
<option name="MAIN_CLASS_NAME" value="net.torvald.tsvm.TsvmEmulator" />
|
||||
<module name="tsvm_executable" />
|
||||
<option name="VM_PARAMETERS" value="-ea --upgrade-module-path=lib/compiler-23.1.10.jar:lib/compiler-management-23.1.10.jar:lib/truffle-compiler-23.1.10.jar:lib/truffle-api-23.1.10.jar:lib/truffle-runtime-23.1.10.jar:lib/polyglot-23.1.10.jar:lib/collections-23.1.10.jar:lib/word-23.1.10.jar:lib/nativeimage-23.1.10.jar:lib/jniutils-23.1.10.jar -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="net.torvald.tsvm.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@@ -2,5 +2,7 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/assets/disk0/home/tetrino" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/assets/disk0/home/tvnes" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -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
|
||||
|
||||
@@ -5,5 +5,6 @@ set PATH=\tvdos\installer;\tvdos\tuidev;$PATH
|
||||
set KEYBOARD=us_colemak
|
||||
|
||||
rem this line specifies which shell to be presented after the boot precess:
|
||||
tvdos/i18n/korean
|
||||
zfm
|
||||
command -fancy
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
if (exec_args[1] === undefined) {
|
||||
println("Usage: compile -le/-lo myfile.js")
|
||||
println(" The compiled and linked file will be myfile.out")
|
||||
println(" The compiled and linked file will be myfile.exc")
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ if (exec_args[2]) {
|
||||
_G.shell.execute(`rm ${tempFilename}.gz`)
|
||||
|
||||
_G.shell.execute(`link -${exec_args[1][2]} ${tempFilename}.bin`)
|
||||
_G.shell.execute(`mv ${tempFilename}.out ${filenameWithoutExt}.out`)
|
||||
_G.shell.execute(`mv ${tempFilename}.exc ${filenameWithoutExt}.exc`)
|
||||
_G.shell.execute(`rm ${tempFilename}.bin`)
|
||||
}
|
||||
// with no linking
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
if (exec_args[1] === undefined) {
|
||||
println("Usage: decompile myfile.bin")
|
||||
println("The compiled file will be myfile.bin.js")
|
||||
println("Usage: decompile myfile.exc")
|
||||
println("The compiled file will be myfile.exc.js")
|
||||
return 1
|
||||
}
|
||||
_G.shell.execute(`enc ${exec_args[1]} ${exec_args[1]}.gz`)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// a simple, symmetric obfuscator with infinite-length key
|
||||
|
||||
function seq(s) {
|
||||
let out = ""
|
||||
let cnt = 0
|
||||
|
||||
@@ -11,7 +11,6 @@ let infile = files.open(infilePath)
|
||||
|
||||
if (!infile.exists) throw Error("No such file: " + infilePath)
|
||||
|
||||
let outfile = files.open(infilePath.substringBeforeLast(".") + ".out")
|
||||
let outMode = exec_args[1].toLowerCase()
|
||||
|
||||
let type = {
|
||||
@@ -21,6 +20,13 @@ let type = {
|
||||
"-c": "\x04"
|
||||
}
|
||||
|
||||
let ext = {
|
||||
"-r": ".exc", // executable
|
||||
"-e": ".exc", // executable
|
||||
"-o": ".lib", // library
|
||||
"-c": ".cob" // core object
|
||||
}
|
||||
|
||||
function toI32(num) {
|
||||
const buffer = new ArrayBuffer(4)
|
||||
const view = new DataView(buffer)
|
||||
@@ -40,6 +46,7 @@ let addr = 0
|
||||
if (exec_args[3] !== undefined && exec_args[3].toLowerCase() == "-a" && exec_args[4] !== undefined)
|
||||
addr = parseInt(exec_args[4], 16)
|
||||
|
||||
let outfile = files.open(infilePath.substringBeforeLast(".") + ext[exec_args[3].toLowerCase()])
|
||||
outfile.sappend("\x20\xC0\xCC\x0A")
|
||||
outfile.sappend(type[outMode] || "\x00")
|
||||
outfile.bappend(toI24(addr))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
if (exec_args[1] === undefined) {
|
||||
println("Usage: load myfile.out")
|
||||
println("Usage: load myfile.exc")
|
||||
println(" This will load the binary image onto the Core Memory")
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ music.pread(samples, 65534)
|
||||
|
||||
audio.setPcmMode(0)
|
||||
audio.setMasterVolume(0, 255)
|
||||
audio.putPcmDataByPtr(samples, 65534, 0)
|
||||
audio.putPcmDataByPtr(0, samples, 65534, 0)
|
||||
audio.setLoopPoint(0, 65534)
|
||||
audio.play(0)*/
|
||||
|
||||
@@ -127,7 +127,7 @@ while (sampleSize > 0) {
|
||||
let readLength = (sampleSize < BLOCK_SIZE) ? sampleSize : BLOCK_SIZE
|
||||
readBytes(readLength, decodePtr)
|
||||
|
||||
audio.putPcmDataByPtr(decodePtr, readLength, 0)
|
||||
audio.putPcmDataByPtr(0, decodePtr, readLength, 0)
|
||||
audio.setSampleUploadLength(0, readLength)
|
||||
audio.startSampleUpload(0)
|
||||
|
||||
|
||||
17
assets/disk0/tracker_play.js
Normal file
17
assets/disk0/tracker_play.js
Normal 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)
|
||||
186
assets/disk0/tracker_test.js
Normal file
186
assets/disk0/tracker_test.js
Normal file
@@ -0,0 +1,186 @@
|
||||
// Tracker Mode — Bach's Prelude in C Major (BWV 846)
|
||||
// Run from the TVDOS shell: js tracker_test.js
|
||||
// Uploads ~92 patterns on startup (takes a moment).
|
||||
|
||||
// -- Note table (12-TET, 4096-TET encoding) ------------------------------------
|
||||
// C3 = 0x4000; each semitone = 4096/12 ≈ 341.33 steps; each octave = 4096 steps.
|
||||
// Sharp suffix: s (e.g. Cs3); flat aliases also provided (e.g. Db3 = Cs3).
|
||||
// Special values: Note.OFF = key-off, Note.CUT = note cut, Note.NOP = no-op.
|
||||
var Note = (function() {
|
||||
var SEMITONE = 4096 / 12;
|
||||
var C3 = 0x4000;
|
||||
function n(oct, semi) { return Math.round(C3 + (oct - 3) * 4096 + semi * SEMITONE) & 0xFFFF; }
|
||||
var t = {};
|
||||
var names = ['C','Cs','D','Ds','E','F','Fs','G','Gs','A','As','B'];
|
||||
var flats = ['C','Db','D','Eb','E','F','Gb','G','Ab','A','Bb','B'];
|
||||
for (var oct = 0; oct <= 9; oct++) {
|
||||
for (var s = 0; s < 12; s++) {
|
||||
t[names[s] + oct] = n(oct, s);
|
||||
if (flats[s] !== names[s]) t[flats[s] + oct] = n(oct, s);
|
||||
}
|
||||
}
|
||||
t.OFF = 0x0000; // key-off
|
||||
t.CUT = 0xFFFE; // note cut (immediate)
|
||||
t.NOP = 0xFFFF; // no-op (empty row)
|
||||
return t;
|
||||
}());
|
||||
|
||||
var PLAYHEAD = 0;
|
||||
|
||||
// -- 1. Sample: triangle wave (256 samples @ C3) --------------------------------
|
||||
var SAMPLE_LEN = 256;
|
||||
var sampleBytes = new Array(SAMPLE_LEN);
|
||||
for (var i = 0; i < SAMPLE_LEN; i++) {
|
||||
var phase = (i / SAMPLE_LEN) * 2.0;
|
||||
var val_ = phase < 1.0 ? phase : 2.0 - phase;
|
||||
sampleBytes[i] = Math.round(val_ * 254) & 0xFF;
|
||||
}
|
||||
var memBase = audio.getMemAddr();
|
||||
for (var i = 0; i < SAMPLE_LEN; i++) {
|
||||
sys.poke(memBase - i, sampleBytes[i]);
|
||||
}
|
||||
|
||||
// -- 2. Instrument 0 -----------------------------------------------------------
|
||||
var instBytes = new Array(64).fill(0);
|
||||
instBytes[2] = 0; instBytes[3] = 1; // sampleLength = 256
|
||||
instBytes[4] = 0x00; instBytes[5] = 0x7D; // samplingRate = 32000
|
||||
instBytes[10] = 0x00; instBytes[11] = 0x01; // sampleLoopEnd = 256 (whole sample)
|
||||
instBytes[12] = 1; // loopMode = 1 (forward)
|
||||
instBytes[16] = 255; instBytes[17] = 0; // envelope: vol=255, hold
|
||||
audio.uploadInstrument(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 + ")");
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -35,7 +35,7 @@ function print_prompt_text() {
|
||||
print(" "+CURRENT_DRIVE+":")
|
||||
con.color_pair(161,253)
|
||||
con.addch(16);con.curs_right()
|
||||
con.color_pair(0,253)
|
||||
con.color_pair(240,253)
|
||||
print(" \\"+shell_pwd.join("\\").substring(1)+" ")
|
||||
if (errorlevel != 0 && errorlevel != "undefined" && errorlevel != undefined) {
|
||||
con.color_pair(166,253)
|
||||
@@ -61,7 +61,7 @@ function greet() {
|
||||
con.clear()
|
||||
con.color_pair(253,255)
|
||||
print(' ');con.addch(17);con.curs_right()
|
||||
con.color_pair(0,253)
|
||||
con.color_pair(240,253)
|
||||
print(" ".repeat(greetLeftPad)+welcome_text+" ".repeat(greetRightPad))
|
||||
con.color_pair(253,255)
|
||||
con.addch(16);con.curs_right();print(' ')
|
||||
|
||||
5
assets/disk0/tvdos/bin/hopper.js
Normal file
5
assets/disk0/tvdos/bin/hopper.js
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Hopper is a package manager for TSVM
|
||||
* Created by CuriousTorvald on 2026-04-16
|
||||
*/
|
||||
|
||||
@@ -326,7 +326,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_LENGTH) {
|
||||
// RAW PCM packets (decode on the fly)
|
||||
else if (packetType == 0x1000 || packetType == 0x1001) {
|
||||
let frame = seqread.readBytes(readLength)
|
||||
audio.putPcmDataByPtr(frame, readLength, 0)
|
||||
audio.putPcmDataByPtr(0, frame, readLength, 0)
|
||||
audio.setSampleUploadLength(0, readLength)
|
||||
audio.startSampleUpload(0)
|
||||
sys.free(frame)
|
||||
|
||||
@@ -162,7 +162,7 @@ while (!stopPlay && seqread.getReadCount() < FILE_SIZE && readLength > 0) {
|
||||
|
||||
seqread.readBytes(readLength, readPtr)
|
||||
|
||||
audio.putPcmDataByPtr(readPtr, readLength, 0)
|
||||
audio.putPcmDataByPtr(0, readPtr, readLength, 0)
|
||||
audio.setSampleUploadLength(0, readLength)
|
||||
audio.startSampleUpload(0)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ const MAXMEM = sys.maxmem()
|
||||
const WIDTH = 560
|
||||
const HEIGHT = 448
|
||||
const TAV_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x41, 0x56] // "\x1FTSVM TAV"
|
||||
const TAP_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x41, 0x50] // "\x1FTSVM TAP"
|
||||
const UCF_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x55, 0x43, 0x46] // "\x1FTSVM UCF"
|
||||
const TAV_VERSION = 1 // Initial DWT version
|
||||
const UCF_VERSION = 1
|
||||
@@ -17,6 +18,7 @@ const ADDRESSING_INTERNAL = 0x02
|
||||
const SND_BASE_ADDR = audio.getBaseAddr()
|
||||
const SND_MEM_ADDR = audio.getMemAddr()
|
||||
const pcm = require("pcm")
|
||||
const AUDIO_DEVICE = 0
|
||||
const MP2_FRAME_SIZE = [144,216,252,288,360,432,504,576,720,864,1008,1152,1440,1728]
|
||||
const TAV_TEMPORAL_LEVELS = 2
|
||||
|
||||
@@ -151,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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}"`),
|
||||
|
||||
305
assets/disk0/tvdos/include/getopt.mjs
Normal file
305
assets/disk0/tvdos/include/getopt.mjs
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* getopt.js: node.js implementation of POSIX getopt() (and then some)
|
||||
*
|
||||
* Copyright 2011 David Pacheco. All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
var ASSERT = require('assert').ok;
|
||||
|
||||
function goError(msg)
|
||||
{
|
||||
return (new Error('getopt: ' + msg));
|
||||
}
|
||||
|
||||
/*
|
||||
* The BasicParser is our primary interface to the outside world. The
|
||||
* documentation for this object and its public methods is contained in
|
||||
* the included README.md.
|
||||
*/
|
||||
function goBasicParser(optstring, argv, optind)
|
||||
{
|
||||
var ii;
|
||||
|
||||
ASSERT(optstring || optstring === '', 'optstring is required');
|
||||
ASSERT(optstring.constructor === String, 'optstring must be a string');
|
||||
ASSERT(argv, 'argv is required');
|
||||
ASSERT(argv.constructor === Array, 'argv must be an array');
|
||||
|
||||
this.gop_argv = new Array(argv.length);
|
||||
this.gop_options = {};
|
||||
this.gop_aliases = {};
|
||||
this.gop_optind = optind !== undefined ? optind : 2;
|
||||
this.gop_subind = 0;
|
||||
|
||||
for (ii = 0; ii < argv.length; ii++) {
|
||||
ASSERT(argv[ii].constructor === String,
|
||||
'argv must be string array');
|
||||
this.gop_argv[ii] = argv[ii];
|
||||
}
|
||||
|
||||
this.parseOptstr(optstring);
|
||||
}
|
||||
|
||||
exports.BasicParser = goBasicParser;
|
||||
|
||||
/*
|
||||
* Parse the option string and update the following fields:
|
||||
*
|
||||
* gop_silent Whether to log errors to stderr. Silent mode is
|
||||
* indicated by a leading ':' in the option string.
|
||||
*
|
||||
* gop_options Maps valid single-letter-options to booleans indicating
|
||||
* whether each option is required.
|
||||
*
|
||||
* gop_aliases Maps valid long options to the corresponding
|
||||
* single-letter short option.
|
||||
*/
|
||||
goBasicParser.prototype.parseOptstr = function (optstr)
|
||||
{
|
||||
var chr, cp, alias, arg, ii;
|
||||
|
||||
ii = 0;
|
||||
if (optstr.length > 0 && optstr[0] == ':') {
|
||||
this.gop_silent = true;
|
||||
ii++;
|
||||
} else {
|
||||
this.gop_silent = false;
|
||||
}
|
||||
|
||||
while (ii < optstr.length) {
|
||||
chr = optstr[ii];
|
||||
arg = false;
|
||||
|
||||
if (!/^[\w\d\u1000-\u1100]$/.test(chr))
|
||||
throw (goError('invalid optstring: only alphanumeric ' +
|
||||
'characters and unicode characters between ' +
|
||||
'\\u1000-\\u1100 may be used as options: ' + chr));
|
||||
|
||||
if (ii + 1 < optstr.length && optstr[ii + 1] == ':') {
|
||||
arg = true;
|
||||
ii++;
|
||||
}
|
||||
|
||||
this.gop_options[chr] = arg;
|
||||
|
||||
while (ii + 1 < optstr.length && optstr[ii + 1] == '(') {
|
||||
ii++;
|
||||
cp = optstr.indexOf(')', ii + 1);
|
||||
if (cp == -1)
|
||||
throw (goError('invalid optstring: missing ' +
|
||||
'")" to match "(" at char ' + ii));
|
||||
|
||||
alias = optstr.substring(ii + 1, cp);
|
||||
this.gop_aliases[alias] = chr;
|
||||
ii = cp;
|
||||
}
|
||||
|
||||
ii++;
|
||||
}
|
||||
};
|
||||
|
||||
goBasicParser.prototype.optind = function ()
|
||||
{
|
||||
return (this.gop_optind);
|
||||
};
|
||||
|
||||
/*
|
||||
* For documentation on what getopt() does, see README.md. The following
|
||||
* implementation invariants are maintained by getopt() and its helper methods:
|
||||
*
|
||||
* this.gop_optind Refers to the element of gop_argv that contains
|
||||
* the next argument to be processed. This may
|
||||
* exceed gop_argv, in which case the end of input
|
||||
* has been reached.
|
||||
*
|
||||
* this.gop_subind Refers to the character inside
|
||||
* this.gop_options[this.gop_optind] which begins
|
||||
* the next option to be processed. This may never
|
||||
* exceed the length of gop_argv[gop_optind], so
|
||||
* when incrementing this value we must always
|
||||
* check if we should instead increment optind and
|
||||
* reset subind to 0.
|
||||
*
|
||||
* That is, when any of these functions is entered, the above indices' values
|
||||
* are as described above. getopt() itself and getoptArgument() may both be
|
||||
* called at the end of the input, so they check whether optind exceeds
|
||||
* argv.length. getoptShort() and getoptLong() are called only when the indices
|
||||
* already point to a valid short or long option, respectively.
|
||||
*
|
||||
* getopt() processes the next option as follows:
|
||||
*
|
||||
* o If gop_optind > gop_argv.length, then we already parsed all arguments.
|
||||
*
|
||||
* o If gop_subind == 0, then we're looking at the start of an argument:
|
||||
*
|
||||
* o Check for special cases like '-', '--', and non-option arguments.
|
||||
* If present, update the indices and return the appropriate value.
|
||||
*
|
||||
* o Check for a long-form option (beginning with '--'). If present,
|
||||
* delegate to getoptLong() and return the result.
|
||||
*
|
||||
* o Otherwise, advance subind past the argument's leading '-' and
|
||||
* continue as though gop_subind != 0 (since that's now the case).
|
||||
*
|
||||
* o Delegate to getoptShort() and return the result.
|
||||
*/
|
||||
goBasicParser.prototype.getopt = function ()
|
||||
{
|
||||
if (this.gop_optind >= this.gop_argv.length)
|
||||
/* end of input */
|
||||
return (undefined);
|
||||
|
||||
var arg = this.gop_argv[this.gop_optind];
|
||||
|
||||
if (this.gop_subind === 0) {
|
||||
if (arg == '-' || arg === '' || arg[0] != '-')
|
||||
return (undefined);
|
||||
|
||||
if (arg == '--') {
|
||||
this.gop_optind++;
|
||||
this.gop_subind = 0;
|
||||
return (undefined);
|
||||
}
|
||||
|
||||
if (arg[1] == '-')
|
||||
return (this.getoptLong());
|
||||
|
||||
this.gop_subind++;
|
||||
ASSERT(this.gop_subind < arg.length);
|
||||
}
|
||||
|
||||
return (this.getoptShort());
|
||||
};
|
||||
|
||||
/*
|
||||
* Implements getopt() for the case where optind/subind point to a short option.
|
||||
*/
|
||||
goBasicParser.prototype.getoptShort = function ()
|
||||
{
|
||||
var arg, chr;
|
||||
|
||||
ASSERT(this.gop_optind < this.gop_argv.length);
|
||||
arg = this.gop_argv[this.gop_optind];
|
||||
ASSERT(this.gop_subind < arg.length);
|
||||
chr = arg[this.gop_subind];
|
||||
|
||||
if (++this.gop_subind >= arg.length) {
|
||||
this.gop_optind++;
|
||||
this.gop_subind = 0;
|
||||
}
|
||||
|
||||
if (!(chr in this.gop_options))
|
||||
return (this.errInvalidOption(chr));
|
||||
|
||||
if (!this.gop_options[chr])
|
||||
return ({ option: chr });
|
||||
|
||||
return (this.getoptArgument(chr));
|
||||
};
|
||||
|
||||
/*
|
||||
* Implements getopt() for the case where optind/subind point to a long option.
|
||||
*/
|
||||
goBasicParser.prototype.getoptLong = function ()
|
||||
{
|
||||
var arg, alias, chr, eq;
|
||||
|
||||
ASSERT(this.gop_subind === 0);
|
||||
ASSERT(this.gop_optind < this.gop_argv.length);
|
||||
arg = this.gop_argv[this.gop_optind];
|
||||
ASSERT(arg.length > 2 && arg[0] == '-' && arg[1] == '-');
|
||||
|
||||
eq = arg.indexOf('=');
|
||||
alias = arg.substring(2, eq == -1 ? arg.length : eq);
|
||||
if (!(alias in this.gop_aliases))
|
||||
return (this.errInvalidOption(alias));
|
||||
|
||||
chr = this.gop_aliases[alias];
|
||||
ASSERT(chr in this.gop_options);
|
||||
|
||||
if (!this.gop_options[chr]) {
|
||||
if (eq != -1)
|
||||
return (this.errExtraArg(alias));
|
||||
|
||||
this.gop_optind++; /* eat this argument */
|
||||
return ({ option: chr });
|
||||
}
|
||||
|
||||
/*
|
||||
* Advance optind/subind for the argument value and retrieve it.
|
||||
*/
|
||||
if (eq == -1)
|
||||
this.gop_optind++;
|
||||
else
|
||||
this.gop_subind = eq + 1;
|
||||
|
||||
return (this.getoptArgument(chr));
|
||||
};
|
||||
|
||||
/*
|
||||
* For the given option letter 'chr' that takes an argument, assumes that
|
||||
* optind/subind point to the argument (or denote the end of input) and return
|
||||
* the appropriate getopt() return value for this option and argument (or return
|
||||
* the appropriate error).
|
||||
*/
|
||||
goBasicParser.prototype.getoptArgument = function (chr)
|
||||
{
|
||||
var arg;
|
||||
|
||||
if (this.gop_optind >= this.gop_argv.length)
|
||||
return (this.errMissingArg(chr));
|
||||
|
||||
arg = this.gop_argv[this.gop_optind].substring(this.gop_subind);
|
||||
this.gop_optind++;
|
||||
this.gop_subind = 0;
|
||||
return ({ option: chr, optarg: arg });
|
||||
};
|
||||
|
||||
goBasicParser.prototype.errMissingArg = function (chr)
|
||||
{
|
||||
if (this.gop_silent)
|
||||
return ({ option: ':', optopt: chr });
|
||||
|
||||
process.stderr.write('option requires an argument -- ' + chr + '\n');
|
||||
return ({ option: '?', optopt: chr, error: true });
|
||||
};
|
||||
|
||||
goBasicParser.prototype.errInvalidOption = function (chr)
|
||||
{
|
||||
if (!this.gop_silent)
|
||||
process.stderr.write('illegal option -- ' + chr + '\n');
|
||||
|
||||
return ({ option: '?', optopt: chr, error: true });
|
||||
};
|
||||
|
||||
/*
|
||||
* This error is not specified by POSIX, but neither is the notion of specifying
|
||||
* long option arguments using "=" in the same argv-argument, but it's common
|
||||
* practice and pretty convenient.
|
||||
*/
|
||||
goBasicParser.prototype.errExtraArg = function (chr)
|
||||
{
|
||||
if (!this.gop_silent)
|
||||
process.stderr.write('option expects no argument -- ' +
|
||||
chr + '\n');
|
||||
|
||||
return ({ option: '?', optopt: chr, error: true });
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
TVDOS Graphics Library
|
||||
|
||||
Has no affiliation with OpenGL by Khronos Group
|
||||
/**
|
||||
* LibGL — TVDOS Graphics Library
|
||||
* Has no affiliation with OpenGL by Khronos Group
|
||||
* @author CuriousTorvald
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* LibPCM — PCM decoder for TSVM
|
||||
* @author CuriousTorvald
|
||||
*/
|
||||
|
||||
const HW_SAMPLING_RATE = 32000
|
||||
function printdbg(s) { if (0) serial.println(s) }
|
||||
function printvis(s) { if (0) println(s) }
|
||||
@@ -29,7 +34,7 @@ function s16Tou8(i) {
|
||||
}
|
||||
function u16Tos16(i) { return (i > 32767) ? i - 65536 : i }
|
||||
function randomRound(k) {
|
||||
let rnd = (Math.random() + Math.random()) / 2.0 // this produces triangular distribution
|
||||
let rnd = Math.random() // note to self: no triangular here
|
||||
return (rnd < (k - (k|0))) ? Math.ceil(k) : Math.floor(k)
|
||||
}
|
||||
function lerp(start, end, x) {
|
||||
|
||||
414
assets/disk0/tvdos/include/psg.mjs
Normal file
414
assets/disk0/tvdos/include/psg.mjs
Normal file
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
* LibPSG — PSG emulator and mixer for TSVM
|
||||
* Software-mixes various PSG channels and sends them to sound device as PCM
|
||||
* @author CuriousTorvald
|
||||
*/
|
||||
|
||||
const HW_SAMPLING_RATE = 32000
|
||||
|
||||
function clamp(val, low, hi) { return (val < low) ? low : (val > hi) ? hi : val }
|
||||
function clampS16(i) { return clamp(i, -32768, 32767) }
|
||||
const uNybToSnyb = [0,1,2,3,4,5,6,7,-8,-7,-6,-5,-4,-3,-2,-1]
|
||||
// returns: [unsigned high, unsigned low, signed high, signed low]
|
||||
function getNybbles(b) { return [b >> 4, b & 15, uNybToSnyb[b >> 4], uNybToSnyb[b & 15]] }
|
||||
function s8Tou8(i) { return i + 128 }
|
||||
function s16Tou8(i) {
|
||||
// return s8Tou8((i >> 8) & 255)
|
||||
// apply dithering
|
||||
let ufval = (i / 65536.0) + 0.5
|
||||
let ival = randomRound(ufval * 255.0)
|
||||
return ival|0
|
||||
}
|
||||
function u16Tos16(i) { return (i > 32767) ? i - 65536 : i }
|
||||
function randomRound(k) {
|
||||
let rnd = Math.random() // note to self: no triangular here
|
||||
return (rnd < (k - (k|0))) ? Math.ceil(k) : Math.floor(k)
|
||||
}
|
||||
function lerp(start, end, x) {
|
||||
return (1 - x) * start + x * end
|
||||
}
|
||||
function lerpAndRound(start, end, x) {
|
||||
return Math.round(lerp(start, end, x))
|
||||
}
|
||||
|
||||
|
||||
|
||||
// output format: immediately uploadable into TSVM audio adapter
|
||||
|
||||
// ── Internal helpers ────────────────────────────────────────────────────────
|
||||
|
||||
function secToSamples(sec) { return Math.round(HW_SAMPLING_RATE * sec) }
|
||||
|
||||
function isNative(buf) { return buf.native }
|
||||
|
||||
function readU8(buf, ch, i) {
|
||||
return isNative(buf) ? (sys.peek(buf[ch] + i) & 255) : buf[ch][i]
|
||||
}
|
||||
function writeU8(buf, ch, i, v) {
|
||||
if (isNative(buf)) sys.poke(buf[ch] + i, v)
|
||||
else buf[ch][i] = v
|
||||
}
|
||||
|
||||
// ── Buffer management ───────────────────────────────────────────────────────
|
||||
|
||||
function makeBuffer(length) {
|
||||
// returns [Uint8Array, Uint8Array] (stereo) that will be used to collect samples made by LibPSG.
|
||||
// Length: seconds. Number of elements: round(HW_SAMPLING_RATE * length)
|
||||
const n = secToSamples(length)
|
||||
const L = new Uint8Array(n)
|
||||
const R = new Uint8Array(n)
|
||||
L.fill(128)
|
||||
R.fill(128)
|
||||
return { 0: L, 1: R, samples: n, native: false }
|
||||
}
|
||||
|
||||
function makeBufferNative(length) {
|
||||
// returns native buffer object (stereo) that will be used to collect samples made by LibPSG.
|
||||
// Length: seconds. Number of elements: round(HW_SAMPLING_RATE * length)
|
||||
// Free with freeBufferNative() when done.
|
||||
const n = secToSamples(length)
|
||||
const L = sys.malloc(n); sys.memset(L, 128, n)
|
||||
const R = sys.malloc(n); sys.memset(R, 128, n)
|
||||
return { 0: L, 1: R, samples: n, native: true }
|
||||
}
|
||||
|
||||
function freeBufferNative(buf) {
|
||||
sys.free(buf[0])
|
||||
sys.free(buf[1])
|
||||
}
|
||||
|
||||
function clearBuffer(buf, offsetSec, lengthSec) {
|
||||
// Re-silence a buffer region (fill with 128) for re-use across frames.
|
||||
const start = (offsetSec != null) ? secToSamples(offsetSec) : 0
|
||||
const total = (lengthSec != null) ? secToSamples(lengthSec) : (buf.samples - start)
|
||||
if (!buf.native) {
|
||||
buf[0].fill(128, start, start + total)
|
||||
buf[1].fill(128, start, start + total)
|
||||
} else {
|
||||
sys.memset(buf[0] + start, 128, total)
|
||||
sys.memset(buf[1] + start, 128, total)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Shared mix core ─────────────────────────────────────────────────────────
|
||||
|
||||
// sampleFn(i) must return a float in [-1, 1].
|
||||
// Mixing maths: decode u8 → s16, apply op, clamp, dither back to u8.
|
||||
function mixInto(buf, lengthSec, offsetSec, op, amp, pan, sampleFn) {
|
||||
const startIdx = secToSamples(offsetSec)
|
||||
const n = secToSamples(lengthSec)
|
||||
// Linear pan law: centre (pan=0) → both channels at full amp
|
||||
const gainL = Math.max(0, Math.min(1, 1.0 - pan))
|
||||
const gainR = Math.max(0, Math.min(1, 1.0 + pan))
|
||||
const opCode = (op === 'sub') ? 1 : (op === 'mul') ? 2 : 0 // default: add
|
||||
for (let i = 0; i < n; i++) {
|
||||
const v = sampleFn(i) // oscillator value in [-1, 1]
|
||||
const oscBase = v * amp * 32767
|
||||
const oscL = Math.round(oscBase * gainL) | 0
|
||||
const oscR = Math.round(oscBase * gainR) | 0
|
||||
for (let ch = 0; ch < 2; ch++) {
|
||||
const osc = (ch === 0) ? oscL : oscR
|
||||
const cur = (readU8(buf, ch, startIdx + i) - 128) << 8
|
||||
let out
|
||||
switch (opCode) {
|
||||
case 0: out = cur + osc; break
|
||||
case 1: out = cur - osc; break
|
||||
case 2: out = (cur * osc) >> 15; break
|
||||
}
|
||||
writeU8(buf, ch, startIdx + i, s16Tou8(clampS16(out)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Waveform generators ─────────────────────────────────────────────────────
|
||||
|
||||
function makeSquare(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) {
|
||||
// buffer: [Uint8Array, Uint8Array] or native buffer
|
||||
// length: in seconds
|
||||
// offset: in seconds
|
||||
// duty: 0.0 to 1.0. default 0.5 (fraction of period where output is +1)
|
||||
// freq: Hz
|
||||
// op: add / mul / sub; default: add
|
||||
// amp: 0.0 to 1.0; default: 0.5
|
||||
// pan: -1.0 to 1.0; default: 0.0
|
||||
// phaseOffset: optional absolute-time base (seconds) added to phase calc only,
|
||||
// not to the buffer write position — use to ensure phase continuity
|
||||
// across successive calls (e.g. frame boundaries).
|
||||
if (duty == null) duty = 0.5
|
||||
if (op == null) op = 'add'
|
||||
if (amp == null) amp = 0.5
|
||||
if (pan == null) pan = 0.0
|
||||
const tBase = (phaseOffset || 0) + offset
|
||||
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
||||
const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1
|
||||
return (phase < duty) ? 1.0 : -1.0
|
||||
})
|
||||
}
|
||||
|
||||
function makeTriangle(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) {
|
||||
// buffer: [Uint8Array, Uint8Array] or native buffer
|
||||
// length: in seconds
|
||||
// offset: in seconds
|
||||
// duty: skew. -1.0 = falling sawtooth, 0.0 = symmetric triangle, 1.0 = rising sawtooth
|
||||
// freq: Hz
|
||||
// op: add / mul / sub; default: add
|
||||
// amp: 0.0 to 1.0; default: 0.5
|
||||
// pan: -1.0 to 1.0; default: 0.0
|
||||
// phaseOffset: optional absolute-time base (seconds) added to phase calc only —
|
||||
// see makeSquare for details.
|
||||
if (duty == null) duty = 0.0
|
||||
if (op == null) op = 'add'
|
||||
if (amp == null) amp = 0.5
|
||||
if (pan == null) pan = 0.0
|
||||
// riseFrac: fraction of period spent rising from -1 to +1
|
||||
// 0.0 → falling saw, 0.5 → symmetric triangle, 1.0 → rising saw
|
||||
const riseFrac = (duty + 1.0) * 0.5
|
||||
const tBase = (phaseOffset || 0) + offset
|
||||
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
||||
const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1
|
||||
if (riseFrac <= 0) {
|
||||
return 1.0 - 2.0 * phase // falling saw
|
||||
} else if (riseFrac >= 1) {
|
||||
return -1.0 + 2.0 * phase // rising saw
|
||||
} else if (phase < riseFrac) {
|
||||
return -1.0 + 2.0 * (phase / riseFrac) // rising slope
|
||||
} else {
|
||||
return 1.0 - 2.0 * ((phase - riseFrac) / (1.0 - riseFrac)) // falling slope
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function makeAliasedTriangle(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) {
|
||||
// buffer: [Uint8Array, Uint8Array] or native buffer
|
||||
// Famicom-style triangle — output is quantised to 16 DAC levels (4-bit, NES APU style).
|
||||
// The staircase quantisation introduces harmonics that mimic NES character.
|
||||
// length: in seconds
|
||||
// offset: in seconds
|
||||
// duty: skew. -1.0 = falling sawtooth, 0.0 = symmetric triangle, 1.0 = rising sawtooth
|
||||
// freq: Hz
|
||||
// op: add / mul / sub; default: add
|
||||
// amp: 0.0 to 1.0; default: 0.5
|
||||
// pan: -1.0 to 1.0; default: 0.0
|
||||
// phaseOffset: optional absolute-time base (seconds) added to phase calc only —
|
||||
// see makeSquare for details.
|
||||
if (duty == null) duty = 0.0
|
||||
if (op == null) op = 'add'
|
||||
if (amp == null) amp = 0.5
|
||||
if (pan == null) pan = 0.0
|
||||
const riseFrac = (duty + 1.0) * 0.5
|
||||
const tBase = (phaseOffset || 0) + offset
|
||||
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
||||
const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1
|
||||
let v
|
||||
if (riseFrac <= 0) {
|
||||
v = 1.0 - 2.0 * phase
|
||||
} else if (riseFrac >= 1) {
|
||||
v = -1.0 + 2.0 * phase
|
||||
} else if (phase < riseFrac) {
|
||||
v = -1.0 + 2.0 * (phase / riseFrac)
|
||||
} else {
|
||||
v = 1.0 - 2.0 * ((phase - riseFrac) / (1.0 - riseFrac))
|
||||
}
|
||||
// Quantise to 16 levels (NES triangle 4-bit DAC: 0..15 → -1..+1)
|
||||
const level = Math.max(0, Math.min(15, Math.round((v + 1.0) * 7.5)))
|
||||
return level / 7.5 - 1.0
|
||||
})
|
||||
}
|
||||
|
||||
// ── LFSR helpers (for noise types 1 and 2) ─────────────────────────────────
|
||||
|
||||
function lfsrStep(state, mode) {
|
||||
// mode 0 (long/NES mode 0): feedback tap at bit 1; period 32767
|
||||
// mode 1 (short/NES mode 1): feedback tap at bit 6; period 93 (metallic/tonal)
|
||||
const bit0 = state & 1
|
||||
const bitTap = (mode === 0) ? (state >> 1) & 1 : (state >> 6) & 1
|
||||
const feed = bit0 ^ bitTap
|
||||
return ((feed << 14) | (state >> 1)) & 0x7FFF
|
||||
}
|
||||
|
||||
function lfsrAdvance(state, steps, mode) {
|
||||
for (let k = 0; k < steps; k++) state = lfsrStep(state, mode)
|
||||
return state
|
||||
}
|
||||
|
||||
// NES APU documented LFSR periods
|
||||
const LFSR_PERIOD_LONG = 32767 // mode 0
|
||||
const LFSR_PERIOD_SHORT = 93 // mode 1
|
||||
|
||||
function makeNoise(buf, length, offset, freq, type, op, amp, pan, phaseOffset) {
|
||||
// buffer: [Uint8Array, Uint8Array] or native buffer
|
||||
// length: in seconds
|
||||
// offset: in seconds
|
||||
// type:
|
||||
// -1: 8-bit white noise (random float per period, sample-and-hold)
|
||||
// 0: 1-bit white noise (random ±1 per period, sample-and-hold)
|
||||
// 1: 1-bit LFSR long mode — NES mode 0, tap=bit0^bit1, period 32767 (full-spectrum)
|
||||
// 2: 1-bit LFSR short mode — NES mode 1, tap=bit0^bit6, period 93 (metallic/tonal)
|
||||
// freq: Hz (clock rate of the noise generator)
|
||||
// op: add / mul / sub; default: add
|
||||
// amp: 0.0 to 1.0; default: 0.5
|
||||
// pan: -1.0 to 1.0; default: 0.0
|
||||
// phaseOffset: optional absolute-time base (seconds) added to phase/LFSR calc only —
|
||||
// see makeSquare for details.
|
||||
//
|
||||
// LFSR types (1 and 2) are deterministic given (phaseOffset+offset, freq): calling
|
||||
// with monotonically advancing phaseOffset+offset produces a seamless noise stream
|
||||
// across frames. White noise types (-1, 0) are random per call.
|
||||
if (op == null) op = 'add'
|
||||
if (amp == null) amp = 0.5
|
||||
if (pan == null) pan = 0.0
|
||||
const tBase = (phaseOffset || 0) + offset
|
||||
|
||||
if (type === -1) {
|
||||
// 8-bit white: new random float in [-1, 1] each clock period
|
||||
let prevClock = -1
|
||||
let noiseVal = 0.0
|
||||
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
||||
const currentClock = Math.floor((tBase + i / HW_SAMPLING_RATE) * freq) | 0
|
||||
if (currentClock !== prevClock) {
|
||||
prevClock = currentClock
|
||||
noiseVal = Math.random() * 2.0 - 1.0
|
||||
}
|
||||
return noiseVal
|
||||
})
|
||||
} else if (type === 0) {
|
||||
// 1-bit white: random ±1 each clock period
|
||||
let prevClock = -1
|
||||
let noiseVal = 1.0
|
||||
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
||||
const currentClock = Math.floor((tBase + i / HW_SAMPLING_RATE) * freq) | 0
|
||||
if (currentClock !== prevClock) {
|
||||
prevClock = currentClock
|
||||
noiseVal = (Math.random() >= 0.5) ? 1.0 : -1.0
|
||||
}
|
||||
return noiseVal
|
||||
})
|
||||
} else {
|
||||
// LFSR-based noise (types 1 and 2)
|
||||
const mode = (type === 2) ? 1 : 0
|
||||
const period = (mode === 0) ? LFSR_PERIOD_LONG : LFSR_PERIOD_SHORT
|
||||
// Advance to deterministic position for this tBase so consecutive frame
|
||||
// calls with monotonically advancing phaseOffset produce a seamless noise stream.
|
||||
const startClock = Math.floor(tBase * freq) | 0
|
||||
let lfsr = lfsrAdvance(1, startClock % period, mode)
|
||||
let prevClock = startClock
|
||||
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
||||
const currentClock = Math.floor((tBase + i / HW_SAMPLING_RATE) * freq) | 0
|
||||
const delta = currentClock - prevClock
|
||||
if (delta > 0) {
|
||||
const steps = delta % period
|
||||
if (steps > 0) lfsr = lfsrAdvance(lfsr, steps, mode)
|
||||
prevClock = currentClock
|
||||
}
|
||||
return (lfsr & 1) ? 1.0 : -1.0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function makeAliasedTriangleNES(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) {
|
||||
// NES APU triangle — quantised to the authentic 32-step, 4-bit (0..15) staircase.
|
||||
// The 32-step sequence is: 15,14,...,1,0, 0,1,...,14,15 (descending then ascending).
|
||||
// This mirrors the real NES triangle DAC which has 32 equal-height steps per period.
|
||||
// duty parameter is accepted for API symmetry but ignored (NES triangle is always symmetric).
|
||||
// phaseOffset: optional absolute-time base (seconds) — see makeSquare for details.
|
||||
if (op == null) op = 'add'
|
||||
if (amp == null) amp = 0.5
|
||||
if (pan == null) pan = 0.0
|
||||
const tBase = (phaseOffset || 0) + offset
|
||||
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
||||
const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1
|
||||
const step32 = Math.floor(phase * 32) | 0 // 0..31
|
||||
// step 0..15: descend from 15 to 0; step 16..31: ascend from 0 to 15
|
||||
const level = (step32 < 16) ? (15 - step32) : (step32 - 16)
|
||||
return level / 7.5 - 1.0 // map 0..15 → -1..+1
|
||||
})
|
||||
}
|
||||
|
||||
// ── Send to audio hardware ──────────────────────────────────────────────────
|
||||
|
||||
function sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr) {
|
||||
// Interleaves the L and R channels into a staging region (LRLRLR…) and uploads
|
||||
// to the audio adapter pcmBin via the standard putPcmDataByPtr pipeline.
|
||||
//
|
||||
// offsetSec: start of region to send (default: 0)
|
||||
// lengthSec: duration to send (default: entire buffer from offsetSec)
|
||||
// stagingPtr: optional caller-owned native buffer (≥ min(chunk, 32768) * 2 bytes).
|
||||
// Pass a pre-allocated pointer to avoid malloc/free per call —
|
||||
// useful for the per-frame tvnes pattern.
|
||||
//
|
||||
// The function auto-chunks at 32768 stereo samples (pcmBin capacity).
|
||||
// Blocks briefly if the audio queue is saturated (queue depth > 2).
|
||||
const start = (offsetSec != null) ? secToSamples(offsetSec) : 0
|
||||
const total = (lengthSec != null) ? secToSamples(lengthSec) : (buf.samples - start)
|
||||
const MAX_CHUNK = 32768 // pcmBin = 65536 bytes; stereo → max 32768 samples per upload
|
||||
const ownsStaging = (stagingPtr == null)
|
||||
if (ownsStaging) stagingPtr = sys.malloc(Math.min(total, MAX_CHUNK) * 2)
|
||||
|
||||
let remaining = total
|
||||
let cursor = start
|
||||
while (remaining > 0) {
|
||||
const take = Math.min(remaining, MAX_CHUNK)
|
||||
// Interleave L, R into staging buffer
|
||||
for (let i = 0; i < take; i++) {
|
||||
sys.poke(stagingPtr + 2 * i, readU8(buf, 0, cursor + i))
|
||||
sys.poke(stagingPtr + 2 * i + 1, readU8(buf, 1, cursor + i))
|
||||
}
|
||||
// Wait for room in the playback queue (mirrors playwav.js idiom)
|
||||
// while (audio.getPosition(playhead) > 2) sys.sleep(2)
|
||||
audio.putPcmDataByPtr(playhead, stagingPtr, take * 2, 0)
|
||||
audio.setSampleUploadLength(playhead, take * 2)
|
||||
audio.startSampleUpload(playhead)
|
||||
remaining -= take
|
||||
cursor += take
|
||||
}
|
||||
|
||||
if (ownsStaging) sys.free(stagingPtr)
|
||||
}
|
||||
|
||||
// Lazily-allocated JS-side interleave scratch; shared across sendBufferFast calls.
|
||||
let _sendFastScratch = null
|
||||
|
||||
function sendBufferFast(buf, playhead, offsetSec, lengthSec, stagingPtr) {
|
||||
// Like sendBuffer but interleaves L/R via a JS Uint8Array + one sys.pokeBytes per chunk,
|
||||
// instead of ~2n sys.poke calls. Requires a non-native (JS-backed) buffer.
|
||||
// Falls back to sendBuffer for native buffers.
|
||||
if (isNative(buf)) { sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr); return }
|
||||
|
||||
const start = (offsetSec != null) ? secToSamples(offsetSec) : 0
|
||||
const total = (lengthSec != null) ? secToSamples(lengthSec) : (buf.samples - start)
|
||||
const MAX_CHUNK = 32768
|
||||
const ownsStaging = (stagingPtr == null)
|
||||
if (ownsStaging) stagingPtr = sys.malloc(Math.min(total, MAX_CHUNK) * 2)
|
||||
|
||||
const scratchNeeded = Math.min(total, MAX_CHUNK) * 2
|
||||
if (_sendFastScratch == null || _sendFastScratch.length < scratchNeeded) {
|
||||
_sendFastScratch = new Uint8Array(scratchNeeded)
|
||||
}
|
||||
|
||||
let remaining = total
|
||||
let cursor = start
|
||||
while (remaining > 0) {
|
||||
const take = Math.min(remaining, MAX_CHUNK)
|
||||
const L = buf[0], R = buf[1], sc = _sendFastScratch
|
||||
for (let i = 0; i < take; i++) {
|
||||
sc[2 * i] = L[cursor + i]
|
||||
sc[2 * i + 1] = R[cursor + i]
|
||||
}
|
||||
sys.pokeBytes(stagingPtr, sc.subarray(0, take * 2), take * 2)
|
||||
// while (audio.getPosition(playhead) > 2) sys.sleep(2)
|
||||
audio.putPcmDataByPtr(playhead, stagingPtr, take * 2, 0)
|
||||
audio.setSampleUploadLength(playhead, take * 2)
|
||||
audio.startSampleUpload(playhead)
|
||||
remaining -= take
|
||||
cursor += take
|
||||
}
|
||||
|
||||
if (ownsStaging) sys.free(stagingPtr)
|
||||
}
|
||||
|
||||
exports = {
|
||||
HW_SAMPLING_RATE,
|
||||
makeBuffer, makeBufferNative, freeBufferNative, clearBuffer,
|
||||
makeSquare, makeTriangle, makeAliasedTriangle, makeAliasedTriangleNES, makeNoise,
|
||||
sendBuffer, sendBufferFast
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
|
||||
/**
|
||||
* LibSeqread — sequentially read files from disk drive
|
||||
* @author CuriousTorvald
|
||||
*/
|
||||
let readCount = 0
|
||||
let port = undefined
|
||||
let fileHeader = new Uint8Array(4096)
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* LibSeqread extension for Tape Drive — sequentially read tape
|
||||
* @author CuriousTorvald
|
||||
*/
|
||||
|
||||
// Sequential reader for HSDPA TAPE devices
|
||||
// Unlike seqread.mjs which is limited to 4096 bytes per read due to serial communication,
|
||||
// this module can read larger chunks efficiently from HSDPA devices.
|
||||
|
||||
259
assets/disk0/tvdos/include/taud.mjs
Normal file
259
assets/disk0/tvdos/include/taud.mjs
Normal 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 }
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* WinTex — TUI window management and renderer
|
||||
* @author CuriousTorvald
|
||||
*/
|
||||
|
||||
class WindowObject {
|
||||
|
||||
constructor(x, y, w, h, inputProcessor, drawContents, title, drawFrame) {
|
||||
|
||||
@@ -22,6 +22,7 @@ cp -r "../out/$RUNTIME" $DESTDIR/
|
||||
|
||||
# Copy over all the assets and a jarfile
|
||||
cp -r "../out/TerranBASIC.jar" $DESTDIR/
|
||||
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/
|
||||
|
||||
# Pack everything to AppImage
|
||||
ARCH=arm_aarch64 "./$APPIMAGETOOL" $DESTDIR "out/$DESTDIR.AppImage" || { echo 'Building AppImage failed' >&2; exit 1; }
|
||||
|
||||
@@ -22,6 +22,7 @@ cp -r "../out/$RUNTIME" $DESTDIR/
|
||||
|
||||
# Copy over all the assets and a jarfile
|
||||
cp -r "../out/TerranBASIC.jar" $DESTDIR/
|
||||
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/
|
||||
|
||||
# Pack everything to AppImage
|
||||
"./$APPIMAGETOOL" $DESTDIR "out/$DESTDIR.AppImage" || { echo 'Building AppImage failed' >&2; exit 1; }
|
||||
|
||||
@@ -23,5 +23,6 @@ cp -r "../out/$RUNTIME" $DESTDIR/Contents/MacOS/
|
||||
|
||||
# Copy over all the assets and a jarfile
|
||||
cp -r "../out/TerranBASIC.jar" $DESTDIR/Contents/MacOS/
|
||||
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/Contents/MacOS/
|
||||
|
||||
echo "Build successful: $DESTDIR"
|
||||
|
||||
@@ -23,5 +23,6 @@ cp -r "../out/$RUNTIME" $DESTDIR/Contents/MacOS/
|
||||
|
||||
# Copy over all the assets and a jarfile
|
||||
cp -r "../out/TerranBASIC.jar" $DESTDIR/Contents/MacOS/
|
||||
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/Contents/MacOS/
|
||||
|
||||
echo "Build successful: $DESTDIR"
|
||||
|
||||
@@ -18,6 +18,7 @@ cp -r "../out/$RUNTIME" $DESTDIR/
|
||||
|
||||
# Copy over all the assets and a jarfile
|
||||
cp -r "../out/TerranBASIC.jar" $DESTDIR/
|
||||
cp "../lib/compiler-23.1.10.jar" "../lib/compiler-management-23.1.10.jar" "../lib/truffle-compiler-23.1.10.jar" "../lib/truffle-api-23.1.10.jar" "../lib/truffle-runtime-23.1.10.jar" "../lib/polyglot-23.1.10.jar" "../lib/collections-23.1.10.jar" "../lib/word-23.1.10.jar" "../lib/nativeimage-23.1.10.jar" "../lib/jniutils-23.1.10.jar" $DESTDIR/
|
||||
|
||||
# Temporary solution: zip everything
|
||||
zip -r -9 -l "out/$DESTDIR.zip" $DESTDIR
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
cd "${0%/*}"
|
||||
./runtime-linux-arm/bin/java -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar
|
||||
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
|
||||
./runtime-linux-arm/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
cd "${0%/*}"
|
||||
./runtime-linux-x86/bin/java -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar
|
||||
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
|
||||
./runtime-linux-x86/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -Xms128M -Xmx2G -Dswing.aatext=true -Dawt.useSystemAAFontSettings=lcd -jar ./TerranBASIC.jar
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
cd "${0%/*}"
|
||||
./runtime-osx-arm/bin/java -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar
|
||||
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
|
||||
./runtime-osx-arm/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
cd "${0%/*}"
|
||||
./runtime-osx-x86/bin/java -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar
|
||||
GRAAL_MODULE_PATH=compiler-23.1.10.jar:compiler-management-23.1.10.jar:truffle-compiler-23.1.10.jar:truffle-api-23.1.10.jar:truffle-runtime-23.1.10.jar:polyglot-23.1.10.jar:collections-23.1.10.jar:word-23.1.10.jar:nativeimage-23.1.10.jar:jniutils-23.1.10.jar
|
||||
./runtime-osx-x86/bin/java --upgrade-module-path=$GRAAL_MODULE_PATH -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -XstartOnFirstThread -Xms128M -Xmx2G -jar ./TerranBASIC.jar
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
cd /D "%~dp0"
|
||||
.\runtime-windows-x86\bin\java -Xms128M -Xmx2G -jar .\TerranBASIC.jar
|
||||
set GRAAL_MODULE_PATH=compiler-23.1.10.jar;compiler-management-23.1.10.jar;truffle-compiler-23.1.10.jar;truffle-api-23.1.10.jar;truffle-runtime-23.1.10.jar;polyglot-23.1.10.jar;collections-23.1.10.jar;word-23.1.10.jar;nativeimage-23.1.10.jar;jniutils-23.1.10.jar
|
||||
.\runtime-windows-x86\bin\java --upgrade-module-path=%GRAAL_MODULE_PATH% -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --add-exports=java.base/jdk.internal.misc=jdk.internal.vm.compiler -Xms128M -Xmx2G -jar .\TerranBASIC.jar
|
||||
|
||||
81
ipf_encoder/Makefile
Normal file
81
ipf_encoder/Makefile
Normal file
@@ -0,0 +1,81 @@
|
||||
# Makefile for iPF (TSVM Interchangeable Picture Format) Encoder
|
||||
# Created by CuriousTorvald and Claude on 2025-12-19.
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -std=c99 -Wall -Wextra -O2 -D_GNU_SOURCE
|
||||
DBGFLAGS =
|
||||
PREFIX = /usr/local
|
||||
|
||||
# Zstd flags (use pkg-config if available, fallback for cross-platform compatibility)
|
||||
ZSTD_CFLAGS = $(shell pkg-config --cflags libzstd 2>/dev/null || echo "")
|
||||
ZSTD_LIBS = $(shell pkg-config --libs libzstd 2>/dev/null || echo "-lzstd")
|
||||
LIBS = -lm $(ZSTD_LIBS)
|
||||
|
||||
# Targets
|
||||
TARGETS = encoder_ipf decoder_ipf
|
||||
|
||||
# Build all (default)
|
||||
all: $(TARGETS)
|
||||
|
||||
encoder_ipf: encoder_ipf.c
|
||||
rm -f encoder_ipf
|
||||
$(CC) $(CFLAGS) $(ZSTD_CFLAGS) -o encoder_ipf encoder_ipf.c $(LIBS)
|
||||
@echo "iPF encoder built: encoder_ipf"
|
||||
|
||||
decoder_ipf: decoder_ipf.c
|
||||
rm -f decoder_ipf
|
||||
$(CC) $(CFLAGS) $(ZSTD_CFLAGS) -o decoder_ipf decoder_ipf.c $(LIBS)
|
||||
@echo "iPF decoder built: decoder_ipf"
|
||||
|
||||
# Build with debug symbols
|
||||
debug: CFLAGS += -g -DDEBUG -fsanitize=address -fno-omit-frame-pointer
|
||||
debug: DBGFLAGS += -fsanitize=address -fno-omit-frame-pointer
|
||||
debug: clean $(TARGETS)
|
||||
|
||||
# Build with optimizations
|
||||
release: CFLAGS = -std=c99 -Wall -Wextra -O3 -D_GNU_SOURCE -march=native
|
||||
release: clean $(TARGETS)
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -f $(TARGETS) *.o
|
||||
|
||||
# Install
|
||||
install: $(TARGETS)
|
||||
cp encoder_ipf $(PREFIX)/bin/
|
||||
cp decoder_ipf $(PREFIX)/bin/
|
||||
|
||||
# Check for required dependencies
|
||||
check-deps:
|
||||
@echo "Checking dependencies..."
|
||||
@pkg-config --exists libzstd || (echo "Error: libzstd-dev not found. Install libzstd-dev or equivalent" && exit 1)
|
||||
@which ffmpeg >/dev/null 2>&1 || (echo "Error: ffmpeg not found in PATH" && exit 1)
|
||||
@which ffprobe >/dev/null 2>&1 || (echo "Error: ffprobe not found in PATH" && exit 1)
|
||||
@echo "All dependencies found."
|
||||
|
||||
# Help
|
||||
help:
|
||||
@echo "iPF (TSVM Interchangeable Picture Format) Tools"
|
||||
@echo ""
|
||||
@echo "Targets:"
|
||||
@echo " all - Build encoder and decoder (default)"
|
||||
@echo " encoder_ipf - Build encoder only"
|
||||
@echo " decoder_ipf - Build decoder only"
|
||||
@echo " debug - Build with debug symbols and AddressSanitizer"
|
||||
@echo " release - Build with full optimizations"
|
||||
@echo " clean - Remove build artifacts"
|
||||
@echo " install - Install to /usr/local/bin"
|
||||
@echo " check-deps - Check for required dependencies"
|
||||
@echo " help - Show this help"
|
||||
@echo ""
|
||||
@echo "Requirements:"
|
||||
@echo " - GCC with C99 support"
|
||||
@echo " - libzstd-dev (Zstd compression library)"
|
||||
@echo " - FFmpeg (for image encoding/decoding)"
|
||||
@echo ""
|
||||
@echo "Usage:"
|
||||
@echo " make # Build all"
|
||||
@echo " ./encoder_ipf -i input.png -o output.ipf # Encode"
|
||||
@echo " ./decoder_ipf -i output.ipf -o decoded.png # Decode"
|
||||
|
||||
.PHONY: all clean install check-deps help debug release
|
||||
592
ipf_encoder/decoder_ipf.c
Normal file
592
ipf_encoder/decoder_ipf.c
Normal file
@@ -0,0 +1,592 @@
|
||||
/**
|
||||
* iPF Decoder - TSVM Interchangeable Picture Format Decoder
|
||||
*
|
||||
* Decodes iPF format (Type 1 or Type 2) images to standard formats via FFmpeg.
|
||||
*
|
||||
* Created by CuriousTorvald and Claude on 2025-12-19.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <getopt.h>
|
||||
#include <zstd.h>
|
||||
|
||||
// =============================================================================
|
||||
// Constants
|
||||
// =============================================================================
|
||||
|
||||
#define IPF_MAGIC "\x1F\x54\x53\x56\x4D\x69\x50\x46" // "\x1FTSVMiPF"
|
||||
#define IPF_HEADER_SIZE 28 // 8 magic + 2 width + 2 height + 1 flags + 1 type + 10 reserved + 4 uncompressed
|
||||
|
||||
#define IPF_TYPE_1 0 // 4:2:0 chroma subsampling
|
||||
#define IPF_TYPE_2 1 // 4:2:2 chroma subsampling
|
||||
|
||||
#define IPF_FLAG_ALPHA 0x01
|
||||
#define IPF_FLAG_ZSTD 0x10
|
||||
#define IPF_FLAG_PROGRESSIVE 0x80
|
||||
|
||||
#define MAX_PATH 4096
|
||||
|
||||
// =============================================================================
|
||||
// Structures
|
||||
// =============================================================================
|
||||
|
||||
typedef struct {
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
uint8_t flags;
|
||||
uint8_t type;
|
||||
uint32_t uncompressed_size;
|
||||
} ipf_header_t;
|
||||
|
||||
typedef struct {
|
||||
char *input_file;
|
||||
char *output_file;
|
||||
int verbose;
|
||||
int raw_output; // Output raw RGB instead of using FFmpeg
|
||||
} decoder_config_t;
|
||||
|
||||
// =============================================================================
|
||||
// Utility Functions
|
||||
// =============================================================================
|
||||
|
||||
static void print_usage(const char *program) {
|
||||
printf("iPF Decoder - TSVM Interchangeable Picture Format\n");
|
||||
printf("\nUsage: %s -i input.ipf -o output.png [options]\n\n", program);
|
||||
printf("Required:\n");
|
||||
printf(" -i, --input FILE Input iPF file\n");
|
||||
printf(" -o, --output FILE Output image file (any format FFmpeg supports)\n");
|
||||
printf("\nOptions:\n");
|
||||
printf(" --raw Output raw RGB24/RGBA data instead of image file\n");
|
||||
printf(" -v, --verbose Verbose output\n");
|
||||
printf(" -h, --help Show this help\n");
|
||||
printf("\nExamples:\n");
|
||||
printf(" %s -i photo.ipf -o photo.png\n", program);
|
||||
printf(" %s -i logo.ipf -o logo.jpg -v\n", program);
|
||||
}
|
||||
|
||||
static float clampf(float v, float lo, float hi) {
|
||||
return v < lo ? lo : (v > hi ? hi : v);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// iPF File Reading
|
||||
// =============================================================================
|
||||
|
||||
static int read_ipf_header(FILE *fp, ipf_header_t *header) {
|
||||
uint8_t magic[8];
|
||||
|
||||
if (fread(magic, 1, 8, fp) != 8) {
|
||||
fprintf(stderr, "Error: Failed to read magic\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (memcmp(magic, IPF_MAGIC, 8) != 0) {
|
||||
fprintf(stderr, "Error: Invalid iPF magic\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read width (uint16 LE)
|
||||
if (fread(&header->width, 2, 1, fp) != 1) return -1;
|
||||
|
||||
// Read height (uint16 LE)
|
||||
if (fread(&header->height, 2, 1, fp) != 1) return -1;
|
||||
|
||||
// Read flags
|
||||
if (fread(&header->flags, 1, 1, fp) != 1) return -1;
|
||||
|
||||
// Read type
|
||||
if (fread(&header->type, 1, 1, fp) != 1) return -1;
|
||||
|
||||
// Skip reserved (10 bytes)
|
||||
fseek(fp, 10, SEEK_CUR);
|
||||
|
||||
// Read uncompressed size (uint32 LE)
|
||||
if (fread(&header->uncompressed_size, 4, 1, fp) != 1) return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// YCoCg to RGB Conversion
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Convert YCoCg to RGB for 4 pixels sharing the same chroma.
|
||||
* y_values: 4 Y values packed as nibbles (Y0|Y1 in low byte, Y2|Y3 in high byte style)
|
||||
* a_values: 4 alpha values packed similarly
|
||||
* co, cg: 4-bit chroma values [0..15]
|
||||
*
|
||||
* Output: fills rgb array with R,G,B[,A] values for 4 pixels
|
||||
*/
|
||||
static void ycocg_to_rgb_quad(int co, int cg, int y0, int y1, int y2, int y3,
|
||||
int a0, int a1, int a2, int a3,
|
||||
int has_alpha, uint8_t *rgb) {
|
||||
// Convert chroma from [0..15] to [-1..1]
|
||||
float co_f = (co - 7) / 8.0f;
|
||||
float cg_f = (cg - 7) / 8.0f;
|
||||
|
||||
int ys[4] = {y0, y1, y2, y3};
|
||||
int as[4] = {a0, a1, a2, a3};
|
||||
|
||||
int stride = has_alpha ? 4 : 3;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
float y = ys[i] / 15.0f;
|
||||
|
||||
// YCoCg to RGB conversion
|
||||
float tmp = y - cg_f / 2.0f;
|
||||
float g = clampf(cg_f + tmp, 0.0f, 1.0f);
|
||||
float b = clampf(tmp - co_f / 2.0f, 0.0f, 1.0f);
|
||||
float r = clampf(b + co_f, 0.0f, 1.0f);
|
||||
|
||||
rgb[i * stride + 0] = (uint8_t)(r * 255.0f + 0.5f);
|
||||
rgb[i * stride + 1] = (uint8_t)(g * 255.0f + 0.5f);
|
||||
rgb[i * stride + 2] = (uint8_t)(b * 255.0f + 0.5f);
|
||||
|
||||
if (has_alpha) {
|
||||
rgb[i * stride + 3] = (uint8_t)(as[i] * 17); // Scale 0-15 to 0-255
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode iPF1 block (4:2:0 chroma subsampling).
|
||||
* Input: 12 bytes (or 20 with alpha)
|
||||
* Output: 16 pixels in RGB24/RGBA format
|
||||
*/
|
||||
static void decode_ipf1_block(const uint8_t *block, int has_alpha, uint8_t *pixels, int stride) {
|
||||
// Read chroma (4 values for 2x2 regions)
|
||||
int co1 = block[0] & 0x0F;
|
||||
int co2 = (block[0] >> 4) & 0x0F;
|
||||
int co3 = block[1] & 0x0F;
|
||||
int co4 = (block[1] >> 4) & 0x0F;
|
||||
|
||||
int cg1 = block[2] & 0x0F;
|
||||
int cg2 = (block[2] >> 4) & 0x0F;
|
||||
int cg3 = block[3] & 0x0F;
|
||||
int cg4 = (block[3] >> 4) & 0x0F;
|
||||
|
||||
// Read Y values (16 values)
|
||||
// Layout: [Y1|Y0|Y5|Y4], [Y3|Y2|Y7|Y6], [Y9|Y8|YD|YC], [YB|YA|YF|YE]
|
||||
int Y[16];
|
||||
Y[0] = block[4] & 0x0F;
|
||||
Y[1] = (block[4] >> 4) & 0x0F;
|
||||
Y[4] = block[5] & 0x0F;
|
||||
Y[5] = (block[5] >> 4) & 0x0F;
|
||||
Y[2] = block[6] & 0x0F;
|
||||
Y[3] = (block[6] >> 4) & 0x0F;
|
||||
Y[6] = block[7] & 0x0F;
|
||||
Y[7] = (block[7] >> 4) & 0x0F;
|
||||
Y[8] = block[8] & 0x0F;
|
||||
Y[9] = (block[8] >> 4) & 0x0F;
|
||||
Y[12] = block[9] & 0x0F;
|
||||
Y[13] = (block[9] >> 4) & 0x0F;
|
||||
Y[10] = block[10] & 0x0F;
|
||||
Y[11] = (block[10] >> 4) & 0x0F;
|
||||
Y[14] = block[11] & 0x0F;
|
||||
Y[15] = (block[11] >> 4) & 0x0F;
|
||||
|
||||
// Read alpha values if present
|
||||
int A[16];
|
||||
if (has_alpha) {
|
||||
A[0] = block[12] & 0x0F;
|
||||
A[1] = (block[12] >> 4) & 0x0F;
|
||||
A[4] = block[13] & 0x0F;
|
||||
A[5] = (block[13] >> 4) & 0x0F;
|
||||
A[2] = block[14] & 0x0F;
|
||||
A[3] = (block[14] >> 4) & 0x0F;
|
||||
A[6] = block[15] & 0x0F;
|
||||
A[7] = (block[15] >> 4) & 0x0F;
|
||||
A[8] = block[16] & 0x0F;
|
||||
A[9] = (block[16] >> 4) & 0x0F;
|
||||
A[12] = block[17] & 0x0F;
|
||||
A[13] = (block[17] >> 4) & 0x0F;
|
||||
A[10] = block[18] & 0x0F;
|
||||
A[11] = (block[18] >> 4) & 0x0F;
|
||||
A[14] = block[19] & 0x0F;
|
||||
A[15] = (block[19] >> 4) & 0x0F;
|
||||
} else {
|
||||
for (int i = 0; i < 16; i++) A[i] = 15;
|
||||
}
|
||||
|
||||
int channels = has_alpha ? 4 : 3;
|
||||
uint8_t quad[16]; // 4 pixels max
|
||||
|
||||
// Decode 4 quads (2x2 regions), each sharing one chroma pair
|
||||
// Top-left quad (pixels 0,1,4,5) uses co1/cg1
|
||||
ycocg_to_rgb_quad(co1, cg1, Y[0], Y[1], Y[4], Y[5], A[0], A[1], A[4], A[5], has_alpha, quad);
|
||||
memcpy(pixels + 0 * stride + 0 * channels, quad + 0 * channels, channels);
|
||||
memcpy(pixels + 0 * stride + 1 * channels, quad + 1 * channels, channels);
|
||||
memcpy(pixels + 1 * stride + 0 * channels, quad + 2 * channels, channels);
|
||||
memcpy(pixels + 1 * stride + 1 * channels, quad + 3 * channels, channels);
|
||||
|
||||
// Top-right quad (pixels 2,3,6,7) uses co2/cg2
|
||||
ycocg_to_rgb_quad(co2, cg2, Y[2], Y[3], Y[6], Y[7], A[2], A[3], A[6], A[7], has_alpha, quad);
|
||||
memcpy(pixels + 0 * stride + 2 * channels, quad + 0 * channels, channels);
|
||||
memcpy(pixels + 0 * stride + 3 * channels, quad + 1 * channels, channels);
|
||||
memcpy(pixels + 1 * stride + 2 * channels, quad + 2 * channels, channels);
|
||||
memcpy(pixels + 1 * stride + 3 * channels, quad + 3 * channels, channels);
|
||||
|
||||
// Bottom-left quad (pixels 8,9,12,13) uses co3/cg3
|
||||
ycocg_to_rgb_quad(co3, cg3, Y[8], Y[9], Y[12], Y[13], A[8], A[9], A[12], A[13], has_alpha, quad);
|
||||
memcpy(pixels + 2 * stride + 0 * channels, quad + 0 * channels, channels);
|
||||
memcpy(pixels + 2 * stride + 1 * channels, quad + 1 * channels, channels);
|
||||
memcpy(pixels + 3 * stride + 0 * channels, quad + 2 * channels, channels);
|
||||
memcpy(pixels + 3 * stride + 1 * channels, quad + 3 * channels, channels);
|
||||
|
||||
// Bottom-right quad (pixels 10,11,14,15) uses co4/cg4
|
||||
ycocg_to_rgb_quad(co4, cg4, Y[10], Y[11], Y[14], Y[15], A[10], A[11], A[14], A[15], has_alpha, quad);
|
||||
memcpy(pixels + 2 * stride + 2 * channels, quad + 0 * channels, channels);
|
||||
memcpy(pixels + 2 * stride + 3 * channels, quad + 1 * channels, channels);
|
||||
memcpy(pixels + 3 * stride + 2 * channels, quad + 2 * channels, channels);
|
||||
memcpy(pixels + 3 * stride + 3 * channels, quad + 3 * channels, channels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode iPF2 block (4:2:2 chroma subsampling).
|
||||
* Input: 16 bytes (or 24 with alpha)
|
||||
* Output: 16 pixels in RGB24/RGBA format
|
||||
*/
|
||||
static void decode_ipf2_block(const uint8_t *block, int has_alpha, uint8_t *pixels, int stride) {
|
||||
// Read chroma (8 values for horizontal pairs)
|
||||
int co[8], cg[8];
|
||||
co[0] = block[0] & 0x0F;
|
||||
co[1] = (block[0] >> 4) & 0x0F;
|
||||
co[2] = block[1] & 0x0F;
|
||||
co[3] = (block[1] >> 4) & 0x0F;
|
||||
co[4] = block[2] & 0x0F;
|
||||
co[5] = (block[2] >> 4) & 0x0F;
|
||||
co[6] = block[3] & 0x0F;
|
||||
co[7] = (block[3] >> 4) & 0x0F;
|
||||
|
||||
cg[0] = block[4] & 0x0F;
|
||||
cg[1] = (block[4] >> 4) & 0x0F;
|
||||
cg[2] = block[5] & 0x0F;
|
||||
cg[3] = (block[5] >> 4) & 0x0F;
|
||||
cg[4] = block[6] & 0x0F;
|
||||
cg[5] = (block[6] >> 4) & 0x0F;
|
||||
cg[6] = block[7] & 0x0F;
|
||||
cg[7] = (block[7] >> 4) & 0x0F;
|
||||
|
||||
// Read Y values (16 values) - same layout as iPF1
|
||||
int Y[16];
|
||||
Y[0] = block[8] & 0x0F;
|
||||
Y[1] = (block[8] >> 4) & 0x0F;
|
||||
Y[4] = block[9] & 0x0F;
|
||||
Y[5] = (block[9] >> 4) & 0x0F;
|
||||
Y[2] = block[10] & 0x0F;
|
||||
Y[3] = (block[10] >> 4) & 0x0F;
|
||||
Y[6] = block[11] & 0x0F;
|
||||
Y[7] = (block[11] >> 4) & 0x0F;
|
||||
Y[8] = block[12] & 0x0F;
|
||||
Y[9] = (block[12] >> 4) & 0x0F;
|
||||
Y[12] = block[13] & 0x0F;
|
||||
Y[13] = (block[13] >> 4) & 0x0F;
|
||||
Y[10] = block[14] & 0x0F;
|
||||
Y[11] = (block[14] >> 4) & 0x0F;
|
||||
Y[14] = block[15] & 0x0F;
|
||||
Y[15] = (block[15] >> 4) & 0x0F;
|
||||
|
||||
// Read alpha values if present
|
||||
int A[16];
|
||||
if (has_alpha) {
|
||||
A[0] = block[16] & 0x0F;
|
||||
A[1] = (block[16] >> 4) & 0x0F;
|
||||
A[4] = block[17] & 0x0F;
|
||||
A[5] = (block[17] >> 4) & 0x0F;
|
||||
A[2] = block[18] & 0x0F;
|
||||
A[3] = (block[18] >> 4) & 0x0F;
|
||||
A[6] = block[19] & 0x0F;
|
||||
A[7] = (block[19] >> 4) & 0x0F;
|
||||
A[8] = block[20] & 0x0F;
|
||||
A[9] = (block[20] >> 4) & 0x0F;
|
||||
A[12] = block[21] & 0x0F;
|
||||
A[13] = (block[21] >> 4) & 0x0F;
|
||||
A[10] = block[22] & 0x0F;
|
||||
A[11] = (block[22] >> 4) & 0x0F;
|
||||
A[14] = block[23] & 0x0F;
|
||||
A[15] = (block[23] >> 4) & 0x0F;
|
||||
} else {
|
||||
for (int i = 0; i < 16; i++) A[i] = 15;
|
||||
}
|
||||
|
||||
int channels = has_alpha ? 4 : 3;
|
||||
|
||||
// iPF2: 4:2:2 - each horizontal pair shares chroma
|
||||
// Row 0: pixels 0,1 share co[0]/cg[0], pixels 2,3 share co[1]/cg[1]
|
||||
// Row 1: pixels 4,5 share co[2]/cg[2], pixels 6,7 share co[3]/cg[3]
|
||||
// Row 2: pixels 8,9 share co[4]/cg[4], pixels 10,11 share co[5]/cg[5]
|
||||
// Row 3: pixels 12,13 share co[6]/cg[6], pixels 14,15 share co[7]/cg[7]
|
||||
|
||||
int pixel_map[8][4] = {
|
||||
{0, 1, 0, 1}, // co/cg index 0: pixels 0,1
|
||||
{2, 3, 2, 3}, // co/cg index 1: pixels 2,3
|
||||
{4, 5, 4, 5}, // co/cg index 2: pixels 4,5
|
||||
{6, 7, 6, 7}, // co/cg index 3: pixels 6,7
|
||||
{8, 9, 8, 9}, // co/cg index 4: pixels 8,9
|
||||
{10, 11, 10, 11}, // co/cg index 5: pixels 10,11
|
||||
{12, 13, 12, 13}, // co/cg index 6: pixels 12,13
|
||||
{14, 15, 14, 15} // co/cg index 7: pixels 14,15
|
||||
};
|
||||
|
||||
for (int ci = 0; ci < 8; ci++) {
|
||||
int p0 = pixel_map[ci][0];
|
||||
int p1 = pixel_map[ci][1];
|
||||
|
||||
uint8_t quad[16]; // 4 pixels max (ycocg_to_rgb_quad writes 4 pixels)
|
||||
ycocg_to_rgb_quad(co[ci], cg[ci], Y[p0], Y[p1], Y[p0], Y[p1],
|
||||
A[p0], A[p1], A[p0], A[p1], has_alpha, quad);
|
||||
|
||||
int row = p0 / 4;
|
||||
int col0 = p0 % 4;
|
||||
int col1 = p1 % 4;
|
||||
|
||||
memcpy(pixels + row * stride + col0 * channels, quad + 0 * channels, channels);
|
||||
memcpy(pixels + row * stride + col1 * channels, quad + 1 * channels, channels);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main Decoding
|
||||
// =============================================================================
|
||||
|
||||
static int decode_ipf(const decoder_config_t *cfg) {
|
||||
FILE *fp = fopen(cfg->input_file, "rb");
|
||||
if (!fp) {
|
||||
fprintf(stderr, "Error: Failed to open input file: %s\n", cfg->input_file);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read header
|
||||
ipf_header_t header;
|
||||
if (read_ipf_header(fp, &header) < 0) {
|
||||
fclose(fp);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int has_alpha = (header.flags & IPF_FLAG_ALPHA) != 0;
|
||||
int use_zstd = (header.flags & IPF_FLAG_ZSTD) != 0;
|
||||
int progressive = (header.flags & IPF_FLAG_PROGRESSIVE) != 0;
|
||||
|
||||
if (cfg->verbose) {
|
||||
printf("iPF Header:\n");
|
||||
printf(" Size: %dx%d\n", header.width, header.height);
|
||||
printf(" Type: iPF%d (%s)\n", header.type + 1,
|
||||
header.type == 0 ? "4:2:0" : "4:2:2");
|
||||
printf(" Flags: %s%s%s\n",
|
||||
has_alpha ? "alpha " : "",
|
||||
use_zstd ? "zstd " : "",
|
||||
progressive ? "progressive " : "");
|
||||
printf(" Uncompressed size: %u bytes\n", header.uncompressed_size);
|
||||
}
|
||||
|
||||
if (progressive) {
|
||||
fprintf(stderr, "Warning: Progressive mode not implemented, decoding as sequential\n");
|
||||
}
|
||||
|
||||
// Read compressed/raw block data
|
||||
fseek(fp, 0, SEEK_END);
|
||||
long file_size = ftell(fp);
|
||||
fseek(fp, IPF_HEADER_SIZE, SEEK_SET);
|
||||
|
||||
size_t compressed_size = file_size - IPF_HEADER_SIZE;
|
||||
uint8_t *compressed_data = malloc(compressed_size);
|
||||
if (!compressed_data) {
|
||||
fclose(fp);
|
||||
fprintf(stderr, "Error: Failed to allocate memory\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (fread(compressed_data, 1, compressed_size, fp) != compressed_size) {
|
||||
free(compressed_data);
|
||||
fclose(fp);
|
||||
fprintf(stderr, "Error: Failed to read block data\n");
|
||||
return -1;
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
// Decompress if needed
|
||||
uint8_t *block_data;
|
||||
size_t block_data_size;
|
||||
|
||||
if (use_zstd) {
|
||||
block_data_size = header.uncompressed_size;
|
||||
block_data = malloc(block_data_size);
|
||||
if (!block_data) {
|
||||
free(compressed_data);
|
||||
fprintf(stderr, "Error: Failed to allocate decompression buffer\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t result = ZSTD_decompress(block_data, block_data_size,
|
||||
compressed_data, compressed_size);
|
||||
if (ZSTD_isError(result)) {
|
||||
fprintf(stderr, "Error: Zstd decompression failed: %s\n",
|
||||
ZSTD_getErrorName(result));
|
||||
free(block_data);
|
||||
free(compressed_data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (cfg->verbose) {
|
||||
printf("Decompressed: %zu -> %zu bytes\n", compressed_size, block_data_size);
|
||||
}
|
||||
|
||||
free(compressed_data);
|
||||
} else {
|
||||
block_data = compressed_data;
|
||||
block_data_size = compressed_size;
|
||||
}
|
||||
|
||||
// Allocate output image
|
||||
int channels = has_alpha ? 4 : 3;
|
||||
size_t image_size = (size_t)header.width * header.height * channels;
|
||||
uint8_t *image = malloc(image_size);
|
||||
if (!image) {
|
||||
free(block_data);
|
||||
fprintf(stderr, "Error: Failed to allocate image buffer\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Decode blocks
|
||||
int blocks_x = (header.width + 3) / 4;
|
||||
int blocks_y = (header.height + 3) / 4;
|
||||
int block_size = (header.type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16);
|
||||
int row_stride = header.width * channels;
|
||||
int block_stride = 4 * channels; // 4 pixels per block row
|
||||
|
||||
size_t block_offset = 0;
|
||||
for (int by = 0; by < blocks_y; by++) {
|
||||
for (int bx = 0; bx < blocks_x; bx++) {
|
||||
// Calculate output position
|
||||
uint8_t *block_pixels = image + by * 4 * row_stride + bx * block_stride;
|
||||
|
||||
if (header.type == IPF_TYPE_1) {
|
||||
decode_ipf1_block(block_data + block_offset, has_alpha, block_pixels, row_stride);
|
||||
} else {
|
||||
decode_ipf2_block(block_data + block_offset, has_alpha, block_pixels, row_stride);
|
||||
}
|
||||
|
||||
block_offset += block_size;
|
||||
}
|
||||
}
|
||||
|
||||
free(block_data);
|
||||
|
||||
if (cfg->verbose) {
|
||||
printf("Decoded %d blocks (%dx%d)\n", blocks_x * blocks_y, blocks_x, blocks_y);
|
||||
}
|
||||
|
||||
// Output image
|
||||
int result = 0;
|
||||
|
||||
if (cfg->raw_output) {
|
||||
// Write raw RGB/RGBA data
|
||||
FILE *out = fopen(cfg->output_file, "wb");
|
||||
if (!out) {
|
||||
fprintf(stderr, "Error: Failed to open output file: %s\n", cfg->output_file);
|
||||
result = -1;
|
||||
} else {
|
||||
fwrite(image, 1, image_size, out);
|
||||
fclose(out);
|
||||
if (cfg->verbose) {
|
||||
printf("Wrote %zu bytes raw %s data\n", image_size, has_alpha ? "RGBA" : "RGB24");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use FFmpeg to write output image
|
||||
char cmd[MAX_PATH * 2];
|
||||
const char *pix_fmt = has_alpha ? "rgba" : "rgb24";
|
||||
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"ffmpeg -hide_banner -v quiet -y -f rawvideo -pix_fmt %s -s %dx%d "
|
||||
"-i - \"%s\"",
|
||||
pix_fmt, header.width, header.height, cfg->output_file);
|
||||
|
||||
if (cfg->verbose) {
|
||||
printf("FFmpeg command: %s\n", cmd);
|
||||
}
|
||||
|
||||
FILE *pipe = popen(cmd, "w");
|
||||
if (!pipe) {
|
||||
fprintf(stderr, "Error: Failed to start FFmpeg\n");
|
||||
result = -1;
|
||||
} else {
|
||||
fwrite(image, 1, image_size, pipe);
|
||||
int status = pclose(pipe);
|
||||
if (status != 0) {
|
||||
fprintf(stderr, "Error: FFmpeg failed with status %d\n", status);
|
||||
result = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(image);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main Entry Point
|
||||
// =============================================================================
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
decoder_config_t cfg = {
|
||||
.input_file = NULL,
|
||||
.output_file = NULL,
|
||||
.verbose = 0,
|
||||
.raw_output = 0
|
||||
};
|
||||
|
||||
static struct option long_options[] = {
|
||||
{"input", required_argument, 0, 'i'},
|
||||
{"output", required_argument, 0, 'o'},
|
||||
{"raw", no_argument, 0, 'R'},
|
||||
{"verbose", no_argument, 0, 'v'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{0, 0, 0, 0}
|
||||
};
|
||||
|
||||
int opt;
|
||||
while ((opt = getopt_long(argc, argv, "i:o:vh", long_options, NULL)) != -1) {
|
||||
switch (opt) {
|
||||
case 'i':
|
||||
cfg.input_file = optarg;
|
||||
break;
|
||||
case 'o':
|
||||
cfg.output_file = optarg;
|
||||
break;
|
||||
case 'R':
|
||||
cfg.raw_output = 1;
|
||||
break;
|
||||
case 'v':
|
||||
cfg.verbose = 1;
|
||||
break;
|
||||
case 'h':
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
default:
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required arguments
|
||||
if (!cfg.input_file || !cfg.output_file) {
|
||||
fprintf(stderr, "Error: Input and output files are required\n\n");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int result = decode_ipf(&cfg);
|
||||
|
||||
if (result == 0) {
|
||||
printf("Successfully decoded: %s\n", cfg.output_file);
|
||||
}
|
||||
|
||||
return result == 0 ? 0 : 1;
|
||||
}
|
||||
787
ipf_encoder/encoder_ipf.c
Normal file
787
ipf_encoder/encoder_ipf.c
Normal file
@@ -0,0 +1,787 @@
|
||||
/**
|
||||
* iPF Encoder - TSVM Interchangeable Picture Format Encoder
|
||||
*
|
||||
* Encodes images to iPF format (Type 1 or Type 2) with:
|
||||
* - YCoCg colour space with chroma subsampling
|
||||
* - 4x4 block encoding
|
||||
* - Optional Zstd compression
|
||||
* - Optional alpha channel
|
||||
* - Optional Adam7 progressive ordering
|
||||
*
|
||||
* Created by CuriousTorvald and Claude on 2025-12-19.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <getopt.h>
|
||||
#include <zstd.h>
|
||||
|
||||
// =============================================================================
|
||||
// Constants
|
||||
// =============================================================================
|
||||
|
||||
#define IPF_MAGIC "\x1F\x54\x53\x56\x4D\x69\x50\x46" // "\x1FTSVMiPF"
|
||||
#define IPF_HEADER_SIZE 28 // 8 magic + 2 width + 2 height + 1 flags + 1 type + 10 reserved + 4 uncompressed size
|
||||
|
||||
#define DEFAULT_WIDTH 560
|
||||
#define DEFAULT_HEIGHT 448
|
||||
|
||||
#define IPF_TYPE_1 0 // 4:2:0 chroma subsampling (12 bytes per block, +8 with alpha)
|
||||
#define IPF_TYPE_2 1 // 4:2:2 chroma subsampling (16 bytes per block, +8 with alpha)
|
||||
|
||||
#define IPF_FLAG_ALPHA 0x01 // Has alpha channel
|
||||
#define IPF_FLAG_ZSTD 0x10 // Zstd compressed
|
||||
#define IPF_FLAG_PROGRESSIVE 0x80 // Adam7 progressive ordering
|
||||
|
||||
#define MAX_PATH 4096
|
||||
|
||||
// Bayer dithering kernel (4x4)
|
||||
static const float BAYER_4X4[16] = {
|
||||
0.0f/16.0f, 8.0f/16.0f, 2.0f/16.0f, 10.0f/16.0f,
|
||||
12.0f/16.0f, 4.0f/16.0f, 14.0f/16.0f, 6.0f/16.0f,
|
||||
3.0f/16.0f, 11.0f/16.0f, 1.0f/16.0f, 9.0f/16.0f,
|
||||
15.0f/16.0f, 7.0f/16.0f, 13.0f/16.0f, 5.0f/16.0f
|
||||
};
|
||||
|
||||
// Adam7 interlace pattern - pass number (1-7) for each pixel in 8x8 block
|
||||
// 0 = not in this standard pattern, we'll adapt for 4x4 blocks
|
||||
static const int ADAM7_PASS[8][8] = {
|
||||
{1, 6, 4, 6, 2, 6, 4, 6},
|
||||
{7, 7, 7, 7, 7, 7, 7, 7},
|
||||
{5, 6, 5, 6, 5, 6, 5, 6},
|
||||
{7, 7, 7, 7, 7, 7, 7, 7},
|
||||
{3, 6, 4, 6, 3, 6, 4, 6},
|
||||
{7, 7, 7, 7, 7, 7, 7, 7},
|
||||
{5, 6, 5, 6, 5, 6, 5, 6},
|
||||
{7, 7, 7, 7, 7, 7, 7, 7}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Structures
|
||||
// =============================================================================
|
||||
|
||||
typedef struct {
|
||||
char *input_file;
|
||||
char *output_file;
|
||||
int width;
|
||||
int height;
|
||||
int ipf_type; // 0 = iPF1, 1 = iPF2
|
||||
int use_zstd; // 1 = compress with Zstd
|
||||
int force_alpha; // 1 = force alpha channel in output
|
||||
int no_alpha; // 1 = strip alpha even if present in input
|
||||
int progressive; // 1 = Adam7 progressive ordering
|
||||
int dither; // Bayer dither pattern index (-1 = no dithering)
|
||||
int verbose;
|
||||
} encoder_config_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t *data; // RGB or RGBA data
|
||||
int width;
|
||||
int height;
|
||||
int channels; // 3 = RGB, 4 = RGBA
|
||||
int has_alpha; // 1 if input image has meaningful alpha
|
||||
} image_t;
|
||||
|
||||
// =============================================================================
|
||||
// Utility Functions
|
||||
// =============================================================================
|
||||
|
||||
static void print_usage(const char *program) {
|
||||
printf("iPF Encoder - TSVM Interchangeable Picture Format\n");
|
||||
printf("\nUsage: %s -i input.png -o output.ipf [options]\n\n", program);
|
||||
printf("Required:\n");
|
||||
printf(" -i, --input FILE Input image file (any format FFmpeg supports)\n");
|
||||
printf(" -o, --output FILE Output iPF file\n");
|
||||
printf("\nOptions:\n");
|
||||
printf(" -s, --size WxH Output size (default: %dx%d)\n", DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
printf(" -t, --type N iPF type: 1 (4:2:0, default) or 2 (4:2:2)\n");
|
||||
printf(" --no-zstd Disable Zstd compression (default: enabled)\n");
|
||||
printf(" --alpha Force alpha channel in output\n");
|
||||
printf(" --no-alpha Strip alpha channel from input\n");
|
||||
printf(" -p, --progressive Use Adam7 progressive ordering\n");
|
||||
printf(" -d, --dither N Bayer dither pattern (0=4x4, -1=none, default: 0)\n");
|
||||
printf(" -v, --verbose Verbose output\n");
|
||||
printf(" -h, --help Show this help\n");
|
||||
printf("\nExamples:\n");
|
||||
printf(" %s -i photo.jpg -o photo.ipf\n", program);
|
||||
printf(" %s -i logo.png -o logo.ipf --alpha\n", program);
|
||||
printf(" %s -i image.png -o image.ipf -s 280x224 -t 2\n", program);
|
||||
}
|
||||
|
||||
static int clampi(int v, int lo, int hi) {
|
||||
return v < lo ? lo : (v > hi ? hi : v);
|
||||
}
|
||||
|
||||
// Convert chroma value [-1..1] to 4-bit [0..15]
|
||||
static int chroma_to_four_bits(float f) {
|
||||
return clampi((int)roundf(f * 8.0f) + 7, 0, 15);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Image Loading via FFmpeg
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Probe input image dimensions using FFmpeg.
|
||||
* Returns 0 on success, -1 on error.
|
||||
*/
|
||||
static int probe_image_dimensions(const char *input_file, int *width, int *height, int *has_alpha) {
|
||||
char cmd[MAX_PATH * 2];
|
||||
|
||||
// Use ffprobe to get dimensions and pixel format
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"ffprobe -v quiet -select_streams v:0 -show_entries stream=width,height,pix_fmt "
|
||||
"-of csv=p=0:s=x \"%s\" 2>/dev/null",
|
||||
input_file);
|
||||
|
||||
FILE *fp = popen(cmd, "r");
|
||||
if (!fp) {
|
||||
fprintf(stderr, "Error: Failed to run ffprobe\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
char buffer[256];
|
||||
if (fgets(buffer, sizeof(buffer), fp) == NULL) {
|
||||
pclose(fp);
|
||||
fprintf(stderr, "Error: Failed to read image info\n");
|
||||
return -1;
|
||||
}
|
||||
pclose(fp);
|
||||
|
||||
// Parse "width x height x pix_fmt"
|
||||
char pix_fmt[64] = "";
|
||||
if (sscanf(buffer, "%dx%dx%63s", width, height, pix_fmt) < 2) {
|
||||
// Try alternate format without pix_fmt
|
||||
if (sscanf(buffer, "%dx%d", width, height) != 2) {
|
||||
fprintf(stderr, "Error: Failed to parse image dimensions\n");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if pixel format indicates alpha
|
||||
*has_alpha = (strstr(pix_fmt, "rgba") != NULL ||
|
||||
strstr(pix_fmt, "argb") != NULL ||
|
||||
strstr(pix_fmt, "bgra") != NULL ||
|
||||
strstr(pix_fmt, "abgr") != NULL ||
|
||||
strstr(pix_fmt, "ya") != NULL ||
|
||||
strstr(pix_fmt, "pal8") != NULL || // palette may have alpha
|
||||
strstr(pix_fmt, "yuva") != NULL);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and resize image using FFmpeg.
|
||||
* Maintains aspect ratio and crops to target size.
|
||||
* Returns image data or NULL on error.
|
||||
*/
|
||||
static image_t* load_image(const char *input_file, int target_width, int target_height,
|
||||
int want_alpha, int verbose) {
|
||||
int src_width, src_height, src_has_alpha;
|
||||
|
||||
// Probe source dimensions
|
||||
if (probe_image_dimensions(input_file, &src_width, &src_height, &src_has_alpha) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
printf("Source image: %dx%d, alpha: %s\n",
|
||||
src_width, src_height, src_has_alpha ? "yes" : "no");
|
||||
}
|
||||
|
||||
// Determine if we need alpha channel
|
||||
int use_alpha = want_alpha || src_has_alpha;
|
||||
int channels = use_alpha ? 4 : 3;
|
||||
const char *pix_fmt = use_alpha ? "rgba" : "rgb24";
|
||||
|
||||
// Build FFmpeg command with scale and crop filter
|
||||
char cmd[MAX_PATH * 2];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"ffmpeg -hide_banner -v quiet -i \"%s\" -f rawvideo -pix_fmt %s -vf "
|
||||
"\"scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d\" -frames:v 1 -",
|
||||
input_file, pix_fmt, target_width, target_height, target_width, target_height);
|
||||
|
||||
if (verbose) {
|
||||
printf("FFmpeg command: %s\n", cmd);
|
||||
}
|
||||
|
||||
FILE *fp = popen(cmd, "r");
|
||||
if (!fp) {
|
||||
fprintf(stderr, "Error: Failed to start FFmpeg\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Allocate image
|
||||
image_t *img = malloc(sizeof(image_t));
|
||||
if (!img) {
|
||||
pclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t data_size = (size_t)target_width * target_height * channels;
|
||||
img->data = malloc(data_size);
|
||||
if (!img->data) {
|
||||
free(img);
|
||||
pclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
img->width = target_width;
|
||||
img->height = target_height;
|
||||
img->channels = channels;
|
||||
img->has_alpha = use_alpha;
|
||||
|
||||
// Read image data
|
||||
size_t bytes_read = fread(img->data, 1, data_size, fp);
|
||||
pclose(fp);
|
||||
|
||||
if (bytes_read != data_size) {
|
||||
fprintf(stderr, "Error: Expected %zu bytes, got %zu\n", data_size, bytes_read);
|
||||
free(img->data);
|
||||
free(img);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
printf("Loaded %dx%d image, %d channels, %zu bytes\n",
|
||||
img->width, img->height, img->channels, data_size);
|
||||
}
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
static void free_image(image_t *img) {
|
||||
if (img) {
|
||||
free(img->data);
|
||||
free(img);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// iPF Block Encoding
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Encode a 4x4 block to YCoCg with dithering.
|
||||
* Returns arrays of Y (16 values), A (16 values), Co (16 values), Cg (16 values).
|
||||
*/
|
||||
static void encode_block_to_ycocg(const image_t *img, int block_x, int block_y,
|
||||
int dither_pattern,
|
||||
int *Y_out, int *A_out, float *Co_out, float *Cg_out) {
|
||||
for (int py = 0; py < 4; py++) {
|
||||
for (int px = 0; px < 4; px++) {
|
||||
int ox = block_x * 4 + px;
|
||||
int oy = block_y * 4 + py;
|
||||
|
||||
// Handle out-of-bounds (extend edge pixels)
|
||||
ox = clampi(ox, 0, img->width - 1);
|
||||
oy = clampi(oy, 0, img->height - 1);
|
||||
|
||||
// Get dither threshold
|
||||
float t = 0.0f;
|
||||
if (dither_pattern >= 0) {
|
||||
t = BAYER_4X4[(py % 4) * 4 + (px % 4)];
|
||||
}
|
||||
|
||||
// Read pixel
|
||||
int offset = (oy * img->width + ox) * img->channels;
|
||||
float r0 = img->data[offset + 0] / 255.0f;
|
||||
float g0 = (img->channels >= 3) ? img->data[offset + 1] / 255.0f : r0;
|
||||
float b0 = (img->channels >= 3) ? img->data[offset + 2] / 255.0f : r0;
|
||||
float a0 = (img->channels == 4) ? img->data[offset + 3] / 255.0f : 1.0f;
|
||||
|
||||
// Apply dithering
|
||||
float r = floorf((t / 15.0f + r0) * 15.0f) / 15.0f;
|
||||
float g = floorf((t / 15.0f + g0) * 15.0f) / 15.0f;
|
||||
float b = floorf((t / 15.0f + b0) * 15.0f) / 15.0f;
|
||||
float a = floorf((t / 15.0f + a0) * 15.0f) / 15.0f;
|
||||
|
||||
// Convert to YCoCg
|
||||
float co = r - b; // [-1..1]
|
||||
float tmp = b + co / 2.0f;
|
||||
float cg = g - tmp; // [-1..1]
|
||||
float y = tmp + cg / 2.0f; // [0..1]
|
||||
|
||||
int index = py * 4 + px;
|
||||
Y_out[index] = (int)roundf(y * 15.0f);
|
||||
A_out[index] = (int)roundf(a * 15.0f);
|
||||
Co_out[index] = co;
|
||||
Cg_out[index] = cg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode iPF1 block (4:2:0 chroma subsampling).
|
||||
* Returns 12 bytes (or 20 with alpha).
|
||||
*/
|
||||
static int encode_ipf1_block(const int *Ys, const int *As, const float *COs, const float *CGs,
|
||||
int has_alpha, uint8_t *out) {
|
||||
// Subsample Co/Cg by averaging 2x2 regions (4:2:0)
|
||||
int cos1 = chroma_to_four_bits((COs[0] + COs[1] + COs[4] + COs[5]) / 4.0f);
|
||||
int cos2 = chroma_to_four_bits((COs[2] + COs[3] + COs[6] + COs[7]) / 4.0f);
|
||||
int cos3 = chroma_to_four_bits((COs[8] + COs[9] + COs[12] + COs[13]) / 4.0f);
|
||||
int cos4 = chroma_to_four_bits((COs[10] + COs[11] + COs[14] + COs[15]) / 4.0f);
|
||||
|
||||
int cgs1 = chroma_to_four_bits((CGs[0] + CGs[1] + CGs[4] + CGs[5]) / 4.0f);
|
||||
int cgs2 = chroma_to_four_bits((CGs[2] + CGs[3] + CGs[6] + CGs[7]) / 4.0f);
|
||||
int cgs3 = chroma_to_four_bits((CGs[8] + CGs[9] + CGs[12] + CGs[13]) / 4.0f);
|
||||
int cgs4 = chroma_to_four_bits((CGs[10] + CGs[11] + CGs[14] + CGs[15]) / 4.0f);
|
||||
|
||||
// Pack according to iPF1 format
|
||||
// uint16 [Co4 | Co3 | Co2 | Co1]
|
||||
out[0] = (cos2 << 4) | cos1;
|
||||
out[1] = (cos4 << 4) | cos3;
|
||||
// uint16 [Cg4 | Cg3 | Cg2 | Cg1]
|
||||
out[2] = (cgs2 << 4) | cgs1;
|
||||
out[3] = (cgs4 << 4) | cgs3;
|
||||
// Y values: [Y1|Y0|Y5|Y4], [Y3|Y2|Y7|Y6], [Y9|Y8|YD|YC], [YB|YA|YF|YE]
|
||||
out[4] = (Ys[1] << 4) | Ys[0];
|
||||
out[5] = (Ys[5] << 4) | Ys[4];
|
||||
out[6] = (Ys[3] << 4) | Ys[2];
|
||||
out[7] = (Ys[7] << 4) | Ys[6];
|
||||
out[8] = (Ys[9] << 4) | Ys[8];
|
||||
out[9] = (Ys[13] << 4) | Ys[12];
|
||||
out[10] = (Ys[11] << 4) | Ys[10];
|
||||
out[11] = (Ys[15] << 4) | Ys[14];
|
||||
|
||||
int block_size = 12;
|
||||
|
||||
if (has_alpha) {
|
||||
// Alpha values: same layout as Y
|
||||
out[12] = (As[1] << 4) | As[0];
|
||||
out[13] = (As[5] << 4) | As[4];
|
||||
out[14] = (As[3] << 4) | As[2];
|
||||
out[15] = (As[7] << 4) | As[6];
|
||||
out[16] = (As[9] << 4) | As[8];
|
||||
out[17] = (As[13] << 4) | As[12];
|
||||
out[18] = (As[11] << 4) | As[10];
|
||||
out[19] = (As[15] << 4) | As[14];
|
||||
block_size = 20;
|
||||
}
|
||||
|
||||
return block_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode iPF2 block (4:2:2 chroma subsampling).
|
||||
* Returns 16 bytes (or 24 with alpha).
|
||||
*/
|
||||
static int encode_ipf2_block(const int *Ys, const int *As, const float *COs, const float *CGs,
|
||||
int has_alpha, uint8_t *out) {
|
||||
// Subsample Co/Cg horizontally only (4:2:2) - 8 values each
|
||||
int cos1 = chroma_to_four_bits((COs[0] + COs[1]) / 2.0f);
|
||||
int cos2 = chroma_to_four_bits((COs[2] + COs[3]) / 2.0f);
|
||||
int cos3 = chroma_to_four_bits((COs[4] + COs[5]) / 2.0f);
|
||||
int cos4 = chroma_to_four_bits((COs[6] + COs[7]) / 2.0f);
|
||||
int cos5 = chroma_to_four_bits((COs[8] + COs[9]) / 2.0f);
|
||||
int cos6 = chroma_to_four_bits((COs[10] + COs[11]) / 2.0f);
|
||||
int cos7 = chroma_to_four_bits((COs[12] + COs[13]) / 2.0f);
|
||||
int cos8 = chroma_to_four_bits((COs[14] + COs[15]) / 2.0f);
|
||||
|
||||
int cgs1 = chroma_to_four_bits((CGs[0] + CGs[1]) / 2.0f);
|
||||
int cgs2 = chroma_to_four_bits((CGs[2] + CGs[3]) / 2.0f);
|
||||
int cgs3 = chroma_to_four_bits((CGs[4] + CGs[5]) / 2.0f);
|
||||
int cgs4 = chroma_to_four_bits((CGs[6] + CGs[7]) / 2.0f);
|
||||
int cgs5 = chroma_to_four_bits((CGs[8] + CGs[9]) / 2.0f);
|
||||
int cgs6 = chroma_to_four_bits((CGs[10] + CGs[11]) / 2.0f);
|
||||
int cgs7 = chroma_to_four_bits((CGs[12] + CGs[13]) / 2.0f);
|
||||
int cgs8 = chroma_to_four_bits((CGs[14] + CGs[15]) / 2.0f);
|
||||
|
||||
// Pack according to iPF2 format
|
||||
// uint32 [Co8 | Co7 | Co6 | Co5 | Co4 | Co3 | Co2 | Co1]
|
||||
out[0] = (cos2 << 4) | cos1;
|
||||
out[1] = (cos4 << 4) | cos3;
|
||||
out[2] = (cos6 << 4) | cos5;
|
||||
out[3] = (cos8 << 4) | cos7;
|
||||
// uint32 [Cg8 | Cg7 | Cg6 | Cg5 | Cg4 | Cg3 | Cg2 | Cg1]
|
||||
out[4] = (cgs2 << 4) | cgs1;
|
||||
out[5] = (cgs4 << 4) | cgs3;
|
||||
out[6] = (cgs6 << 4) | cgs5;
|
||||
out[7] = (cgs8 << 4) | cgs7;
|
||||
// Y values: same as iPF1
|
||||
out[8] = (Ys[1] << 4) | Ys[0];
|
||||
out[9] = (Ys[5] << 4) | Ys[4];
|
||||
out[10] = (Ys[3] << 4) | Ys[2];
|
||||
out[11] = (Ys[7] << 4) | Ys[6];
|
||||
out[12] = (Ys[9] << 4) | Ys[8];
|
||||
out[13] = (Ys[13] << 4) | Ys[12];
|
||||
out[14] = (Ys[11] << 4) | Ys[10];
|
||||
out[15] = (Ys[15] << 4) | Ys[14];
|
||||
|
||||
int block_size = 16;
|
||||
|
||||
if (has_alpha) {
|
||||
// Alpha values: same layout as Y
|
||||
out[16] = (As[1] << 4) | As[0];
|
||||
out[17] = (As[5] << 4) | As[4];
|
||||
out[18] = (As[3] << 4) | As[2];
|
||||
out[19] = (As[7] << 4) | As[6];
|
||||
out[20] = (As[9] << 4) | As[8];
|
||||
out[21] = (As[13] << 4) | As[12];
|
||||
out[22] = (As[11] << 4) | As[10];
|
||||
out[23] = (As[15] << 4) | As[14];
|
||||
block_size = 24;
|
||||
}
|
||||
|
||||
return block_size;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Adam7 Progressive Ordering
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get Adam7 pass number for a block at (block_x, block_y).
|
||||
* For blocks, we use a simplified version based on block position.
|
||||
*/
|
||||
static int get_adam7_pass(int block_x, int block_y) {
|
||||
// Use Adam7 pattern for 8x8 blocks, but adapt for 4x4 block indices
|
||||
int px = (block_x * 4) % 8;
|
||||
int py = (block_y * 4) % 8;
|
||||
return ADAM7_PASS[py][px];
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode blocks in Adam7 progressive order.
|
||||
* Returns the encoded block data in progressive order.
|
||||
*/
|
||||
static uint8_t* encode_progressive(const image_t *img, const encoder_config_t *cfg,
|
||||
int has_alpha, size_t *out_size) {
|
||||
int blocks_x = (img->width + 3) / 4;
|
||||
int blocks_y = (img->height + 3) / 4;
|
||||
int total_blocks = blocks_x * blocks_y;
|
||||
|
||||
int block_size = (cfg->ipf_type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16);
|
||||
size_t max_size = (size_t)total_blocks * block_size;
|
||||
|
||||
uint8_t *output = malloc(max_size);
|
||||
if (!output) return NULL;
|
||||
|
||||
// Temporary storage for all encoded blocks
|
||||
uint8_t *all_blocks = malloc(max_size);
|
||||
if (!all_blocks) {
|
||||
free(output);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Encode all blocks first
|
||||
size_t offset = 0;
|
||||
for (int by = 0; by < blocks_y; by++) {
|
||||
for (int bx = 0; bx < blocks_x; bx++) {
|
||||
int Ys[16], As[16];
|
||||
float COs[16], CGs[16];
|
||||
|
||||
encode_block_to_ycocg(img, bx, by, cfg->dither, Ys, As, COs, CGs);
|
||||
|
||||
if (cfg->ipf_type == IPF_TYPE_1) {
|
||||
encode_ipf1_block(Ys, As, COs, CGs, has_alpha, all_blocks + offset);
|
||||
} else {
|
||||
encode_ipf2_block(Ys, As, COs, CGs, has_alpha, all_blocks + offset);
|
||||
}
|
||||
offset += block_size;
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder blocks according to Adam7 progressive order (7 passes)
|
||||
size_t out_offset = 0;
|
||||
for (int pass = 1; pass <= 7; pass++) {
|
||||
for (int by = 0; by < blocks_y; by++) {
|
||||
for (int bx = 0; bx < blocks_x; bx++) {
|
||||
if (get_adam7_pass(bx, by) == pass) {
|
||||
int block_idx = by * blocks_x + bx;
|
||||
memcpy(output + out_offset, all_blocks + block_idx * block_size, block_size);
|
||||
out_offset += block_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(all_blocks);
|
||||
*out_size = out_offset;
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode blocks in sequential (raster) order.
|
||||
*/
|
||||
static uint8_t* encode_sequential(const image_t *img, const encoder_config_t *cfg,
|
||||
int has_alpha, size_t *out_size) {
|
||||
int blocks_x = (img->width + 3) / 4;
|
||||
int blocks_y = (img->height + 3) / 4;
|
||||
int total_blocks = blocks_x * blocks_y;
|
||||
|
||||
int block_size = (cfg->ipf_type == IPF_TYPE_1) ? (has_alpha ? 20 : 12) : (has_alpha ? 24 : 16);
|
||||
size_t max_size = (size_t)total_blocks * block_size;
|
||||
|
||||
uint8_t *output = malloc(max_size);
|
||||
if (!output) return NULL;
|
||||
|
||||
size_t offset = 0;
|
||||
for (int by = 0; by < blocks_y; by++) {
|
||||
for (int bx = 0; bx < blocks_x; bx++) {
|
||||
int Ys[16], As[16];
|
||||
float COs[16], CGs[16];
|
||||
|
||||
encode_block_to_ycocg(img, bx, by, cfg->dither, Ys, As, COs, CGs);
|
||||
|
||||
if (cfg->ipf_type == IPF_TYPE_1) {
|
||||
offset += encode_ipf1_block(Ys, As, COs, CGs, has_alpha, output + offset);
|
||||
} else {
|
||||
offset += encode_ipf2_block(Ys, As, COs, CGs, has_alpha, output + offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*out_size = offset;
|
||||
return output;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// iPF File Writing
|
||||
// =============================================================================
|
||||
|
||||
static int write_ipf_file(const char *output_file, const encoder_config_t *cfg,
|
||||
const image_t *img, int verbose) {
|
||||
// Determine if we use alpha
|
||||
int has_alpha = 0;
|
||||
if (cfg->force_alpha) {
|
||||
has_alpha = 1;
|
||||
} else if (!cfg->no_alpha && img->has_alpha) {
|
||||
has_alpha = 1;
|
||||
}
|
||||
|
||||
// Encode blocks
|
||||
size_t block_data_size;
|
||||
uint8_t *block_data;
|
||||
|
||||
if (cfg->progressive) {
|
||||
block_data = encode_progressive(img, cfg, has_alpha, &block_data_size);
|
||||
} else {
|
||||
block_data = encode_sequential(img, cfg, has_alpha, &block_data_size);
|
||||
}
|
||||
|
||||
if (!block_data) {
|
||||
fprintf(stderr, "Error: Failed to encode image blocks\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
printf("Encoded %zu bytes of block data\n", block_data_size);
|
||||
}
|
||||
|
||||
// Prepare output data (may be compressed)
|
||||
uint8_t *output_data = block_data;
|
||||
size_t output_size = block_data_size;
|
||||
uint8_t *compressed_data = NULL;
|
||||
|
||||
if (cfg->use_zstd) {
|
||||
size_t max_compressed = ZSTD_compressBound(block_data_size);
|
||||
compressed_data = malloc(max_compressed);
|
||||
if (!compressed_data) {
|
||||
free(block_data);
|
||||
fprintf(stderr, "Error: Failed to allocate compression buffer\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
output_size = ZSTD_compress(compressed_data, max_compressed,
|
||||
block_data, block_data_size, 7);
|
||||
if (ZSTD_isError(output_size)) {
|
||||
fprintf(stderr, "Error: Zstd compression failed: %s\n",
|
||||
ZSTD_getErrorName(output_size));
|
||||
free(block_data);
|
||||
free(compressed_data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
output_data = compressed_data;
|
||||
|
||||
if (verbose) {
|
||||
printf("Compressed: %zu -> %zu bytes (%.1f%%)\n",
|
||||
block_data_size, output_size,
|
||||
100.0 * output_size / block_data_size);
|
||||
}
|
||||
}
|
||||
|
||||
// Open output file
|
||||
FILE *fp = fopen(output_file, "wb");
|
||||
if (!fp) {
|
||||
fprintf(stderr, "Error: Failed to open output file: %s\n", output_file);
|
||||
free(block_data);
|
||||
if (compressed_data) free(compressed_data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Build flags byte
|
||||
uint8_t flags = 0;
|
||||
if (has_alpha) flags |= IPF_FLAG_ALPHA;
|
||||
if (cfg->use_zstd) flags |= IPF_FLAG_ZSTD;
|
||||
if (cfg->progressive) flags |= IPF_FLAG_PROGRESSIVE | IPF_FLAG_ZSTD; // Progressive always sets zstd flag
|
||||
|
||||
// Write header
|
||||
// Magic: "\x1FTSVMiPF" (8 bytes)
|
||||
fwrite(IPF_MAGIC, 1, 8, fp);
|
||||
|
||||
// Width (uint16 LE)
|
||||
uint16_t width_le = (uint16_t)cfg->width;
|
||||
fwrite(&width_le, 2, 1, fp);
|
||||
|
||||
// Height (uint16 LE)
|
||||
uint16_t height_le = (uint16_t)cfg->height;
|
||||
fwrite(&height_le, 2, 1, fp);
|
||||
|
||||
// Flags (uint8)
|
||||
fwrite(&flags, 1, 1, fp);
|
||||
|
||||
// Type (uint8)
|
||||
uint8_t type_byte = (uint8_t)cfg->ipf_type;
|
||||
fwrite(&type_byte, 1, 1, fp);
|
||||
|
||||
// Reserved (10 bytes)
|
||||
uint8_t reserved[10] = {0};
|
||||
fwrite(reserved, 1, 10, fp);
|
||||
|
||||
// Uncompressed size (uint32 LE)
|
||||
uint32_t uncompressed_size_le = (uint32_t)block_data_size;
|
||||
fwrite(&uncompressed_size_le, 4, 1, fp);
|
||||
|
||||
// Write block data
|
||||
fwrite(output_data, 1, output_size, fp);
|
||||
|
||||
fclose(fp);
|
||||
|
||||
if (verbose) {
|
||||
printf("Wrote %zu bytes to %s\n", IPF_HEADER_SIZE + output_size, output_file);
|
||||
printf(" Format: iPF%d, %dx%d\n", cfg->ipf_type + 1, cfg->width, cfg->height);
|
||||
printf(" Flags: %s%s%s\n",
|
||||
has_alpha ? "alpha " : "",
|
||||
cfg->use_zstd ? "zstd " : "",
|
||||
cfg->progressive ? "progressive " : "");
|
||||
}
|
||||
|
||||
free(block_data);
|
||||
if (compressed_data) free(compressed_data);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main Entry Point
|
||||
// =============================================================================
|
||||
|
||||
static int parse_size(const char *arg, int *width, int *height) {
|
||||
return sscanf(arg, "%dx%d", width, height) == 2 ? 0 : -1;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
encoder_config_t cfg = {
|
||||
.input_file = NULL,
|
||||
.output_file = NULL,
|
||||
.width = DEFAULT_WIDTH,
|
||||
.height = DEFAULT_HEIGHT,
|
||||
.ipf_type = IPF_TYPE_1,
|
||||
.use_zstd = 1,
|
||||
.force_alpha = 0,
|
||||
.no_alpha = 0,
|
||||
.progressive = 0,
|
||||
.dither = 0,
|
||||
.verbose = 0
|
||||
};
|
||||
|
||||
static struct option long_options[] = {
|
||||
{"input", required_argument, 0, 'i'},
|
||||
{"output", required_argument, 0, 'o'},
|
||||
{"size", required_argument, 0, 's'},
|
||||
{"type", required_argument, 0, 't'},
|
||||
{"no-zstd", no_argument, 0, 'Z'},
|
||||
{"alpha", no_argument, 0, 'A'},
|
||||
{"no-alpha", no_argument, 0, 'N'},
|
||||
{"progressive", no_argument, 0, 'p'},
|
||||
{"dither", required_argument, 0, 'd'},
|
||||
{"verbose", no_argument, 0, 'v'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{0, 0, 0, 0}
|
||||
};
|
||||
|
||||
int opt;
|
||||
while ((opt = getopt_long(argc, argv, "i:o:s:t:pd:vh", long_options, NULL)) != -1) {
|
||||
switch (opt) {
|
||||
case 'i':
|
||||
cfg.input_file = optarg;
|
||||
break;
|
||||
case 'o':
|
||||
cfg.output_file = optarg;
|
||||
break;
|
||||
case 's':
|
||||
if (parse_size(optarg, &cfg.width, &cfg.height) != 0) {
|
||||
fprintf(stderr, "Error: Invalid size format (use WxH)\n");
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
case 't':
|
||||
cfg.ipf_type = atoi(optarg) - 1; // User specifies 1 or 2
|
||||
if (cfg.ipf_type < 0 || cfg.ipf_type > 1) {
|
||||
fprintf(stderr, "Error: Invalid iPF type (use 1 or 2)\n");
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
case 'Z':
|
||||
cfg.use_zstd = 0;
|
||||
break;
|
||||
case 'A':
|
||||
cfg.force_alpha = 1;
|
||||
break;
|
||||
case 'N':
|
||||
cfg.no_alpha = 1;
|
||||
break;
|
||||
case 'p':
|
||||
cfg.progressive = 1;
|
||||
break;
|
||||
case 'd':
|
||||
cfg.dither = atoi(optarg);
|
||||
break;
|
||||
case 'v':
|
||||
cfg.verbose = 1;
|
||||
break;
|
||||
case 'h':
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
default:
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required arguments
|
||||
if (!cfg.input_file || !cfg.output_file) {
|
||||
fprintf(stderr, "Error: Input and output files are required\n\n");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Load image
|
||||
if (cfg.verbose) {
|
||||
printf("Loading image: %s\n", cfg.input_file);
|
||||
}
|
||||
|
||||
image_t *img = load_image(cfg.input_file, cfg.width, cfg.height,
|
||||
cfg.force_alpha, cfg.verbose);
|
||||
if (!img) {
|
||||
fprintf(stderr, "Error: Failed to load image\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Encode and write iPF file
|
||||
int result = write_ipf_file(cfg.output_file, &cfg, img, cfg.verbose);
|
||||
|
||||
free_image(img);
|
||||
|
||||
if (result == 0) {
|
||||
printf("Successfully encoded: %s\n", cfg.output_file);
|
||||
}
|
||||
|
||||
return result == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -1,20 +1,8 @@
|
||||
## How To Edit the Graaljs Jars
|
||||
## GraalJS JAR Editing (OBSOLETE)
|
||||
|
||||
0. Download following from Maven:
|
||||
The META-INF/services cross-registration hack was needed for GraalJS 22.3.1 where
|
||||
`js` and `regex` JARs each needed the other's `TruffleLanguage$Provider` registered.
|
||||
|
||||
org.graalvm.js:js:00.0.0
|
||||
org.graalvm.js:js-scriptengine:00.0.0
|
||||
|
||||
1. grab `js-00.0.0.jar`
|
||||
2. on `META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider`, edit as shown:
|
||||
|
||||
com.oracle.truffle.js.lang.JavaScriptLanguageProvider (existing line)
|
||||
com.oracle.truffle.regex.RegexLanguageProvider (<< add this line)
|
||||
|
||||
3. grab `regex-00.0.0.jar`
|
||||
4. on `META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider`, edit as shown:
|
||||
|
||||
com.oracle.truffle.regex.RegexLanguageProvider (existing line)
|
||||
com.oracle.truffle.js.lang.JavaScriptLanguageProvider (<< add this line)
|
||||
|
||||
5. Re-zip two files
|
||||
As of GraalJS 24.1.2, the service discovery mechanism changed to
|
||||
`TruffleLanguageProvider` and each JAR registers its own provider independently.
|
||||
No JAR editing is required.
|
||||
|
||||
BIN
lib/collections-23.1.10-javadoc.jar
Normal file
BIN
lib/collections-23.1.10-javadoc.jar
Normal file
Binary file not shown.
BIN
lib/collections-23.1.10-sources.jar
Normal file
BIN
lib/collections-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/collections-23.1.10.jar
Normal file
BIN
lib/collections-23.1.10.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
lib/compiler-23.1.10-sources.jar
Normal file
BIN
lib/compiler-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/compiler-23.1.10.jar
Normal file
BIN
lib/compiler-23.1.10.jar
Normal file
Binary file not shown.
BIN
lib/compiler-management-23.1.10-javadoc.jar
Normal file
BIN
lib/compiler-management-23.1.10-javadoc.jar
Normal file
Binary file not shown.
BIN
lib/compiler-management-23.1.10-sources.jar
Normal file
BIN
lib/compiler-management-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/compiler-management-23.1.10.jar
Normal file
BIN
lib/compiler-management-23.1.10.jar
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
lib/icu4j-23.1.10-javadoc.jar
Normal file
BIN
lib/icu4j-23.1.10-javadoc.jar
Normal file
Binary file not shown.
BIN
lib/icu4j-23.1.10-sources.jar
Normal file
BIN
lib/icu4j-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/icu4j-23.1.10.jar
Normal file
BIN
lib/icu4j-23.1.10.jar
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
lib/jniutils-23.1.10-javadoc.jar
Normal file
BIN
lib/jniutils-23.1.10-javadoc.jar
Normal file
Binary file not shown.
BIN
lib/jniutils-23.1.10-sources.jar
Normal file
BIN
lib/jniutils-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/jniutils-23.1.10.jar
Normal file
BIN
lib/jniutils-23.1.10.jar
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
lib/js-language-23.1.10-sources.jar
Normal file
BIN
lib/js-language-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/js-language-23.1.10.jar
Normal file
BIN
lib/js-language-23.1.10.jar
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
lib/js-scriptengine-23.1.10-javadoc.jar
Normal file
BIN
lib/js-scriptengine-23.1.10-javadoc.jar
Normal file
Binary file not shown.
BIN
lib/js-scriptengine-23.1.10-sources.jar
Normal file
BIN
lib/js-scriptengine-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/js-scriptengine-23.1.10.jar
Normal file
BIN
lib/js-scriptengine-23.1.10.jar
Normal file
Binary file not shown.
BIN
lib/nativeimage-23.1.10-javadoc.jar
Normal file
BIN
lib/nativeimage-23.1.10-javadoc.jar
Normal file
Binary file not shown.
BIN
lib/nativeimage-23.1.10-sources.jar
Normal file
BIN
lib/nativeimage-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/nativeimage-23.1.10.jar
Normal file
BIN
lib/nativeimage-23.1.10.jar
Normal file
Binary file not shown.
BIN
lib/polyglot-23.1.10-javadoc.jar
Normal file
BIN
lib/polyglot-23.1.10-javadoc.jar
Normal file
Binary file not shown.
BIN
lib/polyglot-23.1.10-sources.jar
Normal file
BIN
lib/polyglot-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/polyglot-23.1.10.jar
Normal file
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.
BIN
lib/regex-23.1.10-javadoc.jar
Normal file
BIN
lib/regex-23.1.10-javadoc.jar
Normal file
Binary file not shown.
BIN
lib/regex-23.1.10-sources.jar
Normal file
BIN
lib/regex-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/regex-23.1.10.jar
Normal file
BIN
lib/regex-23.1.10.jar
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
lib/truffle-api-23.1.10-javadoc.jar
Normal file
BIN
lib/truffle-api-23.1.10-javadoc.jar
Normal file
Binary file not shown.
BIN
lib/truffle-api-23.1.10-sources.jar
Normal file
BIN
lib/truffle-api-23.1.10-sources.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
lib/truffle-compiler-23.1.10-javadoc.jar
Normal file
BIN
lib/truffle-compiler-23.1.10-javadoc.jar
Normal file
Binary file not shown.
BIN
lib/truffle-compiler-23.1.10-sources.jar
Normal file
BIN
lib/truffle-compiler-23.1.10-sources.jar
Normal file
Binary file not shown.
BIN
lib/truffle-compiler-23.1.10.jar
Normal file
BIN
lib/truffle-compiler-23.1.10.jar
Normal file
Binary file not shown.
BIN
lib/truffle-runtime-23.1.10-javadoc.jar
Normal file
BIN
lib/truffle-runtime-23.1.10-javadoc.jar
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user