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..ee3c31c63c 100644 --- a/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs +++ b/src/Spice86.Core/Emulator/Devices/Sound/GravisUltraSound.cs @@ -1,49 +1,886 @@ 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): +/// 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 +/// +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 + + // 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 + + 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 = new RWQueue(4096); + + 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 _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; + + // 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(Timer1DefaultDelayMs); + _timers[1] = new GusTimer(Timer2DefaultDelayMs); + + 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.Resize(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 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. + public string UltraSndString => + $"{_gusBase:X3},{_dma},{_dma},{_irq},{_irq}"; + + // I/O port overrides + + /// + 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 _adlibCommandReg; + 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 + _mixControl = value; + _shouldChangeIrqDma = true; + return; + case 0x08: // AdLib command mirror + _adlibCommandReg = value; + return; + case 0x09: // timer control + OnTimerControl(value); + return; + case 0x0B: // IRQ/DMA select + OnIrqDmaSelect(value); + 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; + _registerData = 0; + return; + case 0x04: // data low byte – latch only, no register write yet + _registerData = (ushort)((_registerData & 0xFF00) | value); + return; + case 0x05: // data high byte – latch then commit + _registerData = (ushort)((_registerData & 0x00FF) | (value << 8)); + WriteToRegister(); + 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) { + _registerData = value; + WriteToRegister(); + return; + } + base.WriteWord(port, value); + } + + // Mixer callback + + private void MixerCallback(int framesRequested) { + EnsureRenderBuf(framesRequested); + Array.Clear(_renderBuf, 0, 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); + SoftwareMixer.PullFromQueueCallback(framesRequested, this); + } + + // GF1 register reads + + 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: // Voice wave control + return (ushort)(voice.ReadWaveState() << 8); + case 0x82: // Voice wave start MSW + return (ushort)(voice.WaveStart >> 16); + case 0x83: // Voice wave start LSW + return (ushort)(voice.WaveStart & 0xFFFF); + 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 _registerData; // echo back last written value + } + } + + // GF1 register writes + + private void WriteToRegister() { + GusVoice voice = _voices[_voiceIndex]; + + // 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; + } + + // Voice-specific registers + switch (_selectedReg) { + case 0x00: // Voice wave control + if (voice.UpdateWaveState((byte)(_registerData >> 8))) { + CheckVoiceIrqs(); + } + break; + case 0x01: // Voice wave rate + voice.WriteWaveRate(_registerData); + break; + case 0x02: // Voice wave start MSW + voice.WaveStart = UpdateWaveMsw(voice.WaveStart, _registerData); + break; + case 0x03: // Voice wave start LSW + voice.WaveStart = UpdateWaveLsw(voice.WaveStart, _registerData); + break; + case 0x04: // Voice wave end MSW + voice.WaveEnd = UpdateWaveMsw(voice.WaveEnd, _registerData); + break; + case 0x05: // Voice wave end LSW + voice.WaveEnd = UpdateWaveLsw(voice.WaveEnd, _registerData); + break; + case 0x06: // Voice volume rate + voice.WriteVolRate((byte)(_registerData >> 8)); + break; + case 0x07: { // Voice volume start (EEEEMMMM format) + byte data = (byte)(_registerData >> 8); + voice.VolStart = (data << 4) * VolumeIncScalar; + break; + } + case 0x08: { // Voice volume end (EEEEMMMM format) + byte data = (byte)(_registerData >> 8); + voice.VolEnd = (data << 4) * VolumeIncScalar; + break; + } + case 0x09: // Voice volume current position + voice.VolPos = (_registerData >> 4) * VolumeIncScalar; + break; + case 0x0A: // Voice wave current position MSW + voice.WavePos = UpdateWaveMsw(voice.WavePos, _registerData); + break; + case 0x0B: // Voice wave current position LSW + voice.WavePos = UpdateWaveLsw(voice.WavePos, _registerData); + break; + case 0x0C: // Voice pan pot + voice.WritePanPot((byte)(_registerData >> 8)); + break; + case 0x0D: // Voice volume control + if (voice.UpdateVolState((byte)(_registerData >> 8))) { + CheckVoiceIrqs(); + } + break; + } + } + + // 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; + } + + // Timer handling + + 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; + _scheduler.AddEvent(_ => OnTimerExpired(timerIndex), timer.Delay, 0); + } else if (!start) { + timer.IsCountingDown = false; + } + } + } + + private void OnTimerExpired(int timerIndex) { + GusTimer timer = _timers[timerIndex]; + if (!timer.IsCountingDown) { return; } + if (!timer.IsMasked) { + 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; } + 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 + + private byte GetIrqStatus() { + byte result = _irqStatus; + _irqStatus = 0; + _irqPreviouslyInterrupted = false; + _dualPic.DeactivateIrq(_irq); + return result; + } + + private ushort GetVoiceIrqStatus() { + // 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 + } + 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() { + // 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; + } + + // DMA handling + + private void StartDmaTransfer() { + DmaChannel? channel = _dmaBus.GetChannel(_dma); + 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() { + // DMA was forcibly released by another device. + } + + private void PerformDmaTransfer(DmaChannel channel) { + 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 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) & 0xFFFFF); + byte sample = chunk[i]; + if (invert && (!samples16 || ((dmaOffset + (uint)i) & 1) != 0)) { + sample ^= 0x80; + } + _ram[dest] = sample; + } + + dmaOffset = (dmaOffset + (uint)bytesRead) & 0xFFFFF; + } + + 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) { + int clamped = Math.Max(MinVoices, Math.Min(MaxVoices, (int)count)); + if (clamped != _activeVoices) { + _activeVoices = clamped; + _channel.SampleRate = GetSampleRate(); + } + } + + 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. + // 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[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++) { + 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)); + } + } + + // 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 / AdLib mirror + dispatcher.AddIOPortHandler((ushort)(0x209 + portBase), this); // timer control + dispatcher.AddIOPortHandler((ushort)(0x20A + portBase), this); // adlib command 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 + } + + // 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/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..02ad1a8354 --- /dev/null +++ b/src/Spice86.Core/Emulator/Devices/Sound/GusVoice.cs @@ -0,0 +1,279 @@ +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) { + // 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; + } + + 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 = CeilUdivide(val, 2u); + } + + /// + /// 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 = CeilSdivide(posInBank * GravisUltraSound.VolumeIncScalar, decimator); + } + + /// Updates wave control state; returns true when the voice IRQ flag changed. + public bool UpdateWaveState(byte state) { + uint origIrqState = _voiceIrq.WaveState; + if ((state & 0xA0) == 0xA0) { + _voiceIrq.WaveState |= _irqMask; + } else { + _voiceIrq.WaveState &= ~_irqMask; + } + WaveState = (byte)(state & 0x7F); + return origIrqState != _voiceIrq.WaveState; + } + + /// Updates volume control state; returns true when the voice IRQ flag changed. + public bool UpdateVolState(byte state) { + uint origIrqState = _voiceIrq.VolState; + if ((state & 0xA0) == 0xA0) { + _voiceIrq.VolState |= _irqMask; + } else { + _voiceIrq.VolState &= ~_irqMask; + } + VolState = (byte)(state & 0x7F); + return origIrqState != _voiceIrq.VolState; + } + + // 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; + 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)); + CtrlPosUpdate update = IncrementCtrlPos(VolPos, VolState, VolInc, VolStart, VolEnd, + _voiceIrq.VolState, false); + VolPos = update.Pos; + VolState = update.State; + _voiceIrq.VolState = update.SharedIrqState; + 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 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; } + } + + 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; + } else { + pos += inc; + remaining = pos - end; + } + + if (remaining < 0) { return new CtrlPosUpdate(pos, state, sharedIrqState); } + + if ((state & CtrlRaiseIrq) != 0) { + sharedIrqState |= _irqMask; + } + + if (skipLoopOrRestart) { return new CtrlPosUpdate(pos, state, sharedIrqState); } + + 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; + } + + return new CtrlPosUpdate(pos, state, sharedIrqState); + } + + 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 byte ReadCtrlState(byte state, uint irqState) { + byte result = state; + if ((irqState & _irqMask) != 0) { + result |= 0x80; + } + return result; + } + + 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); + } +} 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);