using System; using System.Collections.Generic; using static Core.Io.TurboSpeedBlock; //using Core.Io; namespace Core.Io { public class TapManager { 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 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 private int _pulseTimer = 0; private int _pilotPulsesRemaining = 0; 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) { _playQueue.Clear(); int position = 0; while (position < fileData.Length) { int blockLength = fileData[position] | (fileData[position + 1] << 8); position += 2; byte[] blockData = new byte[blockLength]; Array.Copy(fileData, position, blockData, 0, blockLength); position += blockLength; // 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 (_playQueue.Count > 0 && _state == TapeState.Idle) { LoadNextBlock(); } } public void Stop() { _state = TapeState.Idle; } private void LoadNextBlock() { if (_playQueue.Count == 0) { _state = TapeState.Idle; return; } _currentBlock = _playQueue.Dequeue(); // If it's metadata (like our ArchiveInfoBlock), just skip it and load the next audio block 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; } _byteIndex = 0; _bitIndex = 7; _pulseCountForCurrentBit = 0; 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; } 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; } // --- THE ENGINE SYNC --- // Call this every time the CPU steps, passing in how many T-States elapsed public void Update(int tStatesElapsed) { if (_state == TapeState.Idle) return; _pulseTimer -= tStatesElapsed; if (_pulseTimer <= 0) { EarBit = !EarBit; // Flip the audio square wave AdvanceState(); } } private void AdvanceState() { switch (_state) { case TapeState.Pilot: _pilotPulsesRemaining--; if (_pilotPulsesRemaining > 0) { _pulseTimer += _pilotLength; } else { _state = TapeState.Sync1; _pulseTimer += _sync1Length; } break; case TapeState.Sync1: _state = TapeState.Sync2; _pulseTimer += _sync2Length; break; case TapeState.Sync2: // --- 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) { CalculateNextDataPulse(); } else { _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) { _bitIndex = 7; _byteIndex++; if (_byteIndex >= dataArray.Length) { _state = TapeState.Pause; _pulseTimer += _pauseTStates; return; } } CalculateNextDataPulse(); } break; case TapeState.Pause: _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() { // 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; _pulseTimer += isBitOne ? _oneLength : _zeroLength; } // --- FAST LOAD METHODS (For ROM Hijack) --- //public byte[] GetNextBlock() //{ // return _blocks.Count > 0 ? _blocks.Dequeue() : null; //} //public bool HasBlocks => _blocks.Count > 0; // Change this line: //public bool HasBlocks => _blocks.Count > 0 || _currentBlock != null; // Only consider _currentBlock valid if we are actively playing it! 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 (block is PureDataBlock pureData) return pureData.Data; // <-- Added this line! 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 = 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; // Dequeue the next TzxBlock and extract its bytes if (_playQueue.Count > 0) { TzxBlock nextBlock = _playQueue.Dequeue(); return ExtractDataFromBlock(nextBlock); } return null; } //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; //} } }