using System; using System.Collections.Generic; using Core.Io; namespace Core.Io { public class TapManager { private Queue _blocks = new Queue(); private byte[] _currentBlock; private List _tzxBlocks = new List(); // State Machine Tracking private enum TapeState { Idle, Pilot, Sync1, Sync2, Data, Pause } 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; public void LoadTapData(byte[] fileData) { _blocks.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; _blocks.Enqueue(blockData); } } public void Play() { if (_blocks.Count > 0 && _state == TapeState.Idle) { LoadNextBlock(); } } public void Stop() { _state = TapeState.Idle; } private void LoadNextBlock() { if (_blocks.Count == 0) { _state = TapeState.Idle; return; } _currentBlock = _blocks.Dequeue(); _byteIndex = 0; _bitIndex = 7; // MSB first _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; } // --- 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 += 2168; } else { _state = TapeState.Sync1; _pulseTimer += 667; } break; case TapeState.Sync1: _state = TapeState.Sync2; _pulseTimer += 735; break; case TapeState.Sync2: _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--; if (_bitIndex < 0) { // Move to next byte _bitIndex = 7; _byteIndex++; if (_byteIndex >= _currentBlock.Length) { // Block finished! 1 second pause before the next block _state = TapeState.Pause; _pulseTimer += 3500000; 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(); break; } } private void CalculateNextDataPulse() { // Extract the current bit from the current byte bool isBitOne = (_currentBlock[_byteIndex] & (1 << _bitIndex)) != 0; // Bit 0 = 855 T-States, Bit 1 = 1710 T-States _pulseTimer += isBitOne ? 1710 : 855; } // --- 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 => _blocks.Count > 0 || (_currentBlock != null && _state != TapeState.Idle && _state != TapeState.Pause); 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 _blocks.Count > 0 ? _blocks.Dequeue() : null; } public void LoadTzxData(List blocks) { _tzxBlocks = blocks; // Just to prove it works before we build the State Machine! System.Diagnostics.Debug.WriteLine($"Successfully loaded {_tzxBlocks.Count} TZX blocks!"); } } }