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;