Files
ZXSpectrum48K/Core/Io/TapManager.cs

294 lines
10 KiB
C#

using System;
using System.Collections.Generic;
//using Core.Io;
namespace Core.Io
{
public class TapManager
{
private Queue<TzxBlock> _playQueue = new Queue<TzxBlock>();
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<TzxBlock> _tzxBlocks = new List<TzxBlock>();
// 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)
{
_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<TzxBlock> 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.BlockId == 0x32)
{
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;
}
// 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:
_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++;
// --- THE FIX ---
// Extract the raw byte array from the generic TzxBlock
byte[] dataArray = ExtractDataFromBlock(_currentBlock);
// Now we can safely check the length!
if (_byteIndex >= dataArray.Length)
{
// Block finished! Enter the Pause state and wait for the dynamic delay
_state = TapeState.Pause;
_pulseTimer += _pauseTStates;
return;
}
}
CalculateNextDataPulse();
}
break;
case TapeState.Pause:
_state = TapeState.Idle;
LoadNextBlock();
break;
}
}
private void CalculateNextDataPulse()
{
// We need to pull the Data array from the base TzxBlock
byte[] dataArray = Array.Empty<byte>();
if (_currentBlock is StandardSpeedBlock std) dataArray = std.Data;
else if (_currentBlock is TurboSpeedBlock turbo) dataArray = turbo.Data;
bool isBitOne = (dataArray[_byteIndex] & (1 << _bitIndex)) != 0;
// Use the dynamic zero/one lengths!
_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 it's metadata or a block type without data, return an empty array
return Array.Empty<byte>();
}
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;
//}
}
}