diff --git a/Contralto.sln b/Contralto.sln index d83d932..095cc50 100644 --- a/Contralto.sln +++ b/Contralto.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +# Visual Studio 15 +VisualStudioVersion = 15.0.26403.7 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contralto", "Contralto\Contralto.csproj", "{CC6D96B3-8099-4497-8AD8-B0795A3353EA}" EndProject @@ -42,4 +42,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection EndGlobal diff --git a/Contralto/AltoSystem.cs b/Contralto/AltoSystem.cs index b8131b5..605b056 100644 --- a/Contralto/AltoSystem.cs +++ b/Contralto/AltoSystem.cs @@ -37,13 +37,15 @@ namespace Contralto _scheduler = new Scheduler(); _memBus = new MemoryBus(); - _mem = new Memory.Memory(); + _mem = new Memory.Memory(); _keyboard = new Keyboard(); _diskController = new DiskController(this); _displayController = new DisplayController(this); _mouse = new Mouse(); _ethernetController = new EthernetController(this); - _musicInterface = new Music(this); + _orbitController = new OrbitController(this); + _audioDAC = new AudioDAC(this); + _organKeyboard = new OrganKeyboard(this); _cpu = new AltoCPU(this); @@ -51,9 +53,10 @@ namespace Contralto _memBus.AddDevice(_mem); _memBus.AddDevice(_keyboard); _memBus.AddDevice(_mouse); - _memBus.AddDevice(_musicInterface); + _memBus.AddDevice(_audioDAC); + _memBus.AddDevice(_organKeyboard); - Reset(); + Reset(); } public void Reset() @@ -70,9 +73,9 @@ namespace Contralto _mouse.Reset(); _cpu.Reset(); _ethernetController.Reset(); - _musicInterface.Reset(); + _orbitController.Reset(); - UCodeMemory.Reset(); + UCodeMemory.Reset(); } /// @@ -81,7 +84,7 @@ namespace Contralto /// public void AttachDisplay(IAltoDisplay d) { - _displayController.AttachDisplay(d); + _displayController.AttachDisplay(d); } public void DetachDisplay() @@ -96,11 +99,16 @@ namespace Contralto { _ethernetController.HostInterface.Shutdown(); } + + // + // Allow the DAC to flush its output + // + _audioDAC.Shutdown(); } public void SingleStep() { - // Run every device that needs attention for a single clock cycle. + // Run every device that needs attention for a single clock cycle. _memBus.Clock(); _cpu.Clock(); @@ -224,6 +232,11 @@ namespace Contralto get { return _ethernetController; } } + public OrbitController OrbitController + { + get { return _orbitController; } + } + public Scheduler Scheduler { get { return _scheduler; } @@ -239,7 +252,9 @@ namespace Contralto private DiskController _diskController; private DisplayController _displayController; private EthernetController _ethernetController; - private Music _musicInterface; + private OrbitController _orbitController; + private AudioDAC _audioDAC; + private OrganKeyboard _organKeyboard; private Scheduler _scheduler; } diff --git a/Contralto/App.config b/Contralto/App.config index 2c94bf2..7212975 100644 --- a/Contralto/App.config +++ b/Contralto/App.config @@ -17,16 +17,16 @@ 2 - + - + 34 - + 0 @@ -45,7 +45,19 @@ True - + + + True + + + True + + + False + + + + diff --git a/Contralto/CPU/CPU.cs b/Contralto/CPU/CPU.cs index f8e949f..b68c7e4 100644 --- a/Contralto/CPU/CPU.cs +++ b/Contralto/CPU/CPU.cs @@ -25,6 +25,7 @@ namespace Contralto.CPU { Invalid = -1, Emulator = 0, + Orbit = 1, DiskSector = 4, Ethernet = 7, MemoryRefresh = 8, @@ -52,6 +53,7 @@ namespace Contralto.CPU _tasks[(int)TaskType.MemoryRefresh] = new MemoryRefreshTask(this); _tasks[(int)TaskType.Ethernet] = new EthernetTask(this); _tasks[(int)TaskType.Parity] = new ParityTask(this); + _tasks[(int)TaskType.Orbit] = new OrbitTask(this); Reset(); } @@ -151,9 +153,9 @@ namespace Contralto.CPU if (_currentTask != _nextTask) { _currentTask = _nextTask; - _currentTask.FirstInstructionAfterSwitch = true; - _currentTask.OnTaskSwitch(); - } + _currentTask.FirstInstructionAfterSwitch = true; + _currentTask.OnTaskSwitch(); + } break; case InstructionCompletion.MemoryWait: @@ -198,7 +200,7 @@ namespace Contralto.CPU // Unsure if there is a deeper issue here or if there are other reset semantics // in play that are not understood. // - WakeupTask(CPU.TaskType.DiskSector); + WakeupTask(CPU.TaskType.DiskSector); } /// diff --git a/Contralto/CPU/MicroInstruction.cs b/Contralto/CPU/MicroInstruction.cs index bf42f62..9151942 100644 --- a/Contralto/CPU/MicroInstruction.cs +++ b/Contralto/CPU/MicroInstruction.cs @@ -194,6 +194,29 @@ namespace Contralto.CPU EISFCT = 14, } + /// + /// Orbit (print rasterizer) from OrbitGuide.press + /// + enum OrbitF1 + { + OrbitBlock = 3, + OrbitDeltaWC = 12, + OrbitDBCWidthRead = 13, + OrbitOutputData = 14, + OrbitStatus = 15, + } + + enum OrbitF2 + { + OrbitDBCWidthSet = 8, + OrbitXY = 9, + OrbitHeight = 10, + OrbitFontData = 11, + OrbitInk = 12, + OrbitControl = 13, + OrbitROSCommand = 14, + } + /// /// MicroInstruction encapsulates the decoding of a microinstruction word. /// It also caches precomputed metadata related to the microinstruction that diff --git a/Contralto/CPU/Tasks/DiskTask.cs b/Contralto/CPU/Tasks/DiskTask.cs index 9f585e2..52b6740 100644 --- a/Contralto/CPU/Tasks/DiskTask.cs +++ b/Contralto/CPU/Tasks/DiskTask.cs @@ -16,7 +16,6 @@ */ using Contralto.IO; -using Contralto.Logging; using System; namespace Contralto.CPU @@ -100,7 +99,7 @@ namespace Contralto.CPU case DiskF1.LoadKSTAT: // "KSTAT[12-15] are loaded from BUS[12-15]. (Actually BUS[13] is ORed onto - // KSTAT[13].)" + // KSTAT[13].)" // From the schematic (and ucode source, based on the values it actually uses for BUS[13]), BUS[13] // is also inverted. So there's that, too. @@ -109,7 +108,7 @@ namespace Contralto.CPU int modifiedBusData = (_busData & 0xb) | ((~_busData) & 0x4); // OR in BUS[12-15] after masking in KSTAT[13] so it is ORed in properly. - _diskController.KSTAT = (ushort)(((_diskController.KSTAT & 0xfff4)) | modifiedBusData); + _diskController.KSTAT = (ushort)(((_diskController.KSTAT & 0xfff4)) | modifiedBusData); break; case DiskF1.STROBE: @@ -127,9 +126,9 @@ namespace Contralto.CPU switch (df2) { - case DiskF2.INIT: - _nextModifier |= GetInitModifier(); - break; + case DiskF2.INIT: + _nextModifier |= GetInitModifier(); + break; case DiskF2.RWC: // "NEXT<-NEXT OR (IF current record to be written THEN 3 ELSE IF diff --git a/Contralto/CPU/Tasks/EmulatorTask.cs b/Contralto/CPU/Tasks/EmulatorTask.cs index 9871498..7c6953e 100644 --- a/Contralto/CPU/Tasks/EmulatorTask.cs +++ b/Contralto/CPU/Tasks/EmulatorTask.cs @@ -88,7 +88,7 @@ namespace Contralto.CPU { case EmulatorF1.RSNF: // - // Early: + // Early: // "...decoded by the Ethernet interface, which gates the host address wired on the // backplane onto BUS[8-15]. BUS[0-7] is not driven and will therefore be -1. If // no Ethernet interface is present, BUS will be -1. @@ -122,7 +122,7 @@ namespace Contralto.CPU break; case EmulatorF1.STARTF: - // Dispatch function to Ethernet I/O based on contents of AC0. + // Dispatch function to Ethernet I/O based on contents of AC0. if ((_busData & 0x8000) != 0) { // @@ -134,28 +134,35 @@ namespace Contralto.CPU // field at the end of the cycle, setting this flag causes the main Task // implementation to skip updating _mpc at the end of this instruction. _softReset = true; - } + } else if(_busData != 0) { // // Dispatch to the appropriate device. - // The Ethernet controller is the only common device that is documented - // to have used STARTF, so we'll just go there directly; if other - // hardware is determined to be worth emulating we'll put together a more flexible dispatch. // - if (_busData < 4) + switch(_busData) { - _cpu._system.EthernetController.STARTF(_busData); - } - else - { - Log.Write(Logging.LogType.Warning, Logging.LogComponent.EmulatorTask, "STARTF for non-Ethernet device (code {0})", - Conversion.ToOctal(_busData)); + case 1: + case 2: + case 3: + // Ethernet + _cpu._system.EthernetController.STARTF(_busData); + break; + + case 4: + // Orbit + _cpu._system.OrbitController.STARTF(_busData); + break; + + default: + Log.Write(Logging.LogType.Warning, Logging.LogComponent.EmulatorTask, "STARTF for non-Ethernet device (code {0})", + Conversion.ToOctal(_busData)); + break; } } break; - case EmulatorF1.SWMODE: + case EmulatorF1.SWMODE: _swMode = true; break; @@ -167,14 +174,14 @@ namespace Contralto.CPU _wrtRam = true; break; - case EmulatorF1.LoadESRB: + case EmulatorF1.LoadESRB: _rb = (ushort)((_busData & 0xe) >> 1); if (_rb != 0 && _systemType != SystemType.ThreeKRam) { // Force bank 0 for machines with only 1K RAM. - _rb = 0; - } + _rb = 0; + } break; default: diff --git a/Contralto/CPU/Tasks/OrbitTask.cs b/Contralto/CPU/Tasks/OrbitTask.cs new file mode 100644 index 0000000..83dbc15 --- /dev/null +++ b/Contralto/CPU/Tasks/OrbitTask.cs @@ -0,0 +1,173 @@ +/* + This file is part of ContrAlto. + + ContrAlto is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + ContrAlto is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with ContrAlto. If not, see . +*/ +using Contralto.Logging; +using System; + +namespace Contralto.CPU +{ + public partial class AltoCPU + { + /// + /// OrbitTask provides the implementation of the Orbit (printer rasterizer) controller + /// specific functions. + /// + private sealed class OrbitTask : Task + { + public OrbitTask(AltoCPU cpu) : base(cpu) + { + _taskType = TaskType.Orbit; + _wakeup = false; + } + + public override void OnTaskSwitch() + { + // We put ourselves back to sleep immediately once we've started running. + //_wakeup = false; + } + + protected override void ExecuteBlock() + { + //_wakeup = false; + _cpu._system.OrbitController.Stop(); + } + + protected override InstructionCompletion ExecuteInstruction(MicroInstruction instruction) + { + // TODO: get rid of polling. + //_wakeup = _cpu._system.OrbitController.Wakeup; + return base.ExecuteInstruction(instruction); + } + + protected override ushort GetBusSource(MicroInstruction instruction) + { + // + // The Orbit task is wired to be a RAM-enabled task so it can use + // S registers. + // This code is stolen from the Emulator task; we should refactor this... + // + EmulatorBusSource ebs = (EmulatorBusSource)instruction.BS; + + switch (ebs) + { + case EmulatorBusSource.ReadSLocation: + if (instruction.RSELECT != 0) + { + return _cpu._s[_rb][instruction.RSELECT]; + } + else + { + // "...when reading data from the S registers onto the processor bus, + // the RSELECT value 0 causes the current value of the M register to + // appear on the bus..." + return _cpu._m; + } + + case EmulatorBusSource.LoadSLocation: + // "When an S register is being loaded from M, the processor bus receives an + // undefined value rather than being set to zero." + _loadS = true; + return 0x0; // Technically this is an "undefined value," we're defining it as -1. + + default: + throw new InvalidOperationException(String.Format("Unhandled bus source {0}", instruction.BS)); + } + + + } + + protected override void ExecuteSpecialFunction1Early(MicroInstruction instruction) + { + OrbitF1 of1 = (OrbitF1)instruction.F1; + switch (of1) + { + + case OrbitF1.OrbitDeltaWC: + _busData &= _cpu._system.OrbitController.GetDeltaWC(); + break; + + case OrbitF1.OrbitDBCWidthRead: + _busData &= _cpu._system.OrbitController.GetDBCWidth(); + break; + + case OrbitF1.OrbitOutputData: + _busData &= _cpu._system.OrbitController.GetOutputDataAlto(); + break; + + case OrbitF1.OrbitStatus: + _busData &= _cpu._system.OrbitController.GetOrbitStatus(); + + // branch: + // "OrbitStatus sets NEXT[7] of IACS os *not* on, i.e. if Orbit is + // not in a character segment." + // + if (!_cpu._system.OrbitController.IACS) + { + _nextModifier |= 0x4; + } + break; + } + } + + protected override void ExecuteSpecialFunction2(MicroInstruction instruction) + { + OrbitF2 of2 = (OrbitF2)instruction.F2; + switch (of2) + { + case OrbitF2.OrbitDBCWidthSet: + _cpu._system.OrbitController.SetDBCWidth(_busData); + break; + + case OrbitF2.OrbitXY: + _cpu._system.OrbitController.SetXY(_busData); + break; + + case OrbitF2.OrbitHeight: + _cpu._system.OrbitController.SetHeight(_busData); + + // branch: + // "OrbitHeight sets NEXT[7] if the refresh timer has expired, i.e. + // if the image buffer needs refreshing." + // + if (_cpu._system.OrbitController.RefreshTimerExpired) + { + _nextModifier |= 0x4; + } + break; + + case OrbitF2.OrbitFontData: + _cpu._system.OrbitController.WriteFontData(_busData); + break; + + case OrbitF2.OrbitInk: + _cpu._system.OrbitController.WriteInkData(_busData); + break; + + case OrbitF2.OrbitControl: + _cpu._system.OrbitController.Control(_busData); + break; + + case OrbitF2.OrbitROSCommand: + _cpu._system.OrbitController.SendROSCommand(_busData); + break; + + default: + throw new InvalidOperationException(String.Format("Unhandled orbit F2 {0}.", of2)); + } + } + } + } +} diff --git a/Contralto/CPU/Tasks/Task.cs b/Contralto/CPU/Tasks/Task.cs index 6de4d3a..0c8612d 100644 --- a/Contralto/CPU/Tasks/Task.cs +++ b/Contralto/CPU/Tasks/Task.cs @@ -494,8 +494,9 @@ namespace Contralto.CPU { _cpu._l = aluData; - // Only RAM-related tasks can modify M. (Currently only the Emulator.) - if (_taskType == TaskType.Emulator) + // Only RAM-related tasks can modify M. + if (_taskType == TaskType.Emulator || + _taskType == TaskType.Orbit) { _cpu._m = aluData; } diff --git a/Contralto/Configuration.cs b/Contralto/Configuration.cs index 80fe981..fa88f8d 100644 --- a/Contralto/Configuration.cs +++ b/Contralto/Configuration.cs @@ -185,6 +185,21 @@ namespace Contralto /// public static bool ThrottleSpeed; + /// + /// Whether to enable the DAC used for the Smalltalk music system. + /// + public static bool EnableAudioDAC; + + /// + /// Whether to enable capture of the DAC output to file. + /// + public static bool EnableAudioDACCapture; + + /// + /// The path to store DAC capture (if enabled). + /// + public static string AudioDACCapturePath; + public static string GetAltoIRomPath(string romFileName) { return Path.Combine("ROM", "AltoI", romFileName); @@ -198,7 +213,7 @@ namespace Contralto public static string GetRomPath(string romFileName) { return Path.Combine("ROM", romFileName); - } + } /// /// Reads the current configuration file from the app's configuration. @@ -218,6 +233,9 @@ namespace Contralto BootFile = (ushort)Properties.Settings.Default["BootFile"]; InterlaceDisplay = (bool)Properties.Settings.Default["InterlaceDisplay"]; ThrottleSpeed = (bool)Properties.Settings.Default["ThrottleSpeed"]; + EnableAudioDAC = (bool)Properties.Settings.Default["EnableAudioDAC"]; + EnableAudioDACCapture = (bool)Properties.Settings.Default["EnableAudioDACCapture"]; + AudioDACCapturePath = (string)Properties.Settings.Default["AudioDACCapturePath"]; } /// @@ -236,6 +254,9 @@ namespace Contralto Properties.Settings.Default["BootFile"] = BootFile; Properties.Settings.Default["InterlaceDisplay"] = InterlaceDisplay; Properties.Settings.Default["ThrottleSpeed"] = ThrottleSpeed; + Properties.Settings.Default["EnableAudioDAC"] = EnableAudioDAC; + Properties.Settings.Default["EnableAudioDACCapture"] = EnableAudioDACCapture; + Properties.Settings.Default["AudioDACCapturePath"] = AudioDACCapturePath; Properties.Settings.Default.Save(); } diff --git a/Contralto/Contralto.csproj b/Contralto/Contralto.csproj index 6ab2499..545c15f 100644 --- a/Contralto/Contralto.csproj +++ b/Contralto/Contralto.csproj @@ -94,6 +94,10 @@ app.manifest + + ..\packages\NAudio.1.8.0\lib\net35\NAudio.dll + True + False pcap\PcapDotNet.Base.dll @@ -121,8 +125,12 @@ + - + + + + @@ -271,6 +279,7 @@ PreserveNewest + SettingsSingleFileGenerator Settings.Designer.cs diff --git a/Contralto/Conversion.cs b/Contralto/Conversion.cs index 60c0d33..38edc1c 100644 --- a/Contralto/Conversion.cs +++ b/Contralto/Conversion.cs @@ -40,7 +40,7 @@ namespace Contralto public static string ToOctal(int i, int digits) { - string octalString = Convert.ToString(i, 8); + string octalString = Convert.ToString(i, 8); return new String('0', digits - octalString.Length) + octalString; } @@ -49,6 +49,11 @@ namespace Contralto /// public static readonly ulong MsecToNsec = 1000000; + /// + /// Conversion from nanoseconds to milliseconds + /// + public static readonly double NsecToMsec = 0.000001; + /// /// Conversion from microseconds to nanoseconds /// diff --git a/Contralto/Disk/diag.dsk b/Contralto/Disk/diag.dsk index d79a068..78b877e 100644 Binary files a/Contralto/Disk/diag.dsk and b/Contralto/Disk/diag.dsk differ diff --git a/Contralto/IO/AudioDAC.cs b/Contralto/IO/AudioDAC.cs new file mode 100644 index 0000000..98eb0fb --- /dev/null +++ b/Contralto/IO/AudioDAC.cs @@ -0,0 +1,231 @@ +/* + This file is part of ContrAlto. + + ContrAlto is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + ContrAlto is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with ContrAlto. If not, see . +*/ + +using Contralto.CPU; +using Contralto.Logging; +using Contralto.Memory; +using NAudio.Wave; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace Contralto.IO +{ + /// + /// Implements the hardware for the Audio DAC used by Ted Kaehler's + /// ST-74 Music System (either the FM-synthesis "TWANG" system or the + /// Sampling system.) + /// + public class AudioDAC : IMemoryMappedDevice + { + public AudioDAC(AltoSystem system) + { + _system = system; + _dacOutput = new Queue(16384); + } + + public void Shutdown() + { + if (_waveOut != null) + { + _waveOut.Stop(); + _waveOut.Dispose(); + } + + if (_waveFile != null) + { + _waveFile.Close(); + } + } + + /// + /// Comments in the FM synthesis microcode indicate: + /// "240 SAMPLES = 18 msec" + /// Which works out to about 13.3Khz. + /// + /// Unsure if this value also applies to the Sampling microcode, but + /// it sounds about right in action. + /// + public static readonly int AudioDACSamplingRate = 13000; + + /// + /// Reads a word from the specified address. + /// + /// + /// + /// + public ushort Read(int address, TaskType task, bool extendedMemory) + { + // The DAC is, as far as I can tell, write-only. + return 0; + } + + /// + /// Writes a word to the specified address. + /// + /// + /// + public void Load(int address, ushort data, TaskType task, bool extendedMemory) + { + if (Configuration.EnableAudioDAC) + { + // Ensure we have a sink for audio output capture if so configured. + if (Configuration.EnableAudioDACCapture && _waveFile == null) + { + string outputFile = Path.Combine( + Configuration.AudioDACCapturePath, + string.Format("AltoAudio-{0}.wav", DateTime.Now.ToString("yyyyMMdd-hhmmss"))); + + try + { + _waveFile = new WaveFileWriter(outputFile, new WaveFormat(AudioDAC.AudioDACSamplingRate, 1)); + } + catch (Exception e) + { + Log.Write(LogType.Error, + LogComponent.DAC, + "Failed to create DAC output file {0}. Error: {1}", outputFile, e.Message); + } + } + + // Ensure we have something to generate audio output with. + if (_waveOut == null) + { + _waveOut = new WaveOut(); + _dacLock = new ReaderWriterLockSlim(); + _waveOut.Init(new DACOutputWaveProvider(_dacOutput, _waveFile, _dacLock)); + _waveOut.Play(); + } + + // + // Enter the Write lock to ensure consistency with the + // consumer (the DACOutputWaveProvider). + // + _dacLock.EnterWriteLock(); + _dacOutput.Enqueue(data); + _dacLock.ExitWriteLock(); + } + } + + /// + /// Specifies the range (or ranges) of addresses decoded by this device. + /// + public MemoryRange[] Addresses + { + get { return _addresses; } + } + + /// + /// From: http://bitsavers.org/pdf/xerox/alto/memos_1975/Reserved_Alto_Memory_Locations_Jan75.pdf + /// + /// #177776: Digital-Analog Converter (DAC Hardware - Kaehler) + /// + private readonly MemoryRange[] _addresses = + { + new MemoryRange(0xfffe, 0xfffe), + }; + + private Queue _dacOutput; + private ReaderWriterLockSlim _dacLock; + + private AltoSystem _system; + + private WaveOut _waveOut; + + private WaveFileWriter _waveFile; + } + + // + // Basic implementation of the NAudio WaveProvider32 class. + // on a Read, we fill the DAC with the samples the Alto has been generated. + // If it hasn't generated enough (it's fallen behind) we'll pad it to try + // and reduce popping/stuttering. + // + // It will also flush the output to a WaveFileWriter if it has been enabled in + // the configuration. + // + public class DACOutputWaveProvider : WaveProvider32 + { + public DACOutputWaveProvider(Queue dacOutput, WaveFileWriter waveFile, ReaderWriterLockSlim dacLock) + { + _dacOutput = dacOutput; + _waveFile = waveFile; + _dacLock = dacLock; + SetWaveFormat(AudioDAC.AudioDACSamplingRate, 1); + _lastSample = 0; + } + + public override int Read(float[] buffer, int offset, int sampleCount) + { + int outSamples = 0; + + _dacLock.EnterReadLock(); + while(_dacOutput.Count > 0 && outSamples < sampleCount) + { + float sample = (float)_dacOutput.Dequeue() / 32768.0f - 1.0f; + buffer[offset + outSamples] = sample; + outSamples++; + _lastSample = sample; + } + _dacLock.ExitReadLock(); + + // Commit the Alto-generated samples to disk if we're saving them. + if (_waveFile != null) + { + _waveFile.WriteSamples(buffer, offset, outSamples); + } + + // + // If we didn't have enough samples to fill the requested buffer, + // This means the Alto has fallen behind; pad the remaining buffer with the + // last written sample. + // + for (; outSamples < sampleCount; outSamples++) + { + buffer[offset + outSamples] = _lastSample; + } + + return sampleCount; + } + + /// + /// Queue containing the samples generated by the Alto. + /// We pull them off as WaveOut requests audio data. + /// + private Queue _dacOutput; + + /// + /// The last sample written. Used to pad the buffer if + /// the Alto falls behind. + /// + private float _lastSample; + + /// + /// Used to ensure thread-safety of the _dacOutput queue + /// between the Alto emulation and the WaveOut thread. + /// + private ReaderWriterLockSlim _dacLock; + + /// + /// Used to write the output to a WAV file, if the option + /// is enabled. + /// + private WaveFileWriter _waveFile; + } + +} diff --git a/Contralto/IO/DoverROS.cs b/Contralto/IO/DoverROS.cs new file mode 100644 index 0000000..075d580 --- /dev/null +++ b/Contralto/IO/DoverROS.cs @@ -0,0 +1,685 @@ +using Contralto.Logging; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contralto.IO +{ + public enum AdapterCommand + { + BufferReset = 0, + SetScales = 1, + SetBitClockRegister = 2, + SetMotorSpeedRegister = 3, + SetLineSyncDelayRegister = 4, + SetPageSyncDelayRegister = 5, + ExternalCommand1 = 6, + ExternalCommand2 = 7, + } + + /// + /// Encapsulates the logic for both the ROS and the printer portions of the + /// print pipeline. + /// + public class DoverROS + { + public DoverROS(AltoSystem system) + { + _system = system; + + _cs5Event = new Event(0, null, ColdStartCallback); + _innerLoopEvent = new Event(0, null, InnerLoopCallback); + _pageBuffer = new Bitmap(4096, 4096, PixelFormat.Format1bppIndexed); + + Reset(); + } + + public void Reset() + { + _testMode = false; + _commandBeamOn = false; + _commandLocal = false; + _testPageSync = false; + _extendVideo = false; + _motorScale = 0; + _bitScale = 0; + _bitClock = 0; + _motorSpeed = 0; + _lineSyncDelay = 0; + _pageSyncDelay = 0; + + _packetsOK = true; + + _state = PrintState.ColdStart; + _innerLoopState = InnerLoopState.Idle; + _coldStartState = ColdStartState.SendVideoLow; + } + + public void RecvCommand(ushort commandWord) + { + // + // Control of the adapter, ROS, and printer is accomplished with 16-bit commands. + // The high-order 4 bits of the command give a command code; the remaining 12 bits are + // used as an argument to the command. + // + _lastCommand = commandWord; + AdapterCommand command = (AdapterCommand)(commandWord >> 12); + int argument = commandWord & 0xfff; + + switch (command) + { + case AdapterCommand.BufferReset: + break; + + case AdapterCommand.SetScales: + _testMode = (argument & 0x1) != 0; + _commandBeamOn = (argument & 0x2) != 0; + _commandLocal = (argument & 0x4) != 0; + _testPageSync = (argument & 0x8) != 0; + _extendVideo = (argument & 0x20) != 0; + _motorScale = (argument & 0x1c0) >> 6; + _bitScale = (argument & 0xe00) >> 9; + break; + + case AdapterCommand.SetBitClockRegister: + _bitClock = argument; + break; + + case AdapterCommand.SetMotorSpeedRegister: + _motorSpeed = argument; + break; + + case AdapterCommand.SetLineSyncDelayRegister: + _lineSyncDelay = argument; + break; + + case AdapterCommand.SetPageSyncDelayRegister: + _pageSyncDelay = argument; + break; + + case AdapterCommand.ExternalCommand1: + + bool lastPrintRequest = (_externalCommand1 & 0x1) != 0; + + _externalCommand1 = argument; + + // + // Dover uses the low-order bit to provide the PrintRequest signal. + // A 0-to-1 transition of the bit tells the printer to start feeding + // sheets. + // + Log.Write(LogComponent.DoverROS, "ExternalCommand1 written {0}", argument); + if (lastPrintRequest && (_externalCommand1 & 0x1) == 0) + { + PrintRequest(); + } + break; + + case AdapterCommand.ExternalCommand2: + _videoGate = argument; + break; + + default: + Log.Write(LogType.Error, LogComponent.DoverROS, + String.Format("Unhandled ROS command {0}", command)); + break; + } + } + + public int ReadStatus(int address) + { + // + // Address is a value from 0-63 (as specified by the Orbit's OrbitControl function) + // specifying the address of 4 status bits in the ROS's status memory. + // 4 status bits -- 4*address to 4*address+3 inclusive are returned. + // + int bitAddress = (address * 4); + ushort statusWord = ReadStatusWord(bitAddress >> 4); + int bitOffset = 12 - (bitAddress & 0xf); + + return (statusWord & ((0xf) << bitOffset)) >> bitOffset; + } + + private ushort ReadStatusWord(int wordNumber) + { + int value = 0; + switch (wordNumber) + { + case 0: + // + // Special status from the ROS: + // bit + // 0 - SendVideo + // 1 - PrintMode + // 2 - Local + // 3 - BeamEnable + // 4 - StatusBeamOn + // + value |= (_sendVideo ? 0x8000 : 0); + value |= (_printMode ? 0x4000 : 0); + value |= (_local ? 0x2000 : 0); + value |= (_beamEnable ? 0x1000 : 0); + value |= (_statusBeamOn ? 0x0800 : 0); + + //Log.Write(LogComponent.DoverROS, "ROS word 0 bits 0-15: {0}", Conversion.ToOctal(value)); + break; + + case 1: + // A copy of the command most recently received by the adapter. + value = _lastCommand; + break; + + case 2: + // + // Bit clock: + // bit + // 0 - VideoPolarity + // 1-3 - BitScale + // 4-15 - BitClock + // + value |= (_videoPolarity ? 0x8000 : 0); + value |= (_bitScale << 12); + value |= _bitClock; + break; + + case 3: + // + // Motor speed + // bit + // 0 - SelectLeadEdge + // 1-3 - MotorScale + // 4-15 - MotorSpeed + value |= (_selectLeadEdge ? 0x8000 : 0); + value |= (_motorScale << 12); + value |= _motorSpeed; + break; + + case 4: + // + // Line sync delay + // bit + // 0 - Switch3 + // 2 - ExtendVideo + // 3 - TestPageSync + // 4-15 - LineSyncDelay + // + value |= (_switch3 ? 0x8000 : 0); + value |= (_extendVideo ? 0x2000 : 0); + value |= (_testPageSync ? 0x1000 : 0); + value |= _lineSyncDelay; + break; + + case 5: + // + // Page sync delay + // bit + // 0 - Switch4 + // 1 - CommandLocal + // 2 - CommandBeamOn + // 3 - TestMode + // 4-15 - PageSyncDelay + // + value |= (_switch4 ? 0x8000 : 0); + value |= (_commandLocal ? 0x4000 : 0); + value |= (_commandBeamOn ? 0x2000 : 0); + value |= (_testMode ? 0x1000 : 0); + value |= _pageSyncDelay; + break; + + case 6: + // + // External Command 1 + // bit + // 0 - LineNoise + // 1 - CompareError + // 2 - BufferUnderflow + // 3 - PacketsOK + // 4-12 - ExternalCommand1 + // + value |= (_lineNoise ? 0x8000 : 0); + value |= (_compareError ? 0x4000 : 0); + value |= (_bufferUnderflow ? 0x2000 : 0); + value |= (_packetsOK ? 0x1000 : 0); + value |= _externalCommand1; + break; + + case 7: + // + // bit + // 0-3 - LineCount + // 4-15 - VideoGate + // + value |= (_lineCount << 12); + value |= _videoGate; + break; + + case 8: + // Special Dover status bits 0-15 + + // Count-H -- indicates that a page is ready to be printed. + value |= (_countH ? 0x1000 : 0); + + // + // OR in status bits that are expected to be "1" + // for normal operation (i.e. no malfunctions). + // These are: + // 8 - LS4 (adequate paper in tray) + // 11 - LaserOn + // 13 - ReadyTemp + value |= 0x214; + + Log.Write(LogComponent.DoverROS, "Dover bits 0-15: {0}", Conversion.ToOctal(value)); + break; + + case 9: + // Special Dover status bits 16-31 + + // + // OR in status bits that are expected to be "1" + // for normal operation (i.e. no malfunctions). + // These are: + // 5 - ACMonitor + // 13 - LS24 & LS31 + + value |= 0x2004; + + Log.Write(LogComponent.DoverROS, "Dover bits 16-31: {0}", Conversion.ToOctal(value)); + break; + + case 10: + value = _id; + Log.Write(LogComponent.DoverROS, "Device ID {0}", Conversion.ToOctal(value)); + break; + + case 11: + value = _serialNumber; + Log.Write(LogComponent.DoverROS, "Device Serial: {0}", Conversion.ToOctal(value)); + break; + + default: + Log.Write(LogComponent.DoverROS, "Unhandled ROS status word {0}", wordNumber); + break; + } + + return (ushort)value; + } + + private void PrintRequest() + { + switch(_state) + { + case PrintState.ColdStart: + + if (!_printMode) + { + _printMode = true; + _sendVideo = false; + + // Queue a 250ms event to fire CS-5(0). + // and 990ms item to cancel printing if a second + // print-request isn't received. + _innerLoopState = InnerLoopState.CS5; + _innerLoopEvent.TimestampNsec = (ulong)(120 * Conversion.MsecToNsec); + _system.Scheduler.Schedule(_innerLoopEvent); + + Log.Write(LogComponent.DoverROS, "Cold Start initialized at {0}ms -- CS-5(0) in 250ms.", _system.Scheduler.CurrentTimeNsec * Conversion.NsecToMsec); + } + else + { + Log.Write(LogComponent.DoverROS, "PrintRequest received in cold start at {0}ms.", _system.Scheduler.CurrentTimeNsec * Conversion.NsecToMsec); + _keepGoing = true; + } + break; + + case PrintState.InnerLoop: + if (!_printMode) + { + // PrintRequest too late. + Log.Write(LogComponent.DoverROS, "PrintRequest too late. Ignoring."); + } + else + { + if (_innerLoopState != InnerLoopState.Idle) + { + Log.Write(LogComponent.DoverROS, "PrintRequest received in inner loop."); + _keepGoing = true; + } + else + { + // + // Currently idle: Kick off the first round of the inner loop. + // Queue a PageSyncDelay (250ms) event to pulse SendVideo + // after the pulse, gather video raster from Orbit + // + Log.Write(LogComponent.DoverROS, "PrintRequest received, starting inner loop."); + _innerLoopState = InnerLoopState.CS5; + _innerLoopEvent.TimestampNsec = 120 * Conversion.MsecToNsec; + _system.Scheduler.Schedule(_innerLoopEvent); + } + } + break; + + case PrintState.Runout: + Log.Write(LogComponent.DoverROS, "Runout."); + break; + + } + + } + + private void InnerLoopCallback(ulong timestampNsec, ulong delta, object context) + { + switch(_innerLoopState) + { + case InnerLoopState.CS5: + _countH = false; + _sendVideo = false; + + // Keep SendVideo low for 125ms + _innerLoopState = InnerLoopState.SendVideo; + _innerLoopEvent.TimestampNsec = 125 * Conversion.MsecToNsec; + _system.Scheduler.Schedule(_innerLoopEvent); + + Log.Write(LogComponent.DoverROS, "Inner loop: CS5"); + break; + + case InnerLoopState.SendVideo: + _sendVideo = true; + + _innerLoopState = InnerLoopState.ReadBands; + _readBands = 0; + + // time for one band of 16 scanlines to be read (approx.) + _innerLoopEvent.TimestampNsec = (ulong)(0.2 * Conversion.MsecToNsec); + _system.Scheduler.Schedule(_innerLoopEvent); + + Log.Write(LogComponent.DoverROS, "Inner loop: SendVideo"); + break; + + case InnerLoopState.ReadBands: + // Assume 3000 scanlines for an 8.5" sheet of paper at 350dpi. + // If the Orbit is allowing us to read the output buffer then + // we will do so, otherwise we emit nothing. + if (_readBands > 3000) + { + if (_state == PrintState.ColdStart) + { + _innerLoopState = InnerLoopState.ColdStartEnd; + } + else + { + _innerLoopState = InnerLoopState.CountH; + } + } + else + { + if (_system.OrbitController.SLOTTAKE) + { + // Read in 16 scanlines of data -- this is 256x16 words + for (int y = 0; y < 16; y++) + { + for (int x = 0; x < 256 - _system.OrbitController.FA; x++) + { + ushort word = _system.OrbitController.GetOutputDataROS(); + + _pageData[(_readBands + y) * 512 + x * 2] = (byte)(word & 0xff); + _pageData[(_readBands + y) * 512 + x * 2 + 1] = (byte)(word >> 8); + + + } + } + + Log.Write(LogComponent.DoverROS, "Read bands {0}", _readBands); + } + else + { + // Nothing right now + Log.Write(LogComponent.DoverROS, "No bands available from Orbit."); + } + + _readBands += 16; + + if (_readBands > 2500 && !_countH) + { + _countH = true; + Log.Write(LogComponent.DoverROS, "Enabling CountH"); + } + } + + // time for one band of 16 scanlines to be read (approx.) + _innerLoopEvent.TimestampNsec = (ulong)(0.2 * Conversion.MsecToNsec); + _system.Scheduler.Schedule(_innerLoopEvent); + break; + + case InnerLoopState.CountH: + _countH = false; + + if (_keepGoing) + { + // PrintRequest during ColdStart -- move to Inner loop + _keepGoing = false; + _state = PrintState.InnerLoop; + _innerLoopState = InnerLoopState.CS5; + Log.Write(LogComponent.DoverROS, "PrintRequest during ColdStart -- moving to inner loop."); + } + else + { + _innerLoopState = InnerLoopState.Idle; + Log.Write(LogComponent.DoverROS, "No PrintRequest during ColdStart -- idling."); + } + + + break; + + case InnerLoopState.ColdStartEnd: + if (_keepGoing) + { + // + // Got a PrintRequest during cold start, start the inner loop. + // + _keepGoing = false; + _state = PrintState.InnerLoop; + _innerLoopState = InnerLoopState.CS5; + Log.Write(LogComponent.DoverROS, "Inner loop: CountH -- continuing"); + } + else + { + // + // No Print Request; idle and shut down. + // + _innerLoopState = InnerLoopState.Idle; + Log.Write(LogComponent.DoverROS, "Inner loop: CountH -- idling"); + } + + // Debug: Put picture in image + /* + BitmapData data = _pageBuffer.LockBits(_pageRect, ImageLockMode.WriteOnly, PixelFormat.Format1bppIndexed); + + IntPtr ptr = data.Scan0; + System.Runtime.InteropServices.Marshal.Copy(_pageData, 0, ptr, _pageData.Length); + + _pageBuffer.UnlockBits(data); + + EncoderParameters p = new EncoderParameters(1); + p.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 100L); + + _pageBuffer.Save(String.Format("c:\\temp\\raster{0}.png", _rasterNum++), GetEncoderForFormat(ImageFormat.Png), p); + */ + + _innerLoopEvent.TimestampNsec = (ulong)(150 * Conversion.MsecToNsec); + _system.Scheduler.Schedule(_innerLoopEvent); + + _innerLoopEvent.TimestampNsec = (ulong)(150 * Conversion.MsecToNsec); + _system.Scheduler.Schedule(_innerLoopEvent); + break; + + case InnerLoopState.Idle: + _countH = false; + _sendVideo = false; + _printMode = false; + _state = PrintState.ColdStart; + Log.Write(LogComponent.DoverROS, "Inner loop: Idle"); + break; + } + } + + private void ColdStartCallback(ulong timestampNsec, ulong delta, object context) + { + switch (_coldStartState) + { + case ColdStartState.SendVideoLow: + _sendVideo = false; + + // Keep SendVideo low for 125ms + _coldStartState = ColdStartState.SendVideoHigh; + _cs5Event.TimestampNsec = 125 * Conversion.MsecToNsec; + _system.Scheduler.Schedule(_cs5Event); + + Log.Write(LogComponent.DoverROS, "Cold start: toggle SendVideo low"); + break; + + case ColdStartState.SendVideoHigh: + _sendVideo = true; + + _coldStartState = ColdStartState.Timeout; + _cs5Event.TimestampNsec = 800 * Conversion.MsecToNsec; + _system.Scheduler.Schedule(_cs5Event); + + Log.Write(LogComponent.DoverROS, "Cold start: toggle SendVideo high"); + break; + + case ColdStartState.Timeout: + if (_state == PrintState.ColdStart) + { + // Never moved from ColdStart, Alto never sent another PageRequest. + Log.Write(LogComponent.DoverROS, "Cold start timeout. Aborting."); + _sendVideo = false; + _printMode = false; + } + break; + } + } + + private ImageCodecInfo GetEncoderForFormat(ImageFormat format) + { + ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders(); + + foreach (ImageCodecInfo codec in codecs) + { + if (codec.FormatID == format.Guid) + { + return codec; + } + } + return null; + } + + private enum PrintState + { + ColdStart = 0, + InnerLoop, + Runout + } + + private enum ColdStartState + { + SendVideoLow, + SendVideoHigh, + Timeout + } + + private enum InnerLoopState + { + Idle = 0, + CS5, + SendVideo, + ReadBands, + CountH, + ColdStartEnd, + } + + private PrintState _state; + private InnerLoopState _innerLoopState; + private ColdStartState _coldStartState; + + private bool _keepGoing; + private int _readBands; + + private AltoSystem _system; + + // Last command sent to us + private ushort _lastCommand; + + // Command registers + private bool _testMode; + private bool _commandBeamOn; + private bool _commandLocal; + private bool _testPageSync; + private bool _extendVideo; + private int _motorScale; + private int _bitScale; + + private int _bitClock; + + private int _motorSpeed; + private int _lineSyncDelay; + private int _pageSyncDelay; + + private int _externalCommand1; + private int _videoGate; + + // Status registers + private bool _sendVideo; + private bool _printMode; + private bool _local; + private bool _beamEnable = true; + private bool _statusBeamOn; + + private bool _lineNoise; + private bool _compareError; + private bool _bufferUnderflow; + private bool _packetsOK = true; + + private int _lineCount; + + // Physical switches (test and otherwise) + private bool _videoPolarity; + private bool _selectLeadEdge; + private bool _switch3; + private bool _switch4; + + // ID and serial number + private ushort _id; + private ushort _serialNumber; + + // + // Dover specific status that we care to report. + // TODO: if we're printing to a real printer, we could choose + // to raise status (malfunction bits, etc.) that correspond to + // real printer status... + // + + // + // This signal is raised if a sheet has been successfully fed to + // receive the image for the page being printed. + // Count-H comes on 896ms after CS-5 and persists until the next + // CS-5. (Note -- CS-5 is transmitted as the PageSync signal) + // + private bool _countH; + + + // Events to drive the print state machine + // + private Event _cs5Event; + private Event _innerLoopEvent; + + private byte[] _pageData = new byte[4096 * 512]; + private Rectangle _pageRect = new Rectangle(0, 0, 4096, 4096); + private Bitmap _pageBuffer; + private int _rasterNum; + } +} diff --git a/Contralto/IO/Keyboard.cs b/Contralto/IO/Keyboard.cs index 0f95d28..4a39971 100644 --- a/Contralto/IO/Keyboard.cs +++ b/Contralto/IO/Keyboard.cs @@ -145,13 +145,13 @@ namespace Contralto.IO } AltoKeyBit bits = _keyMap[key]; - _keyWords[bits.Word] |= _keyMap[key].Bitmask; + _keyWords[bits.Word] |= bits.Bitmask; } public void KeyUp(AltoKey key) { AltoKeyBit bits = _keyMap[key]; - _keyWords[bits.Word] &= (ushort)~_keyMap[key].Bitmask; + _keyWords[bits.Word] &= (ushort)~bits.Bitmask; } public void PressBootKeys(ushort bootAddress, bool netBoot) diff --git a/Contralto/IO/Music.cs b/Contralto/IO/Music.cs deleted file mode 100644 index 19644a5..0000000 --- a/Contralto/IO/Music.cs +++ /dev/null @@ -1,133 +0,0 @@ -/* - This file is part of ContrAlto. - - ContrAlto is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ContrAlto is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with ContrAlto. If not, see . -*/ - -using Contralto.CPU; -using Contralto.Logging; -using Contralto.Memory; -using System.IO; - -namespace Contralto.IO -{ - /// - /// Implements the hardware for Ted Kaehler's organ keyboard and DAC - /// - public class Music : IMemoryMappedDevice - { - public Music(AltoSystem system) - { - //_musicIo = new FileStream("c:\\alto\\mus.snd", FileMode.Create, FileAccess.ReadWrite); - _system = system; - Reset(); - } - - ~Music() - { - //_musicIo.Close(); - } - - public void Reset() - { - _foo = true; - } - - /// - /// Reads a word from the specified address. - /// - /// - /// - /// - public ushort Read(int address, TaskType task, bool extendedMemory) - { - // debug for kaehler's music st - Log.Write(LogType.Verbose, LogComponent.Music, "MUSIC (I/O) read from {0} by task {1} (bank {2}), Nova PC {3}", - Conversion.ToOctal(address), - task, - UCodeMemory.GetBank(task), - Conversion.ToOctal(_system.CPU.R[6])); - - if (address == 0xfffe) - { - return _lastDac; - } - else - { - _foo = !_foo; - - if (!_foo) - { - return 0x800; - } - else - { - return 0; - } - - - - } - } - - /// - /// Writes a word to the specified address. - /// - /// - /// - public void Load(int address, ushort data, TaskType task, bool extendedMemory) - { - Log.Write(LogType.Verbose, LogComponent.Music, "MUSIC (I/O) write to {0} ({1}) by task {2} (bank {3})", - Conversion.ToOctal(address), - Conversion.ToOctal(data), - task, - UCodeMemory.GetBank(task)); - - if (address == 0xfffe) - { - //_musicIo.WriteByte((byte)(data >> 8)); - //_musicIo.WriteByte((byte)data); - _lastDac = data; - } - } - - /// - /// Specifies the range (or ranges) of addresses decoded by this device. - /// - public MemoryRange[] Addresses - { - get { return _addresses; } - } - - - /// - /// From: http://bitsavers.org/pdf/xerox/alto/memos_1975/Reserved_Alto_Memory_Locations_Jan75.pdf - /// - /// #177140 - #177157: Organ Keyboard (Organ Hardware - Kaehler) - /// #177776: Digital-Analog Converter (DAC Hardware - Kaehler) - /// - private readonly MemoryRange[] _addresses = - { - new MemoryRange(0xfe60, 0xfe6f), // Organ - new MemoryRange(0xfffe, 0xfffe) // DAC - }; - - private ushort _lastDac; - - private AltoSystem _system; - - private FileStream _musicIo; - private bool _foo; - } -} diff --git a/Contralto/IO/OrbitController.cs b/Contralto/IO/OrbitController.cs new file mode 100644 index 0000000..bb43560 --- /dev/null +++ b/Contralto/IO/OrbitController.cs @@ -0,0 +1,639 @@ +using Contralto.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contralto.IO +{ + /// + /// Implements the Orbit controller -- hardware for generating + /// rasters to be sent to a ROS (Raster Output Scanner) device + /// such as a laser printer. + /// + public class OrbitController + { + public OrbitController(AltoSystem system) + { + _system = system; + + _ros = new DoverROS(system); + + _refreshEvent = new Event(_refreshInterval, null, RefreshCallback); + + Reset(); + } + + public bool RefreshTimerExpired + { + get { return _refresh; } + } + + public bool IACS + { + get { return _iacs; } + } + + public bool SLOTTAKE + { + get { return _slottake; } + } + + public int FA + { + get { return _fa; } + } + + public void Reset() + { + // + // A Reset performs at least the following functions: + // FA <- 0, SLOTTAKE <- 0, band buffer A is assigned + // to the image buffer, the status and control dialogs + // with the adapter are reset. + // + _fa = 0; + _slottake = false; + + _image = _a; + _output = _b; + _incon = false; + + _outputY = 0; + _outputX = 0; + + _run = false; + _iacs = false; + + _refresh = false; + _goAway = false; + _behind = false; + + _stableROS = true; + _badROS = false; + + Log.Write(LogComponent.Orbit, "Orbit reset."); + } + + public bool Wakeup + { + get + { + return _run && + ((_refresh || !_goAway) && + (true || !_iacs)); // tautology -- the 'true' is "FIFO ready" and since I don't implement the FIFO (yet...) + } + } + + public void STARTF(ushort value) + { + // Kick off the refresh timer if it's not already pending + + // Wake up task, etc. + _run = true; + _system.CPU.WakeupTask(CPU.TaskType.Orbit); + + // "IACS is cleared by StartIO(4)" + _iacs = false; + + if (!_refreshRunning) + { + _refreshRunning = true; + _refreshEvent.TimestampNsec = _refreshInterval; + _system.Scheduler.Schedule(_refreshEvent); + } + + Log.Write(LogComponent.Orbit, "Orbit started."); + } + + public void Stop() + { + _run = false; + + _system.CPU.BlockTask(CPU.TaskType.Orbit); + + //Log.Write(LogComponent.Orbit, "Orbit blocked."); + } + + public void Control(ushort value) + { + // + // This function transfers 16 bits of control information to Orbit. + // + + // + // This 8-bit field has two different interpretations, depending on + // the setting of the WHICH field. + // + int auxControl = (value & 0xff00) >> 8; + + // + // This bit, if 1, will reset Orbit entirely. + // + if ((value & 0x1) != 0) + { + Reset(); + } + + // + // This bit controls refresh logic, and should normally be 0. + // + if ((value & 0x2) != 0) + { + _refresh = false; + } + + // + // This bit controls the use to which the 8-bit auxControl field is + // put. If WHICH=0, auxControl is interpreted as an address (range 0 + // to 63) into the adapter status memory: when OrbitStatus is next + // interrogated, 4 status bits (with numbers 4*auxControl to + // 4 *auxControl + 3 will be reported. If WHICH=1, auxControl is used + // to set FA. + // + if ((value & 0x4) != 0) + { + _fa = auxControl; + + _outputY = _fa; + + // "The setting of FA provided by the Alto is examined only when + // Orbit reads out the last 16 bits of a scanline..." + // (So we *should* leave _outputY alone here). + } + else + { + _statusAddress = auxControl & 0x3f; + } + + // + // This bit controls microcode wakeups, and should normally be 0. + // + if ((value & 0x8) != 0) + { + _goAway = true; + _system.CPU.BlockTask(CPU.TaskType.Orbit); + } + + // + // This bit clears the BEHIND indicator. + // + if ((value & 0x10) != 0) + { + _behind = false; + } + + // + // ESS - This bit must be 1 to enable changing the SLOTTAKE setting. + // SLOTTAKE - This bit setting, enabled by ESS above, controls the + // output buffer logic. Normally (SLOTTAKE=0), Orbit will not honor + // video data requests coming from the adapter. As soon as SLOTTAKE + // is set to 1, however, output data will be passed to the adapter when + // it demands it. + // + if ((value & 0xc0) == 0xc0) + { + _slottake = true; + } + + Log.Write(LogComponent.Orbit, + "Set Control: aux {0}, reset {1} refresh {2} which {3} goaway {4} behind {5} slottake {6}", + auxControl, (value & 0x1) != 0, (value & 0x2) != 0, (value & 0x4) != 0, _goAway, _behind, _slottake); + } + + public void SetHeight(ushort value) + { + // + // This command sends to Orbit a 12-bit field (value[4-15]) which is + // interpreted as the two's complement of the height of the source raster, + // in bits. + // + _height = 4096 - (value & 0xfff); + + Log.Write(LogComponent.Orbit, + "Set Height: {0}", _height); + } + + public void SetXY(ushort value) + { + // + // This commands sets x <- value[0-3] and y <- value[4-15]. + // It is therefore used to set the starting scan-line within + // the band (x) and the vertical position of the bottom of the + // copy of the source raster (y). + _y = value & 0x0fff; + _x = (value & 0xf000) >> 12; + + Log.Write(LogComponent.Orbit, + "Set XY: X={0}, Y={1}", _x, _y); + } + + public void SetDBCWidth(ushort value) + { + // + // value[0-3] tells Orbit which bit (0-15) of the first + // word of raster data is the first bit to be examined. + // + _deltaBC = _deltaBCEnd = (value & 0xf000) >> 12; + + // + // value[4-15] is interpreted as the width of the source + // raster, minus 1. + /// + _width = (value & 0x0fff); + + // + // Third, executing this function initializes a mess of logic + // relating to transferring a source raster to Orbit and sets + // IACS ("in a character segment"). After the function is + // executed it is wise to issue only OrbitFontData functions until + // the image-generation for this character terminates (i.e. IACS + // becomes 0). IACS is cleared by StartIO(4). + // Start a new character + // + _iacs = true; + _firstWord = true; + _bitCount = 0; + _cx = _x; + _cy = _y; + + _scanlines = (_x + (_width + 1) < 16) ? (_width + 1): 16 - _x; + + Log.Write(LogComponent.Orbit, + "Set DBCWidth: DeltaBC {0}, Width {1}", _deltaBC, _width); + } + + public void WriteFontData(ushort value) + { + // + // This function is used to send 16-bit raster data words to Orbit, + // usually in a very tight loop. The loop is exited when IACS becomes + // 0, that is, when the Orbit hardware counts the width counter down to + // zero, or when the x counter overflows (i.e. when it would count up + // from 15 to 16). + // + + if (!_iacs) + { + Log.Write(LogType.Error, + LogComponent.Orbit, "Unxpected OrbitFontData while IACS false."); + return; + } + + // + // We insert the word one bit at a time; this is more costly + // computationally but it's a ton simpler than dealing with word + // and scanline boundaries and merging entire words at a time. + // + int startBit = 15; + if (_firstWord) + { + startBit = 15 - _deltaBC; + } + + for(int i = startBit; i >=0 ;i--) + { + int bitValue = (value & (0x1 << i)) == 0 ? 0 : 1; + + if (bitValue != 0 && _cy < 4096) // clip to end of band + { + SetImageRasterBit(_cx, _cy, bitValue); + } + + _cy++; + _bitCount++; + _deltaBCEnd++; + + if (_cy > _y + _height - 1) + { + _cy = _y; + + _cx++; + _width--; + + if (_cx > 15 || _width < 0) + { + _iacs = false; + + Log.Write(LogComponent.DoverROS, "Image band completed."); + break; + } + } + } + + _firstWord = false; + Log.Write(LogComponent.Orbit, + "Font Data: {0}", Conversion.ToOctal(value)); + } + + public void WriteInkData(ushort value) + { + // + // This function sets 16 bits of INK memory: INK[x,0] through INK[x,15], + // where x has been previously set with OrbitXY. + // + _ink[_x] = value; + + Log.Write(LogComponent.Orbit, + "Ink Data[{0}]: {1}", _x, Conversion.ToOctal(value)); + } + + public void SendROSCommand(ushort value) + { + // + // This function sends a 16-bit command to the ROS. + // Another ROS command should not be issued until 60 microseconds + // have elapsed, to allow time for proper transmission of the command + // to the ROS. + // + _ros.RecvCommand(value); + + Log.Write(LogComponent.Orbit, + "ROS command {0}", Conversion.ToOctal(value)); + } + + public ushort GetOutputDataAlto() + { + + if (_slottake) + { + Log.Write(LogType.Error, LogComponent.Orbit, + "Unexpected OrbitOutputData to Alto with SLOTTAKE set."); + } + + return GetOutputData(); + } + + public ushort GetOutputDataROS() + { + if (!_slottake) + { + Log.Write(LogType.Error, LogComponent.Orbit, + "Unexpected OrbitOutputData to ROS without SLOTTAKE set."); + } + + return GetOutputData(); + } + + public ushort GetDeltaWC() + { + Log.Write(LogComponent.Orbit, + "Delta WC {0} ({1} bits)", (_bitCount >> 4), _bitCount); + + return (ushort)(_bitCount >> 4); + } + + public ushort GetDBCWidth() + { + Log.Write(LogComponent.Orbit, + "Delta DBCWidth {0},{1}", _deltaBCEnd % 16, _width); + + return (ushort)((_deltaBCEnd << 12) | (_width & 0xfff)); + } + + public ushort GetOrbitStatus() + { + int result = _ros.ReadStatus(_statusAddress); + + if (_behind) + { + result |= 0x10; + } + + if (_stableROS) + { + result |= 0x20; + } + + if (_incon) + { + result |= 0x40; + } + + if (_badROS) + { + result |= 0x80; + } + + Log.Write(LogComponent.Orbit, + "OrbitStatus {0}", Conversion.ToOctal(result)); + + return (ushort)result; + } + + private ushort GetOutputData() + { + // + // Basically, the function OrbitOutputData reads 16 bits from the output buffer + // into the Alto. + // + // The programmer (or emulator author) needs to be aware of two tricky details + // concerning readout: + // 1. There is a 16-bit buffer (ROB) that sits between the addressing mechanism + // and the outside world (the taker of data: the Alto or the ROS adapter). As + // a result, immediately after the buffers "switch" the ROB contains the last + // 16-bits of data from the previous buffer (x=15, y=4080 through y=4095). + // The usual convention is to read out this one last word. This leaves the first + // word of the new buffer in ROB. + // + // 2. The output band buffer will not be refreshed ... (not a concern here) + // + + // Return the last value from ROB + ushort value = _rob; + + // Update ROB + _rob = _output[_outputX, _outputY]; + + // Clear read word + _output[_outputX, _outputY] = 0; + + // + // Move to next word + // + _outputY++; + + if (_outputY == 256) + { + Log.Write(LogComponent.DoverROS, + "OutputData - scanline {0} completed", _outputX); + + _outputY = _fa; + _outputX++; + + if (_outputX == 16) + { + // Done reading output band -- swap buffers! + + Log.Write(LogComponent.DoverROS, + "OutputData - buffer read completed."); + + SwapBuffers(); + _outputX = 0; + } + } + + Log.Write(LogComponent.Orbit, + "OutputData {0}", Conversion.ToOctal(value)); + + return value; + } + + private void SwapBuffers() + { + ushort[,] _temp = _image; + _image = _output; + _output = _temp; + + _outputY = _fa; + _outputX = 0; + + // + // If input is still being written, + // the Alto is now behind the consumer... + // + if (!_goAway) + { + Console.WriteLine("BEHIND"); + _behind = true; + } + else + { + // "The buffer switch will turn off GOAWAY and consequently + // allow wakeups again." + _goAway = false; + _system.CPU.WakeupTask(CPU.TaskType.Orbit); + } + + // Flip buffer bit + _incon = !_incon; + } + + private void SetImageRasterBit(int x, int y, int bitValue) + { + // Pick out the word we're interested in + int wordAddress = y >> 4; + ushort inputWord = _image[x, wordAddress]; + + // grab the ink bit and OR it in. + int inkBit = (_ink[x] & (0x8000 >> (y % 16))); + + _image[x, wordAddress] = (ushort)(inputWord | inkBit); + } + + private void RefreshCallback(ulong timeNsec, ulong skewNsec, object context) + { + _refresh = true; + + //Log.Write(LogComponent.Orbit, "Refresh signal raised."); + + if (_run) + { + _system.CPU.WakeupTask(CPU.TaskType.Orbit); + + // do it again + _refreshEvent.TimestampNsec = _refreshInterval; + _system.Scheduler.Schedule(_refreshEvent); + } + else + { + //Log.Write(LogComponent.Orbit, "RUN deasserted, Refresh aborted."); + _refreshRunning = false; + } + } + + // Run status + private bool _run; + + // Raster information + private int _height; // height (in bits) + private int _x; // scanline + private int _y; // bit position in scanline + private int _width; // raster width of character + private int _bitCount; // count of processed bits; used to calculate DeltaWC. + private int _deltaBC; // bit of first word to start with + private int _deltaBCEnd; // number of bits left in last word rasterized + + private int _scanlines; + + // FA -- "First Address" of each scanline -- a value between + // 0 and 255 indicating the starting word to be read by the + // output device (either the Alto or the ROS). + private int _fa; + + // SLOTTAKE - indicates that the ROS takes data (not the Alto) + private bool _slottake; + + // Address to pull ROS status bits from + private int _statusAddress; + + // Signals completion of input from Alto + private bool _goAway; + + // Whether the Alto has fallen behind the ROS's consumption + private bool _behind; + + // Current raster position + private bool _firstWord; + private int _cx; + private int _cy; + + // + // IACS ("in a character segment") is set by OrbitDBCWidthSet + // and remains set until: + // - StartIO(4) is invoked + // - The Width counter counts down to zero + // - The current band is completed (the x counter overflows to 16) + // + private bool _iacs; + + // + // Refresh timer expiry + // + private bool _refresh; + + // + // Output band position and data + // + private int _outputX; + private int _outputY; // in words, starting at FA + private ushort _rob; // Read Output Buffer + + // + // Buffer status -- if 0, A is the image buffer, B is the + // output buffer. If 1, these are swapped. + // + private bool _incon; + + // + // ROS status + // + private bool _badROS; + private bool _stableROS; + + // Raster bands + private ushort[,] _a = new ushort[16, 256]; // 16x4096 bits + private ushort[,] _b = new ushort[16, 256]; + + // Ink data + private ushort[] _ink = new ushort[16]; + + // buffer references that are swapped + private ushort[,] _image; + private ushort[,] _output; + + // + // Refresh event and interval + private bool _refreshRunning; + private Event _refreshEvent; + private readonly ulong _refreshInterval = 2 * Conversion.MsecToNsec; // 2ms in nsec + + // + // The ROS we talk to to get raster onto a "page" + // + private DoverROS _ros; + + private AltoSystem _system; + } +} diff --git a/Contralto/IO/OrganKeyboard.cs b/Contralto/IO/OrganKeyboard.cs new file mode 100644 index 0000000..f9e72ac --- /dev/null +++ b/Contralto/IO/OrganKeyboard.cs @@ -0,0 +1,101 @@ +using Contralto.CPU; +using Contralto.Logging; +using Contralto.Memory; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contralto.IO +{ + /// + /// Implements the Organ Keyboard interface used by the ST-74 + /// Music System. Very little is known about the hardware at this time, + /// so most of this is speculation or based on disassembly/reverse-engineering + /// of the music system code. + /// + /// This is currently a stub that implements the bare minimum to make the + /// music system think there's a keyboard attached to the system. + /// + public class OrganKeyboard : IMemoryMappedDevice + { + public OrganKeyboard(AltoSystem system) + { + _system = system; + Reset(); + } + + public void Reset() + { + // + // Initialize keyboard registers. + // Based on disassembly of the Nova code that drives the keyboard + // interface, the top 6 bits are active low. + // + for (int i = 0; i < 16; i++) + { + _keyData[i] = (ushort)(0xfc00); + } + } + + /// + /// Reads a word from the specified address. + /// + /// + /// + /// + public ushort Read(int address, TaskType task, bool extendedMemory) + { + + Log.Write(LogType.Verbose, LogComponent.Organ, "Organ read from {0} by task {1} (bank {2}), Nova PC {3}", + Conversion.ToOctal(address), + task, + UCodeMemory.GetBank(task), + Conversion.ToOctal(_system.CPU.R[6])); + + return _keyData[address - 0xfe60]; + + } + + /// + /// Writes a word to the specified address. + /// + /// + /// + public void Load(int address, ushort data, TaskType task, bool extendedMemory) + { + + // The registers are write-only as far as I've been able to ascertain. + Log.Write(LogType.Verbose, LogComponent.Organ, "Unexpected organ write to {0} ({1}) by task {2} (bank {3})", + Conversion.ToOctal(address), + Conversion.ToOctal(data), + task, + UCodeMemory.GetBank(task)); + } + + /// + /// Specifies the range (or ranges) of addresses decoded by this device. + /// + public MemoryRange[] Addresses + { + get { return _addresses; } + } + + + /// + /// From: http://bitsavers.org/pdf/xerox/alto/memos_1975/Reserved_Alto_Memory_Locations_Jan75.pdf + /// + /// #177140 - #177157: Organ Keyboard (Organ Hardware - Kaehler) + /// + private readonly MemoryRange[] _addresses = + { + new MemoryRange(0xfe60, 0xfe6f), + }; + + private ushort[] _keyData = new ushort[16]; + + private AltoSystem _system; + } + +} \ No newline at end of file diff --git a/Contralto/Logging/Log.cs b/Contralto/Logging/Log.cs index a931bd6..c803337 100644 --- a/Contralto/Logging/Log.cs +++ b/Contralto/Logging/Log.cs @@ -45,7 +45,10 @@ namespace Contralto.Logging HostNetworkInterface = 0x2000, EthernetPacket = 0x4000, Configuration = 0x8000, - Music = 0x10000, + DAC = 0x10000, + Organ = 0x20000, + Orbit = 0x40000, + DoverROS = 0x80000, Debug = 0x40000000, All = 0x7fffffff @@ -73,7 +76,7 @@ namespace Contralto.Logging static Log() { // TODO: make configurable - _components = LogComponent.None; // LogComponent.HostNetworkInterface | LogComponent.EthernetPacket; // | LogComponent.HostEthernet | LogComponent.EthernetController; // LogComponent.DiskController | LogComponent.DiskSectorTask | LogComponent.Debug | LogComponent.CPU; // LogComponent.EthernetController; // | LogComponent.Microcode | LogComponent.Memory | LogComponent.CPU; + _components = LogComponent.DAC; _type = LogType.Normal | LogType.Warning | LogType.Error | LogType.Verbose; //_logStream = new StreamWriter("log.txt"); diff --git a/Contralto/Properties/AssemblyInfo.cs b/Contralto/Properties/AssemblyInfo.cs index 0732de4..4c51d27 100644 --- a/Contralto/Properties/AssemblyInfo.cs +++ b/Contralto/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.1.*")] +[assembly: AssemblyVersion("1.1.1")] [assembly: AssemblyFileVersion("1.1.0.0")] diff --git a/Contralto/Properties/Settings.Designer.cs b/Contralto/Properties/Settings.Designer.cs index c6bebc4..9cedbe1 100644 --- a/Contralto/Properties/Settings.Designer.cs +++ b/Contralto/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace Contralto.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.1.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -166,5 +166,41 @@ namespace Contralto.Properties { this["EnableWindowsFormsHighDpiAutoResizing"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool EnableAudioDAC { + get { + return ((bool)(this["EnableAudioDAC"])); + } + set { + this["EnableAudioDAC"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool EnableAudioDACCapture { + get { + return ((bool)(this["EnableAudioDACCapture"])); + } + set { + this["EnableAudioDACCapture"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string AudioDACCapturePath { + get { + return ((string)(this["AudioDACCapturePath"])); + } + set { + this["AudioDACCapturePath"] = value; + } + } } } diff --git a/Contralto/Properties/Settings.settings b/Contralto/Properties/Settings.settings index bf5102b..f8f12bb 100644 --- a/Contralto/Properties/Settings.settings +++ b/Contralto/Properties/Settings.settings @@ -38,5 +38,14 @@ True + + True + + + False + + + + \ No newline at end of file diff --git a/Contralto/UI/Debugger.cs b/Contralto/UI/Debugger.cs index 0d53c3b..0f04f9b 100644 --- a/Contralto/UI/Debugger.cs +++ b/Contralto/UI/Debugger.cs @@ -125,7 +125,7 @@ namespace Contralto _otherRegs.Rows[1].Cells[1].Value = Conversion.ToOctal(_system.CPU.T, 6); _otherRegs.Rows[2].Cells[1].Value = Conversion.ToOctal(_system.CPU.M, 6); _otherRegs.Rows[3].Cells[1].Value = Conversion.ToOctal(_system.CPU.IR, 6); - _otherRegs.Rows[4].Cells[1].Value = Conversion.ToOctal(_system.CPU.ALUC0, 1); + _otherRegs.Rows[4].Cells[1].Value = Conversion.ToOctal(_system.CPU.ALUC0, 1); _otherRegs.Rows[5].Cells[1].Value = Conversion.ToOctal(_system.MemoryBus.MAR, 6); _otherRegs.Rows[6].Cells[1].Value = Conversion.ToOctal(_system.MemoryBus.MDLow, 6); _otherRegs.Rows[7].Cells[1].Value = Conversion.ToOctal(_system.MemoryBus.MDHigh, 6); @@ -241,13 +241,13 @@ namespace Contralto _memoryData.RowCount = 65536; _ram0SourceViewer.RowCount = 1024; _ram1SourceViewer.RowCount = 1024; - _ram2SourceViewer.RowCount = 1024; + _ram2SourceViewer.RowCount = 1024; _otherRegs.Rows.Add("L", "0"); _otherRegs.Rows.Add("T", "0"); _otherRegs.Rows.Add("M", "0"); _otherRegs.Rows.Add("IR", "0"); - _otherRegs.Rows.Add("ALUC0", "0"); + _otherRegs.Rows.Add("ALUC0", "0"); _otherRegs.Rows.Add("MAR", "0"); _otherRegs.Rows.Add("←MDL", "0"); _otherRegs.Rows.Add("←MDH", "0"); @@ -279,7 +279,7 @@ namespace Contralto sb.AppendFormat("{0}:{1} {2}\r\n", Conversion.ToOctal(i, 6), _memoryData.Rows[i].Cells[2].Value, - _memoryData.Rows[i].Cells[3].Value); + _memoryData.Rows[i].Cells[3].Value); } Clipboard.SetText(sb.ToString()); @@ -583,7 +583,7 @@ namespace Contralto string[] taskText = { "EM", // 0 - emulator - String.Empty, + "OR", // 1 - orbit String.Empty, String.Empty, "KS", // 4 - disk sector @@ -615,7 +615,7 @@ namespace Contralto Color[] taskColors = { Color.LightBlue, // 0 - emulator - Color.LightGray, // 1 - unused + Color.LightGoldenrodYellow, // 1 - orbit Color.LightGray, // 2 - unused Color.LightGray, // 3 - unused Color.LightGreen, // 4 - disk sector @@ -673,6 +673,10 @@ namespace Contralto Task = TaskType.Emulator; break; + case "OR": + Task = TaskType.Orbit; + break; + case "SE": Task = TaskType.DiskSector; break; diff --git a/Contralto/UI/SystemOptions.Designer.cs b/Contralto/UI/SystemOptions.Designer.cs index 11edf56..5ffb22d 100644 --- a/Contralto/UI/SystemOptions.Designer.cs +++ b/Contralto/UI/SystemOptions.Designer.cs @@ -30,6 +30,7 @@ { this.tabControl1 = new System.Windows.Forms.TabControl(); this.tabPage1 = new System.Windows.Forms.TabPage(); + this.AltoI1KROMRadioButton = new System.Windows.Forms.RadioButton(); this.AltoII3KRAMRadioButton = new System.Windows.Forms.RadioButton(); this.AltoII2KROMRadioButton = new System.Windows.Forms.RadioButton(); this.label1 = new System.Windows.Forms.Label(); @@ -49,13 +50,21 @@ this.InterlaceDisplayCheckBox = new System.Windows.Forms.CheckBox(); this.DialogOKButton = new System.Windows.Forms.Button(); this.DialogCancelButton = new System.Windows.Forms.Button(); - this.AltoI1KROMRadioButton = new System.Windows.Forms.RadioButton(); + this.tabPage4 = new System.Windows.Forms.TabPage(); + this.EnableDACCheckBox = new System.Windows.Forms.CheckBox(); + this.DACOptionsGroupBox = new System.Windows.Forms.GroupBox(); + this.EnableDACCaptureCheckBox = new System.Windows.Forms.CheckBox(); + this.label2 = new System.Windows.Forms.Label(); + this.DACOutputCapturePathTextBox = new System.Windows.Forms.TextBox(); + this.BrowseButton = new System.Windows.Forms.Button(); this.tabControl1.SuspendLayout(); this.tabPage1.SuspendLayout(); this.tabPage2.SuspendLayout(); this.groupBox1.SuspendLayout(); this.HostInterfaceGroupBox.SuspendLayout(); this.tabPage3.SuspendLayout(); + this.tabPage4.SuspendLayout(); + this.DACOptionsGroupBox.SuspendLayout(); this.SuspendLayout(); // // tabControl1 @@ -63,6 +72,7 @@ this.tabControl1.Controls.Add(this.tabPage1); this.tabControl1.Controls.Add(this.tabPage2); this.tabControl1.Controls.Add(this.tabPage3); + this.tabControl1.Controls.Add(this.tabPage4); this.tabControl1.Location = new System.Drawing.Point(3, 5); this.tabControl1.Name = "tabControl1"; this.tabControl1.SelectedIndex = 0; @@ -84,6 +94,17 @@ this.tabPage1.Text = "CPU"; this.tabPage1.UseVisualStyleBackColor = true; // + // AltoI1KROMRadioButton + // + this.AltoI1KROMRadioButton.AutoSize = true; + this.AltoI1KROMRadioButton.Location = new System.Drawing.Point(14, 30); + this.AltoI1KROMRadioButton.Name = "AltoI1KROMRadioButton"; + this.AltoI1KROMRadioButton.Size = new System.Drawing.Size(214, 17); + this.AltoI1KROMRadioButton.TabIndex = 4; + this.AltoI1KROMRadioButton.TabStop = true; + this.AltoI1KROMRadioButton.Text = "Alto I, 1K Control ROM, 1K Control RAM"; + this.AltoI1KROMRadioButton.UseVisualStyleBackColor = true; + // // AltoII3KRAMRadioButton // this.AltoII3KRAMRadioButton.AutoSize = true; @@ -266,36 +287,98 @@ this.InterlaceDisplayCheckBox.Text = "Interlaced Display (headache mode)"; this.InterlaceDisplayCheckBox.UseVisualStyleBackColor = true; // - // OKButton + // DialogOKButton // this.DialogOKButton.Location = new System.Drawing.Point(211, 239); - this.DialogOKButton.Name = "OKButton"; + this.DialogOKButton.Name = "DialogOKButton"; this.DialogOKButton.Size = new System.Drawing.Size(75, 23); this.DialogOKButton.TabIndex = 1; this.DialogOKButton.Text = "OK"; this.DialogOKButton.UseVisualStyleBackColor = true; this.DialogOKButton.Click += new System.EventHandler(this.OKButton_Click); // - // CancelButton + // DialogCancelButton // this.DialogCancelButton.Location = new System.Drawing.Point(292, 239); - this.DialogCancelButton.Name = "CancelButton"; + this.DialogCancelButton.Name = "DialogCancelButton"; this.DialogCancelButton.Size = new System.Drawing.Size(75, 23); this.DialogCancelButton.TabIndex = 2; this.DialogCancelButton.Text = "Cancel"; this.DialogCancelButton.UseVisualStyleBackColor = true; this.DialogCancelButton.Click += new System.EventHandler(this.CancelButton_Click); // - // AltoI1KROMRadioButton + // tabPage4 // - this.AltoI1KROMRadioButton.AutoSize = true; - this.AltoI1KROMRadioButton.Location = new System.Drawing.Point(14, 30); - this.AltoI1KROMRadioButton.Name = "AltoI1KROMRadioButton"; - this.AltoI1KROMRadioButton.Size = new System.Drawing.Size(214, 17); - this.AltoI1KROMRadioButton.TabIndex = 4; - this.AltoI1KROMRadioButton.TabStop = true; - this.AltoI1KROMRadioButton.Text = "Alto I, 1K Control ROM, 1K Control RAM"; - this.AltoI1KROMRadioButton.UseVisualStyleBackColor = true; + this.tabPage4.Controls.Add(this.DACOptionsGroupBox); + this.tabPage4.Controls.Add(this.EnableDACCheckBox); + this.tabPage4.Location = new System.Drawing.Point(4, 22); + this.tabPage4.Name = "tabPage4"; + this.tabPage4.Padding = new System.Windows.Forms.Padding(3); + this.tabPage4.Size = new System.Drawing.Size(360, 201); + this.tabPage4.TabIndex = 3; + this.tabPage4.Text = "DAC"; + this.tabPage4.UseVisualStyleBackColor = true; + // + // EnableDACCheckBox + // + this.EnableDACCheckBox.AutoSize = true; + this.EnableDACCheckBox.Location = new System.Drawing.Point(19, 22); + this.EnableDACCheckBox.Name = "EnableDACCheckBox"; + this.EnableDACCheckBox.Size = new System.Drawing.Size(275, 17); + this.EnableDACCheckBox.TabIndex = 0; + this.EnableDACCheckBox.Text = "Enable Audio DAC (Used by Smalltalk Music System)"; + this.EnableDACCheckBox.UseVisualStyleBackColor = true; + this.EnableDACCheckBox.CheckedChanged += new System.EventHandler(this.OnEnableDACCheckboxChanged); + // + // DACOptionsGroupBox + // + this.DACOptionsGroupBox.Controls.Add(this.BrowseButton); + this.DACOptionsGroupBox.Controls.Add(this.DACOutputCapturePathTextBox); + this.DACOptionsGroupBox.Controls.Add(this.label2); + this.DACOptionsGroupBox.Controls.Add(this.EnableDACCaptureCheckBox); + this.DACOptionsGroupBox.Location = new System.Drawing.Point(15, 52); + this.DACOptionsGroupBox.Name = "DACOptionsGroupBox"; + this.DACOptionsGroupBox.Size = new System.Drawing.Size(335, 139); + this.DACOptionsGroupBox.TabIndex = 1; + this.DACOptionsGroupBox.TabStop = false; + this.DACOptionsGroupBox.Text = "DAC options"; + // + // EnableDACCaptureCheckBox + // + this.EnableDACCaptureCheckBox.AutoSize = true; + this.EnableDACCaptureCheckBox.Location = new System.Drawing.Point(18, 28); + this.EnableDACCaptureCheckBox.Name = "EnableDACCaptureCheckBox"; + this.EnableDACCaptureCheckBox.Size = new System.Drawing.Size(156, 17); + this.EnableDACCaptureCheckBox.TabIndex = 0; + this.EnableDACCaptureCheckBox.Text = "Enable DAC output capture"; + this.EnableDACCaptureCheckBox.UseVisualStyleBackColor = true; + this.EnableDACCaptureCheckBox.CheckedChanged += new System.EventHandler(this.EnableDACCaptureCheckBox_CheckedChanged); + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(16, 58); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(105, 13); + this.label2.TabIndex = 1; + this.label2.Text = "Output capture path:"; + // + // DACOutputCapturePathTextBox + // + this.DACOutputCapturePathTextBox.Location = new System.Drawing.Point(127, 55); + this.DACOutputCapturePathTextBox.Name = "DACOutputCapturePathTextBox"; + this.DACOutputCapturePathTextBox.Size = new System.Drawing.Size(110, 20); + this.DACOutputCapturePathTextBox.TabIndex = 2; + // + // BrowseButton + // + this.BrowseButton.Location = new System.Drawing.Point(251, 53); + this.BrowseButton.Name = "BrowseButton"; + this.BrowseButton.Size = new System.Drawing.Size(75, 23); + this.BrowseButton.TabIndex = 3; + this.BrowseButton.Text = "Browse..."; + this.BrowseButton.UseVisualStyleBackColor = true; + this.BrowseButton.Click += new System.EventHandler(this.BrowseButton_Click); // // SystemOptions // @@ -320,6 +403,10 @@ this.HostInterfaceGroupBox.PerformLayout(); this.tabPage3.ResumeLayout(false); this.tabPage3.PerformLayout(); + this.tabPage4.ResumeLayout(false); + this.tabPage4.PerformLayout(); + this.DACOptionsGroupBox.ResumeLayout(false); + this.DACOptionsGroupBox.PerformLayout(); this.ResumeLayout(false); } @@ -348,5 +435,12 @@ private System.Windows.Forms.RadioButton UDPRadioButton; private System.Windows.Forms.RadioButton NoEncapsulationRadioButton; private System.Windows.Forms.RadioButton AltoI1KROMRadioButton; + private System.Windows.Forms.TabPage tabPage4; + private System.Windows.Forms.GroupBox DACOptionsGroupBox; + private System.Windows.Forms.CheckBox EnableDACCheckBox; + private System.Windows.Forms.CheckBox EnableDACCaptureCheckBox; + private System.Windows.Forms.Button BrowseButton; + private System.Windows.Forms.TextBox DACOutputCapturePathTextBox; + private System.Windows.Forms.Label label2; } } \ No newline at end of file diff --git a/Contralto/UI/SystemOptions.cs b/Contralto/UI/SystemOptions.cs index eeeb5db..c58f801 100644 --- a/Contralto/UI/SystemOptions.cs +++ b/Contralto/UI/SystemOptions.cs @@ -20,6 +20,7 @@ using PcapDotNet.Core; using PcapDotNet.Core.Extensions; using System; using System.Collections.Generic; +using System.IO; using System.Net.NetworkInformation; using System.Windows.Forms; @@ -40,6 +41,9 @@ namespace Contralto.UI /// private void PopulateUI() { + // + // System Tab + // _selectedSystemType = Configuration.SystemType; _selectedInterfaceType = Configuration.HostPacketInterfaceType; @@ -62,9 +66,15 @@ namespace Contralto.UI break; } + // + // Display Tab + // InterlaceDisplayCheckBox.Checked = Configuration.InterlaceDisplay; ThrottleSpeedCheckBox.Checked = Configuration.ThrottleSpeed; + // + // Ethernet Tab + // AltoEthernetAddressTextBox.Text = Conversion.ToOctal(Configuration.HostAddress); if (!Configuration.HostRawEthernetInterfacesAvailable) @@ -94,7 +104,16 @@ namespace Contralto.UI break; } - PopulateNetworkAdapterList(Configuration.HostPacketInterfaceType); + PopulateNetworkAdapterList(Configuration.HostPacketInterfaceType); + + // + // DAC Tab + // + EnableDACCheckBox.Checked = Configuration.EnableAudioDAC; + DACOptionsGroupBox.Enabled = Configuration.EnableAudioDAC; + + DACOutputCapturePathTextBox.Text = Configuration.AudioDACCapturePath; + EnableDACCaptureCheckBox.Checked = Configuration.EnableAudioDACCapture; } private void PopulateNetworkAdapterList(PacketInterfaceType encapType) @@ -249,7 +268,18 @@ namespace Contralto.UI // Display Configuration.InterlaceDisplay = InterlaceDisplayCheckBox.Checked; - Configuration.ThrottleSpeed = ThrottleSpeedCheckBox.Checked; + Configuration.ThrottleSpeed = ThrottleSpeedCheckBox.Checked; + + // DAC + Configuration.EnableAudioDAC = EnableDACCheckBox.Checked; + Configuration.EnableAudioDACCapture = EnableDACCaptureCheckBox.Checked; + Configuration.AudioDACCapturePath = DACOutputCapturePathTextBox.Text; + + // Validate that the output folder exists, if not, warn the user + if (Configuration.EnableAudioDACCapture && !Directory.Exists(Configuration.AudioDACCapturePath)) + { + MessageBox.Show("Warning: the specified DAC output capture path does not exist or is inaccessible."); + } this.Close(); @@ -263,6 +293,37 @@ namespace Contralto.UI private PacketInterfaceType _selectedInterfaceType; private SystemType _selectedSystemType; + private void BrowseButton_Click(object sender, EventArgs e) + { + BrowseForDACOutputFolder(); + } + private void BrowseForDACOutputFolder() + { + FolderBrowserDialog folderDialog = new FolderBrowserDialog(); + folderDialog.Description = "Choose a folder to store captured DAC audio."; + folderDialog.ShowNewFolderButton = true; + + if (folderDialog.ShowDialog() == DialogResult.OK) + { + DACOutputCapturePathTextBox.Text = folderDialog.SelectedPath; + } + } + + private void OnEnableDACCheckboxChanged(object sender, EventArgs e) + { + DACOptionsGroupBox.Enabled = Configuration.EnableAudioDAC; + } + + private void EnableDACCaptureCheckBox_CheckedChanged(object sender, EventArgs e) + { + if (EnableDACCaptureCheckBox.Checked == true && String.IsNullOrWhiteSpace(DACOutputCapturePathTextBox.Text)) + { + // + // When enabled and no output folder is set, force the user to choose an output folder. + // + BrowseForDACOutputFolder(); + } + } } } diff --git a/Contralto/packages.config b/Contralto/packages.config new file mode 100644 index 0000000..14d3468 --- /dev/null +++ b/Contralto/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ContraltoSetup/Product.wxs b/ContraltoSetup/Product.wxs index 5831a21..0e0d745 100644 --- a/ContraltoSetup/Product.wxs +++ b/ContraltoSetup/Product.wxs @@ -107,6 +107,9 @@ + + +