190 lines
5.8 KiB
C#
190 lines
5.8 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();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
} |