all repos — xyw @ 3d73fbe268a4a5b7ffd27c5ce769dd100a94db53

A minimal virtual machine and assembler for terminals.

Re-implemented tetris theme with subroutine, renamed note macros, fixed a few assembler bugs related to processing words in .byte etc. + allowed a pitch of $0000 to be used as a pause.
h3rald h3rald@h3rald.com
Wed, 11 Feb 2026 14:20:19 +0100
commit

3d73fbe268a4a5b7ffd27c5ce769dd100a94db53

parent

f4ae1f7b2b4447693b14a4701aec895785f00871

4 files changed, 132 insertions(+), 106 deletions(-)

jump to
M devices/beeper.cdevices/beeper.c

@@ -123,8 +123,8 @@ pitch = BEEPER_MAX_FREQ;

if (level > BEEPER_MAX_LEVEL) level = BEEPER_MAX_LEVEL; - // Only beep if level > 0 and pitch > 0 and length > 0 - if (level > 0 && pitch > 0 && length > 0) + // Only act if level > 0 and length > 0 + if (level > 0 && length > 0) { // Convert length from 1/256 seconds to milliseconds // 1/256 second = 1000/256 ms ≈ 3.90625 ms

@@ -132,7 +132,11 @@ unsigned long duration_ms = ((unsigned long)length * 1000) / 256;

if (duration_ms == 0) duration_ms = 1; - beeper_play_tone(pitch, duration_ms, level); + // Pitch of 0 means silence (pause for the duration) + if (pitch == 0) + ma_sleep((ma_uint32)duration_ms); + else + beeper_play_tone(pitch, duration_ms, level); } break; }
M examples/korobeiniki.xywexamples/korobeiniki.xyw

@@ -1,58 +1,54 @@

.include "lib/macros.xyw" -;;; Korobeiniki - Traditional Russian folk song (Tetris theme) -;;; Main melody - Type A theme +;;; Korobeiniki - Traditional Russian folk song +;;; Main melody - "Type A" Tetris Theme -;; Note duration macros -.macro quarter $0060 -.macro eighth $0030 -.macro half $00C0 +; note duration macros +.macro n2 $00C0 ; half note +.macro n4 $0060 ; quarter note +.macro n8 $0030 ; eighth note -;; Helper to play a note -.macro PLAY beeper.pitch STW beeper.length STW $01 beeper.level STB +; store address of start song in xw +start-song POPxw -;; Main theme - Part A -;; E5 B4 C5 D5 | C5 B4 A4 A4 | C5 E5 D5 C5 | B4 B4 C5 D5 | E5 C5 A4 A4 +; process <length> <pitch> pairs until end-song +; a16 b16 -[xw]- +.label play + ; set length + LDWxw beeper.length STW + ; go to next word + INCxw INCxw + ; set pitch + LDWxw beeper.pitch STW + ; go to next word + INCxw INCxw + ; set volume to play the note + $01 beeper.level STB + ; repeat unless we are at the end of the song + PSHxw end-song LTHw play JCNw + ; terminate + $00 system.state STB -quarter note.e5 PLAY -eighth note.b4 PLAY -eighth note.c5 PLAY -quarter note.d5 PLAY -eighth note.c5 PLAY -eighth note.b4 PLAY -quarter note.a4 PLAY -eighth note.a4 PLAY -eighth note.c5 PLAY -quarter note.e5 PLAY -eighth note.d5 PLAY -eighth note.c5 PLAY -quarter note.b4 PLAY -eighth note.b4 PLAY -eighth note.c5 PLAY -quarter note.d5 PLAY -quarter note.e5 PLAY -quarter note.c5 PLAY -quarter note.a4 PLAY -half note.a4 PLAY +.label start-song -;; Main theme - Part B -;; D5 F5 A5 G5 | F5 E5 C5 C5 | E5 D5 C5 | B4 B4 C5 D5 | E5 C5 A4 A4 +; Main theme - Part A +; E5 B4 C5 D5 | C5 B4 A4 A4 | C5 E5 D5 C5 | B4 B4 C5 D5 | E5 C5 A4 A4 -quarter note.d5 PLAY -eighth note.f5 PLAY -quarter note.a5 PLAY -eighth note.g5 PLAY -eighth note.f5 PLAY -quarter note.e5 PLAY -eighth note.c5 PLAY -quarter note.e5 PLAY -eighth note.d5 PLAY -eighth note.c5 PLAY -quarter note.b4 PLAY -eighth note.b4 PLAY -eighth note.c5 PLAY -quarter note.d5 PLAY -quarter note.e5 PLAY -quarter note.c5 PLAY -quarter note.a4 PLAY -half note.a4 PLAY +.byte n4 e5 n8 b4 n8 c5 n4 d5 +.byte n8 c5 n8 b4 n4 a4 n8 a4 +.byte n8 c5 n4 e5 n8 d5 n8 c5 +.byte n4 b4 n8 b4 n8 c5 n4 d5 +.byte n4 e5 n4 c5 n4 a4 n2 a4 + +.byte n4 pause ; pause between parts + +; Main theme - Part B +; D5 F5 A5 G5 | F5 E5 C5 C5 | E5 D5 C5 | B4 B4 C5 D5 | E5 C5 A4 A4 + +.byte n4 d5 n8 f5 n4 a5 n8 g5 +.byte n8 f5 n4 e5 n8 c5 n8 c5 +.byte n4 e5 n8 d5 n8 c5 +.byte n4 b4 n8 b4 n8 c5 n4 d5 +.byte n4 e5 n4 c5 n4 a4 n2 a4 + +.label end-song
M examples/lib/macros.xywexamples/lib/macros.xyw

