TZX support fully supported added batman.tzx
This commit is contained in:
@@ -1,13 +1,22 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Core.Io;
|
//using Core.Io;
|
||||||
|
|
||||||
namespace Core.Io
|
namespace Core.Io
|
||||||
{
|
{
|
||||||
public class TapManager
|
public class TapManager
|
||||||
{
|
{
|
||||||
private Queue<byte[]> _blocks = new Queue<byte[]>();
|
private Queue<TzxBlock> _playQueue = new Queue<TzxBlock>();
|
||||||
private byte[] _currentBlock;
|
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>();
|
private List<TzxBlock> _tzxBlocks = new List<TzxBlock>();
|
||||||
|
|
||||||
// State Machine Tracking
|
// State Machine Tracking
|
||||||
@@ -24,7 +33,7 @@ namespace Core.Io
|
|||||||
|
|
||||||
public void LoadTapData(byte[] fileData)
|
public void LoadTapData(byte[] fileData)
|
||||||
{
|
{
|
||||||
_blocks.Clear();
|
_playQueue.Clear();
|
||||||
int position = 0;
|
int position = 0;
|
||||||
|
|
||||||
while (position < fileData.Length)
|
while (position < fileData.Length)
|
||||||
@@ -36,13 +45,25 @@ namespace Core.Io
|
|||||||
Array.Copy(fileData, position, blockData, 0, blockLength);
|
Array.Copy(fileData, position, blockData, 0, blockLength);
|
||||||
position += blockLength;
|
position += blockLength;
|
||||||
|
|
||||||
_blocks.Enqueue(blockData);
|
// 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()
|
public void Play()
|
||||||
{
|
{
|
||||||
if (_blocks.Count > 0 && _state == TapeState.Idle)
|
if (_playQueue.Count > 0 && _state == TapeState.Idle)
|
||||||
{
|
{
|
||||||
LoadNextBlock();
|
LoadNextBlock();
|
||||||
}
|
}
|
||||||
@@ -55,24 +76,54 @@ namespace Core.Io
|
|||||||
|
|
||||||
private void LoadNextBlock()
|
private void LoadNextBlock()
|
||||||
{
|
{
|
||||||
if (_blocks.Count == 0)
|
if (_playQueue.Count == 0)
|
||||||
{
|
{
|
||||||
_state = TapeState.Idle;
|
_state = TapeState.Idle;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentBlock = _blocks.Dequeue();
|
_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;
|
_byteIndex = 0;
|
||||||
_bitIndex = 7; // MSB first
|
_bitIndex = 7;
|
||||||
_pulseCountForCurrentBit = 0;
|
_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;
|
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 ---
|
// --- THE ENGINE SYNC ---
|
||||||
@@ -98,18 +149,18 @@ namespace Core.Io
|
|||||||
_pilotPulsesRemaining--;
|
_pilotPulsesRemaining--;
|
||||||
if (_pilotPulsesRemaining > 0)
|
if (_pilotPulsesRemaining > 0)
|
||||||
{
|
{
|
||||||
_pulseTimer += 2168;
|
_pulseTimer += _pilotLength;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_state = TapeState.Sync1;
|
_state = TapeState.Sync1;
|
||||||
_pulseTimer += 667;
|
_pulseTimer += _sync1Length;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TapeState.Sync1:
|
case TapeState.Sync1:
|
||||||
_state = TapeState.Sync2;
|
_state = TapeState.Sync2;
|
||||||
_pulseTimer += 735;
|
_pulseTimer += _sync2Length;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TapeState.Sync2:
|
case TapeState.Sync2:
|
||||||
@@ -136,32 +187,22 @@ namespace Core.Io
|
|||||||
_bitIndex = 7;
|
_bitIndex = 7;
|
||||||
_byteIndex++;
|
_byteIndex++;
|
||||||
|
|
||||||
if (_byteIndex >= _currentBlock.Length)
|
// --- 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! 1 second pause before the next block
|
// Block finished! Enter the Pause state and wait for the dynamic delay
|
||||||
_state = TapeState.Pause;
|
_state = TapeState.Pause;
|
||||||
_pulseTimer += 3500000;
|
_pulseTimer += _pauseTStates;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CalculateNextDataPulse();
|
CalculateNextDataPulse();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
//case TapeState.Pause:
|
|
||||||
// // The .TAP Format Auto-Stop Heuristic:
|
|
||||||
// if (_currentBlock != null && _currentBlock.Length > 0 && _currentBlock[0] == 0x00)
|
|
||||||
// {
|
|
||||||
// // 1. It was a Header block. The ROM is waiting for the Data right now! Keep spinning.
|
|
||||||
// LoadNextBlock();
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// // 2. It was a Data block (or custom). The "file" is done.
|
|
||||||
// // Auto-Stop the tape deck so we don't accidentally play the next level into the void.
|
|
||||||
// _state = TapeState.Idle;
|
|
||||||
// _currentBlock = null;
|
|
||||||
// }
|
|
||||||
// break;
|
|
||||||
case TapeState.Pause:
|
case TapeState.Pause:
|
||||||
_state = TapeState.Idle;
|
_state = TapeState.Idle;
|
||||||
LoadNextBlock();
|
LoadNextBlock();
|
||||||
@@ -171,11 +212,15 @@ namespace Core.Io
|
|||||||
|
|
||||||
private void CalculateNextDataPulse()
|
private void CalculateNextDataPulse()
|
||||||
{
|
{
|
||||||
// Extract the current bit from the current byte
|
// We need to pull the Data array from the base TzxBlock
|
||||||
bool isBitOne = (_currentBlock[_byteIndex] & (1 << _bitIndex)) != 0;
|
byte[] dataArray = Array.Empty<byte>();
|
||||||
|
if (_currentBlock is StandardSpeedBlock std) dataArray = std.Data;
|
||||||
|
else if (_currentBlock is TurboSpeedBlock turbo) dataArray = turbo.Data;
|
||||||
|
|
||||||
// Bit 0 = 855 T-States, Bit 1 = 1710 T-States
|
bool isBitOne = (dataArray[_byteIndex] & (1 << _bitIndex)) != 0;
|
||||||
_pulseTimer += isBitOne ? 1710 : 855;
|
|
||||||
|
// Use the dynamic zero/one lengths!
|
||||||
|
_pulseTimer += isBitOne ? _oneLength : _zeroLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- FAST LOAD METHODS (For ROM Hijack) ---
|
// --- FAST LOAD METHODS (For ROM Hijack) ---
|
||||||
@@ -188,31 +233,62 @@ namespace Core.Io
|
|||||||
// Change this line:
|
// Change this line:
|
||||||
//public bool HasBlocks => _blocks.Count > 0 || _currentBlock != null;
|
//public bool HasBlocks => _blocks.Count > 0 || _currentBlock != null;
|
||||||
// Only consider _currentBlock valid if we are actively playing it!
|
// Only consider _currentBlock valid if we are actively playing it!
|
||||||
public bool HasBlocks => _blocks.Count > 0 || (_currentBlock != null && _state != TapeState.Idle && _state != TapeState.Pause);
|
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()
|
public byte[] GetNextBlock()
|
||||||
{
|
{
|
||||||
// Yank the current block ONLY if it is actively playing
|
// Yank the current block ONLY if it is actively playing
|
||||||
if (_currentBlock != null && _state != TapeState.Idle && _state != TapeState.Pause)
|
if (_currentBlock != null && _state != TapeState.Idle && _state != TapeState.Pause)
|
||||||
{
|
{
|
||||||
byte[] blockToReturn = _currentBlock;
|
byte[] blockToReturn = ExtractDataFromBlock(_currentBlock);
|
||||||
|
|
||||||
_state = TapeState.Idle; // Ensure the tape deck is stopped
|
_state = TapeState.Idle; // Ensure the tape deck is stopped
|
||||||
_currentBlock = null;
|
_currentBlock = null;
|
||||||
|
|
||||||
return blockToReturn;
|
return blockToReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, pull directly from the unplayed queue
|
// Otherwise, pull directly from the unplayed queue
|
||||||
_state = TapeState.Idle; // Stop the tape deck just in case
|
_state = TapeState.Idle; // Stop the tape deck just in case
|
||||||
_currentBlock = null;
|
_currentBlock = null;
|
||||||
return _blocks.Count > 0 ? _blocks.Dequeue() : null;
|
|
||||||
|
// Dequeue the next TzxBlock and extract its bytes
|
||||||
|
if (_playQueue.Count > 0)
|
||||||
|
{
|
||||||
|
TzxBlock nextBlock = _playQueue.Dequeue();
|
||||||
|
return ExtractDataFromBlock(nextBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadTzxData(List<TzxBlock> blocks)
|
//public byte[] GetNextBlock()
|
||||||
{
|
//{
|
||||||
_tzxBlocks = blocks;
|
// // 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;
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
// Just to prove it works before we build the State Machine!
|
|
||||||
System.Diagnostics.Debug.WriteLine($"Successfully loaded {_tzxBlocks.Count} TZX blocks!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Core.Io
|
namespace Core.Io
|
||||||
{
|
{
|
||||||
@@ -21,4 +22,34 @@ namespace Core.Io
|
|||||||
// The actual tape bytes
|
// The actual tape bytes
|
||||||
public byte[] Data { get; set; } = Array.Empty<byte>();
|
public byte[] Data { get; set; } = Array.Empty<byte>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID 0x32: Archive Info Block (Metadata)
|
||||||
|
public class ArchiveInfoBlock : TzxBlock
|
||||||
|
{
|
||||||
|
public override int BlockId => 0x32;
|
||||||
|
|
||||||
|
// Key: Text ID (e.g., 0 = Full Title, 1 = Software House, 2 = Publisher, 4 = Year)
|
||||||
|
// Value: The actual text string
|
||||||
|
public Dictionary<byte, string> Metadata { get; set; } = new Dictionary<byte, string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID 0x11: Turbo Speed Data Block
|
||||||
|
public class TurboSpeedBlock : TzxBlock
|
||||||
|
{
|
||||||
|
public override int BlockId => 0x11;
|
||||||
|
|
||||||
|
public ushort PilotPulseLength { get; set; }
|
||||||
|
public ushort SyncFirstPulseLength { get; set; }
|
||||||
|
public ushort SyncSecondPulseLength { get; set; }
|
||||||
|
public ushort ZeroBitPulseLength { get; set; }
|
||||||
|
public ushort OneBitPulseLength { get; set; }
|
||||||
|
public ushort PilotToneLength { get; set; }
|
||||||
|
public byte UsedBitsInLastByte { get; set; }
|
||||||
|
public ushort PauseAfterMs { get; set; }
|
||||||
|
|
||||||
|
// This is a 24-bit integer in the file, so we store it in a standard 32-bit int
|
||||||
|
public int DataLength { get; set; }
|
||||||
|
|
||||||
|
public byte[] Data { get; set; } = Array.Empty<byte>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,51 @@ namespace Core.Io
|
|||||||
stdBlock.Data = br.ReadBytes(stdBlock.DataLength);
|
stdBlock.Data = br.ReadBytes(stdBlock.DataLength);
|
||||||
blocks.Add(stdBlock);
|
blocks.Add(stdBlock);
|
||||||
break;
|
break;
|
||||||
|
case 0x11: // Turbo Speed Data Block
|
||||||
|
var turboBlock = new TurboSpeedBlock
|
||||||
|
{
|
||||||
|
PilotPulseLength = br.ReadUInt16(),
|
||||||
|
SyncFirstPulseLength = br.ReadUInt16(),
|
||||||
|
SyncSecondPulseLength = br.ReadUInt16(),
|
||||||
|
ZeroBitPulseLength = br.ReadUInt16(),
|
||||||
|
OneBitPulseLength = br.ReadUInt16(),
|
||||||
|
PilotToneLength = br.ReadUInt16(),
|
||||||
|
UsedBitsInLastByte = br.ReadByte(),
|
||||||
|
PauseAfterMs = br.ReadUInt16()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Z80 files are Little-Endian. Read 3 bytes and combine them into a 24-bit integer.
|
||||||
|
byte len0 = br.ReadByte();
|
||||||
|
byte len1 = br.ReadByte();
|
||||||
|
byte len2 = br.ReadByte();
|
||||||
|
turboBlock.DataLength = len0 | (len1 << 8) | (len2 << 16);
|
||||||
|
|
||||||
|
// Read the actual tape data
|
||||||
|
turboBlock.Data = br.ReadBytes(turboBlock.DataLength);
|
||||||
|
|
||||||
|
blocks.Add(turboBlock);
|
||||||
|
break;
|
||||||
|
case 0x32: // Archive Info Block
|
||||||
|
var archiveBlock = new ArchiveInfoBlock();
|
||||||
|
|
||||||
|
// The total length of the block (we read it to advance the stream,
|
||||||
|
// but the string count is what we actually use to loop)
|
||||||
|
ushort totalLength = br.ReadUInt16();
|
||||||
|
byte stringCount = br.ReadByte();
|
||||||
|
|
||||||
|
for (int i = 0; i < stringCount; i++)
|
||||||
|
{
|
||||||
|
byte textId = br.ReadByte();
|
||||||
|
byte textLength = br.ReadByte();
|
||||||
|
|
||||||
|
// Read the raw bytes and convert them to an ASCII string
|
||||||
|
string text = System.Text.Encoding.ASCII.GetString(br.ReadBytes(textLength));
|
||||||
|
|
||||||
|
archiveBlock.Metadata[textId] = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.Add(archiveBlock);
|
||||||
|
break;
|
||||||
|
|
||||||
// TODO: Add cases for 0x11 (Turbo), 0x12 (Tone), 0x13 (Pulses), etc.
|
// TODO: Add cases for 0x11 (Turbo), 0x12 (Tone), 0x13 (Pulses), etc.
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
<None Remove="ROMS\TAP\Manic Miner.TAP" />
|
<None Remove="ROMS\TAP\Manic Miner.TAP" />
|
||||||
<None Remove="ROMS\TAP\Treasure Island - Dizzy.tap" />
|
<None Remove="ROMS\TAP\Treasure Island - Dizzy.tap" />
|
||||||
<None Remove="ROMS\TAP\zexall.tap" />
|
<None Remove="ROMS\TAP\zexall.tap" />
|
||||||
|
<None Remove="ROMS\TZX\Batman - Release 1.tzx" />
|
||||||
|
<None Remove="ROMS\TZX\Split Personalities1.tzx" />
|
||||||
|
<None Remove="ROMS\TZX\Split Personalities2.tzx" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -76,6 +79,15 @@
|
|||||||
<EmbeddedResource Include="ROMS\TAP\zexall.tap">
|
<EmbeddedResource Include="ROMS\TAP\zexall.tap">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="ROMS\TZX\Batman - Release 1.tzx">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="ROMS\TZX\Split Personalities1.tzx">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="ROMS\TZX\Split Personalities2.tzx">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</EmbeddedResource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
BIN
Desktop/ROMS/TZX/Batman - Release 1.tzx
Normal file
BIN
Desktop/ROMS/TZX/Batman - Release 1.tzx
Normal file
Binary file not shown.
Reference in New Issue
Block a user