diff --git a/Core/Cpu/Z80.cs b/Core/Cpu/Z80.cs index 0b73771..8ff61be 100644 --- a/Core/Cpu/Z80.cs +++ b/Core/Cpu/Z80.cs @@ -1312,6 +1312,9 @@ namespace Core.Cpu case 0x4D: // RETI Does not affect IFF1 or IFF2 PC = Pop(); return 14; + case 0x4F: // LD R, A + R = AF.High; + return 9; case 0x51: // OUT (C), D // BC.Word goes to the address bus, D (DE.High) goes to the data bus _simpleIoBus.WritePort(BC.Word, DE.High); diff --git a/Core/Io/TapManager.cs b/Core/Io/TapManager.cs index 46915b2..39964ce 100644 --- a/Core/Io/TapManager.cs +++ b/Core/Io/TapManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using static Core.Io.TurboSpeedBlock; //using Core.Io; namespace Core.Io @@ -20,7 +21,7 @@ namespace Core.Io private List _tzxBlocks = new List(); // State Machine Tracking - private enum TapeState { Idle, Pilot, Sync1, Sync2, Data, Pause } + private enum TapeState { Idle, Pilot, Sync1, Sync2, Data, Pause, PureTone, PulseSequence } private TapeState _state = TapeState.Idle; public bool EarBit { get; private set; } = false; // The physical audio signal sent to the CPU @@ -30,6 +31,8 @@ namespace Core.Io private int _byteIndex = 0; private int _bitIndex = 0; private int _pulseCountForCurrentBit = 0; + private ushort[] _sequencePulses = Array.Empty(); + private int _sequenceIndex = 0; public void LoadTapData(byte[] fileData) { @@ -85,8 +88,12 @@ namespace Core.Io _currentBlock = _playQueue.Dequeue(); // If it's metadata (like our ArchiveInfoBlock), just skip it and load the next audio block - if (_currentBlock.BlockId == 0x32) + if (_currentBlock is ArchiveInfoBlock || + _currentBlock is TextDescriptionBlock || + _currentBlock is GroupStartBlock || + _currentBlock is GroupEndBlock) { + // It has no audio data! Discard it and recursively pull the next block. LoadNextBlock(); return; } @@ -121,6 +128,70 @@ namespace Core.Io _oneLength = turboBlock.OneBitPulseLength; _pauseTStates = turboBlock.PauseAfterMs * 3500; } + else if (_currentBlock is PauseBlock pauseBlock) + { + if (pauseBlock.PauseDurationMs == 0) + { + // A duration of 0 literally means "Stop the Tape Deck" + Stop(); + return; + } + + // Otherwise, just drop straight into a Pause state! + _state = TapeState.Pause; + _pauseTStates = pauseBlock.PauseDurationMs * 3500; + _pulseTimer = _pauseTStates; + return; + } + else if (_currentBlock is PureToneBlock toneBlock) + { + _state = TapeState.PureTone; + _pilotPulsesRemaining = toneBlock.PulseCount; + _pilotLength = toneBlock.PulseLength; + + _pulseTimer = _pilotLength; // Start the first pulse + return; + } + else if (_currentBlock is PulseSequenceBlock seqBlock) + { + _state = TapeState.PulseSequence; + _sequencePulses = seqBlock.Pulses; + _sequenceIndex = 0; + + // Safety check: only start if the array actually has data + if (_sequencePulses.Length > 0) + { + _pulseTimer = _sequencePulses[_sequenceIndex]; + } + else + { + // Empty block? Just skip it. + _state = TapeState.Idle; + LoadNextBlock(); + } + return; + } + else if (_currentBlock is PureDataBlock pureDataBlock) + { + _zeroLength = pureDataBlock.ZeroBitPulseLength; + _oneLength = pureDataBlock.OneBitPulseLength; + _pauseTStates = pureDataBlock.PauseAfterMs * 3500; + if (pureDataBlock.Data.Length == 0) + { + _state = TapeState.Pause; + _pulseTimer = _pauseTStates; + return; + } + // Skip the preamble completely and jump straight to Data processing! + _state = TapeState.Data; + _byteIndex = 0; + _bitIndex = 7; + _pulseCountForCurrentBit = 0; + + // Prime the very first data pulse into the timer + CalculateNextDataPulse(); + return; + } // Start the very first pilot pulse! _pulseTimer = _pilotLength; @@ -164,37 +235,55 @@ namespace Core.Io break; case TapeState.Sync2: - _state = TapeState.Data; - CalculateNextDataPulse(); - break; + // --- THE FIX: Check if the block actually has data! --- + byte[] upcomingData = ExtractDataFromBlock(_currentBlock); + if (upcomingData.Length == 0) + { + // It was just a timing calibration block! Skip Data and go straight to Pause. + _state = TapeState.Pause; + _pulseTimer += _pauseTStates; + } + else + { + // Normal block. Proceed to extract the bits. + _state = TapeState.Data; + CalculateNextDataPulse(); + } + break; case TapeState.Data: _pulseCountForCurrentBit++; if (_pulseCountForCurrentBit < 2) { - // A bit requires 2 pulses (one high, one low) CalculateNextDataPulse(); } else { - // Move to next bit _pulseCountForCurrentBit = 0; _bitIndex--; + byte[] dataArray = ExtractDataFromBlock(_currentBlock); + + // --- THE SPEEDLOCK FIX: Used Bits In Last Byte --- + byte usedBits = 8; + if (_currentBlock is TurboSpeedBlock t && t.UsedBitsInLastByte > 0) usedBits = t.UsedBitsInLastByte; + if (_currentBlock is PureDataBlock p && p.UsedBitsInLastByte > 0) usedBits = p.UsedBitsInLastByte; + + // Stop exactly when we've processed the required bits in the final byte! + if (_byteIndex == dataArray.Length - 1 && _bitIndex < (8 - usedBits)) + { + _state = TapeState.Pause; + _pulseTimer += _pauseTStates; + return; // Drop out of AdvanceState immediately + } + if (_bitIndex < 0) { - // Move to next byte _bitIndex = 7; _byteIndex++; - // --- THE FIX --- - // Extract the raw byte array from the generic TzxBlock - byte[] dataArray = ExtractDataFromBlock(_currentBlock); - - // Now we can safely check the length! if (_byteIndex >= dataArray.Length) { - // Block finished! Enter the Pause state and wait for the dynamic delay _state = TapeState.Pause; _pulseTimer += _pauseTStates; return; @@ -207,19 +296,52 @@ namespace Core.Io _state = TapeState.Idle; LoadNextBlock(); break; + case TapeState.PureTone: + _pilotPulsesRemaining--; + if (_pilotPulsesRemaining > 0) + { + _pulseTimer += _pilotLength; + } + else + { + // The tone is over! Move immediately to the next block. + _state = TapeState.Idle; + LoadNextBlock(); + } + break; + case TapeState.PulseSequence: + _sequenceIndex++; + + if (_sequenceIndex < _sequencePulses.Length) + { + // Load the length of the next raw pulse + _pulseTimer += _sequencePulses[_sequenceIndex]; + } + else + { + // Array is finished! Move immediately to the next block. + _state = TapeState.Idle; + LoadNextBlock(); + } + break; } } private void CalculateNextDataPulse() { - // We need to pull the Data array from the base TzxBlock - byte[] dataArray = Array.Empty(); - if (_currentBlock is StandardSpeedBlock std) dataArray = std.Data; - else if (_currentBlock is TurboSpeedBlock turbo) dataArray = turbo.Data; + // 1. Use the helper method to grab the array, which automatically supports PureData blocks too! + byte[] dataArray = ExtractDataFromBlock(_currentBlock); + // 2. THE BULLETPROOF VEST: Catch zero-length arrays and out-of-bounds indices before they crash the CPU + if (dataArray.Length == 0 || _byteIndex >= dataArray.Length) + { + _state = TapeState.Pause; + _pulseTimer += _pauseTStates; + return; + } + + // 3. Extract the bit and load the dynamic pulse length into the timer bool isBitOne = (dataArray[_byteIndex] & (1 << _bitIndex)) != 0; - - // Use the dynamic zero/one lengths! _pulseTimer += isBitOne ? _oneLength : _zeroLength; } @@ -240,8 +362,8 @@ namespace Core.Io { if (block is StandardSpeedBlock std) return std.Data; if (block is TurboSpeedBlock turbo) return turbo.Data; + if (block is PureDataBlock pureData) return pureData.Data; // <-- Added this line! - // If it's metadata or a block type without data, return an empty array return Array.Empty(); } diff --git a/Core/Io/TzxBlocks.cs b/Core/Io/TzxBlocks.cs index 521d051..fb37123 100644 --- a/Core/Io/TzxBlocks.cs +++ b/Core/Io/TzxBlocks.cs @@ -51,5 +51,65 @@ namespace Core.Io public int DataLength { get; set; } public byte[] Data { get; set; } = Array.Empty(); + // ID 0x30: Text Description + public class TextDescriptionBlock : TzxBlock + { + public override int BlockId => 0x30; + public string Description { get; set; } = string.Empty; + } + + // ID 0x20: Pause / Stop the Tape + public class PauseBlock : TzxBlock + { + public override int BlockId => 0x20; + public ushort PauseDurationMs { get; set; } // 0 means "Stop the tape" + } + + // ID 0x21: Group Start (Used to group blocks in UI tape browsers) + public class GroupStartBlock : TzxBlock + { + public override int BlockId => 0x21; + public string GroupName { get; set; } = string.Empty; + } + + // ID 0x22: Group End + public class GroupEndBlock : TzxBlock + { + public override int BlockId => 0x22; + } + + // ID 0x12: Pure Tone Block + public class PureToneBlock : TzxBlock + { + public override int BlockId => 0x12; + public ushort PulseLength { get; set; } + public ushort PulseCount { get; set; } + } + // ID 0x13: Pulse Sequence Block + public class PulseSequenceBlock : TzxBlock + { + public override int BlockId => 0x13; + + // Number of pulses (1 to 255) + public int PulseCount { get; set; } + + // The exact T-State lengths of each pulse + public ushort[] Pulses { get; set; } = Array.Empty(); + } + // ID 0x14: Pure Data Block + public class PureDataBlock : TzxBlock + { + public override int BlockId => 0x14; + + public ushort ZeroBitPulseLength { get; set; } + public ushort OneBitPulseLength { get; set; } + public byte UsedBitsInLastByte { get; set; } + public ushort PauseAfterMs { get; set; } + + // The return of the 24-bit integer! + public int DataLength { get; set; } + + public byte[] Data { get; set; } = Array.Empty(); + } } } \ No newline at end of file diff --git a/Core/Io/TzxParser.cs b/Core/Io/TzxParser.cs index 81fe51e..3e1e66c 100644 --- a/Core/Io/TzxParser.cs +++ b/Core/Io/TzxParser.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; +using static Core.Io.TurboSpeedBlock; namespace Core.Io { @@ -65,6 +66,66 @@ namespace Core.Io blocks.Add(turboBlock); break; + case 0x12: // Pure Tone Block + blocks.Add(new PureToneBlock + { + PulseLength = br.ReadUInt16(), + PulseCount = br.ReadUInt16() + }); + break; + case 0x13: // Pulse Sequence Block + var seqBlock = new PulseSequenceBlock(); + + byte rawCount = br.ReadByte(); + // TZX Spec: A raw count of 0 means 256 pulses! + seqBlock.PulseCount = (rawCount == 0) ? 256 : rawCount; + + seqBlock.Pulses = new ushort[seqBlock.PulseCount]; + for (int i = 0; i < seqBlock.PulseCount; i++) + { + seqBlock.Pulses[i] = br.ReadUInt16(); + } + + blocks.Add(seqBlock); + break; + case 0x14: // Pure Data Block + var pureData = new PureDataBlock + { + ZeroBitPulseLength = br.ReadUInt16(), + OneBitPulseLength = br.ReadUInt16(), + UsedBitsInLastByte = br.ReadByte(), + PauseAfterMs = br.ReadUInt16() + }; + + // Assemble the 24-bit length + byte pdLen0 = br.ReadByte(); + byte pdLen1 = br.ReadByte(); + byte pdLen2 = br.ReadByte(); + pureData.DataLength = pdLen0 | (pdLen1 << 8) | (pdLen2 << 16); + + pureData.Data = br.ReadBytes(pureData.DataLength); + blocks.Add(pureData); + break; + case 0x20: // Pause / Stop Tape Block + blocks.Add(new PauseBlock { PauseDurationMs = br.ReadUInt16() }); + break; + + case 0x21: // Group Start Block + byte groupNameLength = br.ReadByte(); + string groupName = System.Text.Encoding.ASCII.GetString(br.ReadBytes(groupNameLength)); + blocks.Add(new GroupStartBlock { GroupName = groupName }); + break; + + case 0x22: // Group End Block + // This block has no payload data at all! Just the ID. + blocks.Add(new GroupEndBlock()); + break; + case 0x30: // Text Description Block + byte textLength30 = br.ReadByte(); + string description = System.Text.Encoding.ASCII.GetString(br.ReadBytes(textLength30)); + blocks.Add(new TextDescriptionBlock { Description = description }); + break; + case 0x32: // Archive Info Block var archiveBlock = new ArchiveInfoBlock(); @@ -76,10 +137,10 @@ namespace Core.Io for (int i = 0; i < stringCount; i++) { byte textId = br.ReadByte(); - byte textLength = br.ReadByte(); + byte textLength32 = br.ReadByte(); // Read the raw bytes and convert them to an ASCII string - string text = System.Text.Encoding.ASCII.GetString(br.ReadBytes(textLength)); + string text = System.Text.Encoding.ASCII.GetString(br.ReadBytes(textLength32)); archiveBlock.Metadata[textId] = text; } diff --git a/Desktop/Desktop.csproj b/Desktop/Desktop.csproj index e7e7761..d1c6a1a 100644 --- a/Desktop/Desktop.csproj +++ b/Desktop/Desktop.csproj @@ -26,6 +26,8 @@ + + @@ -82,6 +84,12 @@ Always + + Always + + + Always + Always diff --git a/Desktop/ROMS/TZX/Renegade 2 - Target Renegade - Side 1.tzx b/Desktop/ROMS/TZX/Renegade 2 - Target Renegade - Side 1.tzx new file mode 100644 index 0000000..15fc8f9 Binary files /dev/null and b/Desktop/ROMS/TZX/Renegade 2 - Target Renegade - Side 1.tzx differ diff --git a/Desktop/ROMS/TZX/Renegade 2 - Target Renegade - Side 2.tzx b/Desktop/ROMS/TZX/Renegade 2 - Target Renegade - Side 2.tzx new file mode 100644 index 0000000..2bd43b5 Binary files /dev/null and b/Desktop/ROMS/TZX/Renegade 2 - Target Renegade - Side 2.tzx differ