Implemented support for simple beeper device.
h3rald h3rald@h3rald.com
Tue, 10 Feb 2026 09:59:52 +0100
6 files changed,
243 insertions(+),
6 deletions(-)
M
Makefile
→
Makefile
@@ -1,8 +1,14 @@
CC = gcc CFLAGS = -Wall -Wextra -g -SOURCES = xyw.c xywasm.c xywrun.c devices/system.c devices/terminal.c devices/clock.c devices/file.c -OBJS = xyw.o xywasm.o xywrun.o devices/system.o devices/terminal.o devices/clock.o devices/file.o +SOURCES = xyw.c xywasm.c xywrun.c devices/system.c devices/terminal.c devices/clock.c devices/file.c devices/beeper.c +OBJS = xyw.o xywasm.o xywrun.o devices/system.o devices/terminal.o devices/clock.o devices/file.o devices/beeper.o + +# Platform-specific linker flags +UNAME_S := $(shell uname -s 2>/dev/null || echo Windows) +ifeq ($(UNAME_S),Darwin) + LDFLAGS += -framework AudioToolbox +endif .PHONY: clean@@ -29,6 +35,9 @@ $(CC) $(CFLAGS) -c devices/clock.c -o devices/clock.o
devices/file.o: devices/file.c xyw.h $(CC) $(CFLAGS) -c devices/file.c -o devices/file.o + +devices/beeper.o: devices/beeper.c xyw.h + $(CC) $(CFLAGS) -c devices/beeper.c -o devices/beeper.o clean: rm -f xyw xyw.exe $(OBJS)
A
devices/beeper.c
@@ -0,0 +1,214 @@
+#include <stdio.h> +#ifdef _WIN32 +#include <windows.h> +#elif defined(__APPLE__) +#include <AudioToolbox/AudioToolbox.h> +#include <unistd.h> +#include <math.h> +#elif defined(__linux__) +#include <unistd.h> +#include <fcntl.h> +#include <sys/ioctl.h> +#include <linux/kd.h> +#else /* Other Unix (BSD, etc.) */ +#include <unistd.h> +#endif +#include "../xyw.h" + +/* beeper device */ +#define BEEPER_PITCH 0x0 +#define BEEPER_LENGTH 0x2 +#define BEEPER_LEVEL 0x4 + +#define BEEPER_MAX_FREQ 0x2000 /* 8192 Hz */ +#define BEEPER_MAX_LEVEL 0x0F + +#ifdef __APPLE__ +/* macOS: Generate sine wave tone using AudioQueue */ +#define APPLE_SAMPLE_RATE 44100.0 +#define APPLE_NUM_BUFFERS 3 + +typedef struct +{ + AudioQueueRef queue; + double phase; + double phase_increment; + float amplitude; + UInt32 samples_remaining; + int finished; +} AppleToneState; + +static void apple_audio_callback(void *user_data, AudioQueueRef queue, AudioQueueBufferRef buffer) +{ + AppleToneState *state = (AppleToneState *)user_data; + SInt16 *samples = (SInt16 *)buffer->mAudioData; + UInt32 num_samples = buffer->mAudioDataByteSize / sizeof(SInt16); + + if (state->samples_remaining == 0) + { + /* Fill with silence and mark finished */ + for (UInt32 i = 0; i < num_samples; i++) + samples[i] = 0; + state->finished = 1; + } + else + { + UInt32 to_generate = num_samples < state->samples_remaining ? num_samples : state->samples_remaining; + for (UInt32 i = 0; i < to_generate; i++) + { + samples[i] = (SInt16)(sin(state->phase) * 32767.0 * state->amplitude); + state->phase += state->phase_increment; + if (state->phase > 2.0 * M_PI) + state->phase -= 2.0 * M_PI; + } + for (UInt32 i = to_generate; i < num_samples; i++) + samples[i] = 0; + state->samples_remaining -= to_generate; + } + AudioQueueEnqueueBuffer(queue, buffer, 0, NULL); +} + +static void apple_play_tone(xyw_word freq, unsigned long duration_ms, xyw_byte level) +{ + AppleToneState state = {0}; + AudioStreamBasicDescription format = {0}; + AudioQueueBufferRef buffers[APPLE_NUM_BUFFERS]; + + format.mSampleRate = APPLE_SAMPLE_RATE; + format.mFormatID = kAudioFormatLinearPCM; + format.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked; + format.mBitsPerChannel = 16; + format.mChannelsPerFrame = 1; + format.mBytesPerFrame = 2; + format.mFramesPerPacket = 1; + format.mBytesPerPacket = 2; + + state.phase = 0.0; + state.phase_increment = 2.0 * M_PI * freq / APPLE_SAMPLE_RATE; + state.amplitude = (float)level / (float)BEEPER_MAX_LEVEL; + state.samples_remaining = (UInt32)(APPLE_SAMPLE_RATE * duration_ms / 1000.0); + state.finished = 0; + + if (AudioQueueNewOutput(&format, apple_audio_callback, &state, NULL, NULL, 0, &state.queue) != noErr) + return; + + UInt32 buffer_size = 4096; + for (int i = 0; i < APPLE_NUM_BUFFERS; i++) + { + AudioQueueAllocateBuffer(state.queue, buffer_size, &buffers[i]); + buffers[i]->mAudioDataByteSize = buffer_size; + apple_audio_callback(&state, state.queue, buffers[i]); + } + + AudioQueueStart(state.queue, NULL); + + /* Wait for playback to complete */ + while (!state.finished) + usleep(10000); + usleep(50000); /* Small delay for buffer drain */ + + AudioQueueStop(state.queue, true); + AudioQueueDispose(state.queue, true); +} +#endif /* __APPLE__ */ + +#if defined(__linux__) +/* Linux: Use KIOCSOUND ioctl for PC speaker frequency control */ +#define CLOCK_TICK_RATE 1193180 + +static void linux_play_tone(xyw_word freq, unsigned long duration_ms) +{ + int fd = open("/dev/console", O_WRONLY); + if (fd < 0) + fd = open("/dev/tty0", O_WRONLY); + + if (fd >= 0 && freq > 0) + { + /* Set frequency: KIOCSOUND takes clock_tick_rate / frequency, or 0 to stop */ + unsigned int period = CLOCK_TICK_RATE / freq; + ioctl(fd, KIOCSOUND, period); + usleep((useconds_t)duration_ms * 1000); + ioctl(fd, KIOCSOUND, 0); /* Stop */ + close(fd); + } + else + { + /* Fallback: BEL character (no frequency control) */ + if (fd >= 0) + close(fd); + fputc('\a', stdout); + fflush(stdout); + usleep((useconds_t)duration_ms * 1000); + } +} +#endif /* __linux__ */ + +void beeper_output(xyw_byte *data, xyw_byte addr, xyw_byte *error) +{ + (void)error; + + switch (addr) + { + case BEEPER_PITCH: + case BEEPER_PITCH + 1: + case BEEPER_LENGTH: + case BEEPER_LENGTH + 1: + /* Just store the value, beep triggers on level write */ + break; + + case BEEPER_LEVEL: + { + /* Get pitch (2 bytes, big-endian) */ + xyw_word pitch = XYW_PEEKW(&data[BEEPER_PITCH]); + + /* Get length in 1/256 second units (2 bytes, big-endian) */ + xyw_word length = XYW_PEEKW(&data[BEEPER_LENGTH]); + + /* Get volume level (0-15) */ + xyw_byte level = data[BEEPER_LEVEL]; + + /* Clamp values */ + if (pitch > BEEPER_MAX_FREQ) + 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) + { + /* Convert length from 1/256 seconds to milliseconds */ + /* 1/256 second = 1000/256 ms ≈ 3.90625 ms */ + unsigned long duration_ms = ((unsigned long)length * 1000) / 256; + if (duration_ms == 0) + duration_ms = 1; + +#ifdef _WIN32 + /* Windows Beep() function - ignores volume level */ + (void)level; + Beep((DWORD)pitch, (DWORD)duration_ms); +#elif defined(__APPLE__) + /* macOS: Generate sine wave via AudioQueue */ + apple_play_tone(pitch, duration_ms, level); +#elif defined(__linux__) + /* Linux: Use PC speaker via KIOCSOUND ioctl (ignores volume) */ + (void)level; + linux_play_tone(pitch, duration_ms); +#else + /* Other Unix: BEL fallback (no frequency control) */ + (void)pitch; + (void)level; + fputc('\a', stdout); + fflush(stdout); + usleep((useconds_t)duration_ms * 1000); +#endif + } + break; + } + } +} + +xyw_byte beeper_input(xyw_byte *data, xyw_byte addr, xyw_byte *error) +{ + (void)error; + return data[addr]; +}
A
devices/beeper.h
@@ -0,0 +1,4 @@
+#include "../xyw.h" + +void beeper_output(xyw_byte *data, xyw_byte addr, xyw_byte *error); +xyw_byte beeper_input(xyw_byte *data, xyw_byte addr, xyw_byte *error);
A
examples/beep.xyw
@@ -0,0 +1,8 @@
+.include "lib/macros.xyw" + +$0400 beeper.pitch STW +$0080 beeper.length STW +$04 beeper.level STB +$0800 beeper.pitch STW +$0080 beeper.length STW +$04 beeper.level STB
M
examples/lib/macros.xyw
→
examples/lib/macros.xyw
@@ -37,10 +37,9 @@ .macro file.write $3B
.macro file.operation $3D ;beeper -.macro beeper.state $40 -.macro beeper.samples $41 -.macro beeper.n_samples $43 -.macro beeper.on_stop $45 +.macro beeper.pitch $40 +.macro beeper.length $42 +.macro beeper.level $44 ;;; Utilities .macro MOD DIV POP
M
xywrun.c
→
xywrun.c
@@ -6,6 +6,7 @@ #include "devices/system.h"
#include "devices/terminal.h" #include "devices/clock.h" #include "devices/file.h" +#include "devices/beeper.h" #define MEMORY_SIZE 0xffff #define STACK_SIZE 0xff@@ -295,6 +296,8 @@ // Clock device
{&xyw_memory[0xff20], clock_init, clock_input, clock_output}, // File device {&xyw_memory[0xff30], file_init, file_input, file_output}, + // Beeper device + {&xyw_memory[0xff40], 0, beeper_input, beeper_output}, // ... }; // clang-format on