From ae14a95e121076422a8fffa6e282a0bd5bd8a4ad Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 13 Aug 2026 16:54:12 +0000
Subject: [PATCH 1/7] Initial plan
From 77e684cdc91f4832bf5774ffed35f3a7d28fdcae Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 13 Aug 2026 17:53:15 +0000
Subject: [PATCH 2/7] feat: implement Gravis UltraSound (GF1) emulation
Co-authored-by: maximilien-noal <1087524+maximilien-noal@users.noreply.github.com>
---
src/Spice86.Core/CLI/Configuration.cs | 26 +
.../CLI/RuntimeOptions/AudioRuntimeOptions.cs | 8 +-
.../RuntimeOptions/RuntimeOptionsMapper.cs | 5 +-
.../Devices/Sound/GravisUltraSound.cs | 640 +++++++++++++++++-
.../Emulator/Devices/Sound/GusTimer.cs | 32 +
.../Emulator/Devices/Sound/GusVoice.cs | 274 ++++++++
.../Emulator/Devices/Sound/GusVoiceIrq.cs | 20 +
src/Spice86/Spice86DependencyInjection.cs | 6 +-
8 files changed, 971 insertions(+), 40 deletions(-)
create mode 100644 src/Spice86.Core/Emulator/Devices/Sound/GusTimer.cs
create mode 100644 src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs
create mode 100644 src/Spice86.Core/Emulator/Devices/Sound/GusVoiceIrq.cs
diff --git a/src/Spice86.Core/CLI/Configuration.cs b/src/Spice86.Core/CLI/Configuration.cs
index 62d46f7024..b5a40c3f11 100644
--- a/src/Spice86.Core/CLI/Configuration.cs
+++ b/src/Spice86.Core/CLI/Configuration.cs
@@ -370,4 +370,30 @@ public sealed class Configuration : CommandSettings {
[DefaultValue(true)]
public bool EnableSpeculativeCfgExploration { get; init; } = true;
+ ///
+ /// Gravis UltraSound I/O base address (default: 0x240).
+ /// Common values: 0x210, 0x220, 0x230, 0x240, 0x250, 0x260.
+ ///
+ [CommandOption("--GusBase ")]
+ [DefaultValue((ushort)0x240)]
+ public ushort GusBase { get; init; } = 0x240;
+
+ ///
+ /// Gravis UltraSound IRQ number (default: 5).
+ /// Valid values: 2, 3, 5, 7, 11, 12, 15.
+ /// Default of 5 allows coexistence with a Sound Blaster card.
+ ///
+ [CommandOption("--GusIrq ")]
+ [DefaultValue((byte)5)]
+ public byte GusIrq { get; init; } = 5;
+
+ ///
+ /// Gravis UltraSound DMA channel (default: 3).
+ /// Valid values: 1, 3, 5, 6, 7.
+ /// Default of 3 allows coexistence with a Sound Blaster card.
+ ///
+ [CommandOption("--GusDma ")]
+ [DefaultValue((byte)3)]
+ public byte GusDma { get; init; } = 3;
+
}
diff --git a/src/Spice86.Core/CLI/RuntimeOptions/AudioRuntimeOptions.cs b/src/Spice86.Core/CLI/RuntimeOptions/AudioRuntimeOptions.cs
index 3f34946900..10f7024b2f 100644
--- a/src/Spice86.Core/CLI/RuntimeOptions/AudioRuntimeOptions.cs
+++ b/src/Spice86.Core/CLI/RuntimeOptions/AudioRuntimeOptions.cs
@@ -16,6 +16,9 @@ namespace Spice86.Core.CLI.RuntimeOptions;
/// 8-bit DMA channel used by the emulated Sound Blaster card.
/// 16-bit DMA channel used by the emulated Sound Blaster card.
/// Sound Blaster model to emulate.
+/// Gravis UltraSound I/O base address.
+/// Gravis UltraSound IRQ number.
+/// Gravis UltraSound DMA channel.
public sealed record class AudioRuntimeOptions(
AudioEngine AudioEngine,
string? Mt32RomsPath,
@@ -25,7 +28,10 @@ public sealed record class AudioRuntimeOptions(
byte SbIrq,
byte SbDma,
byte SbHdma,
- SbType SbType) {
+ SbType SbType,
+ ushort GusBase,
+ byte GusIrq,
+ byte GusDma) {
///
/// Sound Blaster base I/O address.
///
diff --git a/src/Spice86.Core/CLI/RuntimeOptions/RuntimeOptionsMapper.cs b/src/Spice86.Core/CLI/RuntimeOptions/RuntimeOptionsMapper.cs
index 3a95e7a659..a8c446d27c 100644
--- a/src/Spice86.Core/CLI/RuntimeOptions/RuntimeOptionsMapper.cs
+++ b/src/Spice86.Core/CLI/RuntimeOptions/RuntimeOptionsMapper.cs
@@ -113,6 +113,9 @@ public static AudioRuntimeOptions ToAudioRuntimeOptions(Configuration configurat
configuration.SbIrq,
configuration.SbDma,
configuration.SbHdma,
- configuration.SbType);
+ configuration.SbType,
+ configuration.GusBase,
+ configuration.GusIrq,
+ configuration.GusDma);
}
}
\ No newline at end of file
diff --git a/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs b/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
index 0cace645f1..abb40a4594 100644
--- a/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
+++ b/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
@@ -1,49 +1,617 @@
namespace Spice86.Core.Emulator.Devices.Sound;
+using Spice86.Audio.Backend;
+using Spice86.Audio.Common;
+using Spice86.Core.CLI.RuntimeOptions;
using Microsoft.Extensions.Logging;
using Spice86.Core.Emulator.CPU;
+using Spice86.Core.Emulator.Devices.DirectMemoryAccess;
+using Spice86.Core.Emulator.Devices.ExternalInput;
using Spice86.Core.Emulator.IOPorts;
+using Spice86.Core.Emulator.VM.DeviceScheduler;
using Spice86.Shared.Interfaces;
+using System;
+using System.Collections.Generic;
+
///
-/// Gravis Ultra Sound implementation. Emulates an absent card :)
+/// Gravis UltraSound (GF1 chip) emulation.
+/// Supports 14-32 independent PCM voices with independent frequency, panning,
+/// volume-ramp envelopes, and 8/16-bit sample playback from on-board DRAM.
///
-public class GravisUltraSound : DefaultIOPortHandler {
- private const int IrqControlRegister = 0x24B;
- private const int IrqStatusRegister = 0x246;
- private const int MixControlRegister = 0x240;
- private const int ReadDataOrTriggerStatus = 0x241;
- private const int RegisterControls = 0x24F;
- private const int TimerControlRegister = 0x248;
+///
+/// Port map (at default base 0x240, portBase = gusBase - 0x200 = 0x40):
+/// 0x240 – mix control (w); 0x246 – IRQ status (r); 0x248 – timer status (r/w);
+/// 0x249 – timer control (w); 0x24B – IRQ/DMA select (w);
+/// 0x342 – voice index (r/w); 0x343 – GF1 register select (r/w);
+/// 0x344/0x345 – data word (r/w); 0x347 – DRAM byte (r/w).
+///
+/// Reference implementation: dosbox-staging gus.cpp / gus.h
+/// (c) 2022-2025 The DOSBox Staging Team
+///
+public sealed class GravisUltraSound : DefaultIOPortHandler, IRequestInterrupt, IAudioQueueDevice, IMixerQueueNotifier {
+
+ // -----------------------------------------------------------------------
+ // Public constants used by GusVoice
+ // -----------------------------------------------------------------------
+
+ /// Number of pan-position slots (0-15).
+ public const byte PanPositions = 16;
+
+ /// Default (centre) pan-position index.
+ public const byte PanDefaultPosition = 7;
+
+ /// Number of distinct volume-scalar entries in the lookup table.
+ public const int VolLevels = 4096;
+
+ /// Fixed-point scale applied to volume-ramp position values.
+ public const int VolumeIncScalar = 512;
+
+ // -----------------------------------------------------------------------
+ // Hardware constants
+ // -----------------------------------------------------------------------
+
+ private const int MaxVoices = 32;
+ private const int MinVoices = 14;
+ private const int RamSizeBytes = 1024 * 1024; // 1 MiB on-board DRAM
+
+ // IRQ-status byte bits
+ private const byte IrqDmaFinished = 0x80;
+ private const byte IrqTimer1Bit = 0x04;
+ private const byte IrqTimer2Bit = 0x08;
+
+ // GF1 register addresses
+ private const byte RegVoiceControl = 0x00;
+ private const byte RegFreqDivisor = 0x01;
+ private const byte RegLoopStartHigh = 0x02;
+ private const byte RegLoopStartLow = 0x03;
+ private const byte RegLoopEndHigh = 0x04;
+ private const byte RegLoopEndLow = 0x05;
+ private const byte RegVolRate = 0x06;
+ private const byte RegVolStart = 0x07;
+ private const byte RegVolEnd = 0x08;
+ private const byte RegVolPos = 0x09;
+ private const byte RegPanPot = 0x0A;
+ private const byte RegVolControl = 0x0B;
+ private const byte RegActiveVoices = 0x0E;
+ private const byte RegDmaControl = 0x41;
+ private const byte RegDmaAddr = 0x42;
+ private const byte RegDramLow = 0x43;
+ private const byte RegDramHigh = 0x44;
+ private const byte RegTimerControl = 0x45;
+ private const byte RegTimer1 = 0x46;
+ private const byte RegTimer2 = 0x47;
+ private const byte RegSamplingFreq = 0x48;
+ private const byte RegSamplingCtrl = 0x49;
+ private const byte RegIrqStatus = 0x8F;
+
+ // -----------------------------------------------------------------------
+ // Fields
+ // -----------------------------------------------------------------------
+
+ private readonly byte[] _ram = new byte[RamSizeBytes];
+
+ private readonly GusVoice[] _voices;
+ private readonly GusVoiceIrq _voiceIrq = new GusVoiceIrq();
+ private readonly GusTimer[] _timers = new GusTimer[2];
+
+ private readonly float[] _volScalars = new float[VolLevels];
+ private readonly AudioFrame[] _panScalars = new AudioFrame[PanPositions];
+
+ private readonly DmaBus _dmaBus;
+ private readonly DualPic _dualPic;
+ private readonly SoftwareMixer _mixer;
+ private readonly DeviceScheduler _scheduler;
+ private readonly SoundChannel _channel;
+ private readonly RWQueue _outputQueue;
+
+ private readonly byte _irq;
+ private readonly byte _dma;
+ private readonly ushort _gusBase;
+
+ // GF1 register / I/O state
+ private byte _voiceIndex;
+ private byte _selectedReg;
+ private int _dramAddr;
+ private byte _dmaCtrl;
+ private byte _irqStatus;
+
+ private int _activeVoices = MinVoices;
+
+ // Render scratch buffer (grows on demand, never shrinks)
+ private AudioFrame[] _renderBuf;
+
+ // -----------------------------------------------------------------------
+ // Constructor
+ // -----------------------------------------------------------------------
///
- /// Initializes a new instance of the Gravis Ultrasound sound card.
+ /// Initialises the GUS emulation and registers all hardware I/O ports.
///
- /// The CPU state.
- /// The class that is responsible for dispatching ports reads and writes to classes that respond to them.
- /// Whether we throw an exception when an I/O port wasn't handled.
- /// The logger service implementation.
- public GravisUltraSound(State state, IOPortDispatcher ioPortDispatcher, bool failOnUnhandledPort,
- ILogger loggerService) : base(state, failOnUnhandledPort, loggerService) {
- InitPortHandlers(ioPortDispatcher);
- }
-
- private void InitPortHandlers(IOPortDispatcher ioPortDispatcher) {
- ioPortDispatcher.AddIOPortHandler(MixControlRegister, this);
- ioPortDispatcher.AddIOPortHandler(ReadDataOrTriggerStatus, this);
-
- // Not sure what those are but some programs search the card in those ports as well
- ioPortDispatcher.AddIOPortHandler(0x243, this);
- ioPortDispatcher.AddIOPortHandler(0x280, this);
- ioPortDispatcher.AddIOPortHandler(0x281, this);
- ioPortDispatcher.AddIOPortHandler(0x283, this);
- ioPortDispatcher.AddIOPortHandler(0x2C0, this);
- ioPortDispatcher.AddIOPortHandler(0x2C1, this);
- ioPortDispatcher.AddIOPortHandler(0x2C3, this);
- ioPortDispatcher.AddIOPortHandler(IrqStatusRegister, this);
- ioPortDispatcher.AddIOPortHandler(TimerControlRegister, this);
- ioPortDispatcher.AddIOPortHandler(IrqControlRegister, this);
- ioPortDispatcher.AddIOPortHandler(RegisterControls, this);
- }
-}
\ No newline at end of file
+ public GravisUltraSound(
+ State state,
+ IOPortDispatcher ioPortDispatcher,
+ bool failOnUnhandledPort,
+ ILogger loggerService,
+ DmaBus dmaBus,
+ DualPic dualPic,
+ SoftwareMixer mixer,
+ DeviceScheduler scheduler,
+ AudioRuntimeOptions audioOptions)
+ : base(state, failOnUnhandledPort, loggerService) {
+
+ _dmaBus = dmaBus;
+ _dualPic = dualPic;
+ _mixer = mixer;
+ _scheduler = scheduler;
+ _irq = audioOptions.GusIrq;
+ _dma = audioOptions.GusDma;
+ _gusBase = audioOptions.GusBase;
+
+ int portBase = _gusBase - 0x200;
+
+ BuildVolScalars();
+ BuildPanScalars();
+
+ _voices = new GusVoice[MaxVoices];
+ for (byte v = 0; v < MaxVoices; v++) {
+ _voices[v] = new GusVoice(v, _voiceIrq);
+ }
+
+ _timers[0] = new GusTimer(250.0 / 1000.0); // 250 µs in ms
+ _timers[1] = new GusTimer(1000.0 / 1000.0); // 1 ms
+
+ InitPortHandlers(ioPortDispatcher, portBase);
+
+ // Mixer setup
+ mixer.RegisterQueueNotifier(this);
+ mixer.LockMixerThread();
+
+ HashSet features = new HashSet {
+ ChannelFeature.DigitalAudio,
+ ChannelFeature.Stereo,
+ ChannelFeature.Sleep
+ };
+ _channel = mixer.AddChannel(MixerCallback, GetSampleRate(), "GravisUltraSound", features);
+
+ int queueCapacity = (int)Math.Ceiling(_channel.FramesPerBlock * 2.0f);
+ _outputQueue = new RWQueue(queueCapacity);
+ _renderBuf = new AudioFrame[Math.Max(1, (int)_channel.FramesPerBlock)];
+
+ mixer.UnlockMixerThread();
+
+ _dualPic.SetIrqMask(_irq, false);
+
+ DmaChannel? dmaChannel = _dmaBus.GetChannel(_dma);
+ dmaChannel?.ReserveFor("GravisUltraSound", OnDmaChannelEvicted);
+ dmaChannel?.RegisterCallback(OnDmaEvent);
+ }
+
+ // -----------------------------------------------------------------------
+ // Interface implementations
+ // -----------------------------------------------------------------------
+
+ ///
+ public RWQueue OutputQueue => _outputQueue;
+
+ ///
+ public SoundChannel Channel => _channel;
+
+ ///
+ public void NotifyLockMixer() => _outputQueue.Stop();
+
+ ///
+ public void NotifyUnlockMixer() => _outputQueue.Start();
+
+ ///
+ public void RaiseInterruptRequest() {
+ bool pendingVoice = _voiceIrq.WaveState != 0 || _voiceIrq.VolState != 0;
+ bool pendingGlobal = (_irqStatus & (IrqDmaFinished | IrqTimer1Bit | IrqTimer2Bit)) != 0;
+ if (pendingVoice || pendingGlobal) {
+ _dualPic.ActivateIrq(_irq);
+ }
+ }
+
+ /// Returns the ULTRASND environment variable value.
+ public string UltraSndString =>
+ $"{_gusBase:X3},{_dma},{_dma},{_irq},{_irq}";
+
+ // -----------------------------------------------------------------------
+ // I/O port overrides (port argument is always ushort per base class)
+ // -----------------------------------------------------------------------
+
+ ///
+ public override byte ReadByte(ushort port) {
+ int offset = port - _gusBase;
+ switch (offset) {
+ case 0x06: // IRQ status
+ return GetIrqStatus();
+ case 0x08: // Timer/adlib status
+ return GetTimerStatus();
+ case 0x0A: // Adlib command mirror
+ return 0xFF;
+ default:
+ break;
+ }
+
+ int highOffset = port - (_gusBase + 0x100);
+ switch (highOffset) {
+ case 0x02: // voice index
+ return _voiceIndex;
+ case 0x03: // GF1 register select
+ return _selectedReg;
+ case 0x04: // data low byte
+ return (byte)ReadFromRegister();
+ case 0x05: // data high byte
+ return (byte)(ReadFromRegister() >> 8);
+ case 0x07: // DRAM byte
+ return _dramAddr < RamSizeBytes ? _ram[_dramAddr] : (byte)0;
+ default:
+ return base.ReadByte(port);
+ }
+ }
+
+ ///
+ public override ushort ReadWord(ushort port) {
+ int highOffset = port - (_gusBase + 0x100);
+ if (highOffset == 0x04) {
+ return ReadFromRegister();
+ }
+ return base.ReadWord(port);
+ }
+
+ ///
+ public override void WriteByte(ushort port, byte value) {
+ int offset = port - _gusBase;
+ switch (offset) {
+ case 0x00: // mix control
+ return;
+ case 0x08: // timer/adlib command — ignored (adlib-compat path)
+ return;
+ case 0x09: // timer control
+ OnTimerControl(value);
+ return;
+ case 0x0B: // IRQ/DMA select — configuration is fixed at init time
+ return;
+ default:
+ break;
+ }
+
+ int highOffset = port - (_gusBase + 0x100);
+ switch (highOffset) {
+ case 0x02: // voice index
+ _voiceIndex = (byte)(value & (MaxVoices - 1));
+ return;
+ case 0x03: // GF1 register select
+ _selectedReg = value;
+ return;
+ case 0x04: // data low byte
+ WriteToRegister(_selectedReg, value);
+ return;
+ case 0x05: // data high byte
+ WriteToRegisterHigh(_selectedReg, value);
+ return;
+ case 0x07: // DRAM byte
+ if (_dramAddr < RamSizeBytes) {
+ _ram[_dramAddr] = value;
+ }
+ return;
+ default:
+ base.WriteByte(port, value);
+ return;
+ }
+ }
+
+ ///
+ public override void WriteWord(ushort port, ushort value) {
+ int highOffset = port - (_gusBase + 0x100);
+ if (highOffset == 0x04) {
+ WriteToRegister(_selectedReg, value);
+ return;
+ }
+ base.WriteWord(port, value);
+ }
+
+ // -----------------------------------------------------------------------
+ // Mixer callback
+ // -----------------------------------------------------------------------
+
+ private void MixerCallback(int framesRequested) {
+ EnsureRenderBuf(framesRequested);
+ Array.Clear(_renderBuf, 0, framesRequested);
+ for (int v = 0; v < _activeVoices; v++) {
+ _voices[v].RenderFrames(_ram, _volScalars, _panScalars, _renderBuf, framesRequested);
+ }
+ CheckVoiceIrqs();
+
+ _outputQueue.NonblockingBulkEnqueue(_renderBuf.AsSpan(0, framesRequested), framesRequested);
+ SoftwareMixer.PullFromQueueCallback(framesRequested, this);
+ }
+
+ // -----------------------------------------------------------------------
+ // GF1 register reads
+ // -----------------------------------------------------------------------
+
+ private ushort ReadFromRegister() {
+ GusVoice voice = _voices[_voiceIndex];
+ switch (_selectedReg) {
+ case 0x80: // read voice control
+ return voice.ReadWaveState();
+ case 0x8B: // read volume control
+ return voice.ReadVolState();
+ case RegLoopStartHigh:
+ return (ushort)(voice.WaveStart >> 16);
+ case RegLoopStartLow:
+ return (ushort)(voice.WaveStart & 0xFFFF);
+ case RegLoopEndHigh:
+ return (ushort)(voice.WaveEnd >> 16);
+ case RegLoopEndLow:
+ return (ushort)(voice.WaveEnd & 0xFFFF);
+ case RegVolPos:
+ return (ushort)(voice.VolPos / VolumeIncScalar);
+ case RegPanPot:
+ return voice.PanPosition;
+ case RegIrqStatus:
+ return GetVoiceIrqStatus();
+ default:
+ return 0xFF;
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // GF1 register writes
+ // -----------------------------------------------------------------------
+
+ private void WriteToRegister(byte reg, ushort value) {
+ GusVoice voice = _voices[_voiceIndex];
+ switch (reg) {
+ case RegVoiceControl:
+ voice.UpdateWaveState((byte)value);
+ break;
+ case RegFreqDivisor:
+ voice.WriteWaveRate(value);
+ break;
+ case RegLoopStartHigh:
+ voice.WaveStart = (voice.WaveStart & 0x0000FFFF) | ((int)value << 16);
+ break;
+ case RegLoopStartLow:
+ voice.WaveStart = (voice.WaveStart & unchecked((int)0xFFFF0000u)) | (int)value;
+ break;
+ case RegLoopEndHigh:
+ voice.WaveEnd = (voice.WaveEnd & 0x0000FFFF) | ((int)value << 16);
+ break;
+ case RegLoopEndLow:
+ voice.WaveEnd = (voice.WaveEnd & unchecked((int)0xFFFF0000u)) | (int)value;
+ break;
+ case RegVolRate:
+ voice.WriteVolRate((byte)value);
+ break;
+ case RegVolStart:
+ voice.VolStart = value * VolumeIncScalar;
+ break;
+ case RegVolEnd:
+ voice.VolEnd = value * VolumeIncScalar;
+ break;
+ case RegVolPos:
+ voice.VolPos = value * VolumeIncScalar;
+ break;
+ case RegPanPot:
+ voice.WritePanPot((byte)value);
+ break;
+ case RegVolControl:
+ voice.UpdateVolState((byte)value);
+ break;
+ case RegActiveVoices:
+ SetActiveVoices((byte)(value & 0x1F));
+ break;
+ case RegDmaControl:
+ _dmaCtrl = (byte)value;
+ if ((_dmaCtrl & 0x01) != 0) {
+ StartDmaTransfer();
+ }
+ break;
+ case RegDmaAddr:
+ _dramAddr = value << 4;
+ break;
+ case RegDramLow:
+ _dramAddr = (_dramAddr & 0xFF00) | (byte)value;
+ break;
+ case RegDramHigh:
+ _dramAddr = (_dramAddr & 0x00FF) | (value << 8);
+ break;
+ case RegTimerControl:
+ break;
+ case RegTimer1:
+ _timers[0].Value = (byte)value;
+ break;
+ case RegTimer2:
+ _timers[1].Value = (byte)value;
+ break;
+ case RegSamplingFreq:
+ _channel.SampleRate = (int)(1000000.0 / (2.0 * (value + 1)));
+ break;
+ case RegSamplingCtrl:
+ break;
+ }
+ }
+
+ private void WriteToRegisterHigh(byte reg, byte high) {
+ ushort current = ReadFromRegister();
+ WriteToRegister(reg, (ushort)((current & 0x00FF) | (high << 8)));
+ }
+
+ // -----------------------------------------------------------------------
+ // Timer handling
+ // -----------------------------------------------------------------------
+
+ private void OnTimerControl(byte value) {
+ for (int t = 0; t < _timers.Length; t++) {
+ GusTimer timer = _timers[t];
+ bool start = (value & (1 << t)) != 0;
+ if (start && !timer.IsCountingDown) {
+ timer.IsCountingDown = true;
+ timer.HasExpired = false;
+ int timerIndex = t;
+ double delayMs = timer.Delay * (256 - timer.Value);
+ _scheduler.AddEvent(_ => OnTimerExpired(timerIndex), delayMs, 0);
+ } else if (!start) {
+ timer.IsCountingDown = false;
+ }
+ }
+ }
+
+ private void OnTimerExpired(int timerIndex) {
+ GusTimer timer = _timers[timerIndex];
+ if (!timer.IsCountingDown) { return; }
+ timer.HasExpired = true;
+ timer.IsCountingDown = false;
+ if (timer.ShouldRaiseIrq) {
+ _irqStatus |= timerIndex == 0 ? IrqTimer1Bit : IrqTimer2Bit;
+ RaiseInterruptRequest();
+ }
+ }
+
+ private byte GetTimerStatus() {
+ byte status = 0;
+ if (_timers[0].HasExpired) { status |= 0x40; }
+ if (_timers[1].HasExpired) { status |= 0x20; }
+ return status;
+ }
+
+ // -----------------------------------------------------------------------
+ // IRQ handling
+ // -----------------------------------------------------------------------
+
+ private byte GetIrqStatus() {
+ byte result = _irqStatus;
+ _irqStatus = 0;
+ _dualPic.DeactivateIrq(_irq);
+ return result;
+ }
+
+ private ushort GetVoiceIrqStatus() {
+ for (byte v = 0; v < _activeVoices; v++) {
+ uint mask = 1u << v;
+ if ((_voiceIrq.WaveState & mask) != 0) {
+ _voiceIrq.WaveState &= ~mask;
+ return v;
+ }
+ if ((_voiceIrq.VolState & mask) != 0) {
+ _voiceIrq.VolState &= ~mask;
+ return (ushort)(v | 0x80);
+ }
+ }
+ return 0x1F;
+ }
+
+ private void CheckVoiceIrqs() {
+ if (_voiceIrq.WaveState != 0 || _voiceIrq.VolState != 0) {
+ RaiseInterruptRequest();
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // DMA handling
+ // -----------------------------------------------------------------------
+
+ private void StartDmaTransfer() {
+ DmaChannel? channel = _dmaBus.GetChannel(_dma);
+ channel?.RegisterCallback(OnDmaEvent);
+ }
+
+ private void OnDmaEvent(DmaChannel channel, DmaChannel.DmaEvent evt) {
+ if (evt != DmaChannel.DmaEvent.IsUnmasked) { return; }
+ PerformDmaTransfer(channel);
+ }
+
+ private void OnDmaChannelEvicted() {
+ // Nothing to do; DMA was forcibly released by another device.
+ }
+
+ private void PerformDmaTransfer(DmaChannel channel) {
+ const int ChunkSize = 512;
+ byte[] chunk = new byte[ChunkSize];
+
+ while (true) {
+ int bytesRead = channel.Read(ChunkSize, chunk.AsSpan());
+ if (bytesRead <= 0) { break; }
+ for (int i = 0; i < bytesRead; i++) {
+ int dest = _dramAddr + i;
+ if (dest >= RamSizeBytes) { break; }
+ byte sample = chunk[i];
+ if ((_dmaCtrl & 0x80) != 0) {
+ sample ^= 0x80;
+ }
+ _ram[dest] = sample;
+ }
+ _dramAddr = (_dramAddr + bytesRead) & 0xFFFFF; // 20-bit DRAM address
+ }
+
+ if ((_dmaCtrl & 0x02) != 0) {
+ _irqStatus |= IrqDmaFinished;
+ RaiseInterruptRequest();
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Voice and sample-rate management
+ // -----------------------------------------------------------------------
+
+ private void SetActiveVoices(byte count) {
+ _activeVoices = Math.Max(MinVoices, Math.Min(MaxVoices, (int)count));
+ _channel.SampleRate = GetSampleRate();
+ }
+
+ private int GetSampleRate() {
+ return (int)(1000000.0 / (1.619695497 * _activeVoices));
+ }
+
+ // -----------------------------------------------------------------------
+ // Lookup-table construction
+ // -----------------------------------------------------------------------
+
+ private void BuildVolScalars() {
+ for (int i = 0; i < VolLevels; i++) {
+ _volScalars[i] = i == 0 ? 0.0f
+ : (float)Math.Pow(10.0, (i - (VolLevels - 1)) / 20.0 * 0.002);
+ }
+ _volScalars[VolLevels - 1] = 1.0f;
+ }
+
+ private void BuildPanScalars() {
+ for (int p = 0; p < PanPositions; p++) {
+ float angle = p / (float)(PanPositions - 1) * (float)(Math.PI / 2.0);
+ _panScalars[p] = new AudioFrame(
+ (float)Math.Cos(angle),
+ (float)Math.Sin(angle));
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Port registration
+ // -----------------------------------------------------------------------
+
+ private void InitPortHandlers(IOPortDispatcher dispatcher, int portBase) {
+ // Low group (around gusBase)
+ dispatcher.AddIOPortHandler((ushort)(0x200 + portBase), this); // mix control
+ dispatcher.AddIOPortHandler((ushort)(0x206 + portBase), this); // IRQ status
+ dispatcher.AddIOPortHandler((ushort)(0x208 + portBase), this); // timer status
+ dispatcher.AddIOPortHandler((ushort)(0x209 + portBase), this); // timer control
+ dispatcher.AddIOPortHandler((ushort)(0x20A + portBase), this); // adlib mirror
+ dispatcher.AddIOPortHandler((ushort)(0x20B + portBase), this); // IRQ/DMA select
+
+ // High group (gusBase + 0x100)
+ dispatcher.AddIOPortHandler((ushort)(0x302 + portBase), this); // voice index
+ dispatcher.AddIOPortHandler((ushort)(0x303 + portBase), this); // GF1 reg select
+ dispatcher.AddIOPortHandler((ushort)(0x304 + portBase), this); // data word
+ dispatcher.AddIOPortHandler((ushort)(0x305 + portBase), this); // data high byte
+ dispatcher.AddIOPortHandler((ushort)(0x307 + portBase), this); // DRAM byte
+ }
+
+ private void EnsureRenderBuf(int size) {
+ if (_renderBuf.Length < size) {
+ _renderBuf = new AudioFrame[size];
+ }
+ }
+}
diff --git a/src/Spice86.Core/Emulator/Devices/Sound/GusTimer.cs b/src/Spice86.Core/Emulator/Devices/Sound/GusTimer.cs
new file mode 100644
index 0000000000..2b3fbc5ed3
--- /dev/null
+++ b/src/Spice86.Core/Emulator/Devices/Sound/GusTimer.cs
@@ -0,0 +1,32 @@
+namespace Spice86.Core.Emulator.Devices.Sound;
+
+///
+/// State for one of the two GUS hardware timers.
+///
+///
+/// 2022-2025 The DOSBox Staging Team
+///
+internal sealed class GusTimer {
+ /// Countdown period in milliseconds.
+ public double Delay { get; set; }
+
+ /// Reload value written to the timer register.
+ public byte Value { get; set; } = 0xFF;
+
+ /// True once the timer has fired since the last reset.
+ public bool HasExpired { get; set; } = true;
+
+ /// True while the timer is actively counting down.
+ public bool IsCountingDown { get; set; }
+
+ /// True when the timer is masked and will not fire IRQs.
+ public bool IsMasked { get; set; }
+
+ /// True when the timer should raise an IRQ on expiry.
+ public bool ShouldRaiseIrq { get; set; }
+
+ /// Initialises a timer with the given period in milliseconds.
+ public GusTimer(double delayMs) {
+ Delay = delayMs;
+ }
+}
diff --git a/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs b/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs
new file mode 100644
index 0000000000..38fe040230
--- /dev/null
+++ b/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs
@@ -0,0 +1,274 @@
+namespace Spice86.Core.Emulator.Devices.Sound;
+
+using Spice86.Audio.Common;
+
+using System;
+
+///
+/// One of the 32 independent sample-playing voices on the Gravis UltraSound GF1 chip.
+/// Each voice has independent wave-playback control (position, start, end, rate, loop)
+/// and a volume-ramp envelope, plus stereo panning.
+///
+///
+/// 2022-2025 The DOSBox Staging Team
+///
+internal sealed class GusVoice {
+ // Control-state bit flags shared by wave and volume controls.
+ private const byte CtrlReset = 0x01;
+ private const byte CtrlStopped = 0x02;
+ private const byte CtrlDisabled = CtrlReset | CtrlStopped;
+ private const byte CtrlBit16 = 0x04;
+ private const byte CtrlLoop = 0x08;
+ private const byte CtrlBidirectional = 0x10;
+ private const byte CtrlRaiseIrq = 0x20;
+ private const byte CtrlDecreasing = 0x40;
+
+ /// Width used for wave-position fractional interpolation (2^9 = 512 sub-steps per sample).
+ private const int WaveWidth = 1 << 9;
+
+ /// 8-bit samples are scaled to match the 16-bit range.
+ private const float To16BitRange = 256f; // = 2^(16-8)
+
+ private readonly GusVoiceIrq _voiceIrq;
+ private readonly uint _irqMask;
+
+ // Wave-control state
+ public int WaveStart { get; set; }
+ public int WaveEnd { get; set; }
+ public int WavePos { get; set; }
+ public int WaveInc { get; set; }
+ public ushort WaveRate { get; set; }
+ public byte WaveState { get; set; } = CtrlDisabled;
+
+ // Volume-ramp state
+ public int VolStart { get; set; }
+ public int VolEnd { get; set; }
+ public int VolPos { get; set; }
+ public int VolInc { get; set; }
+ public ushort VolRate { get; set; }
+ public byte VolState { get; set; } = CtrlDisabled;
+
+ public byte PanPosition { get; private set; } = GravisUltraSound.PanDefaultPosition;
+
+ /// Milliseconds of 8-bit audio generated (for statistics).
+ public uint Generated8BitMs { get; set; }
+
+ /// Milliseconds of 16-bit audio generated (for statistics).
+ public uint Generated16BitMs { get; set; }
+
+ /// Initialises a voice with its index and a reference to the shared IRQ state.
+ public GusVoice(byte num, GusVoiceIrq voiceIrq) {
+ _voiceIrq = voiceIrq;
+ _irqMask = 1u << num;
+ }
+
+ // -----------------------------------------------------------------------
+ // Public interface
+ // -----------------------------------------------------------------------
+
+ ///
+ /// Accumulates this voice's rendered samples into the first entries of .
+ ///
+ public void RenderFrames(byte[] ram, float[] volScalars, AudioFrame[] panScalars, AudioFrame[] frames, int count) {
+ if ((WaveState & CtrlDisabled) != 0) {
+ return;
+ }
+
+ AudioFrame pan = panScalars[PanPosition];
+
+ for (int i = 0; i < count; i++) {
+ float sample = GetSample(ram);
+ sample *= PopVolScalar(volScalars);
+ frames[i] = new AudioFrame(
+ frames[i].Left + sample * pan.Left,
+ frames[i].Right + sample * pan.Right);
+ }
+
+ if (Is16Bit()) {
+ Generated16BitMs++;
+ } else {
+ Generated8BitMs++;
+ }
+ }
+
+ public byte ReadWaveState() => ReadCtrlState(WaveState, _voiceIrq.WaveState);
+ public byte ReadVolState() => ReadCtrlState(VolState, _voiceIrq.VolState);
+
+ public void ResetCtrls() {
+ VolPos = 0;
+ UpdateVolState(0x01);
+ UpdateWaveState(0x01);
+ WritePanPot(GravisUltraSound.PanDefaultPosition);
+ }
+
+ public void WritePanPot(byte pos) {
+ const byte MaxPos = GravisUltraSound.PanPositions - 1;
+ PanPosition = Math.Min(pos, MaxPos);
+ }
+
+ /// Decodes a wave-rate register value into a per-sample position increment.
+ public void WriteWaveRate(ushort val) {
+ WaveRate = val;
+ WaveInc = (int)Math.Ceiling(val / 2.0);
+ }
+
+ ///
+ /// Decodes a volume-rate register value into a per-sample volume-index increment.
+ /// The register encodes four banks of fractional increments via bits 6-7.
+ ///
+ public void WriteVolRate(byte val) {
+ VolRate = val;
+ const byte BankLength = 63;
+ int posInBank = val & BankLength;
+ int decimator = 1 << (3 * (val >> 6));
+ VolInc = (int)Math.Ceiling((double)(posInBank * GravisUltraSound.VolumeIncScalar) / decimator);
+ }
+
+ /// Updates wave control state; returns true when the voice IRQ flag changed.
+ public bool UpdateWaveState(byte state) {
+ byte old = WaveState;
+ UpdateCtrlStateFlags(ref _voiceIrq.WaveState, state);
+ WaveState = (byte)(state & 0x7F);
+ return old != WaveState || WaveStateIrqChanged(state);
+ }
+
+ /// Updates volume control state; returns true when the voice IRQ flag changed.
+ public bool UpdateVolState(byte state) {
+ byte old = VolState;
+ UpdateCtrlStateFlags(ref _voiceIrq.VolState, state);
+ VolState = (byte)(state & 0x7F);
+ return old != VolState || VolStateIrqChanged(state);
+ }
+
+ // -----------------------------------------------------------------------
+ // Private rendering helpers
+ // -----------------------------------------------------------------------
+
+ private bool Is16Bit() => (WaveState & CtrlBit16) != 0;
+
+ private float GetSample(byte[] ram) {
+ int pos = PopWavePos();
+ int addr = pos / WaveWidth;
+ int fraction = pos & (WaveWidth - 1);
+ bool shouldInterpolate = WaveInc < WaveWidth && fraction != 0;
+ float sample = Is16Bit() ? Read16BitSample(ram, addr) : Read8BitSample(ram, addr);
+
+ if (shouldInterpolate) {
+ float next = Is16Bit() ? Read16BitSample(ram, addr + 1) : Read8BitSample(ram, addr + 1);
+ sample += (next - sample) * fraction / (float)WaveWidth;
+ }
+
+ return sample;
+ }
+
+ private int PopWavePos() {
+ int current = WavePos;
+ int wavePos = WavePos;
+ byte state = WaveState;
+ IncrementCtrlPos(ref wavePos, ref state, WaveInc, WaveStart, WaveEnd,
+ ref _voiceIrq.WaveState, CheckWaveRolloverCondition());
+ WavePos = wavePos;
+ WaveState = state;
+ return current;
+ }
+
+ private float PopVolScalar(float[] volScalars) {
+ int i = CeilSdivide(VolPos, GravisUltraSound.VolumeIncScalar);
+ i = Math.Max(0, Math.Min(i, volScalars.Length - 1));
+ int volPos = VolPos;
+ byte volState = VolState;
+ IncrementCtrlPos(ref volPos, ref volState, VolInc, VolStart, VolEnd,
+ ref _voiceIrq.VolState, false);
+ VolPos = volPos;
+ VolState = volState;
+ return volScalars[i];
+ }
+
+ private bool CheckWaveRolloverCondition() {
+ // Rollover: volume control has BIT16 set and wave control has no LOOP.
+ return (VolState & CtrlBit16) != 0 && (WaveState & CtrlLoop) == 0;
+ }
+
+ private void IncrementCtrlPos(
+ ref int pos,
+ ref byte state,
+ int inc,
+ int start,
+ int end,
+ ref uint sharedIrqState,
+ bool skipLoopOrRestart) {
+
+ if ((state & CtrlDisabled) != 0) { return; }
+
+ int remaining;
+ if ((state & CtrlDecreasing) != 0) {
+ pos -= inc;
+ remaining = start - pos;
+ } else {
+ pos += inc;
+ remaining = pos - end;
+ }
+
+ if (remaining < 0) { return; }
+
+ if ((state & CtrlRaiseIrq) != 0) {
+ sharedIrqState |= _irqMask;
+ }
+
+ if (skipLoopOrRestart) { return; }
+
+ if ((state & CtrlLoop) != 0) {
+ if ((state & CtrlBidirectional) != 0) {
+ state ^= CtrlDecreasing;
+ }
+ pos = (state & CtrlDecreasing) != 0 ? end - remaining : start + remaining;
+ } else {
+ state |= 1; // stop the voice
+ pos = (state & CtrlDecreasing) != 0 ? start : end;
+ }
+ }
+
+ private static float Read8BitSample(byte[] ram, int addr) {
+ int i = addr & 0xFFFFF;
+ return (sbyte)ram[i] * To16BitRange;
+ }
+
+ private static float Read16BitSample(byte[] ram, int addr) {
+ uint upper = (uint)addr & 0xC0000u;
+ uint lower = (uint)addr & 0x1FFFFu;
+ int i = (int)(upper | (lower << 1));
+ return (short)(ram[i] | (ram[i + 1] << 8));
+ }
+
+ private static byte ReadCtrlState(byte state, uint sharedIrqState) {
+ byte result = state;
+ if ((sharedIrqState & 0x80) != 0) { // irq bit lives in shared state
+ result |= 0x80;
+ }
+ return result;
+ }
+
+ private void UpdateCtrlStateFlags(ref uint sharedIrqState, byte newState) {
+ if ((newState & 0xA0) == 0xA0) {
+ sharedIrqState |= _irqMask;
+ } else {
+ sharedIrqState &= ~_irqMask;
+ }
+ }
+
+ private bool WaveStateIrqChanged(byte newState) {
+ uint prev = _voiceIrq.WaveState;
+ return (prev & _irqMask) != ((newState & 0x80) != 0 ? _irqMask : 0);
+ }
+
+ private bool VolStateIrqChanged(byte newState) {
+ uint prev = _voiceIrq.VolState;
+ return (prev & _irqMask) != ((newState & 0x80) != 0 ? _irqMask : 0);
+ }
+
+ private static int CeilSdivide(int a, int b) {
+ if (b == 0) { return 0; }
+ if (a >= 0) { return (a + b - 1) / b; }
+ return a / b;
+ }
+}
diff --git a/src/Spice86.Core/Emulator/Devices/Sound/GusVoiceIrq.cs b/src/Spice86.Core/Emulator/Devices/Sound/GusVoiceIrq.cs
new file mode 100644
index 0000000000..ef4624727d
--- /dev/null
+++ b/src/Spice86.Core/Emulator/Devices/Sound/GusVoiceIrq.cs
@@ -0,0 +1,20 @@
+namespace Spice86.Core.Emulator.Devices.Sound;
+
+///
+/// Shared IRQ state for all GUS voices.
+/// Wave and volume IRQs accumulate here as bitmasks, one bit per voice.
+/// Fields (not auto-properties) so voice code can pass them by reference.
+///
+///
+/// 2022-2025 The DOSBox Staging Team
+///
+internal sealed class GusVoiceIrq {
+ /// Volume-ramp IRQ bitmask: bit N is set when voice N requests a volume IRQ.
+ public uint VolState;
+
+ /// Wave-position IRQ bitmask: bit N is set when voice N requests a wave IRQ.
+ public uint WaveState;
+
+ /// Index of the next voice whose IRQ should be reported via the IRQ-status register.
+ public byte Status;
+}
diff --git a/src/Spice86/Spice86DependencyInjection.cs b/src/Spice86/Spice86DependencyInjection.cs
index e80e1dffea..e5ff287439 100644
--- a/src/Spice86/Spice86DependencyInjection.cs
+++ b/src/Spice86/Spice86DependencyInjection.cs
@@ -447,7 +447,8 @@ internal Spice86DependencyInjection(Configuration configuration, MainWindow? mai
emulationLoopScheduler, _emulatedClock,
audioRuntimeOptions);
GravisUltraSound gravisUltraSound = new(state, ioPortDispatcher,
- configuration.FailOnUnhandledPort, loggerService);
+ configuration.FailOnUnhandledPort, loggerService, dmaSystem, dualPic,
+ mixer, emulationLoopScheduler, audioRuntimeOptions);
loggerService.LogInformation("Sound devices created...");
@@ -498,7 +499,8 @@ internal Spice86DependencyInjection(Configuration configuration, MainWindow? mai
state, biosKeyboardBuffer,
keyboardInt16Handler, biosDataArea, vgaFunctionality,
new Dictionary {
- { "BLASTER", soundBlaster.BlasterString } }, ioPortDispatcher, loggerService,
+ { "BLASTER", soundBlaster.BlasterString },
+ { "ULTRASND", gravisUltraSound.UltraSndString } }, ioPortDispatcher, loggerService,
floppyDiskTimingService,
mixer, driveActivityNotifier, xms);
From 913ae4060604e687f7c29e1df20cfcbe411a8560 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 13 Aug 2026 18:41:47 +0000
Subject: [PATCH 3/7] feat: implement full Gravis UltraSound GF1 emulation
based on dosbox-staging reference
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Fix register data accumulation: port 0x304 byte-write latches low byte only;
port 0x305 triggers the write with accumulated 16-bit value (matching GF1 protocol)
- Fix all voice register byte ordering: control/rate/vol registers use high byte of
accumulated register_data, matching dosbox-staging WriteToRegister() semantics
- Add volume register format: vol start/end use (data << 4) * VolumeIncScalar,
vol pos uses (register_data >> 4) * VolumeIncScalar
- Fix wave address MSW/LSW helpers: proper 13-bit MSW masking and 16-bit LSW masking
- Add ResetRegister state (0x4C): full GF1 reset on is_running=0; render guard checks
is_running && is_dac_enabled before synthesising voices
- Add MixControlRegister: latches_enabled gates IRQ activation; irq_control_selected
steers port 0x20B between IRQ and DMA address selection
- Add port 0x20B IRQ/DMA address selection with GUS SDK lookup tables
- Fix CheckVoiceIrqs: sets irq_status bits 0x20/0x40 for wave/vol IRQs and advances
voice_irq.Status to the next pending voice
- Fix GetVoiceIrqStatus (reg 0x8F): returns voice_irq.Status|0x20 with inverted vol/
wave bits in the high byte, matching dosbox 0x8F read semantics
- Fix timer base delays: 80 µs (timer 1) and 320 µs (timer 2) per GUS SDK spec
- Fix timer control register (0x45): updates ShouldRaiseIrq per timer, clears IRQ
status bits when IRQ is disabled
- Fix timer value writes (0x46/0x47): precompute total delay = (256-value)*baseDelay
- Fix OnTimerControl: handles 0x80 expired-flag reset and IsMasked bits
- Add missing register reads: 0x41 (DMA control with TC IRQ pending), 0x42 (DMA
address), 0x45 (timer ctrl), 0x49 (DMA sample ctrl), 0x4C (reset register),
0x82/0x83 (wave start MSW/LSW), 0x8A/0x8B (wave pos MSW/LSW)
- Fix active voices register (0x0E): use 1 + ((register_data>>8) & 31), high byte
- Fix DMA address registers (0x43/0x44): correct 20-bit DRAM address reconstruction
- Add GetDmaOffset/UpdateDmaAddr for 16-bit DMA channel address translation
- Fix vol scalars: constant-dB spacing using DELTA_DB=0.002709201 (dosbox formula)
- Fix pan scalars: asymmetric normalisation ensures position 7 is exactly at centre
- Fix _outputQueue initialisation: field-level init avoids NullReferenceException in
NotifyLockMixer during mixer registration
- GusVoice: fix ReadCtrlState to check per-voice irqMask instead of fixed bit 0x80
- GusVoice: fix RenderFrames to skip only when both wave AND vol states are disabled
- GusVoice: simplify UpdateWaveState/UpdateVolState to directly compare before/after
IRQ state, removing redundant helper methods
Co-authored-by: maximilien-noal <1087524+maximilien-noal@users.noreply.github.com>
---
.../Devices/Sound/GravisUltraSound.cs | 630 +++++++++++++-----
.../Emulator/Devices/Sound/GusVoice.cs | 55 +-
2 files changed, 495 insertions(+), 190 deletions(-)
diff --git a/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs b/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
index abb40a4594..5e38941d42 100644
--- a/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
+++ b/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
@@ -22,10 +22,12 @@ namespace Spice86.Core.Emulator.Devices.Sound;
///
///
/// Port map (at default base 0x240, portBase = gusBase - 0x200 = 0x40):
-/// 0x240 – mix control (w); 0x246 – IRQ status (r); 0x248 – timer status (r/w);
-/// 0x249 – timer control (w); 0x24B – IRQ/DMA select (w);
-/// 0x342 – voice index (r/w); 0x343 – GF1 register select (r/w);
-/// 0x344/0x345 – data word (r/w); 0x347 – DRAM byte (r/w).
+/// 0x200+pb – mix control (w); 0x206+pb – IRQ status (r); 0x208+pb – timer status (r/w);
+/// 0x209+pb – timer control (w); 0x20A+pb – AdLib command mirror (r);
+/// 0x20B+pb – IRQ/DMA select (w);
+/// 0x302+pb – voice index (r/w); 0x303+pb – GF1 register select (r/w);
+/// 0x304+pb – data word (r/w); 0x305+pb – data high byte (r/w);
+/// 0x307+pb – DRAM byte (r/w).
///
/// Reference implementation: dosbox-staging gus.cpp / gus.h
/// (c) 2022-2025 The DOSBox Staging Team
@@ -56,35 +58,49 @@ public sealed class GravisUltraSound : DefaultIOPortHandler, IRequestInterrupt,
private const int MinVoices = 14;
private const int RamSizeBytes = 1024 * 1024; // 1 MiB on-board DRAM
- // IRQ-status byte bits
- private const byte IrqDmaFinished = 0x80;
- private const byte IrqTimer1Bit = 0x04;
- private const byte IrqTimer2Bit = 0x08;
-
- // GF1 register addresses
- private const byte RegVoiceControl = 0x00;
- private const byte RegFreqDivisor = 0x01;
- private const byte RegLoopStartHigh = 0x02;
- private const byte RegLoopStartLow = 0x03;
- private const byte RegLoopEndHigh = 0x04;
- private const byte RegLoopEndLow = 0x05;
- private const byte RegVolRate = 0x06;
- private const byte RegVolStart = 0x07;
- private const byte RegVolEnd = 0x08;
- private const byte RegVolPos = 0x09;
- private const byte RegPanPot = 0x0A;
- private const byte RegVolControl = 0x0B;
- private const byte RegActiveVoices = 0x0E;
- private const byte RegDmaControl = 0x41;
- private const byte RegDmaAddr = 0x42;
- private const byte RegDramLow = 0x43;
- private const byte RegDramHigh = 0x44;
- private const byte RegTimerControl = 0x45;
- private const byte RegTimer1 = 0x46;
- private const byte RegTimer2 = 0x47;
- private const byte RegSamplingFreq = 0x48;
- private const byte RegSamplingCtrl = 0x49;
- private const byte RegIrqStatus = 0x8F;
+ // GF1 timer base delays in milliseconds (80 µs and 320 µs)
+ private const double Timer1DefaultDelayMs = 0.080;
+ private const double Timer2DefaultDelayMs = 0.320;
+
+ // Volume scaling constant: 0.0235 dB per increment step
+ private const double DeltaDb = 0.002709201;
+
+ // Default AdLib command register value
+ private const byte AdlibCmdDefault = 85;
+
+ // Default mix control register state: latches enabled, line-in and line-out disabled
+ private const byte MixControlDefault = 0x0B;
+
+ // IRQ status byte bits
+ private const byte IrqWaveStateBit = 0x20;
+ private const byte IrqVolStateBit = 0x40;
+ private const byte IrqDmaFinished = 0x80;
+ private const byte IrqTimer1Bit = 0x04;
+ private const byte IrqTimer2Bit = 0x08;
+
+ // Mix control register bits
+ private const byte MixCtrlLatchesEnabled = 0x08;
+ private const byte MixCtrlIrqCtrlSelected = 0x40;
+
+ // Reset register bits
+ private const byte ResetRegIsRunning = 0x01;
+ private const byte ResetRegDacEnabled = 0x02;
+ private const byte ResetRegIrqsEnabled = 0x04;
+
+ // DMA control register bits
+ private const byte DmaCtrlEnabled = 0x01;
+ private const byte DmaCtrlGusToHost = 0x02;
+ private const byte DmaCtrlChannel16Bit = 0x04;
+ private const byte DmaCtrlWantsIrqOnTc = 0x20;
+ private const byte DmaCtrlSamples16Bit = 0x40; // write: samples are 16-bit
+ private const byte DmaCtrlTcIrqPending = 0x40; // read: TC IRQ pending
+ private const byte DmaCtrlInvertHighBit = 0x80;
+
+ // IRQ address lookup table (index → IRQ number), per GUS SDK section 2.14
+ private static readonly byte[] IrqAddresses = { 0, 2, 5, 3, 7, 11, 12, 15 };
+
+ // DMA address lookup table (index → DMA channel), per GUS SDK section 2.15
+ private static readonly byte[] DmaAddresses = { 0, 1, 3, 5, 6, 7 };
// -----------------------------------------------------------------------
// Fields
@@ -104,7 +120,7 @@ public sealed class GravisUltraSound : DefaultIOPortHandler, IRequestInterrupt,
private readonly SoftwareMixer _mixer;
private readonly DeviceScheduler _scheduler;
private readonly SoundChannel _channel;
- private readonly RWQueue _outputQueue;
+ private readonly RWQueue _outputQueue = new RWQueue(4096);
private readonly byte _irq;
private readonly byte _dma;
@@ -113,9 +129,19 @@ public sealed class GravisUltraSound : DefaultIOPortHandler, IRequestInterrupt,
// GF1 register / I/O state
private byte _voiceIndex;
private byte _selectedReg;
- private int _dramAddr;
- private byte _dmaCtrl;
+ private int _dramAddr;
+ private byte _dmaControlReg;
private byte _irqStatus;
+ private ushort _registerData;
+ private byte _resetReg;
+ private byte _mixControl = MixControlDefault;
+ private byte _timerCtrl;
+ private byte _sampleCtrl;
+ private byte _adlibCommandReg = AdlibCmdDefault;
+ private ushort _dmaAddr;
+ private byte _dmaAddressNibble;
+ private bool _shouldChangeIrqDma;
+ private bool _irqPreviouslyInterrupted;
private int _activeVoices = MinVoices;
@@ -159,8 +185,8 @@ public GravisUltraSound(
_voices[v] = new GusVoice(v, _voiceIrq);
}
- _timers[0] = new GusTimer(250.0 / 1000.0); // 250 µs in ms
- _timers[1] = new GusTimer(1000.0 / 1000.0); // 1 ms
+ _timers[0] = new GusTimer(Timer1DefaultDelayMs);
+ _timers[1] = new GusTimer(Timer2DefaultDelayMs);
InitPortHandlers(ioPortDispatcher, portBase);
@@ -176,7 +202,7 @@ public GravisUltraSound(
_channel = mixer.AddChannel(MixerCallback, GetSampleRate(), "GravisUltraSound", features);
int queueCapacity = (int)Math.Ceiling(_channel.FramesPerBlock * 2.0f);
- _outputQueue = new RWQueue(queueCapacity);
+ _outputQueue.Resize(queueCapacity);
_renderBuf = new AudioFrame[Math.Max(1, (int)_channel.FramesPerBlock)];
mixer.UnlockMixerThread();
@@ -206,11 +232,13 @@ public GravisUltraSound(
///
public void RaiseInterruptRequest() {
- bool pendingVoice = _voiceIrq.WaveState != 0 || _voiceIrq.VolState != 0;
- bool pendingGlobal = (_irqStatus & (IrqDmaFinished | IrqTimer1Bit | IrqTimer2Bit)) != 0;
- if (pendingVoice || pendingGlobal) {
+ bool shouldInterrupt = (_irqStatus & (IsIrqsEnabled() ? 0xFF : 0x9F)) != 0;
+ if (shouldInterrupt && IsLatchesEnabled()) {
_dualPic.ActivateIrq(_irq);
+ } else if (_irqPreviouslyInterrupted) {
+ _dualPic.DeactivateIrq(_irq);
}
+ _irqPreviouslyInterrupted = shouldInterrupt;
}
/// Returns the ULTRASND environment variable value.
@@ -218,7 +246,7 @@ public void RaiseInterruptRequest() {
$"{_gusBase:X3},{_dma},{_dma},{_irq},{_irq}";
// -----------------------------------------------------------------------
- // I/O port overrides (port argument is always ushort per base class)
+ // I/O port overrides
// -----------------------------------------------------------------------
///
@@ -227,10 +255,10 @@ public override byte ReadByte(ushort port) {
switch (offset) {
case 0x06: // IRQ status
return GetIrqStatus();
- case 0x08: // Timer/adlib status
+ case 0x08: // Timer/AdLib status
return GetTimerStatus();
- case 0x0A: // Adlib command mirror
- return 0xFF;
+ case 0x0A: // AdLib command mirror
+ return _adlibCommandReg;
default:
break;
}
@@ -266,13 +294,17 @@ public override void WriteByte(ushort port, byte value) {
int offset = port - _gusBase;
switch (offset) {
case 0x00: // mix control
+ _mixControl = value;
+ _shouldChangeIrqDma = true;
return;
- case 0x08: // timer/adlib command — ignored (adlib-compat path)
+ case 0x08: // AdLib command mirror
+ _adlibCommandReg = value;
return;
case 0x09: // timer control
OnTimerControl(value);
return;
- case 0x0B: // IRQ/DMA select — configuration is fixed at init time
+ case 0x0B: // IRQ/DMA select
+ OnIrqDmaSelect(value);
return;
default:
break;
@@ -284,13 +316,15 @@ public override void WriteByte(ushort port, byte value) {
_voiceIndex = (byte)(value & (MaxVoices - 1));
return;
case 0x03: // GF1 register select
- _selectedReg = value;
+ _selectedReg = value;
+ _registerData = 0;
return;
- case 0x04: // data low byte
- WriteToRegister(_selectedReg, value);
+ case 0x04: // data low byte – latch only, no register write yet
+ _registerData = (ushort)((_registerData & 0xFF00) | value);
return;
- case 0x05: // data high byte
- WriteToRegisterHigh(_selectedReg, value);
+ case 0x05: // data high byte – latch then commit
+ _registerData = (ushort)((_registerData & 0x00FF) | (value << 8));
+ WriteToRegister();
return;
case 0x07: // DRAM byte
if (_dramAddr < RamSizeBytes) {
@@ -307,7 +341,8 @@ public override void WriteByte(ushort port, byte value) {
public override void WriteWord(ushort port, ushort value) {
int highOffset = port - (_gusBase + 0x100);
if (highOffset == 0x04) {
- WriteToRegister(_selectedReg, value);
+ _registerData = value;
+ WriteToRegister();
return;
}
base.WriteWord(port, value);
@@ -320,9 +355,14 @@ public override void WriteWord(ushort port, ushort value) {
private void MixerCallback(int framesRequested) {
EnsureRenderBuf(framesRequested);
Array.Clear(_renderBuf, 0, framesRequested);
- for (int v = 0; v < _activeVoices; v++) {
- _voices[v].RenderFrames(_ram, _volScalars, _panScalars, _renderBuf, framesRequested);
+
+ // Only render when the GF1 is running and the DAC is enabled
+ if (IsRunning() && IsDacEnabled()) {
+ for (int v = 0; v < _activeVoices; v++) {
+ _voices[v].RenderFrames(_ram, _volScalars, _panScalars, _renderBuf, framesRequested);
+ }
}
+
CheckVoiceIrqs();
_outputQueue.NonblockingBulkEnqueue(_renderBuf.AsSpan(0, framesRequested), framesRequested);
@@ -334,28 +374,53 @@ private void MixerCallback(int framesRequested) {
// -----------------------------------------------------------------------
private ushort ReadFromRegister() {
+ // Global DSP registers
+ switch (_selectedReg) {
+ case 0x41: { // DMA control register – read clears TC IRQ
+ byte reg = _dmaControlReg;
+ if ((_irqStatus & IrqDmaFinished) != 0) {
+ reg |= DmaCtrlTcIrqPending;
+ }
+ _irqStatus &= unchecked((byte)~IrqDmaFinished);
+ RaiseInterruptRequest();
+ return (ushort)(reg << 8);
+ }
+ case 0x42: // DMA address register
+ return _dmaAddr;
+ case 0x45: // Timer control
+ return (ushort)(_timerCtrl << 8);
+ case 0x49: // DMA sample control
+ return (ushort)(_dmaControlReg << 8);
+ case 0x4C: // Reset register
+ return (ushort)(_resetReg << 8);
+ case 0x8F: // Voice IRQ status
+ return GetVoiceIrqStatus();
+ default:
+ break;
+ }
+
+ // Voice-specific registers
GusVoice voice = _voices[_voiceIndex];
switch (_selectedReg) {
- case 0x80: // read voice control
- return voice.ReadWaveState();
- case 0x8B: // read volume control
- return voice.ReadVolState();
- case RegLoopStartHigh:
+ case 0x80: // Voice wave control
+ return (ushort)(voice.ReadWaveState() << 8);
+ case 0x82: // Voice wave start MSW
return (ushort)(voice.WaveStart >> 16);
- case RegLoopStartLow:
+ case 0x83: // Voice wave start LSW
return (ushort)(voice.WaveStart & 0xFFFF);
- case RegLoopEndHigh:
- return (ushort)(voice.WaveEnd >> 16);
- case RegLoopEndLow:
- return (ushort)(voice.WaveEnd & 0xFFFF);
- case RegVolPos:
- return (ushort)(voice.VolPos / VolumeIncScalar);
- case RegPanPot:
- return voice.PanPosition;
- case RegIrqStatus:
- return GetVoiceIrqStatus();
+ case 0x89: { // Voice volume position
+ int i = CeilSdivide(voice.VolPos, VolumeIncScalar);
+ i = Math.Max(0, Math.Min(i, VolLevels - 1));
+ return (ushort)(i << 4);
+ }
+ case 0x8A: // Voice wave current position MSW
+ return (ushort)(voice.WavePos >> 16);
+ case 0x8B: // Voice wave current position LSW
+ return (ushort)(voice.WavePos & 0xFFFF);
+ case 0x8D: // Voice volume control
+ return (ushort)(voice.ReadVolState() << 8);
default:
- return 0xFF;
+ return _registerData; // echo back last written value
}
}
@@ -363,82 +428,144 @@ private ushort ReadFromRegister() {
// GF1 register writes
// -----------------------------------------------------------------------
- private void WriteToRegister(byte reg, ushort value) {
+ private void WriteToRegister() {
GusVoice voice = _voices[_voiceIndex];
- switch (reg) {
- case RegVoiceControl:
- voice.UpdateWaveState((byte)value);
- break;
- case RegFreqDivisor:
- voice.WriteWaveRate(value);
- break;
- case RegLoopStartHigh:
- voice.WaveStart = (voice.WaveStart & 0x0000FFFF) | ((int)value << 16);
- break;
- case RegLoopStartLow:
- voice.WaveStart = (voice.WaveStart & unchecked((int)0xFFFF0000u)) | (int)value;
- break;
- case RegLoopEndHigh:
- voice.WaveEnd = (voice.WaveEnd & 0x0000FFFF) | ((int)value << 16);
- break;
- case RegLoopEndLow:
- voice.WaveEnd = (voice.WaveEnd & unchecked((int)0xFFFF0000u)) | (int)value;
- break;
- case RegVolRate:
- voice.WriteVolRate((byte)value);
- break;
- case RegVolStart:
- voice.VolStart = value * VolumeIncScalar;
+
+ // Global DSP registers
+ switch (_selectedReg) {
+ case 0x0E: { // Set number of active voices
+ // Jazz Jackrabbit reads back the register select from this write
+ _selectedReg = (byte)(_registerData >> 8);
+ byte num = (byte)(1 + ((_registerData >> 8) & 31));
+ SetActiveVoices(num);
+ return;
+ }
+ case 0x10: // Undocumented register (Fast Tracker 2)
+ return;
+ case 0x41: // DMA control register
+ _dmaControlReg = (byte)(_registerData >> 8);
+ if ((_dmaControlReg & DmaCtrlEnabled) != 0) {
+ StartDmaTransfer();
+ }
+ return;
+ case 0x42: // DMA DRAM address register
+ _dmaAddr = _registerData;
+ _dmaAddressNibble = 0;
+ return;
+ case 0x43: // DRAM address LSW (bits 0-15)
+ _dramAddr = (_dramAddr & 0xF0000) | _registerData;
+ return;
+ case 0x44: // DRAM address MSW (bits 16-19 in upper nibble of high byte)
+ _dramAddr = (_dramAddr & 0x0FFFF) | ((_registerData & 0x0F00) << 8);
+ return;
+ case 0x45: // Timer control register
+ _timerCtrl = (byte)(_registerData >> 8);
+ _timers[0].ShouldRaiseIrq = (_timerCtrl & IrqTimer1Bit) != 0;
+ _timers[1].ShouldRaiseIrq = (_timerCtrl & IrqTimer2Bit) != 0;
+ if (!_timers[0].ShouldRaiseIrq) {
+ _irqStatus &= unchecked((byte)~IrqTimer1Bit);
+ }
+ if (!_timers[1].ShouldRaiseIrq) {
+ _irqStatus &= unchecked((byte)~IrqTimer2Bit);
+ }
+ if (!_timers[0].ShouldRaiseIrq && !_timers[1].ShouldRaiseIrq) {
+ RaiseInterruptRequest();
+ }
+ return;
+ case 0x46: // Timer 1 value
+ _timers[0].Value = (byte)(_registerData >> 8);
+ _timers[0].Delay = (0x100 - _timers[0].Value) * Timer1DefaultDelayMs;
+ return;
+ case 0x47: // Timer 2 value
+ _timers[1].Value = (byte)(_registerData >> 8);
+ _timers[1].Delay = (0x100 - _timers[1].Value) * Timer2DefaultDelayMs;
+ return;
+ case 0x49: // DMA sampling control register
+ _sampleCtrl = (byte)(_registerData >> 8);
+ if ((_sampleCtrl & 0x01) != 0) {
+ StartDmaTransfer();
+ }
+ return;
+ case 0x4C: // Reset register
+ _resetReg = (byte)(_registerData >> 8);
+ if ((_resetReg & ResetRegIsRunning) == 0) {
+ DoReset();
+ }
+ return;
+ default:
break;
- case RegVolEnd:
- voice.VolEnd = value * VolumeIncScalar;
+ }
+
+ // Voice-specific registers
+ switch (_selectedReg) {
+ case 0x00: // Voice wave control
+ if (voice.UpdateWaveState((byte)(_registerData >> 8))) {
+ CheckVoiceIrqs();
+ }
break;
- case RegVolPos:
- voice.VolPos = value * VolumeIncScalar;
+ case 0x01: // Voice wave rate
+ voice.WriteWaveRate(_registerData);
break;
- case RegPanPot:
- voice.WritePanPot((byte)value);
+ case 0x02: // Voice wave start MSW
+ voice.WaveStart = UpdateWaveMsw(voice.WaveStart, _registerData);
break;
- case RegVolControl:
- voice.UpdateVolState((byte)value);
+ case 0x03: // Voice wave start LSW
+ voice.WaveStart = UpdateWaveLsw(voice.WaveStart, _registerData);
break;
- case RegActiveVoices:
- SetActiveVoices((byte)(value & 0x1F));
+ case 0x04: // Voice wave end MSW
+ voice.WaveEnd = UpdateWaveMsw(voice.WaveEnd, _registerData);
break;
- case RegDmaControl:
- _dmaCtrl = (byte)value;
- if ((_dmaCtrl & 0x01) != 0) {
- StartDmaTransfer();
- }
+ case 0x05: // Voice wave end LSW
+ voice.WaveEnd = UpdateWaveLsw(voice.WaveEnd, _registerData);
break;
- case RegDmaAddr:
- _dramAddr = value << 4;
+ case 0x06: // Voice volume rate
+ voice.WriteVolRate((byte)(_registerData >> 8));
break;
- case RegDramLow:
- _dramAddr = (_dramAddr & 0xFF00) | (byte)value;
+ case 0x07: { // Voice volume start (EEEEMMMM format)
+ byte data = (byte)(_registerData >> 8);
+ voice.VolStart = (data << 4) * VolumeIncScalar;
break;
- case RegDramHigh:
- _dramAddr = (_dramAddr & 0x00FF) | (value << 8);
+ }
+ case 0x08: { // Voice volume end (EEEEMMMM format)
+ byte data = (byte)(_registerData >> 8);
+ voice.VolEnd = (data << 4) * VolumeIncScalar;
break;
- case RegTimerControl:
+ }
+ case 0x09: // Voice volume current position
+ voice.VolPos = (_registerData >> 4) * VolumeIncScalar;
break;
- case RegTimer1:
- _timers[0].Value = (byte)value;
+ case 0x0A: // Voice wave current position MSW
+ voice.WavePos = UpdateWaveMsw(voice.WavePos, _registerData);
break;
- case RegTimer2:
- _timers[1].Value = (byte)value;
+ case 0x0B: // Voice wave current position LSW
+ voice.WavePos = UpdateWaveLsw(voice.WavePos, _registerData);
break;
- case RegSamplingFreq:
- _channel.SampleRate = (int)(1000000.0 / (2.0 * (value + 1)));
+ case 0x0C: // Voice pan pot
+ voice.WritePanPot((byte)(_registerData >> 8));
break;
- case RegSamplingCtrl:
+ case 0x0D: // Voice volume control
+ if (voice.UpdateVolState((byte)(_registerData >> 8))) {
+ CheckVoiceIrqs();
+ }
break;
}
}
- private void WriteToRegisterHigh(byte reg, byte high) {
- ushort current = ReadFromRegister();
- WriteToRegister(reg, (ushort)((current & 0x00FF) | (high << 8)));
+ // -----------------------------------------------------------------------
+ // Wave address helpers (match dosbox UpdateWaveMsw / UpdateWaveLsw)
+ // -----------------------------------------------------------------------
+
+ private static int UpdateWaveMsw(int addr, ushort regData) {
+ // Keep bits 0-15 of addr; replace bits 16-28 with the 13-bit regData value.
+ int lower = addr & 0x0000FFFF;
+ int upper = (regData & 0x1FFF) << 16;
+ return lower | upper;
+ }
+
+ private static int UpdateWaveLsw(int addr, ushort regData) {
+ // Keep bits 16-31 of addr; replace bits 0-15 with regData.
+ int upper = addr & unchecked((int)0xFFFF0000);
+ return upper | regData;
}
// -----------------------------------------------------------------------
@@ -446,15 +573,24 @@ private void WriteToRegisterHigh(byte reg, byte high) {
// -----------------------------------------------------------------------
private void OnTimerControl(byte value) {
+ if ((value & 0x80) != 0) {
+ // Reset timer expired flags
+ _timers[0].HasExpired = false;
+ _timers[1].HasExpired = false;
+ return;
+ }
+
+ _timers[0].IsMasked = (value & 0x40) != 0;
+ _timers[1].IsMasked = (value & 0x20) != 0;
+
for (int t = 0; t < _timers.Length; t++) {
GusTimer timer = _timers[t];
bool start = (value & (1 << t)) != 0;
if (start && !timer.IsCountingDown) {
timer.IsCountingDown = true;
timer.HasExpired = false;
- int timerIndex = t;
- double delayMs = timer.Delay * (256 - timer.Value);
- _scheduler.AddEvent(_ => OnTimerExpired(timerIndex), delayMs, 0);
+ int timerIndex = t;
+ _scheduler.AddEvent(_ => OnTimerExpired(timerIndex), timer.Delay, 0);
} else if (!start) {
timer.IsCountingDown = false;
}
@@ -464,7 +600,9 @@ private void OnTimerControl(byte value) {
private void OnTimerExpired(int timerIndex) {
GusTimer timer = _timers[timerIndex];
if (!timer.IsCountingDown) { return; }
- timer.HasExpired = true;
+ if (!timer.IsMasked) {
+ timer.HasExpired = true;
+ }
timer.IsCountingDown = false;
if (timer.ShouldRaiseIrq) {
_irqStatus |= timerIndex == 0 ? IrqTimer1Bit : IrqTimer2Bit;
@@ -476,9 +614,45 @@ private byte GetTimerStatus() {
byte status = 0;
if (_timers[0].HasExpired) { status |= 0x40; }
if (_timers[1].HasExpired) { status |= 0x20; }
+ if ((status & 0x60) != 0) { status |= 0x80; } // combined expired bit
+ if ((_irqStatus & IrqTimer1Bit) != 0) { status |= 0x04; }
+ if ((_irqStatus & IrqTimer2Bit) != 0) { status |= 0x02; }
return status;
}
+ // -----------------------------------------------------------------------
+ // IRQ/DMA port 0x20B selection
+ // -----------------------------------------------------------------------
+
+ private void OnIrqDmaSelect(byte value) {
+ if (!_shouldChangeIrqDma) {
+ return;
+ }
+ _shouldChangeIrqDma = false;
+
+ byte ch1Selector = (byte)(value & 0x07);
+ byte ch2Selector = (byte)((value >> 3) & 0x07);
+ bool ch2Combined = (value & 0x40) != 0;
+
+ if ((_mixControl & MixCtrlIrqCtrlSelected) != 0) {
+ // Selecting IRQ numbers
+ if (ch1Selector < IrqAddresses.Length && IrqAddresses[ch1Selector] != 0) {
+ // IRQ reassignment is not supported at runtime; configuration is fixed at startup.
+ }
+ } else {
+ // Selecting DMA channels
+ if (ch1Selector < DmaAddresses.Length && DmaAddresses[ch1Selector] != 0) {
+ byte newDma = DmaAddresses[ch1Selector];
+ UpdatePlaybackDmaAddress(newDma);
+ }
+ if (ch2Combined) {
+ // ch2 mirrors ch1
+ } else if (ch2Selector < DmaAddresses.Length && DmaAddresses[ch2Selector] != 0) {
+ // Recording DMA – not currently emulated
+ }
+ }
+ }
+
// -----------------------------------------------------------------------
// IRQ handling
// -----------------------------------------------------------------------
@@ -486,29 +660,90 @@ private byte GetTimerStatus() {
private byte GetIrqStatus() {
byte result = _irqStatus;
_irqStatus = 0;
+ _irqPreviouslyInterrupted = false;
_dualPic.DeactivateIrq(_irq);
return result;
}
private ushort GetVoiceIrqStatus() {
- for (byte v = 0; v < _activeVoices; v++) {
- uint mask = 1u << v;
- if ((_voiceIrq.WaveState & mask) != 0) {
- _voiceIrq.WaveState &= ~mask;
- return v;
- }
- if ((_voiceIrq.VolState & mask) != 0) {
- _voiceIrq.VolState &= ~mask;
- return (ushort)(v | 0x80);
- }
+ // Returns the voice index with pending IRQ and clears its bits.
+ // Bit 5 is always set; bit 6 clear = vol IRQ, bit 7 clear = wave IRQ.
+ byte reg = (byte)(_voiceIrq.Status | 0x20);
+ uint mask = 1u << _voiceIrq.Status;
+
+ if ((_voiceIrq.VolState & mask) == 0) {
+ reg |= 0x40; // no vol IRQ for this voice
}
- return 0x1F;
+ if ((_voiceIrq.WaveState & mask) == 0) {
+ reg |= 0x80; // no wave IRQ for this voice
+ }
+
+ _voiceIrq.VolState &= ~mask;
+ _voiceIrq.WaveState &= ~mask;
+ CheckVoiceIrqs();
+ return (ushort)(reg << 8);
}
private void CheckVoiceIrqs() {
- if (_voiceIrq.WaveState != 0 || _voiceIrq.VolState != 0) {
+ // Clear voice IRQ bits in irq_status then re-evaluate
+ _irqStatus &= 0x9F;
+ uint activeVoiceMask = 0xFFFFFFFFu >> (MaxVoices - _activeVoices);
+ uint totalMask = (_voiceIrq.VolState | _voiceIrq.WaveState) & activeVoiceMask;
+
+ if (totalMask == 0) {
RaiseInterruptRequest();
+ return;
+ }
+
+ if (_voiceIrq.VolState != 0) {
+ _irqStatus |= IrqVolStateBit;
+ }
+ if (_voiceIrq.WaveState != 0) {
+ _irqStatus |= IrqWaveStateBit;
}
+
+ RaiseInterruptRequest();
+
+ // Advance status to the next voice with a pending IRQ
+ while ((totalMask & (1u << _voiceIrq.Status)) == 0) {
+ _voiceIrq.Status++;
+ if (_voiceIrq.Status >= _activeVoices) {
+ _voiceIrq.Status = 0;
+ }
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Reset
+ // -----------------------------------------------------------------------
+
+ private void DoReset() {
+ _channel.Enable(false);
+
+ _irqStatus = 0;
+ _irqPreviouslyInterrupted = false;
+ _adlibCommandReg = AdlibCmdDefault;
+ _dmaControlReg = 0;
+ _sampleCtrl = 0;
+ _timerCtrl = 0;
+ _timers[0] = new GusTimer(Timer1DefaultDelayMs);
+ _timers[1] = new GusTimer(Timer2DefaultDelayMs);
+
+ for (int v = 0; v < MaxVoices; v++) {
+ _voices[v].ResetCtrls();
+ }
+
+ _voiceIrq.VolState = 0;
+ _voiceIrq.WaveState = 0;
+ _voiceIrq.Status = 0;
+
+ UpdateDmaAddr(0);
+ _dramAddr = 0;
+ _registerData = 0;
+ _selectedReg = 0;
+ _shouldChangeIrqDma = false;
+ _mixControl = MixControlDefault;
+ _activeVoices = MinVoices;
}
// -----------------------------------------------------------------------
@@ -520,47 +755,97 @@ private void StartDmaTransfer() {
channel?.RegisterCallback(OnDmaEvent);
}
+ private void UpdatePlaybackDmaAddress(byte newDma) {
+ DmaChannel? oldChannel = _dmaBus.GetChannel(_dma);
+ oldChannel?.Reset();
+ DmaChannel? newChannel = _dmaBus.GetChannel(newDma);
+ newChannel?.ReserveFor("GravisUltraSound", OnDmaChannelEvicted);
+ newChannel?.RegisterCallback(OnDmaEvent);
+ }
+
private void OnDmaEvent(DmaChannel channel, DmaChannel.DmaEvent evt) {
if (evt != DmaChannel.DmaEvent.IsUnmasked) { return; }
PerformDmaTransfer(channel);
}
private void OnDmaChannelEvicted() {
- // Nothing to do; DMA was forcibly released by another device.
+ // DMA was forcibly released by another device.
}
private void PerformDmaTransfer(DmaChannel channel) {
const int ChunkSize = 512;
byte[] chunk = new byte[ChunkSize];
+ uint dmaOffset = GetDmaOffset();
+ bool invert = (_dmaControlReg & DmaCtrlInvertHighBit) != 0;
+ bool samples16 = (_dmaControlReg & DmaCtrlSamples16Bit) != 0;
+
while (true) {
int bytesRead = channel.Read(ChunkSize, chunk.AsSpan());
if (bytesRead <= 0) { break; }
+
for (int i = 0; i < bytesRead; i++) {
- int dest = _dramAddr + i;
+ int dest = (int)(dmaOffset + (uint)i);
if (dest >= RamSizeBytes) { break; }
byte sample = chunk[i];
- if ((_dmaCtrl & 0x80) != 0) {
- sample ^= 0x80;
+ if (invert) {
+ if (!samples16 || (i & 1) != 0) {
+ sample ^= 0x80;
+ }
}
_ram[dest] = sample;
}
- _dramAddr = (_dramAddr + bytesRead) & 0xFFFFF; // 20-bit DRAM address
+
+ dmaOffset = (dmaOffset + (uint)bytesRead) & 0xFFFFF;
}
- if ((_dmaCtrl & 0x02) != 0) {
+ UpdateDmaAddr(dmaOffset);
+
+ if ((_dmaControlReg & DmaCtrlWantsIrqOnTc) != 0) {
_irqStatus |= IrqDmaFinished;
RaiseInterruptRequest();
}
}
+ private uint GetDmaOffset() {
+ uint adjusted;
+ if (IsDmaXfer16Bit()) {
+ uint upper = (uint)_dmaAddr & 0xC000u;
+ uint lower = (uint)_dmaAddr & 0x1FFFu;
+ adjusted = upper | (lower << 1);
+ } else {
+ adjusted = _dmaAddr;
+ }
+ return (adjusted << 4) + _dmaAddressNibble;
+ }
+
+ private void UpdateDmaAddr(uint offset) {
+ uint adjusted;
+ if (IsDmaXfer16Bit()) {
+ uint upper = offset & 0xC0000u;
+ uint lower = offset & 0x3FFFEu;
+ adjusted = upper | (lower >> 1);
+ } else {
+ adjusted = offset & 0xFFFF0u;
+ }
+ _dmaAddr = (ushort)(adjusted >> 4);
+ _dmaAddressNibble = (byte)(adjusted & 0x0F);
+ }
+
+ private bool IsDmaXfer16Bit() {
+ return (_dmaControlReg & DmaCtrlChannel16Bit) != 0 && _dma >= 4;
+ }
+
// -----------------------------------------------------------------------
// Voice and sample-rate management
// -----------------------------------------------------------------------
private void SetActiveVoices(byte count) {
- _activeVoices = Math.Max(MinVoices, Math.Min(MaxVoices, (int)count));
- _channel.SampleRate = GetSampleRate();
+ int clamped = Math.Max((int)MinVoices, Math.Min((int)MaxVoices, (int)count));
+ if (clamped != _activeVoices) {
+ _activeVoices = clamped;
+ _channel.SampleRate = GetSampleRate();
+ }
}
private int GetSampleRate() {
@@ -572,16 +857,24 @@ private int GetSampleRate() {
// -----------------------------------------------------------------------
private void BuildVolScalars() {
- for (int i = 0; i < VolLevels; i++) {
- _volScalars[i] = i == 0 ? 0.0f
- : (float)Math.Pow(10.0, (i - (VolLevels - 1)) / 20.0 * 0.002);
+ // Build the table from the end downward, dividing by (1 + DeltaDb) each step.
+ // This produces constant-dB spacing, matching the GUS hardware.
+ double scalar = 1.0;
+ double divisor = 1.0 + DeltaDb;
+ for (int i = VolLevels - 1; i >= 1; i--) {
+ _volScalars[i] = (float)scalar;
+ scalar /= divisor;
}
- _volScalars[VolLevels - 1] = 1.0f;
+ _volScalars[0] = 0.0f;
}
private void BuildPanScalars() {
+ // Constant-power panning: positions 0-15 map from full-left to full-right,
+ // with position 7 exactly at centre. The asymmetric normalization (÷7 vs ÷8)
+ // keeps position 7 at exactly π/4.
for (int p = 0; p < PanPositions; p++) {
- float angle = p / (float)(PanPositions - 1) * (float)(Math.PI / 2.0);
+ double norm = (p - 7.0) / (p < 7 ? 7.0 : 8.0);
+ double angle = (norm + 1.0) * Math.PI / 4.0;
_panScalars[p] = new AudioFrame(
(float)Math.Cos(angle),
(float)Math.Sin(angle));
@@ -596,9 +889,9 @@ private void InitPortHandlers(IOPortDispatcher dispatcher, int portBase) {
// Low group (around gusBase)
dispatcher.AddIOPortHandler((ushort)(0x200 + portBase), this); // mix control
dispatcher.AddIOPortHandler((ushort)(0x206 + portBase), this); // IRQ status
- dispatcher.AddIOPortHandler((ushort)(0x208 + portBase), this); // timer status
+ dispatcher.AddIOPortHandler((ushort)(0x208 + portBase), this); // timer status / AdLib mirror
dispatcher.AddIOPortHandler((ushort)(0x209 + portBase), this); // timer control
- dispatcher.AddIOPortHandler((ushort)(0x20A + portBase), this); // adlib mirror
+ dispatcher.AddIOPortHandler((ushort)(0x20A + portBase), this); // adlib command mirror
dispatcher.AddIOPortHandler((ushort)(0x20B + portBase), this); // IRQ/DMA select
// High group (gusBase + 0x100)
@@ -609,9 +902,24 @@ private void InitPortHandlers(IOPortDispatcher dispatcher, int portBase) {
dispatcher.AddIOPortHandler((ushort)(0x307 + portBase), this); // DRAM byte
}
+ // -----------------------------------------------------------------------
+ // Helpers
+ // -----------------------------------------------------------------------
+
private void EnsureRenderBuf(int size) {
if (_renderBuf.Length < size) {
_renderBuf = new AudioFrame[size];
}
}
+
+ private bool IsRunning() => (_resetReg & ResetRegIsRunning) != 0;
+ private bool IsDacEnabled() => (_resetReg & ResetRegDacEnabled) != 0;
+ private bool IsIrqsEnabled() => (_resetReg & ResetRegIrqsEnabled) != 0;
+ private bool IsLatchesEnabled() => (_mixControl & MixCtrlLatchesEnabled) != 0;
+
+ private static int CeilSdivide(int a, int b) {
+ if (b == 0) { return 0; }
+ if (a >= 0) { return (a + b - 1) / b; }
+ return a / b;
+ }
}
diff --git a/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs b/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs
index 38fe040230..d83874fb72 100644
--- a/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs
+++ b/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs
@@ -70,7 +70,9 @@ public GusVoice(byte num, GusVoiceIrq voiceIrq) {
/// Accumulates this voice's rendered samples into the first entries of .
///
public void RenderFrames(byte[] ram, float[] volScalars, AudioFrame[] panScalars, AudioFrame[] frames, int count) {
- if ((WaveState & CtrlDisabled) != 0) {
+ // Skip rendering only when BOTH wave AND vol controls are independently disabled,
+ // matching the GF1 hardware behaviour documented in the UltraSound SDK.
+ if ((WaveState & VolState & CtrlDisabled) != 0) {
return;
}
@@ -109,7 +111,7 @@ public void WritePanPot(byte pos) {
/// Decodes a wave-rate register value into a per-sample position increment.
public void WriteWaveRate(ushort val) {
WaveRate = val;
- WaveInc = (int)Math.Ceiling(val / 2.0);
+ WaveInc = CeilUdivide(val, 2u);
}
///
@@ -121,23 +123,31 @@ public void WriteVolRate(byte val) {
const byte BankLength = 63;
int posInBank = val & BankLength;
int decimator = 1 << (3 * (val >> 6));
- VolInc = (int)Math.Ceiling((double)(posInBank * GravisUltraSound.VolumeIncScalar) / decimator);
+ VolInc = CeilSdivide(posInBank * GravisUltraSound.VolumeIncScalar, decimator);
}
/// Updates wave control state; returns true when the voice IRQ flag changed.
public bool UpdateWaveState(byte state) {
- byte old = WaveState;
- UpdateCtrlStateFlags(ref _voiceIrq.WaveState, state);
+ uint origIrqState = _voiceIrq.WaveState;
+ if ((state & 0xA0) == 0xA0) {
+ _voiceIrq.WaveState |= _irqMask;
+ } else {
+ _voiceIrq.WaveState &= ~_irqMask;
+ }
WaveState = (byte)(state & 0x7F);
- return old != WaveState || WaveStateIrqChanged(state);
+ return origIrqState != _voiceIrq.WaveState;
}
/// Updates volume control state; returns true when the voice IRQ flag changed.
public bool UpdateVolState(byte state) {
- byte old = VolState;
- UpdateCtrlStateFlags(ref _voiceIrq.VolState, state);
+ uint origIrqState = _voiceIrq.VolState;
+ if ((state & 0xA0) == 0xA0) {
+ _voiceIrq.VolState |= _irqMask;
+ } else {
+ _voiceIrq.VolState &= ~_irqMask;
+ }
VolState = (byte)(state & 0x7F);
- return old != VolState || VolStateIrqChanged(state);
+ return origIrqState != _voiceIrq.VolState;
}
// -----------------------------------------------------------------------
@@ -240,35 +250,22 @@ private static float Read16BitSample(byte[] ram, int addr) {
return (short)(ram[i] | (ram[i + 1] << 8));
}
- private static byte ReadCtrlState(byte state, uint sharedIrqState) {
+ private byte ReadCtrlState(byte state, uint irqState) {
byte result = state;
- if ((sharedIrqState & 0x80) != 0) { // irq bit lives in shared state
+ if ((irqState & _irqMask) != 0) {
result |= 0x80;
}
return result;
}
- private void UpdateCtrlStateFlags(ref uint sharedIrqState, byte newState) {
- if ((newState & 0xA0) == 0xA0) {
- sharedIrqState |= _irqMask;
- } else {
- sharedIrqState &= ~_irqMask;
- }
- }
-
- private bool WaveStateIrqChanged(byte newState) {
- uint prev = _voiceIrq.WaveState;
- return (prev & _irqMask) != ((newState & 0x80) != 0 ? _irqMask : 0);
- }
-
- private bool VolStateIrqChanged(byte newState) {
- uint prev = _voiceIrq.VolState;
- return (prev & _irqMask) != ((newState & 0x80) != 0 ? _irqMask : 0);
- }
-
private static int CeilSdivide(int a, int b) {
if (b == 0) { return 0; }
if (a >= 0) { return (a + b - 1) / b; }
return a / b;
}
+
+ private static int CeilUdivide(uint a, uint b) {
+ if (b == 0) { return 0; }
+ return (int)((a + b - 1) / b);
+ }
}
From 7752678f2b8bcf3f29afe802cc71f49b1b053267 Mon Sep 17 00:00:00 2001
From: Maximilien Noal
Date: Thu, 13 Aug 2026 21:38:19 +0200
Subject: [PATCH 4/7] Potential fix for pull request finding 'CodeQL / Cast to
same type'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
---
src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs b/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
index 5e38941d42..3dc99e2133 100644
--- a/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
+++ b/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
@@ -841,7 +841,7 @@ private bool IsDmaXfer16Bit() {
// -----------------------------------------------------------------------
private void SetActiveVoices(byte count) {
- int clamped = Math.Max((int)MinVoices, Math.Min((int)MaxVoices, (int)count));
+ int clamped = Math.Max(MinVoices, Math.Min((int)MaxVoices, (int)count));
if (clamped != _activeVoices) {
_activeVoices = clamped;
_channel.SampleRate = GetSampleRate();
From 4b42cf4a1d0bc0862cec202a52edbf982466ede0 Mon Sep 17 00:00:00 2001
From: Maximilien Noal
Date: Thu, 13 Aug 2026 21:38:33 +0200
Subject: [PATCH 5/7] Potential fix for pull request finding 'CodeQL / Nested
'if' statements can be combined'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
---
src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs b/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
index 3dc99e2133..185ca69f09 100644
--- a/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
+++ b/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
@@ -788,10 +788,8 @@ private void PerformDmaTransfer(DmaChannel channel) {
int dest = (int)(dmaOffset + (uint)i);
if (dest >= RamSizeBytes) { break; }
byte sample = chunk[i];
- if (invert) {
- if (!samples16 || (i & 1) != 0) {
- sample ^= 0x80;
- }
+ if (invert && (!samples16 || (i & 1) != 0)) {
+ sample ^= 0x80;
}
_ram[dest] = sample;
}
From a65f224267d79edfa35fe0bcc7807de605f450c9 Mon Sep 17 00:00:00 2001
From: Maximilien Noal
Date: Thu, 13 Aug 2026 21:38:41 +0200
Subject: [PATCH 6/7] Potential fix for pull request finding 'CodeQL / Too many
'ref' parameters'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
---
.../Emulator/Devices/Sound/GusVoice.cs | 70 +++++++++++--------
1 file changed, 41 insertions(+), 29 deletions(-)
diff --git a/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs b/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs
index d83874fb72..fb39ad4ba9 100644
--- a/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs
+++ b/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs
@@ -173,24 +173,22 @@ private float GetSample(byte[] ram) {
private int PopWavePos() {
int current = WavePos;
- int wavePos = WavePos;
- byte state = WaveState;
- IncrementCtrlPos(ref wavePos, ref state, WaveInc, WaveStart, WaveEnd,
- ref _voiceIrq.WaveState, CheckWaveRolloverCondition());
- WavePos = wavePos;
- WaveState = state;
+ CtrlPosUpdate update = IncrementCtrlPos(WavePos, WaveState, WaveInc, WaveStart, WaveEnd,
+ _voiceIrq.WaveState, CheckWaveRolloverCondition());
+ WavePos = update.Pos;
+ WaveState = update.State;
+ _voiceIrq.WaveState = update.SharedIrqState;
return current;
}
private float PopVolScalar(float[] volScalars) {
- int i = CeilSdivide(VolPos, GravisUltraSound.VolumeIncScalar);
- i = Math.Max(0, Math.Min(i, volScalars.Length - 1));
- int volPos = VolPos;
- byte volState = VolState;
- IncrementCtrlPos(ref volPos, ref volState, VolInc, VolStart, VolEnd,
- ref _voiceIrq.VolState, false);
- VolPos = volPos;
- VolState = volState;
+ int i = CeilSdivide(VolPos, GravisUltraSound.VolumeIncScalar);
+ i = Math.Max(0, Math.Min(i, volScalars.Length - 1));
+ CtrlPosUpdate update = IncrementCtrlPos(VolPos, VolState, VolInc, VolStart, VolEnd,
+ _voiceIrq.VolState, false);
+ VolPos = update.Pos;
+ VolState = update.State;
+ _voiceIrq.VolState = update.SharedIrqState;
return volScalars[i];
}
@@ -199,33 +197,45 @@ private bool CheckWaveRolloverCondition() {
return (VolState & CtrlBit16) != 0 && (WaveState & CtrlLoop) == 0;
}
- private void IncrementCtrlPos(
- ref int pos,
- ref byte state,
- int inc,
- int start,
- int end,
- ref uint sharedIrqState,
- bool skipLoopOrRestart) {
+ private readonly struct CtrlPosUpdate {
+ public CtrlPosUpdate(int pos, byte state, uint sharedIrqState) {
+ Pos = pos;
+ State = state;
+ SharedIrqState = sharedIrqState;
+ }
+
+ public int Pos { get; }
+ public byte State { get; }
+ public uint SharedIrqState { get; }
+ }
- if ((state & CtrlDisabled) != 0) { return; }
+ private CtrlPosUpdate IncrementCtrlPos(
+ int pos,
+ byte state,
+ int inc,
+ int start,
+ int end,
+ uint sharedIrqState,
+ bool skipLoopOrRestart) {
+
+ if ((state & CtrlDisabled) != 0) { return new CtrlPosUpdate(pos, state, sharedIrqState); }
int remaining;
if ((state & CtrlDecreasing) != 0) {
- pos -= inc;
- remaining = start - pos;
+ pos -= inc;
+ remaining = start - pos;
} else {
- pos += inc;
- remaining = pos - end;
+ pos += inc;
+ remaining = pos - end;
}
- if (remaining < 0) { return; }
+ if (remaining < 0) { return new CtrlPosUpdate(pos, state, sharedIrqState); }
if ((state & CtrlRaiseIrq) != 0) {
sharedIrqState |= _irqMask;
}
- if (skipLoopOrRestart) { return; }
+ if (skipLoopOrRestart) { return new CtrlPosUpdate(pos, state, sharedIrqState); }
if ((state & CtrlLoop) != 0) {
if ((state & CtrlBidirectional) != 0) {
@@ -236,6 +246,8 @@ private void IncrementCtrlPos(
state |= 1; // stop the voice
pos = (state & CtrlDecreasing) != 0 ? start : end;
}
+
+ return new CtrlPosUpdate(pos, state, sharedIrqState);
}
private static float Read8BitSample(byte[] ram, int addr) {
From 8197da5b459bb878904e601ac9bd70d6ab6d3ed6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 13 Aug 2026 19:44:58 +0000
Subject: [PATCH 7/7] fix: correct DMA word/byte usage, DRAM wrap, invert
logic; remove separator comments; fix redundant cast
Co-authored-by: maximilien-noal <1087524+maximilien-noal@users.noreply.github.com>
---
.../Devices/Sound/GravisUltraSound.cs | 55 +++----------------
.../Emulator/Devices/Sound/GusVoice.cs | 4 --
2 files changed, 9 insertions(+), 50 deletions(-)
diff --git a/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs b/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
index 185ca69f09..ee3c31c63c 100644
--- a/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
+++ b/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs
@@ -34,9 +34,7 @@ namespace Spice86.Core.Emulator.Devices.Sound;
///
public sealed class GravisUltraSound : DefaultIOPortHandler, IRequestInterrupt, IAudioQueueDevice, IMixerQueueNotifier {
- // -----------------------------------------------------------------------
// Public constants used by GusVoice
- // -----------------------------------------------------------------------
/// Number of pan-position slots (0-15).
public const byte PanPositions = 16;
@@ -50,9 +48,7 @@ public sealed class GravisUltraSound : DefaultIOPortHandler, IRequestInterrupt,
/// Fixed-point scale applied to volume-ramp position values.
public const int VolumeIncScalar = 512;
- // -----------------------------------------------------------------------
// Hardware constants
- // -----------------------------------------------------------------------
private const int MaxVoices = 32;
private const int MinVoices = 14;
@@ -102,9 +98,7 @@ public sealed class GravisUltraSound : DefaultIOPortHandler, IRequestInterrupt,
// DMA address lookup table (index → DMA channel), per GUS SDK section 2.15
private static readonly byte[] DmaAddresses = { 0, 1, 3, 5, 6, 7 };
- // -----------------------------------------------------------------------
// Fields
- // -----------------------------------------------------------------------
private readonly byte[] _ram = new byte[RamSizeBytes];
@@ -148,9 +142,7 @@ public sealed class GravisUltraSound : DefaultIOPortHandler, IRequestInterrupt,
// Render scratch buffer (grows on demand, never shrinks)
private AudioFrame[] _renderBuf;
- // -----------------------------------------------------------------------
// Constructor
- // -----------------------------------------------------------------------
///
/// Initialises the GUS emulation and registers all hardware I/O ports.
@@ -214,9 +206,7 @@ public GravisUltraSound(
dmaChannel?.RegisterCallback(OnDmaEvent);
}
- // -----------------------------------------------------------------------
// Interface implementations
- // -----------------------------------------------------------------------
///
public RWQueue OutputQueue => _outputQueue;
@@ -245,9 +235,7 @@ public void RaiseInterruptRequest() {
public string UltraSndString =>
$"{_gusBase:X3},{_dma},{_dma},{_irq},{_irq}";
- // -----------------------------------------------------------------------
// I/O port overrides
- // -----------------------------------------------------------------------
///
public override byte ReadByte(ushort port) {
@@ -348,9 +336,7 @@ public override void WriteWord(ushort port, ushort value) {
base.WriteWord(port, value);
}
- // -----------------------------------------------------------------------
// Mixer callback
- // -----------------------------------------------------------------------
private void MixerCallback(int framesRequested) {
EnsureRenderBuf(framesRequested);
@@ -369,9 +355,7 @@ private void MixerCallback(int framesRequested) {
SoftwareMixer.PullFromQueueCallback(framesRequested, this);
}
- // -----------------------------------------------------------------------
// GF1 register reads
- // -----------------------------------------------------------------------
private ushort ReadFromRegister() {
// Global DSP registers
@@ -424,9 +408,7 @@ private ushort ReadFromRegister() {
}
}
- // -----------------------------------------------------------------------
// GF1 register writes
- // -----------------------------------------------------------------------
private void WriteToRegister() {
GusVoice voice = _voices[_voiceIndex];
@@ -551,9 +533,7 @@ private void WriteToRegister() {
}
}
- // -----------------------------------------------------------------------
// Wave address helpers (match dosbox UpdateWaveMsw / UpdateWaveLsw)
- // -----------------------------------------------------------------------
private static int UpdateWaveMsw(int addr, ushort regData) {
// Keep bits 0-15 of addr; replace bits 16-28 with the 13-bit regData value.
@@ -568,9 +548,7 @@ private static int UpdateWaveLsw(int addr, ushort regData) {
return upper | regData;
}
- // -----------------------------------------------------------------------
// Timer handling
- // -----------------------------------------------------------------------
private void OnTimerControl(byte value) {
if ((value & 0x80) != 0) {
@@ -620,9 +598,7 @@ private byte GetTimerStatus() {
return status;
}
- // -----------------------------------------------------------------------
// IRQ/DMA port 0x20B selection
- // -----------------------------------------------------------------------
private void OnIrqDmaSelect(byte value) {
if (!_shouldChangeIrqDma) {
@@ -653,9 +629,7 @@ private void OnIrqDmaSelect(byte value) {
}
}
- // -----------------------------------------------------------------------
// IRQ handling
- // -----------------------------------------------------------------------
private byte GetIrqStatus() {
byte result = _irqStatus;
@@ -713,9 +687,7 @@ private void CheckVoiceIrqs() {
}
}
- // -----------------------------------------------------------------------
// Reset
- // -----------------------------------------------------------------------
private void DoReset() {
_channel.Enable(false);
@@ -746,9 +718,7 @@ private void DoReset() {
_activeVoices = MinVoices;
}
- // -----------------------------------------------------------------------
// DMA handling
- // -----------------------------------------------------------------------
private void StartDmaTransfer() {
DmaChannel? channel = _dmaBus.GetChannel(_dma);
@@ -773,22 +743,23 @@ private void OnDmaChannelEvicted() {
}
private void PerformDmaTransfer(DmaChannel channel) {
- const int ChunkSize = 512;
- byte[] chunk = new byte[ChunkSize];
+ const int ChunkSizeBytes = 512;
+ byte[] chunk = new byte[ChunkSizeBytes];
+ int chunkWords = ChunkSizeBytes >> channel.ShiftCount;
uint dmaOffset = GetDmaOffset();
bool invert = (_dmaControlReg & DmaCtrlInvertHighBit) != 0;
bool samples16 = (_dmaControlReg & DmaCtrlSamples16Bit) != 0;
while (true) {
- int bytesRead = channel.Read(ChunkSize, chunk.AsSpan());
- if (bytesRead <= 0) { break; }
+ int wordsRead = channel.Read(chunkWords, chunk.AsSpan());
+ if (wordsRead <= 0) { break; }
+ int bytesRead = wordsRead << channel.ShiftCount;
for (int i = 0; i < bytesRead; i++) {
- int dest = (int)(dmaOffset + (uint)i);
- if (dest >= RamSizeBytes) { break; }
+ int dest = (int)((dmaOffset + (uint)i) & 0xFFFFF);
byte sample = chunk[i];
- if (invert && (!samples16 || (i & 1) != 0)) {
+ if (invert && (!samples16 || ((dmaOffset + (uint)i) & 1) != 0)) {
sample ^= 0x80;
}
_ram[dest] = sample;
@@ -834,12 +805,10 @@ private bool IsDmaXfer16Bit() {
return (_dmaControlReg & DmaCtrlChannel16Bit) != 0 && _dma >= 4;
}
- // -----------------------------------------------------------------------
// Voice and sample-rate management
- // -----------------------------------------------------------------------
private void SetActiveVoices(byte count) {
- int clamped = Math.Max(MinVoices, Math.Min((int)MaxVoices, (int)count));
+ int clamped = Math.Max(MinVoices, Math.Min(MaxVoices, (int)count));
if (clamped != _activeVoices) {
_activeVoices = clamped;
_channel.SampleRate = GetSampleRate();
@@ -850,9 +819,7 @@ private int GetSampleRate() {
return (int)(1000000.0 / (1.619695497 * _activeVoices));
}
- // -----------------------------------------------------------------------
// Lookup-table construction
- // -----------------------------------------------------------------------
private void BuildVolScalars() {
// Build the table from the end downward, dividing by (1 + DeltaDb) each step.
@@ -879,9 +846,7 @@ private void BuildPanScalars() {
}
}
- // -----------------------------------------------------------------------
// Port registration
- // -----------------------------------------------------------------------
private void InitPortHandlers(IOPortDispatcher dispatcher, int portBase) {
// Low group (around gusBase)
@@ -900,9 +865,7 @@ private void InitPortHandlers(IOPortDispatcher dispatcher, int portBase) {
dispatcher.AddIOPortHandler((ushort)(0x307 + portBase), this); // DRAM byte
}
- // -----------------------------------------------------------------------
// Helpers
- // -----------------------------------------------------------------------
private void EnsureRenderBuf(int size) {
if (_renderBuf.Length < size) {
diff --git a/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs b/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs
index fb39ad4ba9..02ad1a8354 100644
--- a/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs
+++ b/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs
@@ -62,9 +62,7 @@ public GusVoice(byte num, GusVoiceIrq voiceIrq) {
_irqMask = 1u << num;
}
- // -----------------------------------------------------------------------
// Public interface
- // -----------------------------------------------------------------------
///
/// Accumulates this voice's rendered samples into the first entries of .
@@ -150,9 +148,7 @@ public bool UpdateVolState(byte state) {
return origIrqState != _voiceIrq.VolState;
}
- // -----------------------------------------------------------------------
// Private rendering helpers
- // -----------------------------------------------------------------------
private bool Is16Bit() => (WaveState & CtrlBit16) != 0;