@@ -72,42 +72,42 @@ .macro file.type.file $01

.macro file.type.directory $02 ;;; Beeper notes +.macro pause $0000 ; silence +.macro c4 $0106 ; 262 Hz +.macro cs4 $0115 ; 277 Hz +.macro d4 $0126 ; 294 Hz +.macro ds4 $0137 ; 311 Hz +.macro e4 $014A ; 330 Hz +.macro f4 $015D ; 349 Hz +.macro fs4 $0172 ; 370 Hz +.macro g4 $0188 ; 392 Hz +.macro gs4 $019F ; 415 Hz +.macro a4 $01B8 ; 440 Hz +.macro as4 $01D2 ; 466 Hz +.macro b4 $01EE ; 494 Hz -.macro note.c4 $0106 ; 262 Hz -.macro note.cs4 $0115 ; 277 Hz -.macro note.d4 $0126 ; 294 Hz -.macro note.ds4 $0137 ; 311 Hz -.macro note.e4 $014A ; 330 Hz -.macro note.f4 $015D ; 349 Hz -.macro note.fs4 $0172 ; 370 Hz -.macro note.g4 $0188 ; 392 Hz -.macro note.gs4 $019F ; 415 Hz -.macro note.a4 $01B8 ; 440 Hz -.macro note.as4 $01D2 ; 466 Hz -.macro note.b4 $01EE ; 494 Hz - -.macro note.c5 $020B ; 523 Hz -.macro note.cs5 $022A ; 554 Hz -.macro note.d5 $024B ; 587 Hz -.macro note.ds5 $026E ; 622 Hz -.macro note.e5 $0293 ; 659 Hz -.macro note.f5 $02BA ; 698 Hz -.macro note.fs5 $02E4 ; 740 Hz -.macro note.g5 $0310 ; 784 Hz -.macro note.gs5 $033F ; 831 Hz -.macro note.a5 $0370 ; 880 Hz -.macro note.as5 $03A4 ; 932 Hz -.macro note.b5 $03DC ; 988 Hz +.macro c5 $020B ; 523 Hz +.macro cs5 $022A ; 554 Hz +.macro d5 $024B ; 587 Hz +.macro ds5 $026E ; 622 Hz +.macro e5 $0293 ; 659 Hz +.macro f5 $02BA ; 698 Hz +.macro fs5 $02E4 ; 740 Hz +.macro g5 $0310 ; 784 Hz +.macro gs5 $033F ; 831 Hz +.macro a5 $0370 ; 880 Hz +.macro as5 $03A4 ; 932 Hz +.macro b5 $03DC ; 988 Hz -.macro note.c6 $0417 ; 1047 Hz -.macro note.cs6 $0455 ; 1109 Hz -.macro note.d6 $0497 ; 1175 Hz -.macro note.ds6 $04DD ; 1245 Hz -.macro note.e6 $0527 ; 1319 Hz -.macro note.f6 $0575 ; 1397 Hz -.macro note.fs6 $05C8 ; 1480 Hz -.macro note.g6 $0620 ; 1568 Hz -.macro note.gs6 $067D ; 1661 Hz -.macro note.a6 $06E0 ; 1760 Hz -.macro note.as6 $0749 ; 1865 Hz -.macro note.b6 $07B8 ; 1976 Hz +.macro c6 $0417 ; 1047 Hz +.macro cs6 $0455 ; 1109 Hz +.macro d6 $0497 ; 1175 Hz +.macro ds6 $04DD ; 1245 Hz +.macro e6 $0527 ; 1319 Hz +.macro f6 $0575 ; 1397 Hz +.macro fs6 $05C8 ; 1480 Hz +.macro g6 $0620 ; 1568 Hz +.macro gs6 $067D ; 1661 Hz +.macro a6 $06E0 ; 1760 Hz +.macro as6 $0749 ; 1865 Hz +.macro b6 $07B8 ; 1976 Hz
M xywasm.cxywasm.c

