416 lines
15 KiB
C#
416 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using static Core.Io.TurboSpeedBlock;
|
|
//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, PureTone, PulseSequence }
|
|
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;
|
|
private ushort[] _sequencePulses = Array.Empty<ushort>();
|
|
private int _sequenceIndex = 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 is ArchiveInfoBlock ||
|
|
_currentBlock is TextDescriptionBlock ||
|
|
_currentBlock is GroupStartBlock ||
|
|
_currentBlock is GroupEndBlock)
|
|
{
|
|
// It has no audio data! Discard it and recursively pull the next block.
|
|
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;
|
|
}
|
|
else if (_currentBlock is PauseBlock pauseBlock)
|
|
{
|
|
if (pauseBlock.PauseDurationMs == 0)
|
|
{
|
|
// A duration of 0 literally means "Stop the Tape Deck"
|
|
Stop();
|
|
return;
|
|
}
|
|
|
|
// Otherwise, just drop straight into a Pause state!
|
|
_state = TapeState.Pause;
|
|
_pauseTStates = pauseBlock.PauseDurationMs * 3500;
|
|
_pulseTimer = _pauseTStates;
|
|
return;
|
|
}
|
|
else if (_currentBlock is PureToneBlock toneBlock)
|
|
{
|
|
_state = TapeState.PureTone;
|
|
_pilotPulsesRemaining = toneBlock.PulseCount;
|
|
_pilotLength = toneBlock.PulseLength;
|
|
|
|
_pulseTimer = _pilotLength; // Start the first pulse
|
|
return;
|
|
}
|
|
else if (_currentBlock is PulseSequenceBlock seqBlock)
|
|
{
|
|
_state = TapeState.PulseSequence;
|
|
_sequencePulses = seqBlock.Pulses;
|
|
_sequenceIndex = 0;
|
|
|
|
// Safety check: only start if the array actually has data
|
|
if (_sequencePulses.Length > 0)
|
|
{
|
|
_pulseTimer = _sequencePulses[_sequenceIndex];
|
|
}
|
|
else
|
|
{
|
|
// Empty block? Just skip it.
|
|
_state = TapeState.Idle;
|
|
LoadNextBlock();
|
|
}
|
|
return;
|
|
}
|
|
else if (_currentBlock is PureDataBlock pureDataBlock)
|
|
{
|
|
_zeroLength = pureDataBlock.ZeroBitPulseLength;
|
|
_oneLength = pureDataBlock.OneBitPulseLength;
|
|
_pauseTStates = pureDataBlock.PauseAfterMs * 3500;
|
|
if (pureDataBlock.Data.Length == 0)
|
|
{
|
|
_state = TapeState.Pause;
|
|
_pulseTimer = _pauseTStates;
|
|
return;
|
|
}
|
|
// Skip the preamble completely and jump straight to Data processing!
|
|
_state = TapeState.Data;
|
|
_byteIndex = 0;
|
|
_bitIndex = 7;
|
|
_pulseCountForCurrentBit = 0;
|
|
|
|
// Prime the very first data pulse into the timer
|
|
CalculateNextDataPulse();
|
|
return;
|
|
}
|
|
|
|
// 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:
|
|
// --- THE FIX: Check if the block actually has data! ---
|
|
byte[] upcomingData = ExtractDataFromBlock(_currentBlock);
|
|
|
|
if (upcomingData.Length == 0)
|
|
{
|
|
// It was just a timing calibration block! Skip Data and go straight to Pause.
|
|
_state = TapeState.Pause;
|
|
_pulseTimer += _pauseTStates;
|
|
}
|
|
else
|
|
{
|
|
// Normal block. Proceed to extract the bits.
|
|
_state = TapeState.Data;
|
|
CalculateNextDataPulse();
|
|
}
|
|
break;
|
|
case TapeState.Data:
|
|
_pulseCountForCurrentBit++;
|
|
if (_pulseCountForCurrentBit < 2)
|
|
{
|
|
CalculateNextDataPulse();
|
|
}
|
|
else
|
|
{
|
|
_pulseCountForCurrentBit = 0;
|
|
_bitIndex--;
|
|
|
|
byte[] dataArray = ExtractDataFromBlock(_currentBlock);
|
|
|
|
// --- THE SPEEDLOCK FIX: Used Bits In Last Byte ---
|
|
byte usedBits = 8;
|
|
if (_currentBlock is TurboSpeedBlock t && t.UsedBitsInLastByte > 0) usedBits = t.UsedBitsInLastByte;
|
|
if (_currentBlock is PureDataBlock p && p.UsedBitsInLastByte > 0) usedBits = p.UsedBitsInLastByte;
|
|
|
|
// Stop exactly when we've processed the required bits in the final byte!
|
|
if (_byteIndex == dataArray.Length - 1 && _bitIndex < (8 - usedBits))
|
|
{
|
|
_state = TapeState.Pause;
|
|
_pulseTimer += _pauseTStates;
|
|
return; // Drop out of AdvanceState immediately
|
|
}
|
|
|
|
if (_bitIndex < 0)
|
|
{
|
|
_bitIndex = 7;
|
|
_byteIndex++;
|
|
|
|
if (_byteIndex >= dataArray.Length)
|
|
{
|
|
_state = TapeState.Pause;
|
|
_pulseTimer += _pauseTStates;
|
|
return;
|
|
}
|
|
}
|
|
CalculateNextDataPulse();
|
|
}
|
|
break;
|
|
case TapeState.Pause:
|
|
_state = TapeState.Idle;
|
|
LoadNextBlock();
|
|
break;
|
|
case TapeState.PureTone:
|
|
_pilotPulsesRemaining--;
|
|
if (_pilotPulsesRemaining > 0)
|
|
{
|
|
_pulseTimer += _pilotLength;
|
|
}
|
|
else
|
|
{
|
|
// The tone is over! Move immediately to the next block.
|
|
_state = TapeState.Idle;
|
|
LoadNextBlock();
|
|
}
|
|
break;
|
|
case TapeState.PulseSequence:
|
|
_sequenceIndex++;
|
|
|
|
if (_sequenceIndex < _sequencePulses.Length)
|
|
{
|
|
// Load the length of the next raw pulse
|
|
_pulseTimer += _sequencePulses[_sequenceIndex];
|
|
}
|
|
else
|
|
{
|
|
// Array is finished! Move immediately to the next block.
|
|
_state = TapeState.Idle;
|
|
LoadNextBlock();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void CalculateNextDataPulse()
|
|
{
|
|
// 1. Use the helper method to grab the array, which automatically supports PureData blocks too!
|
|
byte[] dataArray = ExtractDataFromBlock(_currentBlock);
|
|
|
|
// 2. THE BULLETPROOF VEST: Catch zero-length arrays and out-of-bounds indices before they crash the CPU
|
|
if (dataArray.Length == 0 || _byteIndex >= dataArray.Length)
|
|
{
|
|
_state = TapeState.Pause;
|
|
_pulseTimer += _pauseTStates;
|
|
return;
|
|
}
|
|
|
|
// 3. Extract the bit and load the dynamic pulse length into the timer
|
|
bool isBitOne = (dataArray[_byteIndex] & (1 << _bitIndex)) != 0;
|
|
_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 (block is PureDataBlock pureData) return pureData.Data; // <-- Added this line!
|
|
|
|
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;
|
|
//}
|
|
|
|
|
|
}
|
|
} |