Files
ZXSpectrum48K/Core/Io/TapManager.cs

211 lines
6.2 KiB
C#

using System;
using System.Collections.Generic;
namespace Core.Io
{
public class TapManager
{
private Queue<byte[]> _blocks = new Queue<byte[]>();
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();
}
}
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:
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;
}
}
//using System;
//using System.Collections.Generic;
//namespace Core.Io
//{
// public class TapManager
// {
// private Queue<byte[]> _blocks = new Queue<byte[]>();
// public void LoadTapData(byte[] fileData)
// {
// _blocks.Clear();
// int position = 0;
// while (position < fileData.Length)
// {
// // 1. Read the 16-bit block length (Little Endian)
// int blockLength = fileData[position] | (fileData[position + 1] << 8);
// position += 2;
// // 2. Extract the block payload
// byte[] blockData = new byte[blockLength];
// Array.Copy(fileData, position, blockData, 0, blockLength);
// position += blockLength;
// // 3. Queue it up
// _blocks.Enqueue(blockData);
// }
// }
// public byte[] GetNextBlock()
// {
// return _blocks.Count > 0 ? _blocks.Dequeue() : null;
// }
// public bool HasBlocks => _blocks.Count > 0;
// }
//}