From 4e647f9fe1990d20eeb8b4018a29ac08030eab9c Mon Sep 17 00:00:00 2001 From: minjaesong Date: Mon, 20 Apr 2026 17:38:02 +0900 Subject: [PATCH] tracker wip --- assets/disk0/tvdos/bin/taut.js | 126 +++++++++++ assets/disk0/tvdos/bin/tautfont.kra | 3 + assets/disk0/tvdos/bin/tautfont_high.chr | Bin 0 -> 1920 bytes assets/disk0/tvdos/tuidev/Makefile | 6 + assets/disk0/tvdos/tuidev/font_rom_builder.c | 202 ++++++++++++++++++ .../torvald/tsvm/peripheral/AudioAdapter.kt | 4 +- 6 files changed, 339 insertions(+), 2 deletions(-) create mode 100644 assets/disk0/tvdos/bin/taut.js create mode 100644 assets/disk0/tvdos/bin/tautfont.kra create mode 100644 assets/disk0/tvdos/bin/tautfont_high.chr create mode 100644 assets/disk0/tvdos/tuidev/Makefile create mode 100644 assets/disk0/tvdos/tuidev/font_rom_builder.c diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js new file mode 100644 index 0000000..86eb898 --- /dev/null +++ b/assets/disk0/tvdos/bin/taut.js @@ -0,0 +1,126 @@ +/** + * TSVM Audio Device Tracker + * + * Created by minjaesong on 2026-04-20 + */ + +const win = require("wintex") + +const sym = { +/* accidentals */ +accnull:"\u0094\u0095" +demisharp:"\u0094\u0080", +sharp:"\u0094\u0081", +sesquisharp:"\u0082\u0083", +doublesharp:"\u0094\u0084", +triplesharp:"\u0081\u0084", +quadsharp:"\u0085\u0086", +demiflat:"\u0094\u0087", +flat:"\u0094\u0088", +sesquiflat:"\u0090\u0091", +doubleflat:"\u0089\u008A", +tripleflat:"\u008B\u008C", +quadflat:"\u008D\u008E", + +/* special notes */ +keyoff:"\u0092\u00CD\u00CD\u0093", +notecut:"\u008F\u008F\u008F\u008F", + +/* miscellaneous */ +cent:"\u009B", +unticked:"\u009E", +ticked:"\u009F", +} + +const pitchTablePresets = [ +{name:"null", table:[]}, +/* Xenharmonic, equal temperament */ +{name:"5-TET", table:[0x0,0x333,0x666,0x99A,0xCCD]}, +{name:"7-TET", table:[0x0,0x249,0x492,0x6DB,0x925,0xB6E,0xDB7]}, +{name:"10-TET", table:[0x0,0x19A,0x333,0x4CD,0x666,0x800,0x99A,0xB33,0xCCD,0xE66]}, +{name:"16-TET", table:[0x0,0x100,0x200,0x300,0x400,0x500,0x600,0x700,0x800,0x900,0xA00,0xB00,0xC00,0xD00,0xE00,0xF00]}, +{name:"19-TET", table:[0x0,0xD8,0x1AF,0x287,0x35E,0x436,0x50D,0x5E5,0x6BD,0x794,0x86C,0x943,0xA1B,0xAF3,0xBCA,0xCA2,0xD79,0xE51,0xF28]}, +{name:"22-TET", table:[0x0,0xBA,0x174,0x22F,0x2E9,0x3A3,0x45D,0x517,0x5D1,0x68C,0x746,0x800,0x8BA,0x974,0xA2F,0xAE9,0xBA3,0xC5D,0xD17,0xDD1,0xE8C,0xF46]}, +{name:"24-TET", table:[0x0,0xAB,0x155,0x200,0x2AB,0x355,0x400,0x4AB,0x555,0x600,0x6AB,0x755,0x800,0x8AB,0x955,0xA00,0xAAB,0xB55,0xC00,0xCAB,0xD55,0xE00,0xEAB,0xF55]}, +{name:"31-TET", table:[0x0,0x84,0x108,0x18C,0x211,0x295,0x319,0x39D,0x421,0x4A5,0x529,0x5AD,0x632,0x6B6,0x73A,0x7BE,0x842,0x8C6,0x94A,0x9CE,0xA53,0xAD7,0xB5B,0xBDF,0xC63,0xCE7,0xD6B,0xDEF,0xE74,0xEF8,0xF7C]}, +{name:"53-TET", table:[0x0,0x4D,0x9B,0xE8,0x135,0x182,0x1D0,0x21D,0x26A,0x2B8,0x305,0x352,0x39F,0x3ED,0x43A,0x487,0x4D5,0x522,0x56F,0x5BC,0x60A,0x657,0x6A4,0x6F2,0x73F,0x78C,0x7D9,0x827,0x874,0x8C1,0x90E,0x95C,0x9A9,0x9F6,0xA44,0xA91,0xADE,0xB2B,0xB79,0xBC6,0xC13,0xC61,0xCAE,0xCFB,0xD48,0xD96,0xDE3,0xE30,0xE7E,0xECB,0xF18,0xF65,0xFB3]}, +/* 12-TET variations */ +{name:"12-TET", table:[0x0,0x155,0x2AB,0x400,0x555,0x6AB,0x800,0x955,0xAAB,0xC00,0xD55,0xEAB]}, +{name:"Pythagorean Diminished Fifth", table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x7D8,0x95C,0xA90,0xC14,0xD48,0xECC]}, +{name:"Pythagorean Augmented Fourth", table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x828,0x95C,0xA90,0xC14,0xD48,0xECC]}, +{name:"Shierlu", table:[0x0,0x184,0x2B8,0x43C,0x570,0x6F4,0x828,0x95C,0xAE0,0xC14,0xD98,0xECC]}, + + +] + +function noteName4096(n) { + const N = 4096; + + // 12-TET natural note positions (C..B) + const d12 = [0, 2, 4, 5, 7, 9, 11]; + const letters = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; + + // Precompute bases (integer-rounded) + // base[i] = round(d12[i] * N / 12) + const base = d12.map(d => ((d * N + 6) / 12) | 0); + + // Scale everything by 24 (quarter-semitone grid) + const pitch24 = n * 24; + const N24 = N; // because k multiplies N directly in scaled space + + let best_i = 0; + let best_k = 0; + let best_score = Infinity; + + const KMAX = 8; // up to quad accidentals (±8 quarter-steps) + + for (let i = 0; i < 7; i++) { + const base24 = base[i] * 24; + const delta = pitch24 - base24; + + // nearest integer k ≈ delta / N + let k = ((delta + (delta >= 0 ? N24 / 2 : -N24 / 2)) / N24) | 0; + + // clamp to allowed accidentals + if (k > KMAX) k = KMAX; + if (k < -KMAX) k = -KMAX; + + const err = Math.abs(delta - k * N24); + + // scoring: prioritize pitch accuracy, then smaller accidentals + const score = err * 1000 + Math.abs(k) * 10; + + if (score < best_score) { + best_score = score; + best_i = i; + best_k = k; + } + } + + // accidental mapping + function accidental(k) { + switch (k) { + case 0: return sym.accnull; + case 1: return sym.demisharp; + case 2: return sym.sharp; + case 3: return sym.sesquisharp; + case 4: return sym.doublesharp; + case 5: return sym.triplesharp; + case 6: return sym.quadsharp; + case -1: return sym.demiflat; + case -2: return sym.flat; + case -3: return sym.sesquiflat; + case -4: return sym.doubleflat; + case -5: return sym.tripleflat; + case -6: return sym.quadflat; + default: + // fallback if you extend beyond quad + return (k > 0 ? '+' : '-') + k.toString(36); + } + } + + // octave (C-based) + const octave = ((n / N)|0) + 1; + + return letters[best_i] + accidental(best_k) + octave; +} \ No newline at end of file diff --git a/assets/disk0/tvdos/bin/tautfont.kra b/assets/disk0/tvdos/bin/tautfont.kra new file mode 100644 index 0000000..ac29e1c --- /dev/null +++ b/assets/disk0/tvdos/bin/tautfont.kra @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:388d2efdf4c8e7c0f13c44afa9ede8740a88eb1d84a54e3f11444096cfb0c089 +size 83324 diff --git a/assets/disk0/tvdos/bin/tautfont_high.chr b/assets/disk0/tvdos/bin/tautfont_high.chr new file mode 100644 index 0000000000000000000000000000000000000000..96c8ef89801e02048d56b404b406c3cc2c72e9d3 GIT binary patch literal 1920 zcmd5+QES^U5WaLFQ%hPqtszdA#UX@HMjwtLe<0vUR8${xbv2haZN zJLx1(f<2b8&ylR}zPr=Ob`F5Ct8}ggAjXrl_%6p7N#p$7i+XAx^TYgy8oqRt*zW@t z?Ds@qI=yIKOsASHmv{c%a*6Eh?Cdt3jApl2Eo8&t@IIZ5CiioWtkdaqqh9Bx`?g1M zJYTVp?>-@0EEd~JEH>MdK!$NyIU9*g#jV%tr|O_4)O_EMt8cwGaq$(IM3~4JiAJMw z8QpYxm#<%u!P-F@9{2;<)m*kpi-9cx6t?sN>vlWcA<{5Z$@($p1?Rc|k%T2)Bti9A zp`u-XJwCRUhmTJ$P{V(d;yTXa>$;j%7H2ET*vP)VytA*<*1p(ZklVJLX&NXT=@!tQ zh0R>gW7D{5AG)cy!qJXR{fUr=`Ce2Jn3Hy**{O_qTB36GTh_M9J_GYKBhh&3INQ@R zu2f6cuD$C=I=Q?>zAc`>B$ZkPCo8aPJf(!1_Ea7x`bSTP?E1C>UY7K#0p8gjzbU M-T5&8#Q(e1Z^qWN1poj5 literal 0 HcmV?d00001 diff --git a/assets/disk0/tvdos/tuidev/Makefile b/assets/disk0/tvdos/tuidev/Makefile new file mode 100644 index 0000000..42be8b3 --- /dev/null +++ b/assets/disk0/tvdos/tuidev/Makefile @@ -0,0 +1,6 @@ +CC = gcc +CFLAGS = -std=c99 -O3 -Wall -Wextra -Ofast -D_GNU_SOURCE + +font_rom_builder: + rm -f font_rom_builder + $(CC) $(CFLAGS) font_rom_builder.c -o font_rom_builder diff --git a/assets/disk0/tvdos/tuidev/font_rom_builder.c b/assets/disk0/tvdos/tuidev/font_rom_builder.c new file mode 100644 index 0000000..d14d53a --- /dev/null +++ b/assets/disk0/tvdos/tuidev/font_rom_builder.c @@ -0,0 +1,202 @@ +/* + * font_rom_builder.c + * Build TSVM 7x14 font ROM from human-readable images (.png, .tga) + * + * Input: Image with no gaps between characters (7x14 pixels per glyph) + * Output: TSVM-compatible font ROM file(s) padded to 1920 bytes + * + * Usage: + * gcc -O2 -std=c99 -Wall font_rom_builder.c -o font_rom_builder + * ./font_rom_builder + * + * For 128-char images: outputs _high.chr + * For 256-char images: outputs _low.chr and _high.chr + * + * Image layout: + * - 128 chars: 16 columns × 8 rows = 112×112 pixels + * - 256 chars: 16 columns × 16 rows = 112×224 pixels + * or 32 columns × 8 rows = 224×112 pixels + * + * ROM format: + * - Each glyph: 14 bytes (one byte per row) + * - Bit 6 = leftmost pixel, Bit 0 = rightmost pixel + * - Each ROM padded to 1920 bytes + */ + +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include + +#define GLYPH_W 7 +#define GLYPH_H 14 +#define GLYPH_BYTES 14 +#define ROM_PADDED_SIZE 1920 + +static void die(const char *msg) { + fprintf(stderr, "Error: %s\n", msg); + exit(1); +} + +static void write_rom(const char *filename, const uint8_t *glyphs, int glyph_count) { + FILE *out = fopen(filename, "wb"); + if (!out) { + fprintf(stderr, "Failed to open output file: %s\n", filename); + exit(1); + } + + // Write glyph data + size_t data_size = glyph_count * GLYPH_BYTES; + fwrite(glyphs, 1, data_size, out); + + // Pad to 1920 bytes + if (data_size < ROM_PADDED_SIZE) { + size_t padding = ROM_PADDED_SIZE - data_size; + uint8_t *pad = calloc(padding, 1); + fwrite(pad, 1, padding, out); + free(pad); + fprintf(stderr, " Wrote %zu bytes + %zu bytes padding = %d bytes total\n", + data_size, padding, ROM_PADDED_SIZE); + } else { + fprintf(stderr, " Wrote %zu bytes (no padding needed)\n", data_size); + } + + fclose(out); + fprintf(stderr, " Output: %s\n", filename); +} + +int main(int argc, char **argv) { + if (argc < 3) { + fprintf(stderr, "Usage: %s \n", argv[0]); + fprintf(stderr, "\n"); + fprintf(stderr, "Converts human-readable font images to TSVM font ROM format.\n"); + fprintf(stderr, "\n"); + fprintf(stderr, "Input requirements:\n"); + fprintf(stderr, " - Image with no gaps between characters\n"); + fprintf(stderr, " - Each character is 7x14 pixels\n"); + fprintf(stderr, " - 128 chars: typically 112x112 (16 cols × 8 rows)\n"); + fprintf(stderr, " - 256 chars: typically 112x224 (16 cols × 16 rows)\n"); + fprintf(stderr, "\n"); + fprintf(stderr, "Output:\n"); + fprintf(stderr, " - 128 chars: _high.chr (high ROM only)\n"); + fprintf(stderr, " - 256 chars: _low.chr + _high.chr\n"); + fprintf(stderr, " - Each ROM padded to 1920 bytes\n"); + return 1; + } + + const char *input_path = argv[1]; + const char *output_prefix = argv[2]; + + // Get image dimensions using ImageMagick identify + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "identify -format '%%w %%h' \"%s\" 2>/dev/null", input_path); + + FILE *pipe = popen(cmd, "r"); + if (!pipe) die("Failed to run 'identify' command (ImageMagick required)"); + + int img_w = 0, img_h = 0; + if (fscanf(pipe, "%d %d", &img_w, &img_h) != 2) { + pclose(pipe); + die("Failed to read image dimensions (is ImageMagick installed?)"); + } + pclose(pipe); + + fprintf(stderr, "Input: %s (%dx%d)\n", input_path, img_w, img_h); + + // Calculate grid dimensions + int cols = img_w / GLYPH_W; + int rows = img_h / GLYPH_H; + int total_chars = cols * rows; + + if (img_w % GLYPH_W != 0 || img_h % GLYPH_H != 0) { + fprintf(stderr, "Warning: Image dimensions not evenly divisible by %dx%d\n", + GLYPH_W, GLYPH_H); + } + + fprintf(stderr, "Grid: %d columns × %d rows = %d characters\n", cols, rows, total_chars); + + // Validate character count + if (total_chars != 128 && total_chars != 256) { + fprintf(stderr, "Error: Expected 128 or 256 characters, got %d\n", total_chars); + fprintf(stderr, " For 128 chars: use 112x112 (16×8) or similar layout\n"); + fprintf(stderr, " For 256 chars: use 112x224 (16×16) or 224x112 (32×8)\n"); + return 1; + } + + // Read image as grayscale using ImageMagick convert + // IMPORTANT: Flatten alpha onto black background first, so transparent pixels become black + size_t img_size = img_w * img_h; + uint8_t *img_data = malloc(img_size); + if (!img_data) die("Memory allocation failed"); + + snprintf(cmd, sizeof(cmd), + "convert \"%s\" -background black -alpha remove -colorspace Gray -depth 8 gray:- 2>/dev/null", + input_path); + + pipe = popen(cmd, "r"); + if (!pipe) die("Failed to run 'convert' command (ImageMagick required)"); + + if (fread(img_data, 1, img_size, pipe) != img_size) { + pclose(pipe); + die("Failed to read image data from ImageMagick"); + } + pclose(pipe); + + fprintf(stderr, "Read %zu bytes of grayscale data\n", img_size); + + // Extract glyphs + uint8_t *glyphs = calloc(total_chars, GLYPH_BYTES); + if (!glyphs) die("Memory allocation failed"); + + for (int gy = 0; gy < rows; gy++) { + for (int gx = 0; gx < cols; gx++) { + int glyph_idx = gy * cols + gx; + uint8_t *glyph = &glyphs[glyph_idx * GLYPH_BYTES]; + + for (int row = 0; row < GLYPH_H; row++) { + uint8_t byte = 0; + for (int col = 0; col < GLYPH_W; col++) { + int px = gx * GLYPH_W + col; + int py = gy * GLYPH_H + row; + uint8_t pixel = img_data[py * img_w + px]; + + // Threshold: >= 128 is foreground (white/lit) + int is_set = (pixel >= 128) ? 1 : 0; + + // Pack: bit 6 = leftmost, bit 0 = rightmost + if (is_set) { + byte |= (1u << (6 - col)); + } + } + glyph[row] = byte; + } + } + } + + free(img_data); + fprintf(stderr, "Extracted %d glyphs\n", total_chars); + + // Write output ROM file(s) + char out_path[1024]; + + if (total_chars == 128) { + // High ROM only (chars 128-255) + snprintf(out_path, sizeof(out_path), "%s.chr", output_prefix); + fprintf(stderr, "\nWriting high ROM (128 chars):\n"); + write_rom(out_path, glyphs, 128); + } else { + // 256 chars: low ROM (0-127) and high ROM (128-255) + snprintf(out_path, sizeof(out_path), "%s_low.chr", output_prefix); + fprintf(stderr, "\nWriting low ROM (chars 0-127):\n"); + write_rom(out_path, glyphs, 128); + + snprintf(out_path, sizeof(out_path), "%s_high.chr", output_prefix); + fprintf(stderr, "\nWriting high ROM (chars 128-255):\n"); + write_rom(out_path, &glyphs[128 * GLYPH_BYTES], 128); + } + + free(glyphs); + fprintf(stderr, "\nDone.\n"); + return 0; +} diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index 1b2329c..6d47b12 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -299,7 +299,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { in 0..770047 -> sampleBin[addr] in 770048..786431 -> (adi - 770048).let { instruments[it / 64].getByte(it % 64) } in 786432..851967 -> { val off = adi - 786432; playdata[playheads[0].patBank1 * 128 + off / 512][(off % 512) / 8].getByte(off % 8) } - in 851968..917503 -> { val off = adi - 851968; playdata[playheads[0].patBank2 * 128 + 128 + off / 512][(off % 512) / 8].getByte(off % 8) } + in 851968..917503 -> { val off = adi - 851968; playdata[playheads[0].patBank2 * 128 + off / 512][(off % 512) / 8].getByte(off % 8) } in 917504..983039 -> tadInputBin[addr - 917504] // TAD input buffer (65536 bytes) in 983040..1048575 -> tadDecodedBin[addr - 983040] // TAD decoded output (65536 bytes) else -> peek(addr % 1048576) @@ -313,7 +313,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { in 0..770047 -> { sampleBin[addr] = byte } in 770048..786431 -> (adi - 770048).let { instruments[it / 64].setByte(it % 64, bi) } in 786432..851967 -> { val off = adi - 786432; playdata[playheads[0].patBank1 * 128 + off / 512][(off % 512) / 8].setByte(off % 8, bi) } - in 851968..917503 -> { val off = adi - 851968; playdata[playheads[0].patBank2 * 128 + 128 + off / 512][(off % 512) / 8].setByte(off % 8, bi) } + in 851968..917503 -> { val off = adi - 851968; playdata[playheads[0].patBank2 * 128 + off / 512][(off % 512) / 8].setByte(off % 8, bi) } in 917504..983039 -> tadInputBin[addr - 917504] = byte // TAD input buffer in 983040..1048575 -> tadDecodedBin[addr - 983040] = byte // TAD decoded output }