Pissing about with TZX support
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using static Core.Io.TurboSpeedBlock;
|
||||
//using Core.Io;
|
||||
|
||||
namespace Core.Io
|
||||
@@ -20,7 +21,7 @@ namespace Core.Io
|
||||
private List<TzxBlock> _tzxBlocks = new List<TzxBlock>();
|
||||
|
||||
// State Machine Tracking
|
||||
private enum TapeState { Idle, Pilot, Sync1, Sync2, Data, Pause }
|
||||
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
|
||||
@@ -30,6 +31,8 @@ namespace Core.Io
|
||||
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)
|
||||
{
|
||||
@@ -85,8 +88,12 @@ namespace Core.Io
|
||||
_currentBlock = _playQueue.Dequeue();
|
||||
|
||||
// If it's metadata (like our ArchiveInfoBlock), just skip it and load the next audio block
|
||||
if (_currentBlock.BlockId == 0x32)
|
||||
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;
|
||||
}
|
||||
@@ -121,6 +128,70 @@ namespace Core.Io
|
||||
_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;
|
||||
@@ -164,37 +235,55 @@ namespace Core.Io
|
||||
break;
|
||||
|
||||
case TapeState.Sync2:
|
||||
_state = TapeState.Data;
|
||||
CalculateNextDataPulse();
|
||||
break;
|
||||
// --- 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)
|
||||
{
|
||||
// A bit requires 2 pulses (one high, one low)
|
||||
CalculateNextDataPulse();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Move to next bit
|
||||
_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)
|
||||
{
|
||||
// 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;
|
||||
@@ -207,19 +296,52 @@ namespace Core.Io
|
||||
_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()
|
||||
{
|
||||
// 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;
|
||||
// 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;
|
||||
|
||||
// Use the dynamic zero/one lengths!
|
||||
_pulseTimer += isBitOne ? _oneLength : _zeroLength;
|
||||
}
|
||||
|
||||
@@ -240,8 +362,8 @@ namespace Core.Io
|
||||
{
|
||||
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!
|
||||
|
||||
// If it's metadata or a block type without data, return an empty array
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user