mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-08 14:24:05 +09:00
tracker impl, s3m converter, larger tracker sample bin
This commit is contained in:
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)
|
||||
@@ -1024,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 = {}
|
||||
|
||||
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 }
|
||||
Reference in New Issue
Block a user