@@ -66,6 +66,7 @@ static char token[TOKEN_MAX_LENGTH];

static unsigned int token_length = 0; static xyw_token_type token_type = TOKEN_TYPE_UNKNOWN; static xyw_word token_value = 0; +static int token_is_word = 0; static int token_line = 1; static int token_column = 1;

@@ -322,7 +323,7 @@ static int process_number(xyw_context *ctx)

{ xyw_word value = 0; unsigned int i = 0; - int is_word = 0; // Track if this is a word-sized number (based on digit count) + token_is_word = 0; // Track if this is a word-sized number (based on digit count) if (token[0] == '$') { unsigned int hex_digits = token_length - 1;

@@ -331,7 +332,7 @@ {

token_error("Hex numbers must have exactly 2 or 4 digits"); return -1; } - is_word = (hex_digits == 4); + token_is_word = (hex_digits == 4); for (i = 1; i < token_length; i++) { char c = token[i];

@@ -354,7 +355,7 @@ token_error("Invalid hex digit");

return -1; } } - xyw_dbg("Hex number:\t%s\t(%04X) [%s]\n", token, value, is_word ? "word" : "byte"); + xyw_dbg("Hex number:\t%s\t(%04X) [%s]\n", token, value, token_is_word ? "word" : "byte"); } else if (token[0] == '%') {

@@ -364,7 +365,7 @@ {

token_error("Binary numbers must have exactly 8 digits"); return -1; } - is_word = 0; // Binary numbers are always bytes (8 bits) + token_is_word = 0; // Binary numbers are always bytes (8 bits) for (i = 1; i < token_length; i++) { char c = token[i];

@@ -391,7 +392,7 @@ token_value = value;

if (!directive) { // Use digit count to determine byte vs word, not value - if (is_word) + if (token_is_word) { append(PSH | XYW_MODE_W); XYW_POKEW(&data[ptr], value);

@@ -597,20 +598,49 @@ if (r == TOKEN_END_INVALID)

{ return -1; } - if (r == TOKEN_END_EOL || r == TOKEN_END_EOF || r == TOKEN_END_COMMENT) + // Break only if no token was read (empty line or end of tokens) + if ((r == TOKEN_END_EOL || r == TOKEN_END_EOF || r == TOKEN_END_COMMENT) && token_length == 0) { break; } + // Check if token is a macro and substitute if so + if (token[0] != '$' && token[0] != '%') + { + int sym_idx = find_symbol(token); + if (sym_idx != -1 && symbols[sym_idx].type == SYMBOL_TYPE_MACRO) + { + xyw_dbg("Macro in .byte:\t%s -> %s\n", token, symbols[sym_idx].contents); + // Copy macro contents into token buffer, skipping leading whitespace + const char *contents = symbols[sym_idx].contents; + while (*contents == ' ' || *contents == '\t') + contents++; + token_length = 0; + while (*contents && token_length < TOKEN_MAX_LENGTH - 1) + { + token[token_length++] = *contents++; + } + token[token_length] = '\0'; + } + } if (process_number(ctx) != 0) { return -1; } - if (token_value > 0xFF) + if (token_is_word) + { + // Word value: emit high byte then low byte + data[ptr++] = (xyw_byte)(token_value >> 8); + data[ptr++] = (xyw_byte)(token_value & 0xFF); + } + else { - token_error("Byte value out of range (0-255)"); - return -1; + data[ptr++] = (xyw_byte)token_value; } - data[ptr++] = (xyw_byte)token_value; + // Now break if we were at end of line/file + if (r == TOKEN_END_EOL || r == TOKEN_END_EOF || r == TOKEN_END_COMMENT) + { + break; + } } return 0; }

@@ -934,13 +964,9 @@ token[token_length] = '\0';

return TOKEN_END_COMMENT; } in_comment = 1; - token_line = ctx->line; - token_column = ctx->column; - token_started = 1; ctx->column++; - // Start of comment token - token[token_length++] = (char)c; - token[token_length] = '\0'; + // Comment consumes rest of line; don't store in token buffer + // so that directives see token_length == 0 and break cleanly } // Whitespace characters else if (c == ' ' || c == '\t')