using System; using System.Collections.Generic; namespace Core.Io { public class TapManager { private Queue _blocks = new Queue(); private byte[] _currentBlock; // 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: _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; public byte[] GetNextBlock() { // If a block is loaded into the tape deck, yank it immediately if (_currentBlock != null) { byte[] blockToReturn = _currentBlock; _state = TapeState.Idle; // Ensure the tape deck is stopped _currentBlock = null; return blockToReturn; } // Otherwise, pull directly from the queue return _blocks.Count > 0 ? _blocks.Dequeue() : null; } } }