From 6c9a3fdc1ee512b0577b275b8593f126ae755c99 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 20 Jun 2026 21:10:28 +0900 Subject: [PATCH] beeper: prescaler 32 --- assets/disk0/tvdos/bin/monplay.js | 29 +++++++------ assets/disk0/tvdos/include/typesetter.mjs | 2 +- terranmon.txt | 10 ++--- .../net/torvald/tsvm/peripheral/IOSpace.kt | 43 ++++++++++--------- 4 files changed, 43 insertions(+), 41 deletions(-) diff --git a/assets/disk0/tvdos/bin/monplay.js b/assets/disk0/tvdos/bin/monplay.js index 353dce7..d0318d5 100644 --- a/assets/disk0/tvdos/bin/monplay.js +++ b/assets/disk0/tvdos/bin/monplay.js @@ -14,21 +14,22 @@ // --------------------------------------------------------------------------- // Beeper hardware (IOSpace). MMIO byte m is reached at JS address -(m+1): // 93 RO -> reading uploads the staged command (the strobe) -// 94..99 -> PPPPPPPP / pppppp_QQ / qqAABBCC / aaaaaaaa / bbbbbbbb / cccccccc -// AA/BB/CC are the high two bits of the 10-bit arpeggio deltas A/B/C. -// The square wave is f = (3579545/16) / (2 * divider); divider 0 = silence. +// 94..99 -> PPPPPPPP / C_ppppp_QQ / AAABBBCC / aaaaaaaa / bbbbbbbb / cccccccc +// The 13-bit divider is PPPPPPPP:ppppp. The arpeggio deltas A/B/C are 11-bit: +// AAA/BBB are the high 3 bits of A/B; C's high 3 bits split as C(byte95 bit7):CC. +// The square wave is f = (3579545/32) / (2 * divider); divider 0 = silence. // --------------------------------------------------------------------------- const BEEP_UPLOAD = -94 // read MMIO 93 to upload const BEEP_P_HI = -95 // MMIO 94: PPPPPPPP -const BEEP_P_LO = -96 // MMIO 95: pppppp_QQ -const BEEP_EXT = -97 // MMIO 96: qqAABBCC (high 2 bits of A/B/C) +const BEEP_P_LO = -96 // MMIO 95: C_ppppp_QQ +const BEEP_EXT = -97 // MMIO 96: AAABBBCC (high 3 bits of A/B, low 2 of C's high bits) const BEEP_A = -98 // MMIO 97: aaaaaaaa (A low 8 bits) const BEEP_B = -99 // MMIO 98: bbbbbbbb (B low 8 bits) const BEEP_C = -100 // MMIO 99: cccccccc (C low 8 bits) -const BEEP_HALFCLOCK = (3579545.4545454545 / 16.0) / 2 // f = BEEP_HALFCLOCK / divider -const DIVIDER_MAX = 0x3FFF // 14-bit -const ARG_MAX = 0x3FF // 10-bit arpeggio delta +const BEEP_HALFCLOCK = (3579545.4545454545 / 32.0) / 2 // f = BEEP_HALFCLOCK / divider +const DIVIDER_MAX = 0x1FFF // 13-bit +const ARG_MAX = 0x7FF // 11-bit arpeggio delta const QQ_NONE = 0, QQ_FOUR = 1, QQ_TWO = 2, QQ_THREE = 3 // beeper note-effect (QQ field) @@ -36,9 +37,9 @@ function uploadBeeper(divider, effect, a, b, c) { if (divider < 0) divider = 0 if (divider > DIVIDER_MAX) divider = DIVIDER_MAX a &= ARG_MAX; b &= ARG_MAX; c &= ARG_MAX - sys.poke(BEEP_P_HI, (divider >> 6) & 0xFF) - sys.poke(BEEP_P_LO, ((divider & 0x3F) << 2) | (effect & 3)) - sys.poke(BEEP_EXT, (((a >> 8) & 3) << 4) | (((b >> 8) & 3) << 2) | ((c >> 8) & 3)) + sys.poke(BEEP_P_HI, (divider >> 5) & 0xFF) + sys.poke(BEEP_P_LO, (((c >> 10) & 1) << 7) | ((divider & 0x1F) << 2) | (effect & 3)) + sys.poke(BEEP_EXT, (((a >> 8) & 7) << 5) | (((b >> 8) & 7) << 2) | ((c >> 8) & 3)) sys.poke(BEEP_A, a & 0xFF) sys.poke(BEEP_B, b & 0xFF) sys.poke(BEEP_C, c & 0xFF) @@ -100,9 +101,9 @@ const intervalHz = (interval) => NOTESHZ[clampInterval(interval)] // subtraction. Returns either a single hardware command {sw:false, cmd:[...]} // or, when the notes don't fit, a software-arpeggio plan {sw:true, dividers:[...]}. // 1 note -> effect 0 (none) -// 2 notes -> effect 2 (single 10-bit delta A: only when <= 1023) -// 3 notes -> effect 3 (two 10-bit deltas: only when both <= 1023) -// 4 notes -> effect 1 (three 10-bit deltas: only when all <= 1023) +// 2 notes -> effect 2 (single 11-bit delta A: only when <= 2047) +// 3 notes -> effect 3 (two 11-bit deltas: only when both <= 2047) +// 4 notes -> effect 1 (three 11-bit deltas: only when all <= 2047) // otherwise (wide chords / 5+ voices) -> software arpeggio over ALL the notes // --------------------------------------------------------------------------- function planMultiplex(dividers) { diff --git a/assets/disk0/tvdos/include/typesetter.mjs b/assets/disk0/tvdos/include/typesetter.mjs index 6051f36..88f8d92 100644 --- a/assets/disk0/tvdos/include/typesetter.mjs +++ b/assets/disk0/tvdos/include/typesetter.mjs @@ -70,7 +70,7 @@ function expandEntities(s) { .replaceAll('&udlr;', '\u008428u\u008429u') .replaceAll('&keyoffsym;', '\u00A0\u00B1\u00B1\u00A1') .replaceAll('¬ecutsym;', '\u00A4\u00A4\u00A4\u00A4') - .replaceAll(' ', ' ') + .replaceAll(' ', '\u00840u') .replaceAll('­', '') .replaceAll('<', '<') .replaceAll('>', '>') diff --git a/terranmon.txt b/terranmon.txt index 0bc0330..ffa7285 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -120,13 +120,13 @@ MMIO 93 RO: Set beeper status (aka upload beeper command) READING causes the side effect (and returns beeper status — 1 if a tone is currently sounding, 0 otherwise). WRITING DOES NOTHING 94..99 RW: Beeper command - 0bPPPPPPPP 0bpppppp_QQ 0bqqAABBCC 0baaaaaaaa 0bbbbbbbbb 0bcccccccc + 0bPPPPPPPP 0bC_ppppp_QQ 0bAAABBBCC 0baaaaaaaa 0bbbbbbbbb 0bcccccccc - Master clock: 3579545.4545... Hz with prescaler of 16 + Master clock: 3579545.4545... Hz with prescaler of 32 - PPPPPPPPpppppp: 14-bit frequency divider, determines pitch. + PPPPPPPPppppp: 13-bit frequency divider, determines pitch. 0: no sound - QQ: note effect + QQ: arpeggiator 00: none 10: two-note arpeggio (rate = 60 Hz) tick 1: base note at divisor P is played @@ -140,7 +140,7 @@ MMIO tick 2: second note at divisor (P - A) is played tick 3: third note at divisor (P - A - B) is played tick 4: fourth note at divisor (P - A - B - C) is played - Aa/Bb/Cc: note effect arguments (10-bit divisor delta for arpeggiator; byte 96 has high two bits) + Aa/Bb/Cc: arpeggiator arguments (11-bit divisor delta for arpeggiator; byte 96 has high three bits; byte 95 has MSB for C) 1024..2047 RW: Reserved for integrated peripherals (e.g. built-in status display) diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/IOSpace.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/IOSpace.kt index 0092341..985b197 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/IOSpace.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/IOSpace.kt @@ -501,11 +501,11 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor { /** * Built-in beeper / PSG speaker (terranmon.txt §93..99). * - * A single square-wave tone generator modelled on the SN76489: a 14-bit frequency - * divider over a 3579545/16 Hz master clock, with optional 60 Hz arpeggio - * note-effects (two-, three- or four-note). The six command bytes (MMIO 94..99) - * are write staging; reading MMIO 93 latches them into the live tone ("upload - * beeper command"). + * A single square-wave tone generator modelled on the SN76489: a 13-bit frequency + * divider over a 3579545/32 Hz master clock, with optional 60 Hz arpeggio + * note-effects (two-, three- or four-note) whose deltas are 11-bit. The six command + * bytes (MMIO 94..99) are write staging; reading MMIO 93 latches them into the live + * tone ("upload beeper command"). * * The OpenAL device and its render thread are created lazily on the first non-silent * upload, so a headless VM (no LibGDX OpenAL backend) simply stays silent. @@ -514,11 +514,11 @@ private class Beeper { companion object { private const val SAMPLE_RATE = 48000 - // SN76489 NTSC colourburst clock (3579545 Hz) after the chip's internal /16 + // SN76489 NTSC colourburst clock (3579545 Hz) after the chip's internal /32 // prescaler. The square wave toggles every `divider` master ticks, so one full // period spans 2*divider ticks -> f = MASTER_CLOCK / (2 * divider). - // (divider 254 -> 440.4 Hz, matching real SN76489 hardware.) - private const val MASTER_CLOCK = 3579545.4545454545 / 16.0 + // (divider 127 -> 440.4 Hz.) + private const val MASTER_CLOCK = 3579545.4545454545 / 32.0 // Arpeggio note-effects step at 60 Hz: 48000 / 60 = 800 samples per step. private const val SAMPLES_PER_ARP_TICK = SAMPLE_RATE / 60 private const val CHUNK = 512 @@ -526,16 +526,17 @@ private class Beeper { } // MMIO 94..99 write-staging registers: - // PPPPPPPP / pppppp_QQ / qqAABBCC / aaaaaaaa / bbbbbbbb / cccccccc - // where AA/BB/CC are the high two bits of the 10-bit arpeggio deltas A/B/C. + // PPPPPPPP / C_ppppp_QQ / AAABBBCC / aaaaaaaa / bbbbbbbb / cccccccc + // The 13-bit divider is PPPPPPPP:ppppp. AAA/BBB are the high three bits of the + // 11-bit deltas A/B; C's three high bits are split as C (byte 95 bit 7) : CC. private val cmd = ByteArray(6) // Latched ("uploaded") live command, read by the render thread. - @Volatile private var divider = 0 // 14-bit frequency divider; 0 = no sound + @Volatile private var divider = 0 // 13-bit frequency divider; 0 = no sound @Volatile private var effect = 0 // QQ note-effect: 0 none, 1 four-note, 2 two-note, 3 three-note - @Volatile private var argA = 0 // A (10-bit divisor delta) - @Volatile private var argB = 0 // B (10-bit divisor delta) - @Volatile private var argC = 0 // C (10-bit divisor delta) + @Volatile private var argA = 0 // A (11-bit divisor delta) + @Volatile private var argB = 0 // B (11-bit divisor delta) + @Volatile private var argC = 0 // C (11-bit divisor delta) @Volatile private var running = false private var renderThread: Thread? = null @@ -550,13 +551,13 @@ private class Beeper { */ fun upload(): Byte { val hi = cmd[0].toInt() and 255 // PPPPPPPP - val lo = cmd[1].toInt() and 255 // pppppp_QQ - val ext = cmd[2].toInt() and 255 // qqAABBCC: high two bits of A/B/C - divider = (hi shl 6) or (lo ushr 2) // 14-bit frequency divider - effect = lo and 0b11 // QQ - argA = (((ext ushr 4) and 0b11) shl 8) or (cmd[3].toInt() and 255) // 10-bit A - argB = (((ext ushr 2) and 0b11) shl 8) or (cmd[4].toInt() and 255) // 10-bit B - argC = (((ext ) and 0b11) shl 8) or (cmd[5].toInt() and 255) // 10-bit C + val lo = cmd[1].toInt() and 255 // C_ppppp_QQ + val ext = cmd[2].toInt() and 255 // AAABBBCC: high three bits of A/B and the CC of C + divider = (hi shl 5) or ((lo ushr 2) and 0b11111) // 13-bit frequency divider + effect = lo and 0b11 // QQ + argA = (((ext ushr 5) and 0b111) shl 8) or (cmd[3].toInt() and 255) // 11-bit A + argB = (((ext ushr 2) and 0b111) shl 8) or (cmd[4].toInt() and 255) // 11-bit B + argC = (((lo ushr 7) and 0b1) shl 10) or ((ext and 0b11) shl 8) or (cmd[5].toInt() and 255) // 11-bit C if (divider != 0) ensureStarted() return (if (divider != 0) 1 else 0).toByte() }