diff --git a/Core/Io/TapManager.cs b/Core/Io/TapManager.cs index 87d6ac0..46915b2 100644 --- a/Core/Io/TapManager.cs +++ b/Core/Io/TapManager.cs @@ -1,13 +1,22 @@ using System; using System.Collections.Generic; -using Core.Io; +//using Core.Io; namespace Core.Io { public class TapManager { - private Queue _blocks = new Queue(); - private byte[] _currentBlock; + private Queue _playQueue = new Queue(); + private TzxBlock _currentBlock; // Now holds a TzxBlock instead of byte[] + + // Dynamic Timings for the current block + private int _pilotLength; + private int _sync1Length; + private int _sync2Length; + private int _zeroLength; + private int _oneLength; + private int _pauseTStates; + //private byte[] _currentBlock; private List _tzxBlocks = new List(); // State Machine Tracking @@ -24,7 +33,7 @@ namespace Core.Io public void LoadTapData(byte[] fileData) { - _blocks.Clear(); + _playQueue.Clear(); int position = 0; while (position < fileData.Length) @@ -36,13 +45,25 @@ namespace Core.Io Array.Copy(fileData, position, blockData, 0, blockLength); position += blockLength; - _blocks.Enqueue(blockData); + // Wrap the raw TAP data into our new TZX Standard Block! + _playQueue.Enqueue(new StandardSpeedBlock + { + DataLength = (ushort)blockLength, + Data = blockData, + PauseAfterMs = 1000 // Standard TAP blocks have a ~1 second pause + }); } } + public void LoadTzxData(List blocks) + { + _playQueue.Clear(); + foreach (var block in blocks) _playQueue.Enqueue(block); + } + public void Play() { - if (_blocks.Count > 0 && _state == TapeState.Idle) + if (_playQueue.Count > 0 && _state == TapeState.Idle) { LoadNextBlock(); } @@ -55,24 +76,54 @@ namespace Core.Io private void LoadNextBlock() { - if (_blocks.Count == 0) + if (_playQueue.Count == 0) { _state = TapeState.Idle; return; } - _currentBlock = _blocks.Dequeue(); + _currentBlock = _playQueue.Dequeue(); + + // If it's metadata (like our ArchiveInfoBlock), just skip it and load the next audio block + if (_currentBlock.BlockId == 0x32) + { + LoadNextBlock(); + return; + } + _byteIndex = 0; - _bitIndex = 7; // MSB first + _bitIndex = 7; _pulseCountForCurrentBit = 0; - - // Header blocks (flag 0x00) have longer pilot tones than Data blocks - bool isHeader = _currentBlock.Length > 0 && _currentBlock[0] == 0; - _pilotPulsesRemaining = isHeader ? 8063 : 3223; - - _state = TapeState.Pilot; - _pulseTimer = 2168; // Length of a pilot pulse EarBit = false; + _state = TapeState.Pilot; + + // --- CONFIGURE TIMINGS BASED ON BLOCK TYPE --- + if (_currentBlock is StandardSpeedBlock stdBlock) + { + bool isHeader = stdBlock.Data.Length > 0 && stdBlock.Data[0] == 0; + _pilotPulsesRemaining = isHeader ? 8063 : 3223; + + _pilotLength = 2168; + _sync1Length = 667; + _sync2Length = 735; + _zeroLength = 855; + _oneLength = 1710; + _pauseTStates = stdBlock.PauseAfterMs * 3500; // 3.5 MHz = 3500 T-States per millisecond + } + else if (_currentBlock is TurboSpeedBlock turboBlock) + { + _pilotPulsesRemaining = turboBlock.PilotToneLength; + + _pilotLength = turboBlock.PilotPulseLength; + _sync1Length = turboBlock.SyncFirstPulseLength; + _sync2Length = turboBlock.SyncSecondPulseLength; + _zeroLength = turboBlock.ZeroBitPulseLength; + _oneLength = turboBlock.OneBitPulseLength; + _pauseTStates = turboBlock.PauseAfterMs * 3500; + } + + // Start the very first pilot pulse! + _pulseTimer = _pilotLength; } // --- THE ENGINE SYNC --- @@ -98,18 +149,18 @@ namespace Core.Io _pilotPulsesRemaining--; if (_pilotPulsesRemaining > 0) { - _pulseTimer += 2168; + _pulseTimer += _pilotLength; } else { _state = TapeState.Sync1; - _pulseTimer += 667; + _pulseTimer += _sync1Length; } break; case TapeState.Sync1: _state = TapeState.Sync2; - _pulseTimer += 735; + _pulseTimer += _sync2Length; break; case TapeState.Sync2: @@ -136,32 +187,22 @@ namespace Core.Io _bitIndex = 7; _byteIndex++; - if (_byteIndex >= _currentBlock.Length) + // --- 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! 1 second pause before the next block + // Block finished! Enter the Pause state and wait for the dynamic delay _state = TapeState.Pause; - _pulseTimer += 3500000; + _pulseTimer += _pauseTStates; return; } } CalculateNextDataPulse(); } break; - //case TapeState.Pause: - // // The .TAP Format Auto-Stop Heuristic: - // if (_currentBlock != null && _currentBlock.Length > 0 && _currentBlock[0] == 0x00) - // { - // // 1. It was a Header block. The ROM is waiting for the Data right now! Keep spinning. - // LoadNextBlock(); - // } - // else - // { - // // 2. It was a Data block (or custom). The "file" is done. - // // Auto-Stop the tape deck so we don't accidentally play the next level into the void. - // _state = TapeState.Idle; - // _currentBlock = null; - // } - // break; case TapeState.Pause: _state = TapeState.Idle; LoadNextBlock(); @@ -171,11 +212,15 @@ namespace Core.Io private void CalculateNextDataPulse() { - // Extract the current bit from the current byte - bool isBitOne = (_currentBlock[_byteIndex] & (1 << _bitIndex)) != 0; + // 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; - // Bit 0 = 855 T-States, Bit 1 = 1710 T-States - _pulseTimer += isBitOne ? 1710 : 855; + bool isBitOne = (dataArray[_byteIndex] & (1 << _bitIndex)) != 0; + + // Use the dynamic zero/one lengths! + _pulseTimer += isBitOne ? _oneLength : _zeroLength; } // --- FAST LOAD METHODS (For ROM Hijack) --- @@ -188,31 +233,62 @@ namespace Core.Io // Change this line: //public bool HasBlocks => _blocks.Count > 0 || _currentBlock != null; // Only consider _currentBlock valid if we are actively playing it! - public bool HasBlocks => _blocks.Count > 0 || (_currentBlock != null && _state != TapeState.Idle && _state != TapeState.Pause); + public bool HasBlocks => _playQueue.Count > 0 || (_currentBlock != null && _state != TapeState.Idle && _state != TapeState.Pause); + + // Helper to grab the raw bytes out of whatever TZX block is currently loaded + private byte[] ExtractDataFromBlock(TzxBlock block) + { + if (block is StandardSpeedBlock std) return std.Data; + if (block is TurboSpeedBlock turbo) return turbo.Data; + + // If it's metadata or a block type without data, return an empty array + return Array.Empty(); + } public byte[] GetNextBlock() { // Yank the current block ONLY if it is actively playing if (_currentBlock != null && _state != TapeState.Idle && _state != TapeState.Pause) { - byte[] blockToReturn = _currentBlock; + byte[] blockToReturn = ExtractDataFromBlock(_currentBlock); + _state = TapeState.Idle; // Ensure the tape deck is stopped _currentBlock = null; + return blockToReturn; } // Otherwise, pull directly from the unplayed queue _state = TapeState.Idle; // Stop the tape deck just in case _currentBlock = null; - return _blocks.Count > 0 ? _blocks.Dequeue() : null; + + // Dequeue the next TzxBlock and extract its bytes + if (_playQueue.Count > 0) + { + TzxBlock nextBlock = _playQueue.Dequeue(); + return ExtractDataFromBlock(nextBlock); + } + + return null; } - public void LoadTzxData(List blocks) - { - _tzxBlocks = blocks; + //public byte[] GetNextBlock() + //{ + // // Yank the current block ONLY if it is actively playing + // if (_currentBlock != null && _state != TapeState.Idle && _state != TapeState.Pause) + // { + // byte[] blockToReturn = _currentBlock; + // _state = TapeState.Idle; // Ensure the tape deck is stopped + // _currentBlock = null; + // return blockToReturn; + // } + + // // Otherwise, pull directly from the unplayed queue + // _state = TapeState.Idle; // Stop the tape deck just in case + // _currentBlock = null; + // return _playQueue.Count > 0 ? _blocks.Dequeue() : null; + //} + - // Just to prove it works before we build the State Machine! - System.Diagnostics.Debug.WriteLine($"Successfully loaded {_tzxBlocks.Count} TZX blocks!"); - } } } \ No newline at end of file diff --git a/Core/Io/TzxBlocks.cs b/Core/Io/TzxBlocks.cs index 1fc212c..521d051 100644 --- a/Core/Io/TzxBlocks.cs +++ b/Core/Io/TzxBlocks.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Core.Io { @@ -21,4 +22,34 @@ namespace Core.Io // The actual tape bytes public byte[] Data { get; set; } = Array.Empty(); } + + // ID 0x32: Archive Info Block (Metadata) + public class ArchiveInfoBlock : TzxBlock + { + public override int BlockId => 0x32; + + // Key: Text ID (e.g., 0 = Full Title, 1 = Software House, 2 = Publisher, 4 = Year) + // Value: The actual text string + public Dictionary Metadata { get; set; } = new Dictionary(); + } + + // ID 0x11: Turbo Speed Data Block + public class TurboSpeedBlock : TzxBlock + { + public override int BlockId => 0x11; + + public ushort PilotPulseLength { get; set; } + public ushort SyncFirstPulseLength { get; set; } + public ushort SyncSecondPulseLength { get; set; } + public ushort ZeroBitPulseLength { get; set; } + public ushort OneBitPulseLength { get; set; } + public ushort PilotToneLength { get; set; } + public byte UsedBitsInLastByte { get; set; } + public ushort PauseAfterMs { get; set; } + + // This is a 24-bit integer in the file, so we store it in a standard 32-bit int + 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 659f06f..81fe51e 100644 --- a/Core/Io/TzxParser.cs +++ b/Core/Io/TzxParser.cs @@ -41,6 +41,51 @@ namespace Core.Io stdBlock.Data = br.ReadBytes(stdBlock.DataLength); blocks.Add(stdBlock); break; + case 0x11: // Turbo Speed Data Block + var turboBlock = new TurboSpeedBlock + { + PilotPulseLength = br.ReadUInt16(), + SyncFirstPulseLength = br.ReadUInt16(), + SyncSecondPulseLength = br.ReadUInt16(), + ZeroBitPulseLength = br.ReadUInt16(), + OneBitPulseLength = br.ReadUInt16(), + PilotToneLength = br.ReadUInt16(), + UsedBitsInLastByte = br.ReadByte(), + PauseAfterMs = br.ReadUInt16() + }; + + // Z80 files are Little-Endian. Read 3 bytes and combine them into a 24-bit integer. + byte len0 = br.ReadByte(); + byte len1 = br.ReadByte(); + byte len2 = br.ReadByte(); + turboBlock.DataLength = len0 | (len1 << 8) | (len2 << 16); + + // Read the actual tape data + turboBlock.Data = br.ReadBytes(turboBlock.DataLength); + + blocks.Add(turboBlock); + break; + case 0x32: // Archive Info Block + var archiveBlock = new ArchiveInfoBlock(); + + // The total length of the block (we read it to advance the stream, + // but the string count is what we actually use to loop) + ushort totalLength = br.ReadUInt16(); + byte stringCount = br.ReadByte(); + + for (int i = 0; i < stringCount; i++) + { + byte textId = br.ReadByte(); + byte textLength = br.ReadByte(); + + // Read the raw bytes and convert them to an ASCII string + string text = System.Text.Encoding.ASCII.GetString(br.ReadBytes(textLength)); + + archiveBlock.Metadata[textId] = text; + } + + blocks.Add(archiveBlock); + break; // TODO: Add cases for 0x11 (Turbo), 0x12 (Tone), 0x13 (Pulses), etc. diff --git a/Desktop/Desktop.csproj b/Desktop/Desktop.csproj index 08e6b8a..e7e7761 100644 --- a/Desktop/Desktop.csproj +++ b/Desktop/Desktop.csproj @@ -25,6 +25,9 @@ + + + @@ -76,6 +79,15 @@ Always + + Always + + + Always + + + Always + diff --git a/Desktop/ROMS/TZX/Batman - Release 1.tzx b/Desktop/ROMS/TZX/Batman - Release 1.tzx new file mode 100644 index 0000000..c90584f Binary files /dev/null and b/Desktop/ROMS/TZX/Batman - Release 1.tzx differ