From 51a67a947ad8e5da6ac20383529565dbf0f0fdd8 Mon Sep 17 00:00:00 2001 From: Matt Burgess Date: Fri, 11 Jun 2021 23:41:14 +0100 Subject: [PATCH] ZXHawk: Overhaul datacorder and cassette loading code. This should now be more accurate and fixes a number of loading isues with particular games and loading schemes. There be desync dragons here!! Fixes #1446 --- .../Hardware/Datacorder/DatacorderDevice.cs | 33 +- .../SinclairSpectrum/Machine/SpectrumBase.cs | 33 +- .../SinclairSpectrum/Media/MediaConverter.cs | 30 +- .../Media/Tape/CSW/CswConverter.cs | 8 + .../Media/Tape/PZX/PzxConverter.cs | 24 + .../Media/Tape/TAP/TapConverter.cs | 62 + .../Media/Tape/TZX/TzxConverter.cs | 2404 +++++++++-------- .../Media/Tape/TapeDataBlock.cs | 28 +- .../Media/Tape/WAV/WavConverter.cs | 26 +- 9 files changed, 1525 insertions(+), 1123 deletions(-) diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs index a949c29a7d..bd00c81079 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs @@ -122,14 +122,6 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum MonitorFrame(); } - /// - /// No longer in use - /// - public void StartFrame() - { - //_buzzer.ProcessPulseValue(currentState); - } - /// /// Starts the tape playing from the beginning of the current block /// @@ -466,27 +458,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum cycles -= _waitEdge; if (_position == 0 && _tapeIsPlaying) - { - // start of block - take care of initial pulse level for PZX - switch (_dataBlocks[_currentDataBlockIndex].BlockDescription) - { - case BlockType.PULS: - // initial pulse level is always low - if (currentState) - FlipTapeState(); - break; - case BlockType.DATA: - // initial pulse level is stored in block - if (currentState != _dataBlocks[_currentDataBlockIndex].InitialPulseLevel) - FlipTapeState(); - break; - case BlockType.PAUS: - // initial pulse level is stored in block - if (currentState != _dataBlocks[_currentDataBlockIndex].InitialPulseLevel) - FlipTapeState(); - break; - } - + { // notify about the current block var bl = _dataBlocks[_currentDataBlockIndex]; @@ -595,7 +567,8 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum _waitEdge = _dataBlocks[_currentDataBlockIndex].DataPeriods.Count > 0 ? _dataBlocks[_currentDataBlockIndex].DataPeriods[_position] : 0; // flip the current state - FlipTapeState(); + //FlipTapeState(); + currentState = _dataBlocks[_currentDataBlockIndex].DataLevels[_position]; } // update lastCycle and return currentstate diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs index 7df1e0ac16..fe654b1516 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs @@ -208,17 +208,17 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum CPU.RegPC = 0; Spectrum.SetCpuRegister("SP", 0xFFFF); - Spectrum.SetCpuRegister("IY", 0xFFFF); - Spectrum.SetCpuRegister("IX", 0xFFFF); + Spectrum.SetCpuRegister("IY", 0); + Spectrum.SetCpuRegister("IX", 0); Spectrum.SetCpuRegister("AF", 0xFFFF); - Spectrum.SetCpuRegister("BC", 0xFFFF); - Spectrum.SetCpuRegister("DE", 0xFFFF); - Spectrum.SetCpuRegister("HL", 0xFFFF); + Spectrum.SetCpuRegister("BC", 0); + Spectrum.SetCpuRegister("DE", 0); + Spectrum.SetCpuRegister("HL", 0); Spectrum.SetCpuRegister("SP", 0xFFFF); Spectrum.SetCpuRegister("Shadow AF", 0xFFFF); - Spectrum.SetCpuRegister("Shadow BC", 0xFFFF); - Spectrum.SetCpuRegister("Shadow DE", 0xFFFF); - Spectrum.SetCpuRegister("Shadow HL", 0xFFFF); + Spectrum.SetCpuRegister("Shadow BC", 0); + Spectrum.SetCpuRegister("Shadow DE", 0); + Spectrum.SetCpuRegister("Shadow HL", 0); CPU.Regs[CPU.I] = 0; CPU.Regs[CPU.R] = 0; @@ -260,17 +260,16 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum CPU.RegPC = 0; Spectrum.SetCpuRegister("SP", 0xFFFF); - Spectrum.SetCpuRegister("IY", 0xFFFF); - Spectrum.SetCpuRegister("IX", 0xFFFF); + //Spectrum.SetCpuRegister("IY", 0xFFFF); + //Spectrum.SetCpuRegister("IX", 0xFFFF); Spectrum.SetCpuRegister("AF", 0xFFFF); - Spectrum.SetCpuRegister("BC", 0xFFFF); - Spectrum.SetCpuRegister("DE", 0xFFFF); - Spectrum.SetCpuRegister("HL", 0xFFFF); - Spectrum.SetCpuRegister("SP", 0xFFFF); + //Spectrum.SetCpuRegister("BC", 0xFFFF); + //Spectrum.SetCpuRegister("DE", 0xFFFF); + //Spectrum.SetCpuRegister("HL", 0xFFFF); Spectrum.SetCpuRegister("Shadow AF", 0xFFFF); - Spectrum.SetCpuRegister("Shadow BC", 0xFFFF); - Spectrum.SetCpuRegister("Shadow DE", 0xFFFF); - Spectrum.SetCpuRegister("Shadow HL", 0xFFFF); + //Spectrum.SetCpuRegister("Shadow BC", 0xFFFF); + //Spectrum.SetCpuRegister("Shadow DE", 0xFFFF); + //Spectrum.SetCpuRegister("Shadow HL", 0xFFFF); CPU.Regs[CPU.I] = 0; CPU.Regs[CPU.R] = 0; diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverter.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverter.cs index ecb1cdafb9..20d2385330 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverter.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaConverter.cs @@ -145,15 +145,35 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum /// Takes a PauseInMilliseconds value and returns the value in T-States /// public static int TranslatePause(int pauseInMS) - { - // t-states per millisecond - var tspms = (69888 * 50) / 1000; - // get value - int res = pauseInMS * tspms; + { + var tspms = (double)(69888 * 50) / (double)1000; + int res = (int)(pauseInMS * tspms); return res; } + /// + /// Caluclate a data block XOR checksum + /// + /// + /// + /// + public static bool CheckChecksum(byte[] buf, int len) + { + byte c = 0; + for (int n = 0; n < len - 1; n++) + { + c ^= buf[n]; + } + + if (c == buf[len - 1]) + { + return true; + } + + return false; + } + /// /// Decompresses a byte array that is Z-RLE compressed /// diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/CSW/CswConverter.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/CSW/CswConverter.cs index a40d551c78..22f2cc4fd2 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/CSW/CswConverter.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/CSW/CswConverter.cs @@ -197,12 +197,15 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum t.BlockDescription = BlockType.CSW_Recording; t.BlockID = 0x18; t.DataPeriods = new List(); + t.DataLevels = new List(); if (flags.Bit(0)) t.InitialPulseLevel = true; else t.InitialPulseLevel = false; + bool currLevel = !t.InitialPulseLevel; + var rate = (69888 * 50) / sampleRate; for (int i = 0; i < cswDataUncompressed.Length;) @@ -215,10 +218,15 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum } t.DataPeriods.Add(length); + + currLevel = !currLevel; + t.DataLevels.Add(currLevel); } // add closing period t.DataPeriods.Add((69888 * 50) / 10); + currLevel = !currLevel; + t.DataLevels.Add(currLevel); // add to datacorder _datacorder.DataBlocks.Add(t); diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs index 397c089a04..8f79645d50 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/PZX/PzxConverter.cs @@ -158,8 +158,10 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum t.BlockID = GetInt32(b, 0); t.DataPeriods = new List(); + t.DataLevels = new List(); t.InitialPulseLevel = false; + bool pLevel = !t.InitialPulseLevel; List pulses = new List(); @@ -197,6 +199,8 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum for (int i = 0; i < x[0]; i++) { t.DataPeriods.Add(x[1]); + pLevel = !pLevel; + t.DataLevels.Add(pLevel); } } @@ -220,6 +224,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum t.BlockID = GetInt32(b, 0); t.DataPeriods = new List(); + t.DataLevels = new List(); List s0 = new List(); List s1 = new List(); @@ -235,6 +240,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum initPulseLevel = (uint)((dCount & 0x80000000) == 0 ? 0 : 1); t.InitialPulseLevel = initPulseLevel == 1; + bool bLevel = !t.InitialPulseLevel; dCount = (int)(dCount & 0x7FFFFFFF); pos += 4; @@ -274,6 +280,8 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum foreach (var pu in s1) { t.DataPeriods.Add((int)pu); + bLevel = !bLevel; + t.DataLevels.Add(bLevel); } } @@ -282,13 +290,20 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum foreach (var pu in s0) { t.DataPeriods.Add((int)pu); + bLevel = !bLevel; + t.DataLevels.Add(bLevel); } } } } if (tail > 0) + { t.DataPeriods.Add(tail); + bLevel = !bLevel; + t.DataLevels.Add(bLevel); + } + dData.Clear(); } @@ -320,12 +335,21 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum var d = GetInt32(b, pos); iniPulseLevel = ((d & 0x80000000) == 0 ? 0 : 1); t.InitialPulseLevel = iniPulseLevel == 1; + bool paLevel = !t.InitialPulseLevel; pCount = (d & 0x7FFFFFFF); // convert to tape block t.BlockDescription = BlockType.PAUS; + paLevel = !paLevel; + t.DataLevels.Add(paLevel); t.DataPeriods.Add(0); + + paLevel = !paLevel; + t.DataLevels.Add(paLevel); t.DataPeriods.Add(pCount); + + paLevel = !paLevel; + t.DataLevels.Add(paLevel); t.DataPeriods.Add(0); _datacorder.DataBlocks.Add(t); diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapConverter.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapConverter.cs index a63257299e..cdc0546da9 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapConverter.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TAP/TapConverter.cs @@ -277,16 +277,25 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum // create a list to hold the data periods List dataPeriods = new List(); + List dataLevels = new List(); + + bool currLevel = false; // generate pilot pulses for (int i = 0; i < pilotLength; i++) { dataPeriods.Add(PILOT_PL); + currLevel = !currLevel; + dataLevels.Add(currLevel); } // add syncro pulses dataPeriods.Add(SYNC_1_PL); + currLevel = !currLevel; + dataLevels.Add(currLevel); dataPeriods.Add(SYNC_2_PL); + currLevel = !currLevel; + dataLevels.Add(currLevel); int pos = 0; @@ -296,13 +305,33 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum for (byte b = 0x80; b != 0; b >>= 1) { if ((blockdata[i] & b) != 0) + { dataPeriods.Add(BIT_1_PL); + currLevel = !currLevel; + dataLevels.Add(currLevel); + } + else + { dataPeriods.Add(BIT_0_PL); + currLevel = !currLevel; + dataLevels.Add(currLevel); + } + if ((blockdata[i] & b) != 0) + { dataPeriods.Add(BIT_1_PL); + currLevel = !currLevel; + dataLevels.Add(currLevel); + } + else + { dataPeriods.Add(BIT_0_PL); + currLevel = !currLevel; + dataLevels.Add(currLevel); + } + } } @@ -310,13 +339,29 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum for (byte c = 0x80; c != (byte)(0x80 >> BIT_COUNT_IN_LAST); c >>= 1) { if ((blockdata[pos] & c) != 0) + { dataPeriods.Add(BIT_1_PL); + currLevel = !currLevel; + dataLevels.Add(currLevel); + } else + { dataPeriods.Add(BIT_0_PL); + currLevel = !currLevel; + dataLevels.Add(currLevel); + } if ((blockdata[pos] & c) != 0) + { dataPeriods.Add(BIT_1_PL); + currLevel = !currLevel; + dataLevels.Add(currLevel); + } else + { dataPeriods.Add(BIT_0_PL); + currLevel = !currLevel; + dataLevels.Add(currLevel); + } } // add block pause @@ -325,14 +370,27 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum // default pause for tap files tdb.PauseInMS = 1000; + tdb.PauseInTStates = TranslatePause(tdb.PauseInMS); + + // small inversion + dataPeriods.Add(3476); + currLevel = !currLevel; + dataLevels.Add(currLevel); + + // actual pause + dataPeriods.Add(tdb.PauseInTStates - 3476); + currLevel = !currLevel; + dataLevels.Add(currLevel); // add to the tapedatablock object tdb.DataPeriods = dataPeriods; + tdb.DataLevels = dataLevels; // add the raw data tdb.BlockData = blockdata; // generate separate PAUS block + /* TapeDataBlock tdbPause = new TapeDataBlock(); tdbPause.DataPeriods = new List(); tdbPause.BlockDescription = BlockType.PAUSE_BLOCK; @@ -341,10 +399,13 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum //if (pauseInTStates > 0) //tdbPause.DataPeriods.Add(pauseInTStates); tdb.PauseInMS = 0; + tdb.PauseInTStates = pauseInTStates; + */ // add block to the tape _datacorder.DataBlocks.Add(tdb); + /* // PAUS block if neccessary if (pauseInTStates > 0) { @@ -366,6 +427,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum _datacorder.DataBlocks.Add(tdbPause); } + */ } } } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxConverter.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxConverter.cs index 91c237b7e6..a5fdf64718 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxConverter.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TZX/TzxConverter.cs @@ -41,8 +41,15 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum /// private readonly List> _loopCounter = new List>(); + /// + /// The virtual cassette deck + /// private readonly DatacorderDevice _datacorder; + /// + /// Constructor + /// + /// public TzxConverter(DatacorderDevice _tapeDevice) { _datacorder = _tapeDevice; @@ -51,7 +58,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum /// /// Returns TRUE if tzx header is detected /// - public override bool CheckType(byte[] data) + public override bool CheckType(byte[] tzxRaw) { /* // TZX Header @@ -65,13 +72,13 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum // check whether this is a valid tzx format file by looking at the identifier in the header // (first 7 bytes of the file) - string ident = Encoding.ASCII.GetString(data, 0, 7); + string ident = Encoding.ASCII.GetString(tzxRaw, 0, 7); // and 'end of text' marker - byte eotm = data[7]; + byte eotm = tzxRaw[7]; // version info - int majorVer = data[8]; - int minorVer = data[9]; + int majorVer = tzxRaw[8]; + int minorVer = tzxRaw[9]; if (ident != "ZXTape!" || eotm != 0x1A) { @@ -84,11 +91,91 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum } } + /// + /// The raw TZX data array + /// + private byte[] data; + + /// + /// The current BizHawk TapeDataBlock object + /// + private TapeDataBlock t; + + /// + /// Block ID of the current TZX block + /// + private int blockId; + + /// + /// The length of the data in the current block (in bytes) + /// + private int blockLen; + + /// + /// Length of the pilot tone in pulses (half-periods) + /// + private int pilotToneLength; + + /// + /// Length of the syncro first pulse + /// + private int sync1PulseLength; + + /// + /// Length of the syncro second pulse + /// + private int sync2PulseLength; + + /// + /// Length of the zero bit pulse + /// + private int bit0PulseLength; + + /// + /// Length of the one bit pulse length + /// + private int bit1PulseLength; + + /// + /// Length of pilot tone (number of pulses or half-periods) + /// + private int pilotCount; + + /// + /// Used bits in the last byte (MSb first) + /// + private int bitsInLastByte; + + /// + /// Pause length (in ms) after this block + /// + private int pauseLen; + + /// + /// Represents whether the current signal is HIGH (true) or LOW (false) + /// + private bool signal = false; + + /// + /// Remaining bits in current byte to process + /// + private int bitCount; + + /* + private int At(int tStates) + { + double res = 0.98385608 * (double)tStates; + return (int)res; + } + */ + /// /// DeSerialization method /// - public override void Read(byte[] data) + public override void Read(byte[] tzxRaw) { + data = tzxRaw; + // clear existing tape blocks _datacorder.DataBlocks.Clear(); @@ -124,494 +211,720 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum while (_position < data.Length) { // block ID is the first byte in a new block - int ID = data[_position++]; + blockId = data[_position++]; // process the data - ProcessBlock(data, ID); + // this will return leaving us at the start of relevant block data (if there is any) + ProcessBlock(); } + // final pause (some games require this to finish loading properly - eg Kong2) + pauseLen = 1000; + t.PauseInTStates = TranslatePause(1000); + DoPause(); + + /* debugging stuff + StringBuilder export = new StringBuilder(); + foreach (var b in _datacorder.DataBlocks) + { + export.Append(b.BlockDescription); + for (int i = 0; i < b.DataPeriods.Count; i++) + { + export.Append("\t\t"); + export.Append(b.PulseDescription[i]); + export.Append("\t\t"); + export.Append(b.DataPeriods[i].ToString()); + export.Append("\t\t"); + export.AppendLine(b.DataLevels[i].ToString()); + } + } + + System.IO.File.WriteAllText("c:\\data\\output.txt", export.ToString()); + //string o = export.ToString(); + */ + } + + /// + /// Inverts the audio signal + /// + private void ToggleSignal() + { + signal = !signal; } /// /// Processes a TZX block /// - private void ProcessBlock(byte[] data, int id) + private void ProcessBlock() { // process based on detected block ID - switch (id) + switch (blockId) { // ID 10 - Standard Speed Data Block case 0x10: - ProcessBlockID10(data); + ProcessBlockID10(); break; // ID 11 - Turbo Speed Data Block case 0x11: - ProcessBlockID11(data); + ProcessBlockID11(); break; // ID 12 - Pure Tone case 0x12: - ProcessBlockID12(data); + ProcessBlockID12(); break; // ID 13 - Pulse sequence case 0x13: - ProcessBlockID13(data); + ProcessBlockID13(); break; // ID 14 - Pure Data Block case 0x14: - ProcessBlockID14(data); + ProcessBlockID14(); break; // ID 15 - Direct Recording case 0x15: - ProcessBlockID15(data); + ProcessBlockID15(); break; // ID 18 - CSW Recording case 0x18: - ProcessBlockID18(data); + ProcessBlockID18(); break; // ID 19 - Generalized Data Block case 0x19: - ProcessBlockID19(data); + ProcessBlockID19(); break; // ID 20 - Pause (silence) or 'Stop the Tape' command case 0x20: - ProcessBlockID20(data); + ProcessBlockID20(); break; // ID 21 - Group start case 0x21: - ProcessBlockID21(data); + ProcessBlockID21(); break; // ID 22 - Group end case 0x22: - ProcessBlockID22(data); + ProcessBlockID22(); break; // ID 23 - Jump to block case 0x23: - ProcessBlockID23(data); + ProcessBlockID23(); break; // ID 24 - Loop start case 0x24: - ProcessBlockID24(data); + ProcessBlockID24(); break; // ID 25 - Loop end case 0x25: - ProcessBlockID25(data); + ProcessBlockID25(); break; // ID 26 - Call sequence case 0x26: - ProcessBlockID26(data); + ProcessBlockID26(); break; // ID 27 - Return from sequence case 0x27: - ProcessBlockID27(data); + ProcessBlockID27(); break; // ID 28 - Select block case 0x28: - ProcessBlockID28(data); + ProcessBlockID28(); break; // ID 2A - Stop the tape if in 48K mode case 0x2A: - ProcessBlockID2A(data); + ProcessBlockID2A(); break; // ID 2B - Set signal level case 0x2B: - ProcessBlockID2B(data); + ProcessBlockID2B(); break; // ID 30 - Text description case 0x30: - ProcessBlockID30(data); + ProcessBlockID30(); break; // ID 31 - Message block case 0x31: - ProcessBlockID31(data); + ProcessBlockID31(); break; // ID 32 - Archive info case 0x32: - ProcessBlockID32(data); + ProcessBlockID32(); break; // ID 33 - Hardware type case 0x33: - ProcessBlockID33(data); + ProcessBlockID33(); break; // ID 35 - Custom info block case 0x35: - ProcessBlockID35(data); + ProcessBlockID35(); break; // ID 5A - "Glue" block case 0x5A: - ProcessBlockID5A(data); + ProcessBlockID5A(); break; // ID 16 - (deprecated) C64 ROM Type Data Block case 0x16: - ProcessBlockID16(data); + ProcessBlockID16(); break; // ID 17 - (deprecated) C64 Turbo Tape Data Block case 0x17: - ProcessBlockID17(data); + ProcessBlockID17(); break; // ID 34 - (deprecated) Emulation info case 0x34: - ProcessBlockID34(data); + ProcessBlockID34(); break; // ID 40 - (deprecated) Snapshot block case 0x40: - ProcessBlockID40(data); + ProcessBlockID40(); break; default: - ProcessUnidentifiedBlock(data); + ProcessUnidentifiedBlock(); break; } } - /* length: [02,03]+04 - Offset Value Type Description - 0x00 - WORD Pause after this block (ms.) {1000} - 0x02 N WORD Length of data that follow - 0x04 - BYTE[N] Data as in .TAP files - - This block must be replayed with the standard Spectrum ROM timing values - see the values in - curly brackets in block ID 11. The pilot tone consists in 8063 pulses if the first data byte - (flag byte) is < 128, 3223 otherwise. This block can be used for the ROM loading routines AND - for custom loading routines that use the same timings as ROM ones do. */ - private void ProcessBlockID10(byte[] data) + /// + /// 0x10 - Standard Data Block (ROM) + /// + private void ProcessBlockID10() { - TapeDataBlock t = new TapeDataBlock(); - t.BlockID = 0x10; - t.BlockDescription = BlockType.Standard_Speed_Data_Block; - t.DataPeriods = new List(); + /* length: [02,03]+04 + + Offset Value Type Description + 0x00 - WORD Pause after this block (ms.) {1000} + 0x02 N WORD Length of data that follow + 0x04 - BYTE[N] Data as in .TAP files - int pauseLen = GetWordValue(data, _position); + This block must be replayed with the standard Spectrum ROM timing values - see the values in + curly brackets in block ID 11. The pilot tone consists in 8063 pulses if the first data byte + (flag byte) is < 128, 3223 otherwise. This block can be used for the ROM loading routines AND + for custom loading routines that use the same timings as ROM ones do. */ + + t = new TapeDataBlock + { + BlockID = 0x10, + BlockDescription = BlockType.Standard_Speed_Data_Block + }; + + pauseLen = GetWordValue(data, _position); if (pauseLen == 0) pauseLen = 1000; t.PauseInMS = pauseLen; + t.PauseInTStates = TranslatePause(pauseLen); - int blockLen = GetWordValue(data, _position + 2); + blockLen = GetWordValue(data, _position + 2); _position += 4; - byte[] tmp = new byte[blockLen]; - tmp = data.Skip(_position).Take(blockLen).ToArray(); + // get the block data as a new array + byte[] blockdata = new byte[blockLen]; + blockdata = data.Skip(_position).Take(blockLen).ToArray(); - var t2 = DecodeDataBlock(t, tmp, DataBlockType.Standard, pauseLen); + // pilot count needs to be ascertained from flag byte + //pilotCount = blockdata[0] < 128 ? 8063 : 3222; // 3223; + pilotCount = blockdata[0] == 0 ? 8064 : 3219; - // add the block - _datacorder.DataBlocks.Add(t2); + pilotToneLength = 2168; // 2133;// 2168; + sync1PulseLength = 667; // 632; // 667; + sync2PulseLength = 735; // 711; // 735; + bit0PulseLength = 855; // 869; // 855; + bit1PulseLength = 1710; // 1738; // 1710; + bitsInLastByte = 8; - // advance the position to the next block - _position += blockLen; + // metadata + string description = string.Empty; - // generate PAUSE block - CreatePauseBlock(_datacorder.DataBlocks.Last()); + if (blockdata[0] == 0x00 && blockLen == 19) + { + string fileName = Encoding.ASCII.GetString(blockdata.Skip(2).Take(10).ToArray()).Trim(); + string type = "Unknown Type"; + StringBuilder sb = new StringBuilder(); + + var param1 = GetWordValue(blockdata, 12); + var param2 = GetWordValue(blockdata, 14); + + // header block - examine first byte of header + if (blockdata[1] == 0) + { + type = "Program"; + sb.Append(type + ": "); + sb.Append(fileName + " "); + } + else if (blockdata[1] == 1) + { + type = "NumArray"; + sb.Append(type + ": "); + sb.Append(fileName + " "); + } + else if (blockdata[1] == 2) + { + type = "CharArray"; + sb.Append(type + ": "); + sb.Append(fileName + " "); + } + else if (blockdata[1] == 3) + { + type = "Code"; + sb.Append(type + ": "); + sb.Append(fileName + " "); + } + } + else if (blockdata[0] == 0xff) + { + // data block + description = "Data Block " + (blockLen - 2) + "bytes"; + t.AddMetaData(BlockDescriptorTitle.Data_Bytes, (blockLen - 2) + " Bytes"); + } + else + { + // some other type (turbo data etc..) + description = $"#{blockdata[0].ToString("X2")} block, {blockLen} bytes"; + //description += (crc != 0) ? $", crc bad (#{crcFile:X2}!=#{crcValue:X2})" : ", crc ok"; + t.AddMetaData(BlockDescriptorTitle.Undefined, description); + } + + t.AddMetaData(BlockDescriptorTitle.Pilot_Pulse_Length, pilotToneLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.Pilot_Pulse_Count, pilotCount + " Pulses"); + t.AddMetaData(BlockDescriptorTitle.First_Sync_Length, sync1PulseLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.Second_Sync_Length, sync2PulseLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.Zero_Bit_Length, bit0PulseLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.One_Bit_Length, bit1PulseLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.Data_Length, blockLen + " Bytes"); + t.AddMetaData(BlockDescriptorTitle.Bits_In_Last_Byte, bitsInLastByte + " Bits"); + t.AddMetaData(BlockDescriptorTitle.Pause_After_Data, t.PauseInMS + " ms / " + t.PauseInTStates + "TStates"); + + // add the raw data + t.BlockData = blockdata; + + // decode + DecodeData(); + + // add the block to the datacorder + _datacorder.DataBlocks.Add(t); + + // ready for next block + ZeroVars(); } - /* length: [0F,10,11]+12 - Offset Value Type Description - 0x00 - WORD Length of PILOT pulse {2168} - 0x02 - WORD Length of SYNC first pulse {667} - 0x04 - WORD Length of SYNC second pulse {735} - 0x06 - WORD Length of ZERO bit pulse {855} - 0x08 - WORD Length of ONE bit pulse {1710} - 0x0A - WORD Length of PILOT tone (number of pulses) {8063 header (flag<128), 3223 data (flag>=128)} - 0x0C - BYTE Used bits in the last byte (other bits should be 0) {8} - (e.g. if this is 6, then the bits used (x) in the last byte are: xxxxxx00, - where MSb is the leftmost bit, LSb is the rightmost bit) - 0x0D - WORD Pause after this block (ms.) {1000} - 0x0F N BYTE[3] Length of data that follow - 0x12 - BYTE[N] Data as in .TAP files - - This block is very similar to the normal TAP block but with some additional info on the timings and other important - differences. The same tape encoding is used as for the standard speed data block. If a block should use some non-standard - sync or pilot tones (i.e. all sorts of protection schemes) then use the next three blocks to describe it.*/ - private void ProcessBlockID11(byte[] data) + /// + /// 0x11 - Custom Data Block (Turbo) + /// + private void ProcessBlockID11() { - TapeDataBlock t = new TapeDataBlock(); - t.BlockID = 0x11; - t.BlockDescription = BlockType.Turbo_Speed_Data_Block; - t.DataPeriods = new List(); + /* length: [0F,10,11]+12 - int pilotPL = GetWordValue(data, _position); - int sync1P = GetWordValue(data, _position + 2); - int sync2P = GetWordValue(data, _position + 4); - int bit0P = GetWordValue(data, _position + 6); - int bit1P = GetWordValue(data, _position + 8); - int pilotTL = GetWordValue(data, _position + 10); - int bitinbyte = data[_position + 12]; - int pause = GetWordValue(data, _position + 13); + Offset Value Type Description + 0x00 - WORD Length of PILOT pulse {2168} + 0x02 - WORD Length of SYNC first pulse {667} + 0x04 - WORD Length of SYNC second pulse {735} + 0x06 - WORD Length of ZERO bit pulse {855} + 0x08 - WORD Length of ONE bit pulse {1710} + 0x0A - WORD Length of PILOT tone (number of pulses) {8063 header (flag<128), 3223 data (flag>=128)} + 0x0C - BYTE Used bits in the last byte (other bits should be 0) {8} + (e.g. if this is 6, then the bits used (x) in the last byte are: xxxxxx00, + where MSb is the leftmost bit, LSb is the rightmost bit) + 0x0D - WORD Pause after this block (ms.) {1000} + 0x0F N BYTE[3] Length of data that follow + 0x12 - BYTE[N] Data as in .TAP files + This block is very similar to the normal TAP block but with some additional info on the timings and other important + differences. The same tape encoding is used as for the standard speed data block. If a block should use some non-standard + sync or pilot tones (i.e. all sorts of protection schemes) then use the next three blocks to describe it.*/ - int blockLen = 0xFFFFFF & GetInt32(data, _position + 0x0F); + t = new TapeDataBlock + { + BlockID = 0x11, + BlockDescription = BlockType.Turbo_Speed_Data_Block + }; - byte[] bLenArr = data.Skip(_position + 0x0F).Take(3).ToArray(); + pilotToneLength = GetWordValue(data, _position); + sync1PulseLength = GetWordValue(data, _position + 2); + sync2PulseLength = GetWordValue(data, _position + 4); + bit0PulseLength = GetWordValue(data, _position + 6); + bit1PulseLength = GetWordValue(data, _position + 8); + pilotCount = GetWordValue(data, _position + 10); + bitsInLastByte = data[_position + 12]; + pauseLen = GetWordValue(data, _position + 13); + blockLen = 0xFFFFFF & GetInt32(data, _position + 0x0F); + + t.PauseInMS = pauseLen; + t.PauseInTStates = TranslatePause(pauseLen); _position += 0x12; - byte[] tmp = new byte[blockLen]; - tmp = data.Skip(_position).Take(blockLen).ToArray(); + byte[] blockdata = new byte[blockLen]; + blockdata = data.Skip(_position).Take(blockLen).ToArray(); - var t2 = DecodeDataBlock(t, tmp, DataBlockType.Turbo, pause, pilotTL, pilotPL, sync1P, sync2P, bit0P, bit1P, bitinbyte); + // metadata + string description = string.Empty; - t.PauseInMS = pause; - - // add the block - _datacorder.DataBlocks.Add(t2); - - // advance the position to the next block - _position += blockLen; - - // generate PAUSE block - CreatePauseBlock(_datacorder.DataBlocks.Last()); - } - - /* length: 04 - Offset Value Type Description - 0x00 - WORD Length of one pulse in T-states - 0x02 - WORD Number of pulses - - This will produce a tone which is basically the same as the pilot tone in the ID 10, ID 11 blocks. You can define how - long the pulse is and how many pulses are in the tone. */ - private void ProcessBlockID12(byte[] data) - { - int blockLen = 4; - - TapeDataBlock t = new TapeDataBlock(); - t.BlockID = 0x12; - t.BlockDescription = BlockType.Pure_Tone; - t.DataPeriods = new List(); - t.PauseInMS = 0; - - // get values - int pulseLength = GetWordValue(data, _position); - int pulseCount = GetWordValue(data, _position + 2); - - t.AddMetaData(BlockDescriptorTitle.Pulse_Length, pulseLength + " T-States"); - t.AddMetaData(BlockDescriptorTitle.Pulse_Count, pulseCount.ToString()); - - // build period information - for (int p = 0; p < pulseCount; p++) + if (blockdata[0] == 0x00 && blockLen == 19) { - t.DataPeriods.Add(pulseLength); + string fileName = Encoding.ASCII.GetString(blockdata.Skip(2).Take(10).ToArray()).Trim(); + string type = "Unknown Type"; + StringBuilder sb = new StringBuilder(); + + var param1 = GetWordValue(blockdata, 12); + var param2 = GetWordValue(blockdata, 14); + + // header block - examine first byte of header + if (blockdata[1] == 0) + { + type = "Program"; + sb.Append(type + ": "); + sb.Append(fileName + " "); + } + else if (blockdata[1] == 1) + { + type = "NumArray"; + sb.Append(type + ": "); + sb.Append(fileName + " "); + } + else if (blockdata[1] == 2) + { + type = "CharArray"; + sb.Append(type + ": "); + sb.Append(fileName + " "); + } + else if (blockdata[1] == 3) + { + type = "Code"; + sb.Append(type + ": "); + sb.Append(fileName + " "); + } + } + else if (blockdata[0] == 0xff) + { + // data block + description = "Data Block " + (blockLen - 2) + "bytes"; + t.AddMetaData(BlockDescriptorTitle.Data_Bytes, (blockLen - 2) + " Bytes"); + } + else + { + // some other type (turbo data etc..) + description = $"#{blockdata[0].ToString("X2")} block, {blockLen} bytes"; + //description += (crc != 0) ? $", crc bad (#{crcFile:X2}!=#{crcValue:X2})" : ", crc ok"; + t.AddMetaData(BlockDescriptorTitle.Undefined, description); } - // add the block + t.AddMetaData(BlockDescriptorTitle.Pilot_Pulse_Length, pilotToneLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.Pilot_Pulse_Count, pilotCount + " Pulses"); + t.AddMetaData(BlockDescriptorTitle.First_Sync_Length, sync1PulseLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.Second_Sync_Length, sync2PulseLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.Zero_Bit_Length, bit0PulseLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.One_Bit_Length, bit1PulseLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.Data_Length, blockLen + " Bytes"); + t.AddMetaData(BlockDescriptorTitle.Bits_In_Last_Byte, bitsInLastByte + " Bits"); + t.AddMetaData(BlockDescriptorTitle.Pause_After_Data, t.PauseInMS + " ms / " + t.PauseInTStates + "TStates"); + + // add the raw data + t.BlockData = blockdata; + + // decode + DecodeData(); + + // add the block to the datacorder _datacorder.DataBlocks.Add(t); + // ready for next block + ZeroVars(); + } + + /// + /// 0x12 - Pure Tone + /// + private void ProcessBlockID12() + { + /* length: 04 + + Offset Value Type Description + 0x00 - WORD Length of one pulse in T-states + 0x02 - WORD Number of pulses + + This will produce a tone which is basically the same as the pilot tone in the ID 10, ID 11 blocks. You can define how + long the pulse is and how many pulses are in the tone. */ + + int blockLen = 4; + + t = new TapeDataBlock + { + BlockID = 0x12, + BlockDescription = BlockType.Pure_Tone, + PauseInMS = 0, + PauseInTStates = 0 + }; + + // get values + pilotToneLength = GetWordValue(data, _position); + pilotCount = GetWordValue(data, _position + 2); + + t.AddMetaData(BlockDescriptorTitle.Pulse_Length, pilotToneLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.Pulse_Count, pilotCount.ToString()); + + // build period information + while (pilotCount > 0) + { + ToggleSignal(); + t.DataPeriods.Add(pilotToneLength); + t.DataLevels.Add(signal); + t.PulseDescription.Add("Pilot " + pilotCount); + pilotCount--; + } + + // add the block to the datacorder + _datacorder.DataBlocks.Add(t); + // advance the position to the next block _position += blockLen; + + // ready for next block + ZeroVars(); } - /* length: [00]*02+01 - Offset Value Type Description - 0x00 N BYTE Number of pulses - 0x01 - WORD[N] Pulses' lengths - - This will produce N pulses, each having its own timing. Up to 255 pulses can be stored in this block; this is useful for non-standard - sync tones used by some protection schemes. */ - private void ProcessBlockID13(byte[] data) + /// + /// 0x13 - Pulse Sequence + /// + private void ProcessBlockID13() { - TapeDataBlock t = new TapeDataBlock + /* length: [00]*02+01 + * + Offset Value Type Description + 0x00 N BYTE Number of pulses + 0x01 - WORD[N] Pulses' lengths + + This will produce N pulses, each having its own timing. Up to 255 pulses can be stored in this block; this is useful for non-standard + sync tones used by some protection schemes. */ + + t = new TapeDataBlock { BlockID = 0x13, BlockDescription = BlockType.Pulse_Sequence, - DataPeriods = new List(), - PauseInMS = 0 + PauseInMS = 0, + PauseInTStates = 0 }; // get pulse count - int pulseCount = data[_position]; - t.AddMetaData(BlockDescriptorTitle.Pulse_Count, pulseCount.ToString()); + pilotCount = data[_position]; + t.AddMetaData(BlockDescriptorTitle.Pulse_Count, pilotCount.ToString()); _position++; // build period information - for (int p = 0; p < pulseCount; p++, _position += 2) + while (pilotCount > 0) { - // get pulse length - int pulseLength = GetWordValue(data, _position); - t.AddMetaData(BlockDescriptorTitle.Needs_Parsing, "Pulse " + p + " Length\t" + pulseLength + " T-States"); - t.DataPeriods.Add(pulseLength); + pilotToneLength = GetWordValue(data, _position); + t.AddMetaData(BlockDescriptorTitle.Needs_Parsing, "Pulse " + pilotCount + " Length\t" + pilotToneLength + " T-States"); + t.DataPeriods.Add(pilotToneLength); + ToggleSignal(); + t.DataLevels.Add(signal); + t.PulseDescription.Add("Pilot " + pilotCount); + + pilotCount--; + _position += 2; } - // add the block + // add the block to the datacorder _datacorder.DataBlocks.Add(t); + + // ready for next block + ZeroVars(); } - /* length: [07,08,09]+0A - Offset Value Type Description - 0x00 - WORD Length of ZERO bit pulse - 0x02 - WORD Length of ONE bit pulse - 0x04 - BYTE Used bits in last byte (other bits should be 0) - (e.g. if this is 6, then the bits used (x) in the last byte are: xxxxxx00, - where MSb is the leftmost bit, LSb is the rightmost bit) - 0x05 - WORD Pause after this block (ms.) - 0x07 N BYTE[3] Length of data that follow - 0x0A - BYTE[N] Data as in .TAP files - - This is the same as in the turbo loading data block, except that it has no pilot or sync pulses. */ - private void ProcessBlockID14(byte[] data) + /// + /// 0x14 - Pure Data BLock + /// + private void ProcessBlockID14() { - TapeDataBlock t = new TapeDataBlock(); - t.BlockID = 0x14; - t.BlockDescription = BlockType.Pure_Data_Block; - t.DataPeriods = new List(); + /* length: [07,08,09]+0A - int pilotPL = 0; - int sync1P = 0; - int sync2P = 0; - int bit0P = GetWordValue(data, _position + 0); - int bit1P = GetWordValue(data, _position + 2); - int pilotTL = 0; - int bitinbyte = data[_position + 4]; - int pause = GetWordValue(data, _position + 5); + Offset Value Type Description + 0x00 - WORD Length of ZERO bit pulse + 0x02 - WORD Length of ONE bit pulse + 0x04 - BYTE Used bits in last byte (other bits should be 0) + (e.g. if this is 6, then the bits used (x) in the last byte are: xxxxxx00, + where MSb is the leftmost bit, LSb is the rightmost bit) + 0x05 - WORD Pause after this block (ms.) + 0x07 N BYTE[3] Length of data that follow + 0x0A - BYTE[N] Data as in .TAP files - int blockLen = 0xFFFFFF & GetInt32(data, _position + 0x07); + This is the same as in the turbo loading data block, except that it has no pilot or sync pulses. */ + + t = new TapeDataBlock + { + BlockID = 0x14, + BlockDescription = BlockType.Pure_Data_Block + }; + + pilotToneLength = 0; + sync1PulseLength = 0; + sync2PulseLength = 0; + pilotCount = 0; + + bit0PulseLength = GetWordValue(data, _position + 0); + bit1PulseLength = GetWordValue(data, _position + 2); + bitsInLastByte = data[_position + 4]; + pauseLen = GetWordValue(data, _position + 5); + blockLen = 0xFFFFFF & GetInt32(data, _position + 0x07); + + t.PauseInMS = pauseLen; + t.PauseInTStates = TranslatePause(pauseLen); _position += 0x0A; - byte[] tmp = new byte[blockLen]; - tmp = data.Skip(_position).Take(blockLen).ToArray(); + byte[] blockdata = new byte[blockLen]; + blockdata = data.Skip(_position).Take(blockLen).ToArray(); - var t2 = DecodeDataBlock(t, tmp, DataBlockType.Pure, pause, pilotTL, pilotPL, sync1P, sync2P, bit0P, bit1P, bitinbyte); + t.AddMetaData(BlockDescriptorTitle.Pilot_Pulse_Length, pilotToneLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.Pilot_Pulse_Count, pilotCount + " Pulses"); + t.AddMetaData(BlockDescriptorTitle.First_Sync_Length, sync1PulseLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.Second_Sync_Length, sync2PulseLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.Zero_Bit_Length, bit0PulseLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.One_Bit_Length, bit1PulseLength + " T-States"); + t.AddMetaData(BlockDescriptorTitle.Data_Length, blockLen + " Bytes"); + t.AddMetaData(BlockDescriptorTitle.Bits_In_Last_Byte, bitsInLastByte + " Bits"); + t.AddMetaData(BlockDescriptorTitle.Pause_After_Data, t.PauseInMS + " ms / " + t.PauseInTStates + "TStates"); - t.PauseInMS = pause; + // add the raw data + t.BlockData = blockdata; - // add the block - _datacorder.DataBlocks.Add(t2); + // decode + DecodeData(); - // advance the position to the next block - _position += blockLen; + // add the block to the datacorder + _datacorder.DataBlocks.Add(t); - // generate PAUSE block - CreatePauseBlock(_datacorder.DataBlocks.Last()); + // ready for next block + ZeroVars(); } - /* length: [05,06,07]+08 - Offset Value Type Description - 0x00 - WORD Number of T-states per sample (bit of data) - 0x02 - WORD Pause after this block in milliseconds (ms.) - 0x04 - BYTE Used bits (samples) in last byte of data (1-8) - (e.g. if this is 2, only first two samples of the last byte will be played) - 0x05 N BYTE[3] Length of samples' data - 0x08 - BYTE[N] Samples data. Each bit represents a state on the EAR port (i.e. one sample). - MSb is played first. - - This block is used for tapes which have some parts in a format such that the turbo loader block cannot be used. - This is not like a VOC file, since the information is much more compact. Each sample value is represented by one bit only - (0 for low, 1 for high) which means that the block will be at most 1/8 the size of the equivalent VOC. - The preferred sampling frequencies are 22050 or 44100 Hz (158 or 79 T-states/sample). - Please, if you can, don't use other sampling frequencies. - Please use this block only if you cannot use any other block. */ - private void ProcessBlockID15(byte[] data) + /// + /// 0x15 - Direct Recording Block + /// + private void ProcessBlockID15() { - TapeDataBlock t = new TapeDataBlock(); - t.BlockID = 0x15; - t.BlockDescription = BlockType.Direct_Recording; - t.DataPeriods = new List(); + /* length: [05,06,07]+08 - // get values - int samLen = GetInt32(data, _position + 5); - int samSize = 0xFFFFFF & samLen; + Offset Value Type Description + 0x00 - WORD Number of T-states per sample (bit of data) + 0x02 - WORD Pause after this block in milliseconds (ms.) + 0x04 - BYTE Used bits (samples) in last byte of data (1-8) + (e.g. if this is 2, only first two samples of the last byte will be played) + 0x05 N BYTE[3] Length of samples' data + 0x08 - BYTE[N] Samples data. Each bit represents a state on the EAR port (i.e. one sample). + MSb is played first. + + This block is used for tapes which have some parts in a format such that the turbo loader block cannot be used. + This is not like a VOC file, since the information is much more compact. Each sample value is represented by one bit only + (0 for low, 1 for high) which means that the block will be at most 1/8 the size of the equivalent VOC. + The preferred sampling frequencies are 22050 or 44100 Hz (158 or 79 T-states/sample). + Please, if you can, don't use other sampling frequencies. + Please use this block only if you cannot use any other block. */ + + TapeDataBlock t = new TapeDataBlock + { + BlockID = 0x15, + BlockDescription = BlockType.Direct_Recording + }; int tStatesPerSample = GetWordValue(data, _position); - int pauseAfterBlock = GetWordValue(data, _position + 2); - int usedBitsInLastByte = data[_position + 4]; + pauseLen = GetWordValue(data, _position + 2); + bitsInLastByte = data[_position + 4]; + blockLen = 0xFFFFFF & GetInt32(data, _position + 0x05); + + t.PauseInMS = pauseLen; + t.PauseInTStates = TranslatePause(pauseLen); + + t.AddMetaData(BlockDescriptorTitle.TStatesPerSample, tStatesPerSample + " TStates per sample"); + t.AddMetaData(BlockDescriptorTitle.Data_Length, blockLen + " Bytes (one bit per sample"); + t.AddMetaData(BlockDescriptorTitle.Bits_In_Last_Byte, bitsInLastByte + " Bits (samples)"); + t.AddMetaData(BlockDescriptorTitle.Pause_After_Data, t.PauseInMS + " ms / " + t.PauseInTStates + "TStates"); // skip to samples data _position += 8; - int pulseLength = 0; - int pulseCount = 0; - - // ascertain the pulse count - for (int i = 0; i < samSize; i++) + while (blockLen > 0) { - for (int p = 0x80; p != 0; p >>= 1) + if (blockLen == 1) { - if (((data[_position + i] ^ pulseLength) & p) != 0) - { - pulseCount++; - pulseLength ^= -1; - } + // last byte - need to look at number of bits + bitCount = bitsInLastByte; } - } - - // get the pulses - t.DataPeriods = new List(pulseCount + 2); - int tStateCount = 0; - pulseLength = 0; - for (int i = 1; i < samSize; i++) - { - for (int p = 0x80; p != 0; p >>= 1) + else { - tStateCount += tStatesPerSample; - if (((data[_position] ^ pulseLength) & p) != 0) - { - t.DataPeriods.Add(tStateCount); - pulseLength ^= -1; - tStateCount = 0; - } + // this is a full byte + bitCount = 8; } - // incrememt position + // get the byte to be processed + var currByte = data[_position++]; + + // do the bits + while (bitCount > 0) + { + if ((currByte & 0x80) != 0) + { + // high signal + signal = true; + } + else + { + // low signal + signal = false; + } + + t.DataPeriods.Add(tStatesPerSample); + t.DataLevels.Add(signal); + + currByte <<= 1; + bitCount--; + } + + blockLen--; _position++; } - // get the pulses in the last byte of data - for (int p = 0x80; p != (byte)(0x80 >> usedBitsInLastByte); p >>= 1) - { - tStateCount += tStatesPerSample; - if (((data[_position] ^ pulseLength) & p) != 0) - { - t.DataPeriods.Add(tStateCount); - pulseLength ^= -1; - tStateCount = 0; - } - } + // pause processing + DoPause(); - // add final pulse - t.DataPeriods.Add(tStateCount); - - // add end of block pause - if (pauseAfterBlock > 0) - { - //t.DataPeriods.Add(3500 * pauseAfterBlock); - } - - t.PauseInMS = pauseAfterBlock; - - // increment position - _position++; - - // add the block + // add the block to the datacorder _datacorder.DataBlocks.Add(t); - // generate PAUSE block - CreatePauseBlock(_datacorder.DataBlocks.Last()); + // ready for next block + ZeroVars(); } - /* length: [00,01,02,03]+04 - Offset Value Type Description - 0x00 10+N DWORD Block length (without these four bytes) - 0x04 - WORD Pause after this block (in ms). - 0x06 - BYTE[3] Sampling rate - 0x09 - BYTE Compression type - 0x01: RLE - 0x02: Z-RLE - 0x0A - DWORD Number of stored pulses (after decompression, for validation purposes) - 0x0E - BYTE[N] CSW data, encoded according to the CSW file format specification. - - This block contains a sequence of raw pulses encoded in CSW format v2 (Compressed Square Wave). */ - private void ProcessBlockID18(byte[] data) + + /// + /// 0x18 - CSW Recording Block + /// + private void ProcessBlockID18() { - TapeDataBlock t = new TapeDataBlock(); - t.BlockID = 0x18; - t.BlockDescription = BlockType.CSW_Recording; - t.DataPeriods = new List(); + /* length: [00,01,02,03]+04 - int blockLen = GetInt32(data, _position); + Offset Value Type Description + 0x00 10+N DWORD Block length (without these four bytes) + 0x04 - WORD Pause after this block (in ms). + 0x06 - BYTE[3] Sampling rate + 0x09 - BYTE Compression type + 0x01: RLE + 0x02: Z-RLE + 0x0A - DWORD Number of stored pulses (after decompression, for validation purposes) + 0x0E - BYTE[N] CSW data, encoded according to the CSW file format specification. + + This block contains a sequence of raw pulses encoded in CSW format v2 (Compressed Square Wave). */ + + t = new TapeDataBlock + { + BlockID = 0x18, + BlockDescription = BlockType.CSW_Recording + }; + + blockLen = GetInt32(data, _position); _position += 4; t.PauseInMS = GetWordValue(data, _position); + t.PauseInTStates = TranslatePause(pauseLen); _position += 2; @@ -649,153 +962,45 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum t.DataPeriods.Add((69888 * 50) / 10); _position += dataLen; - //_position += blockLen; - // add the block + // pause processing + DoPause(); + + // add the block to the datacorder _datacorder.DataBlocks.Add(t); - // generate PAUSE block - CreatePauseBlock(_datacorder.DataBlocks.Last()); + // ready for next block + ZeroVars(); } - - /* length: [00,01,02,03]+04 - Offset Value Type Description - 0x00 - DWORD Block length (without these four bytes) - 0x04 - WORD Pause after this block (ms) - 0x06 TOTP DWORD Total number of symbols in pilot/sync block (can be 0) - 0x0A NPP BYTE Maximum number of pulses per pilot/sync symbol - 0x0B ASP BYTE Number of pilot/sync symbols in the alphabet table (0=256) - 0x0C TOTD DWORD Total number of symbols in data stream (can be 0) - 0x10 NPD BYTE Maximum number of pulses per data symbol - 0x11 ASD BYTE Number of data symbols in the alphabet table (0=256) - 0x12 - SYMDEF[ASP] Pilot and sync symbols definition table - This field is present only if TOTP>0 - 0x12+ - (2*NPP+1)*ASP - PRLE[TOTP] Pilot and sync data stream - This field is present only if TOTP>0 - 0x12+ - (TOTP>0)*((2*NPP+1)*ASP)+ - TOTP*3 - SYMDEF[ASD] Data symbols definition table - This field is present only if TOTD>0 - 0x12+ - (TOTP>0)*((2*NPP+1)*ASP)+ - TOTP*3+ - (2*NPD+1)*ASD - BYTE[DS] Data stream - This field is present only if TOTD>0 - - This block has been specifically developed to represent an extremely wide range of data encoding techniques. - The basic idea is that each loading component (pilot tone, sync pulses, data) is associated to a specific sequence - of pulses, where each sequence (wave) can contain a different number of pulses from the others. - In this way we can have a situation where bit 0 is represented with 4 pulses and bit 1 with 8 pulses. - - ---- - SYMDEF structure format - Offset Value Type Description - 0x00 - BYTE Symbol flags - b0-b1: starting symbol polarity - 00: opposite to the current level (make an edge, as usual) - default - 01: same as the current level (no edge - prolongs the previous pulse) - 10: force low level - 11: force high level - 0x01 - WORD[MAXP] Array of pulse lengths. - - The alphabet is stored using a table where each symbol is a row of pulses. The number of columns (i.e. pulses) of the table is the - length of the longest sequence amongst all (MAXP=NPP or NPD, for pilot/sync or data blocks respectively); shorter waves are terminated by a - zero-length pulse in the sequence. - Any number of data symbols is allowed, so we can have more than two distinct waves; for example, imagine a loader which writes two bits at a - time by encoding them with four distinct pulse lengths: this loader would have an alphabet of four symbols, each associated to a specific - sequence of pulses (wave). - ---- - ---- - PRLE structure format - Offset Value Type Description - 0x00 - BYTE Symbol to be represented - 0x01 - WORD Number of repetitions - - Most commonly, pilot and sync are repetitions of the same pulse, thus they are represented using a very simple RLE encoding structure which stores - the symbol and the number of times it must be repeated. - Each symbol in the data stream is represented by a string of NB bits of the block data, where NB = ceiling(Log2(ASD)). - Thus the length of the whole data stream in bits is NB*TOTD, or in bytes DS=ceil(NB*TOTD/8). - ---- */ - private void ProcessBlockID19(byte[] data) + + /// + /// Pause Block + /// + private void ProcessBlockID20() { - // not currently implemented properly + /* length: 02 + * + Offset Value Type Description + 0x00 - WORD Pause duration (ms.) - TapeDataBlock t = new TapeDataBlock(); - t.BlockID = 0x19; - t.BlockDescription = BlockType.Generalized_Data_Block; - t.DataPeriods = new List(); - - int blockLen = GetInt32(data, _position); - _position += 4; - - int pause = GetWordValue(data, _position); - _position += 2; - - int totp = GetInt32(data, _position); - _position += 4; - - int npp = data[_position++]; - - int asp = data[_position++]; - - int totd = GetInt32(data, _position); - _position += 4; - - int npd = data[_position++]; - - int asd = data[_position++]; - - // add the block - _datacorder.DataBlocks.Add(t); - - // advance the position to the next block - _position += blockLen; - } - - /* length: 02 - Offset Value Type Description - 0x00 - WORD Pause duration (ms.) - - This will make a silence (low amplitude level (0)) for a given time in milliseconds. If the value is 0 then the - emulator or utility should (in effect) STOP THE TAPE, i.e. should not continue loading until the user or emulator requests it. */ - private void ProcessBlockID20(byte[] data) - { - TapeDataBlock t = new TapeDataBlock(); + This will make a silence (low amplitude level (0)) for a given time in milliseconds. If the value is 0 then the + emulator or utility should (in effect) STOP THE TAPE, i.e. should not continue loading until the user or emulator requests it. */ + t = new TapeDataBlock(); t.BlockID = 0x20; t.DataPeriods = new List(); t.BlockDescription = BlockType.Pause_or_Stop_the_Tape; - int pauseDuration = GetWordValue(data, _position); - if (pauseDuration != 0) - { - //t.BlockDescription = "Pause: " + pauseDuration + " ms"; - } - else - { - //t.BlockDescription = "[STOP THE TAPE]"; - } + pauseLen = GetWordValue(data, _position); - t.PauseInMS = pauseDuration; + t.PauseInMS = pauseLen; + t.PauseInTStates = TranslatePause(t.PauseInMS); - if (pauseDuration == 0) + if (pauseLen == 0) { // issue stop the tape command t.Command = TapeCommand.STOP_THE_TAPE; - // add 1ms period - //t.DataPeriods.Add(3500); - //pauseDuration = -1; } - else - { - // this is actually just a pause - //pauseDuration = 3500 * pauseDuration; - //t.DataPeriods.Add(pauseDuration); - } - - // add end of block pause - //t.DataPeriods.Add(pauseDuration); // add to tape _datacorder.DataBlocks.Add(t); @@ -804,22 +1009,29 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum _position += 2; // generate PAUSE block - CreatePauseBlock(_datacorder.DataBlocks.Last()); + DoPause(); + // ready for next block + ZeroVars(); } - - /* length: [00]+01 - Offset Value Type Description - 0x00 L BYTE Length of the group name string - 0x01 - CHAR[L] Group name in ASCII format (please keep it under 30 characters long) - - This block marks the start of a group of blocks which are to be treated as one single (composite) block. - This is very handy for tapes that use lots of subblocks like Bleepload (which may well have over 160 custom loading blocks). - You can also give the group a name (example 'Bleepload Block 1'). - For each group start block, there must be a group end block. Nesting of groups is not allowed. */ - private void ProcessBlockID21(byte[] data) + + /// + /// 0x21 - Group Start + /// + private void ProcessBlockID21() { - TapeDataBlock t = new TapeDataBlock(); + /* length: [00]+01 + * + Offset Value Type Description + 0x00 L BYTE Length of the group name string + 0x01 - CHAR[L] Group name in ASCII format (please keep it under 30 characters long) + + This block marks the start of a group of blocks which are to be treated as one single (composite) block. + This is very handy for tapes that use lots of subblocks like Bleepload (which may well have over 160 custom loading blocks). + You can also give the group a name (example 'Bleepload Block 1'). + For each group start block, there must be a group end block. Nesting of groups is not allowed. */ + + t = new TapeDataBlock(); t.BlockID = 0x21; t.DataPeriods = new List(); t.BlockDescription = BlockType.Group_Start; @@ -832,93 +1044,59 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum t.Command = TapeCommand.BEGIN_GROUP; t.PauseInMS = 0; + t.PauseInTStates = 0; // add to tape _datacorder.DataBlocks.Add(t); // advance to next block _position += nameLength; + + // ready for next block + ZeroVars(); } - - /* length: 00 - - This indicates the end of a group. This block has no body. */ - private void ProcessBlockID22(byte[] data) + + /// + /// 0x22 - Group End + /// + private void ProcessBlockID22() { - TapeDataBlock t = new TapeDataBlock(); + /* length: 00 + + This indicates the end of a group. This block has no body. */ + + t = new TapeDataBlock(); t.BlockID = 0x22; t.DataPeriods = new List(); t.BlockDescription = BlockType.Group_End; t.Command = TapeCommand.END_GROUP; t.PauseInMS = 0; - - // add to tape - _datacorder.DataBlocks.Add(t); - } - - /* length: 02 - Offset Value Type Description - 0x00 - WORD Relative jump value - - This block will enable you to jump from one block to another within the file. The value is a signed short word - (usually 'signed short' in C); Some examples: - Jump 0 = 'Loop Forever' - this should never happen - Jump 1 = 'Go to the next block' - it is like NOP in assembler ;) - Jump 2 = 'Skip one block' - Jump -1 = 'Go to the previous block' - All blocks are included in the block count!. */ - private void ProcessBlockID23(byte[] data) - { - // not implemented properly - - TapeDataBlock t = new TapeDataBlock(); - t.BlockID = 0x23; - t.DataPeriods = new List(); - t.BlockDescription = BlockType.Jump_to_Block; - - int relativeJumpValue = GetWordValue(data, _position); - string result = string.Empty; - - switch (relativeJumpValue) - { - case 0: - result = "Loop Forever"; - break; - case 1: - result = "To Next Block"; - break; - case 2: - result = "Skip One Block"; - break; - case -1: - result = "Go to Previous Block"; - break; - } - - //t.BlockDescription = "[JUMP BLOCK - " + result +"]"; - - t.PauseInMS = 0; + t.PauseInTStates = 0; // add to tape _datacorder.DataBlocks.Add(t); - // advance to next block - _position += 2; + // ready for next block + ZeroVars(); } - - /* length: 02 - Offset Value Type Description - 0x00 - WORD Number of repetitions (greater than 1) - - If you have a sequence of identical blocks, or of identical groups of blocks, you can use this block to tell how many times they should - be repeated. This block is the same as the FOR statement in BASIC. - For simplicity reasons don't nest loop blocks! */ - private void ProcessBlockID24(byte[] data) + + /// + /// 0x24 - Loop Start + /// + private void ProcessBlockID24() { - TapeDataBlock t = new TapeDataBlock(); + /* length: 02 + + Offset Value Type Description + 0x00 - WORD Number of repetitions (greater than 1) + + If you have a sequence of identical blocks, or of identical groups of blocks, you can use this block to tell how many times they should + be repeated. This block is the same as the FOR statement in BASIC. + For simplicity reasons don't nest loop blocks! */ + + t = new TapeDataBlock(); t.BlockID = 0x24; - t.DataPeriods = new List(); t.BlockDescription = BlockType.Loop_Start; // loop should start from the next block @@ -936,22 +1114,30 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum //t.BlockDescription = "[LOOP START - " + numberOfRepetitions + " times]"; t.PauseInMS = 0; + t.PauseInTStates = 0; // add to tape _datacorder.DataBlocks.Add(t); // advance to next block _position += 2; + + // ready for next block + ZeroVars(); } - - /* length: 00 - - This is the same as BASIC's NEXT statement. It means that the utility should jump back to the start of the loop if it hasn't - been run for the specified number of times. - This block has no body. */ - private void ProcessBlockID25(byte[] data) + + /// + /// 0x25 - Loop End + /// + private void ProcessBlockID25() { - TapeDataBlock t = new TapeDataBlock(); + /* length: 00 + + This is the same as BASIC's NEXT statement. It means that the utility should jump back to the start of the loop if it hasn't + been run for the specified number of times. + This block has no body. */ + + t = new TapeDataBlock(); t.BlockID = 0x25; t.DataPeriods = new List(); t.BlockDescription = BlockType.Loop_End; @@ -987,103 +1173,26 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum _datacorder.DataBlocks.Add(block); } } + + // ready for next block + ZeroVars(); } - /* length: [00,01]*02+02 - Offset Value Type Description - 0x00 N WORD Number of calls to be made - 0x02 - WORD[N] Array of call block numbers (relative-signed offsets) - - This block is an analogue of the CALL Subroutine statement. It basically executes a sequence of blocks that are somewhere else and - then goes back to the next block. Because more than one call can be normally used you can include a list of sequences to be called. - The 'nesting' of call blocks is also not allowed for the simplicity reasons. You can, of course, use the CALL blocks in the LOOP - sequences and vice versa. The value is relative for the obvious reasons - so that you can add some blocks in the beginning of the - file without disturbing the call values. Please take a look at 'Jump To Block' for reference on the values. */ - private void ProcessBlockID26(byte[] data) + /// + /// 0x2A - Stop the Tape if in 48K Mode + /// + private void ProcessBlockID2A() { - // block processing not implemented for this - just gets added for informational purposes only - TapeDataBlock t = new TapeDataBlock(); - t.BlockID = 0x26; - t.DataPeriods = new List(); - t.BlockDescription = BlockType.Call_Sequence; + /* length: 04 + * + Offset Value Type Description + 0x00 0 DWORD Length of the block without these four bytes (0) - int blockSize = 2 + 2 * GetWordValue(data, _position); - t.PauseInMS = 0; + When this block is encountered, the tape will stop ONLY if the machine is an 48K Spectrum. This block is to be used for + multiloading games that load one level at a time in 48K mode, but load the entire tape at once if in 128K mode. + This block has no body of its own, but follows the extension rule. */ - - // add to tape - _datacorder.DataBlocks.Add(t); - - // advance to next block - _position += blockSize; - } - - /* length: 00 - - This block indicates the end of the Called Sequence. The next block played will be the block after the last CALL block (or the next Call, - if the Call block had multiple calls). - Again, this block has no body. */ - private void ProcessBlockID27(byte[] data) - { - // block processing not implemented for this - just gets added for informational purposes only - TapeDataBlock t = new TapeDataBlock(); - t.BlockID = 0x27; - t.DataPeriods = new List(); - t.BlockDescription = BlockType.Return_From_Sequence; - t.PauseInMS = 0; - - - // add to tape - _datacorder.DataBlocks.Add(t); - } - - /* length: [00,01]+02 - Offset Value Type Description - 0x00 - WORD Length of the whole block (without these two bytes) - 0x02 N BYTE Number of selections - 0x03 - SELECT[N] List of selections - - ---- - SELECT structure format - Offset Value Type Description - 0x00 - WORD Relative Offset - 0x02 L BYTE Length of description text - 0x03 - CHAR[L] Description text (please use single line and max. 30 chars) - ---- - - This block is useful when the tape consists of two or more separately-loadable parts. With this block, you are able to select - one of the parts and the utility/emulator will start loading from that block. For example you can use it when the game has a - separate Trainer or when it is a multiload. Of course, to make some use of it the emulator/utility has to show a menu with the - selections when it encounters such a block. All offsets are relative signed words. */ - private void ProcessBlockID28(byte[] data) - { - // block processing not implemented for this - just gets added for informational purposes only - TapeDataBlock t = new TapeDataBlock(); - t.BlockID = 0x28; - t.DataPeriods = new List(); - t.BlockDescription = BlockType.Select_Block; - - int blockSize = 2 + GetWordValue(data, _position); - - t.PauseInMS = 0; - - // add to tape - _datacorder.DataBlocks.Add(t); - - // advance to next block - _position += blockSize; - } - - /* length: 04 - Offset Value Type Description - 0x00 0 DWORD Length of the block without these four bytes (0) - - When this block is encountered, the tape will stop ONLY if the machine is an 48K Spectrum. This block is to be used for - multiloading games that load one level at a time in 48K mode, but load the entire tape at once if in 128K mode. - This block has no body of its own, but follows the extension rule. */ - private void ProcessBlockID2A(byte[] data) - { - TapeDataBlock t = new TapeDataBlock(); + t = new TapeDataBlock(); t.BlockID = 0x2A; t.DataPeriods = new List(); t.BlockDescription = BlockType.Stop_the_Tape_48K; @@ -1092,51 +1201,77 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum int blockSize = 4 + GetWordValue(data, _position); t.PauseInMS = 0; + t.PauseInTStates = 0; // add to tape _datacorder.DataBlocks.Add(t); // advance to next block _position += blockSize; + + // ready for next block + ZeroVars(); } - - /* length: 05 - Offset Value Type Description - 0x00 1 DWORD Block length (without these four bytes) - 0x04 - BYTE Signal level (0=low, 1=high) - - This block sets the current signal level to the specified value (high or low). It should be used whenever it is necessary to avoid any - ambiguities, e.g. with custom loaders which are level-sensitive. */ - private void ProcessBlockID2B(byte[] data) + + /// + /// 0x2B - Set Signal Level + /// + private void ProcessBlockID2B() { - TapeDataBlock t = new TapeDataBlock(); + /* length: 05 + + Offset Value Type Description + 0x00 1 DWORD Block length (without these four bytes) + 0x04 - BYTE Signal level (0=low, 1=high) + + This block sets the current signal level to the specified value (high or low). It should be used whenever it is necessary to avoid any + ambiguities, e.g. with custom loaders which are level-sensitive. */ + + t = new TapeDataBlock(); t.BlockID = 0x2B; t.DataPeriods = new List(); t.BlockDescription = BlockType.Set_Signal_Level; t.PauseInMS = 0; + t.PauseInTStates = 0; + + // we already flip the signal *before* adding to the buffer elsewhere + // so set the opposite level specified in this block + byte signalLev = data[_position + 4]; + if (signalLev == 0) + signal = true; + else + signal = false; // add to tape _datacorder.DataBlocks.Add(t); // advance to next block _position += 5; + + // ready for next block + ZeroVars(); } - - /* length: [00]+01 - Offset Value Type Description - 0x00 N BYTE Length of the text description - 0x01 - CHAR[N] Text description in ASCII format - - This is meant to identify parts of the tape, so you know where level 1 starts, where to rewind to when the game ends, etc. - This description is not guaranteed to be shown while the tape is playing, but can be read while browsing the tape or changing - the tape pointer. - The description can be up to 255 characters long but please keep it down to about 30 so the programs can show it in one line - (where this is appropriate). - Please use 'Archive Info' block for title, authors, publisher, etc. */ - private void ProcessBlockID30(byte[] data) + + /// + /// Text Description + /// + private void ProcessBlockID30() { - TapeDataBlock t = new TapeDataBlock(); + /* length: [00]+01 + + Offset Value Type Description + 0x00 N BYTE Length of the text description + 0x01 - CHAR[N] Text description in ASCII format + + This is meant to identify parts of the tape, so you know where level 1 starts, where to rewind to when the game ends, etc. + This description is not guaranteed to be shown while the tape is playing, but can be read while browsing the tape or changing + the tape pointer. + The description can be up to 255 characters long but please keep it down to about 30 so the programs can show it in one line + (where this is appropriate). + Please use 'Archive Info' block for title, authors, publisher, etc. */ + + t = new TapeDataBlock(); t.BlockID = 0x30; t.DataPeriods = new List(); t.BlockDescription = BlockType.Text_Description; @@ -1147,97 +1282,63 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum string desc = Encoding.ASCII.GetString(data, _position, textLen); t.PauseInMS = 0; + t.PauseInTStates = 0; - //t.BlockDescription = "[" + desc + "]"; + t.AddMetaData(BlockDescriptorTitle.Text_Description, desc); // add to tape _datacorder.DataBlocks.Add(t); // advance to next block _position += textLen; + + // ready for next block + ZeroVars(); } - - /* length: [01]+02 - Offset Value Type Description - 0x00 - BYTE Time (in seconds) for which the message should be displayed - 0x01 N BYTE Length of the text message - 0x02 - CHAR[N] Message that should be displayed in ASCII format - - This will enable the emulators to display a message for a given time. This should not stop the tape and it should not make silence. - If the time is 0 then the emulator should wait for the user to press a key. - The text message should: - stick to a maximum of 30 chars per line; - use single 0x0D (13 decimal) to separate lines; - stick to a maximum of 8 lines. - If you do not obey these rules, emulators may display your message in any way they like. */ - private void ProcessBlockID31(byte[] data) + + /// + /// 0x32 - Archive Info + /// + private void ProcessBlockID32() { - // currently not implemented properly in ZXHawk + /* length: [00,01]+02 - TapeDataBlock t = new TapeDataBlock(); - t.BlockID = 0x31; - t.DataPeriods = new List(); - t.BlockDescription = BlockType.Message_Block; + Offset Value Type Description + 0x00 - WORD Length of the whole block (without these two bytes) + 0x02 N BYTE Number of text strings + 0x03 - TEXT[N] List of text strings - _position++; + ---- + TEXT structure format + Offset Value Type Description + 0x00 - BYTE Text identification byte: + 00 - Full title + 01 - Software house/publisher + 02 - Author(s) + 03 - Year of publication + 04 - Language + 05 - Game/utility type + 06 - Price + 07 - Protection scheme/loader + 08 - Origin + FF - Comment(s) + 0x01 L BYTE Length of text string + 0x02 - CHAR[L] Text string in ASCII format + ---- - int msgLen = data[_position]; - _position++; + Use this block at the beginning of the tape to identify the title of the game, author, publisher, year of publication, price (including + the currency), type of software (arcade adventure, puzzle, word processor, ...), protection scheme it uses (Speedlock 1, Alkatraz, ...) + and its origin (Original, Budget re-release, ...), etc. This block is built in a way that allows easy future expansion. + The block consists of a series of text strings. Each text has its identification number (which tells us what the text means) and then + the ASCII text. To make it possible to skip this block, if needed, the length of the whole block is at the beginning of it. + If all texts on the tape are in English language then you don't have to supply the 'Language' field + The information about what hardware the tape uses is in the 'Hardware Type' block, so no need for it here. */ - string desc = Encoding.ASCII.GetString(data, _position, msgLen); - - t.Command = TapeCommand.SHOW_MESSAGE; - - //t.BlockDescription = "[MESSAGE: " + desc + "]"; - - t.PauseInMS = 0; - - // add to tape - _datacorder.DataBlocks.Add(t); - - // advance to next block - _position += msgLen; - } - - /* length: [00,01]+02 - Offset Value Type Description - 0x00 - WORD Length of the whole block (without these two bytes) - 0x02 N BYTE Number of text strings - 0x03 - TEXT[N] List of text strings - - ---- - TEXT structure format - Offset Value Type Description - 0x00 - BYTE Text identification byte: - 00 - Full title - 01 - Software house/publisher - 02 - Author(s) - 03 - Year of publication - 04 - Language - 05 - Game/utility type - 06 - Price - 07 - Protection scheme/loader - 08 - Origin - FF - Comment(s) - 0x01 L BYTE Length of text string - 0x02 - CHAR[L] Text string in ASCII format - ---- - - Use this block at the beginning of the tape to identify the title of the game, author, publisher, year of publication, price (including - the currency), type of software (arcade adventure, puzzle, word processor, ...), protection scheme it uses (Speedlock 1, Alkatraz, ...) - and its origin (Original, Budget re-release, ...), etc. This block is built in a way that allows easy future expansion. - The block consists of a series of text strings. Each text has its identification number (which tells us what the text means) and then - the ASCII text. To make it possible to skip this block, if needed, the length of the whole block is at the beginning of it. - If all texts on the tape are in English language then you don't have to supply the 'Language' field - The information about what hardware the tape uses is in the 'Hardware Type' block, so no need for it here. */ - private void ProcessBlockID32(byte[] data) - { - TapeDataBlock t = new TapeDataBlock(); + t = new TapeDataBlock(); t.BlockID = 0x32; - t.DataPeriods = new List(); t.BlockDescription = BlockType.Archive_Info; - int blockLen = GetWordValue(data, _position); + blockLen = GetWordValue(data, _position); _position += 2; int stringCount = data[_position++]; @@ -1289,95 +1390,48 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum break; } - // add title to description - //t.BlockDescription += title; - // get string data string val = Encoding.ASCII.GetString(data, _position, strLen); - //t.BlockDescription += val + " \n"; + t.AddMetaData(BlockDescriptorTitle.Archive_Info, val); t.PauseInMS = 0; + t.PauseInTStates = 0; // advance to next string block - _position += strLen; + _position += strLen; } // add to tape _datacorder.DataBlocks.Add(t); + + // ready for next block + ZeroVars(); } - - /* length: [00]*03+01 - Offset Value Type Description - 0x00 N BYTE Number of machines and hardware types for which info is supplied - 0x01 - HWINFO[N] List of machines and hardware - - ---- - HWINFO structure format - Offset Value Type Description - 0x00 - BYTE Hardware type - 0x01 - BYTE Hardware ID - 0x02 - BYTE Hardware information: - 00 - The tape RUNS on this machine or with this hardware, - but may or may not use the hardware or special features of the machine. - 01 - The tape USES the hardware or special features of the machine, - such as extra memory or a sound chip. - 02 - The tape RUNS but it DOESN'T use the hardware - or special features of the machine. - 03 - The tape DOESN'T RUN on this machine or with this hardware. - ---- - - This blocks contains information about the hardware that the programs on this tape use. Please include only machines and hardware for - which you are 100% sure that it either runs (or doesn't run) on or with, or you know it uses (or doesn't use) the hardware or special - features of that machine. - If the tape runs only on the ZX81 (and TS1000, etc.) then it clearly won't work on any Spectrum or Spectrum variant, so there's no - need to list this information. - If you are not sure or you haven't tested a tape on some particular machine/hardware combination then do not include it in the list. - The list of hardware types and IDs is somewhat large, and may be found at the end of the format description. */ - private void ProcessBlockID33(byte[] data) + + /// + /// 0x35 - Custom Info Block + /// + private void ProcessBlockID35() { - // currently not implemented properly in ZXHawk + /* length: [10,11,12,13]+14 - TapeDataBlock t = new TapeDataBlock(); - t.BlockID = 0x33; - t.DataPeriods = new List(); - t.BlockDescription = BlockType.Hardware_Type; + Offset Value Type Description + 0x00 - CHAR[10] Identification string (in ASCII) + 0x10 L DWORD Length of the custom info + 0x14 - BYTE[L] Custom info - t.PauseInMS = 0; + This block can be used to save any information you want. For example, it might contain some information written by a utility, + extra settings required by a particular emulator, or even poke data. */ - // first byte contains number of HWINFOs - int infos = data[_position]; - - _position += 1; - - // now starts the HW infos (each block 3 bytes) - for (int i = 0; i < infos; i++) - { - _position += 3; - } - - // add to tape - _datacorder.DataBlocks.Add(t); - } - - /* length: [10,11,12,13]+14 - Offset Value Type Description - 0x00 - CHAR[10] Identification string (in ASCII) - 0x10 L DWORD Length of the custom info - 0x14 - BYTE[L] Custom info - - This block can be used to save any information you want. For example, it might contain some information written by a utility, - extra settings required by a particular emulator, or even poke data. */ - private void ProcessBlockID35(byte[] data) - { - TapeDataBlock t = new TapeDataBlock(); + t = new TapeDataBlock(); t.BlockID = 0x35; - t.DataPeriods = new List(); t.BlockDescription = BlockType.Custom_Info_Block; t.PauseInMS = 0; + t.PauseInTStates = 0; string info = Encoding.ASCII.GetString(data, _position, 0x10); - //t.BlockDescription = "[CUSTOM INFO: " + info + "]"; + t.AddMetaData(BlockDescriptorTitle.Custom_Info, info); _position += 0x10; int blockLen = BitConverter.ToInt32(data, _position); @@ -1388,21 +1442,29 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum // advance to next block _position += blockLen; + + // ready for next block + ZeroVars(); } - /* length: 09 - Offset Value Type Description - 0x00 - BYTE[9] Value: { "XTape!",0x1A,MajR,MinR } - Just skip these 9 bytes and you will end up on the next ID. - - This block is generated when you merge two ZX Tape files together. It is here so that you can easily copy the files together and use - them. Of course, this means that resulting file would be 10 bytes longer than if this block was not used. All you have to do - if you encounter this block ID is to skip next 9 bytes. - If you can avoid using this block for this purpose, then do so; it is preferable to use a utility to join the two files and - ensure that they are both of the higher version number. */ - private void ProcessBlockID5A(byte[] data) + /// + /// 0x5A - Glue Block + /// + private void ProcessBlockID5A() { - TapeDataBlock t = new TapeDataBlock(); + /* length: 09 + + Offset Value Type Description + 0x00 - BYTE[9] Value: { "XTape!",0x1A,MajR,MinR } + Just skip these 9 bytes and you will end up on the next ID. + + This block is generated when you merge two ZX Tape files together. It is here so that you can easily copy the files together and use + them. Of course, this means that resulting file would be 10 bytes longer than if this block was not used. All you have to do + if you encounter this block ID is to skip next 9 bytes. + If you can avoid using this block for this purpose, then do so; it is preferable to use a utility to join the two files and + ensure that they are both of the higher version number. */ + + t = new TapeDataBlock(); t.BlockID = 0x5A; t.DataPeriods = new List(); t.BlockDescription = BlockType.Glue_Block; @@ -1414,9 +1476,15 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum // advance to next block _position += 9; + + // ready for next block + ZeroVars(); } - private void ProcessUnidentifiedBlock(byte[] data) + /// + /// Skips an unknown block + /// + private void ProcessUnidentifiedBlock() { TapeDataBlock t = new TapeDataBlock(); t.BlockID = -2; @@ -1433,9 +1501,394 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum _position += 4; } + #region Not Implemented Yet + + /// + /// 0x33 - Hardware Type + /// + private void ProcessBlockID33() + { + /* length: [00]*03+01 + + Offset Value Type Description + 0x00 N BYTE Number of machines and hardware types for which info is supplied + 0x01 - HWINFO[N] List of machines and hardware + + ---- + HWINFO structure format + Offset Value Type Description + 0x00 - BYTE Hardware type + 0x01 - BYTE Hardware ID + 0x02 - BYTE Hardware information: + 00 - The tape RUNS on this machine or with this hardware, + but may or may not use the hardware or special features of the machine. + 01 - The tape USES the hardware or special features of the machine, + such as extra memory or a sound chip. + 02 - The tape RUNS but it DOESN'T use the hardware + or special features of the machine. + 03 - The tape DOESN'T RUN on this machine or with this hardware. + ---- + + This blocks contains information about the hardware that the programs on this tape use. Please include only machines and hardware for + which you are 100% sure that it either runs (or doesn't run) on or with, or you know it uses (or doesn't use) the hardware or special + features of that machine. + If the tape runs only on the ZX81 (and TS1000, etc.) then it clearly won't work on any Spectrum or Spectrum variant, so there's no + need to list this information. + If you are not sure or you haven't tested a tape on some particular machine/hardware combination then do not include it in the list. + The list of hardware types and IDs is somewhat large, and may be found at the end of the format description. */ + + // currently not implemented properly in ZXHawk + + t = new TapeDataBlock(); + t.BlockID = 0x33; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Hardware_Type; + + t.PauseInMS = 0; + t.PauseInTStates = 0; + + // first byte contains number of HWINFOs + int infos = data[_position]; + + _position += 1; + + // now starts the HW infos (each block 3 bytes) + for (int i = 0; i < infos; i++) + { + _position += 3; + } + + // add to tape + _datacorder.DataBlocks.Add(t); + + // ready for next block + ZeroVars(); + } + + /// + /// 0x31 - Message Block + /// + private void ProcessBlockID31() + { + /* length: [01]+02 + + Offset Value Type Description + 0x00 - BYTE Time (in seconds) for which the message should be displayed + 0x01 N BYTE Length of the text message + 0x02 - CHAR[N] Message that should be displayed in ASCII format + + This will enable the emulators to display a message for a given time. This should not stop the tape and it should not make silence. + If the time is 0 then the emulator should wait for the user to press a key. + The text message should: + stick to a maximum of 30 chars per line; + use single 0x0D (13 decimal) to separate lines; + stick to a maximum of 8 lines. + If you do not obey these rules, emulators may display your message in any way they like. */ + + // currently not implemented properly in ZXHawk + + t = new TapeDataBlock(); + t.BlockID = 0x31; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Message_Block; + + _position++; + + int msgLen = data[_position]; + _position++; + + string desc = Encoding.ASCII.GetString(data, _position, msgLen); + + t.Command = TapeCommand.SHOW_MESSAGE; + + t.PauseInMS = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += msgLen; + + // ready for next block + ZeroVars(); + } + + /// + /// 0x26 - Call Sequence + /// + private void ProcessBlockID26() + { + /* length: [00,01]*02+02 + + Offset Value Type Description + 0x00 N WORD Number of calls to be made + 0x02 - WORD[N] Array of call block numbers (relative-signed offsets) + + This block is an analogue of the CALL Subroutine statement. It basically executes a sequence of blocks that are somewhere else and + then goes back to the next block. Because more than one call can be normally used you can include a list of sequences to be called. + The 'nesting' of call blocks is also not allowed for the simplicity reasons. You can, of course, use the CALL blocks in the LOOP + sequences and vice versa. The value is relative for the obvious reasons - so that you can add some blocks in the beginning of the + file without disturbing the call values. Please take a look at 'Jump To Block' for reference on the values. */ + + // block processing not implemented for this - just gets added for informational purposes only + t = new TapeDataBlock(); + t.BlockID = 0x26; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Call_Sequence; + + int blockSize = 2 + 2 * GetWordValue(data, _position); + + t.PauseInMS = 0; + t.PauseInTStates = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += blockSize; + + // ready for next block + ZeroVars(); + } + + /// + /// 0x27 - Return From Sequence + /// + private void ProcessBlockID27() + { + /* length: 00 + + This block indicates the end of the Called Sequence. The next block played will be the block after the last CALL block (or the next Call, + if the Call block had multiple calls). + Again, this block has no body. */ + + // block processing not implemented for this - just gets added for informational purposes only + t = new TapeDataBlock(); + t.BlockID = 0x27; + t.BlockDescription = BlockType.Return_From_Sequence; + t.PauseInMS = 0; + t.PauseInTStates = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // ready for next block + ZeroVars(); + } + + /// + /// 0x28 - Select Block + /// + private void ProcessBlockID28() + { + /* length: [00,01]+02 + + Offset Value Type Description + 0x00 - WORD Length of the whole block (without these two bytes) + 0x02 N BYTE Number of selections + 0x03 - SELECT[N] List of selections + + ---- + SELECT structure format + Offset Value Type Description + 0x00 - WORD Relative Offset + 0x02 L BYTE Length of description text + 0x03 - CHAR[L] Description text (please use single line and max. 30 chars) + ---- + + This block is useful when the tape consists of two or more separately-loadable parts. With this block, you are able to select + one of the parts and the utility/emulator will start loading from that block. For example you can use it when the game has a + separate Trainer or when it is a multiload. Of course, to make some use of it the emulator/utility has to show a menu with the + selections when it encounters such a block. All offsets are relative signed words. */ + + // block processing not implemented for this - just gets added for informational purposes only + t = new TapeDataBlock(); + t.BlockID = 0x28; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Select_Block; + + int blockSize = 2 + GetWordValue(data, _position); + + t.PauseInMS = 0; + t.PauseInTStates = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += blockSize; + + // ready for next block + ZeroVars(); + } + + /// + /// 0x23 - Jump to Block + /// + private void ProcessBlockID23() + { + /* length: 02 + * + Offset Value Type Description + 0x00 - WORD Relative jump value + + This block will enable you to jump from one block to another within the file. The value is a signed short word + (usually 'signed short' in C); Some examples: + Jump 0 = 'Loop Forever' - this should never happen + Jump 1 = 'Go to the next block' - it is like NOP in assembler ;) + Jump 2 = 'Skip one block' + Jump -1 = 'Go to the previous block' + All blocks are included in the block count!. */ + + + // not implemented properly + + t = new TapeDataBlock(); + t.BlockID = 0x23; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Jump_to_Block; + + int relativeJumpValue = GetWordValue(data, _position); + string result = string.Empty; + + switch (relativeJumpValue) + { + case 0: + result = "Loop Forever"; + break; + case 1: + result = "To Next Block"; + break; + case 2: + result = "Skip One Block"; + break; + case -1: + result = "Go to Previous Block"; + break; + } + + //t.BlockDescription = "[JUMP BLOCK - " + result +"]"; + + t.PauseInMS = 0; + t.PauseInTStates = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += 2; + + // ready for next block + ZeroVars(); + } + + /// + /// 0x19 - Generalized Data Block + /// + private void ProcessBlockID19() + { + /* length: [00,01,02,03]+04 + * + Offset Value Type Description + 0x00 - DWORD Block length (without these four bytes) + 0x04 - WORD Pause after this block (ms) + 0x06 TOTP DWORD Total number of symbols in pilot/sync block (can be 0) + 0x0A NPP BYTE Maximum number of pulses per pilot/sync symbol + 0x0B ASP BYTE Number of pilot/sync symbols in the alphabet table (0=256) + 0x0C TOTD DWORD Total number of symbols in data stream (can be 0) + 0x10 NPD BYTE Maximum number of pulses per data symbol + 0x11 ASD BYTE Number of data symbols in the alphabet table (0=256) + 0x12 - SYMDEF[ASP] Pilot and sync symbols definition table + This field is present only if TOTP>0 + 0x12+ + (2*NPP+1)*ASP - PRLE[TOTP] Pilot and sync data stream + This field is present only if TOTP>0 + 0x12+ + (TOTP>0)*((2*NPP+1)*ASP)+ + TOTP*3 - SYMDEF[ASD] Data symbols definition table + This field is present only if TOTD>0 + 0x12+ + (TOTP>0)*((2*NPP+1)*ASP)+ + TOTP*3+ + (2*NPD+1)*ASD - BYTE[DS] Data stream + This field is present only if TOTD>0 + + This block has been specifically developed to represent an extremely wide range of data encoding techniques. + The basic idea is that each loading component (pilot tone, sync pulses, data) is associated to a specific sequence + of pulses, where each sequence (wave) can contain a different number of pulses from the others. + In this way we can have a situation where bit 0 is represented with 4 pulses and bit 1 with 8 pulses. + + ---- + SYMDEF structure format + Offset Value Type Description + 0x00 - BYTE Symbol flags + b0-b1: starting symbol polarity + 00: opposite to the current level (make an edge, as usual) - default + 01: same as the current level (no edge - prolongs the previous pulse) + 10: force low level + 11: force high level + 0x01 - WORD[MAXP] Array of pulse lengths. + + The alphabet is stored using a table where each symbol is a row of pulses. The number of columns (i.e. pulses) of the table is the + length of the longest sequence amongst all (MAXP=NPP or NPD, for pilot/sync or data blocks respectively); shorter waves are terminated by a + zero-length pulse in the sequence. + Any number of data symbols is allowed, so we can have more than two distinct waves; for example, imagine a loader which writes two bits at a + time by encoding them with four distinct pulse lengths: this loader would have an alphabet of four symbols, each associated to a specific + sequence of pulses (wave). + ---- + ---- + PRLE structure format + Offset Value Type Description + 0x00 - BYTE Symbol to be represented + 0x01 - WORD Number of repetitions + + Most commonly, pilot and sync are repetitions of the same pulse, thus they are represented using a very simple RLE encoding structure which stores + the symbol and the number of times it must be repeated. + Each symbol in the data stream is represented by a string of NB bits of the block data, where NB = ceiling(Log2(ASD)). + Thus the length of the whole data stream in bits is NB*TOTD, or in bytes DS=ceil(NB*TOTD/8). + ---- */ + + // not currently implemented properly + + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x19; + t.BlockDescription = BlockType.Generalized_Data_Block; + t.DataPeriods = new List(); + + int blockLen = GetInt32(data, _position); + _position += 4; + + int pause = GetWordValue(data, _position); + _position += 2; + + int totp = GetInt32(data, _position); + _position += 4; + + int npp = data[_position++]; + + int asp = data[_position++]; + + int totd = GetInt32(data, _position); + _position += 4; + + int npd = data[_position++]; + + int asd = data[_position++]; + + // add the block + _datacorder.DataBlocks.Add(t); + + // advance the position to the next block + _position += blockLen; + + // ready for next block + ZeroVars(); + } + + // These mostly should be ignored by ZXHawk - here for completeness - private void ProcessBlockID16(byte[] data) + private void ProcessBlockID16() { // zxhawk will not implement this block. it will however handle it so subsequent blocks can be parsed TapeDataBlock t = new TapeDataBlock(); @@ -1453,7 +1906,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum _position += blockLen; } - private void ProcessBlockID17(byte[] data) + private void ProcessBlockID17() { // zxhawk will not implement this block. it will however handle it so subsequent blocks can be parsed TapeDataBlock t = new TapeDataBlock(); @@ -1471,7 +1924,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum _position += blockLen; } - private void ProcessBlockID34(byte[] data) + private void ProcessBlockID34() { // currently not implemented properly in ZXHawk @@ -1499,7 +1952,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum Only .Z80 and .SNA snapshots are supported for compatibility reasons! The emulator should take care of that the snapshot is not taken while the actual Tape loading is taking place (which doesn't do much sense). And when an emulator encounters the snapshot block it should load it and then continue with the next block. */ - private void ProcessBlockID40(byte[] data) + private void ProcessBlockID40() { // currently not implemented properly in ZXHawk @@ -1522,310 +1975,125 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum _position += blockLen; } + + #endregion + /// - /// Used to process either a standard or turbo data block + /// Sets up variables for the next block /// - private TapeDataBlock DecodeDataBlock - ( - TapeDataBlock block, - byte[] blockdata, - DataBlockType dataBlockType, - int pauseAfterBlock, - int pilotCount, - int pilotToneLength = 2168, - int sync1PulseLength = 667, - int sync2PulseLength = 735, - int bit0PulseLength = 855, - int bit1PulseLength = 1710, - int bitsInLastByte = 8 - ) + private void ZeroVars() { - // first get the block description - string description = string.Empty; + pilotToneLength = 0; + pilotCount = 0; + sync1PulseLength = 0; + sync2PulseLength = 0; + bit0PulseLength = 0; + bit1PulseLength = 0; + bitsInLastByte = 0; + pauseLen = 0; + blockLen = 0; + bitCount = 0; + } - // process the type byte - /* (The type is 0,1,2 or 3 for a Program, Number array, Character array or Code file. - A SCREEN$ file is regarded as a Code file with start address 16384 and length 6912 decimal. - If the file is a Program file, parameter 1 holds the autostart line number (or a number >=32768 if no LINE parameter was given) - and parameter 2 holds the start of the variable area relative to the start of the program. If it's a Code file, parameter 1 holds - the start of the code block when saved, and parameter 2 holds 32768. For data files finally, the byte at position 14 decimal holds the variable name.) - */ - - int blockSize = blockdata.Length; - - // don't get description info for Pure Data Blocks - if (dataBlockType != DataBlockType.Pure) + /// + /// Decode method for standard, turbo and pure data blocks + /// + private void DecodeData() + { + // generate pilot tone + while (pilotCount > 0) { - if (blockdata[0] == 0x00 && blockSize == 19) - { - string fileName = Encoding.ASCII.GetString(blockdata.Skip(2).Take(10).ToArray()).Trim(); - string type = "Unknown Type"; - StringBuilder sb = new StringBuilder(); + t.DataPeriods.Add(pilotToneLength); + ToggleSignal(); + t.DataLevels.Add(signal); + t.PulseDescription.Add("Pilot " + pilotCount); + pilotCount--; + } - var param1 = GetWordValue(blockdata, 12); - var param2 = GetWordValue(blockdata, 14); + // syncro pulses + if (sync1PulseLength > 0) + { + t.DataPeriods.Add(sync1PulseLength); + ToggleSignal(); + t.DataLevels.Add(signal); + t.PulseDescription.Add("Syncro 1"); + } - // header block - examine first byte of header - if (blockdata[1] == 0) - { - type = "Program"; - sb.Append(type + ": "); - sb.Append(fileName + " "); - } - else if (blockdata[1] == 1) - { - type = "NumArray"; - sb.Append(type + ": "); - sb.Append(fileName + " "); - } - else if (blockdata[1] == 2) - { - type = "CharArray"; - sb.Append(type + ": "); - sb.Append(fileName + " "); - } - else if (blockdata[1] == 3) - { - type = "Code"; - sb.Append(type + ": "); - sb.Append(fileName + " "); - } - } - else if (blockdata[0] == 0xff) + if (sync2PulseLength > 0) + { + t.DataPeriods.Add(sync2PulseLength); + ToggleSignal(); + t.DataLevels.Add(signal); + t.PulseDescription.Add("Syncro 2"); + } + + // generate periods for actual data + while (blockLen > 0) + { + if (blockLen == 1) { - // data block - description = "Data Block " + (blockSize - 2) + "bytes"; - block.AddMetaData(BlockDescriptorTitle.Data_Bytes, (blockSize - 2) + " Bytes"); + // last byte - need to look at number of bits + bitCount = bitsInLastByte; } else { - // some other type (turbo data etc..) - description = $"#{blockdata[0].ToString("X2")} block, {blockSize} bytes"; - //description += (crc != 0) ? $", crc bad (#{crcFile:X2}!=#{crcValue:X2})" : ", crc ok"; - block.AddMetaData(BlockDescriptorTitle.Undefined, description); - } - /* - if (blockdata[0] == 0x00 && blockSize == 19 && (blockdata[1] == 0x00) || (blockdata[1] == 3 && blockdata.Length > 3)) - { - if (dataBlockType != DataBlockType.Turbo) - { - // This is the program header - string fileName = Encoding.ASCII.GetString(blockdata.Skip(2).Take(10).ToArray()).Trim(); - - string type = ""; - if (blockdata[0] == 0x00) - { - type = "Program"; - block.AddMetaData(BlockDescriptorTitle.Program, fileName); - } - else - { - type = "Bytes"; - block.AddMetaData(BlockDescriptorTitle.Bytes, fileName); - } - - // now build the description string - StringBuilder sb = new StringBuilder(); - sb.Append(type + ": "); - sb.Append(fileName + " "); - sb.Append(GetWordValue(blockdata, 14)); - sb.Append(':'); - sb.Append(GetWordValue(blockdata, 12)); - description = sb.ToString(); - } - } - else if (blockdata[0] == 0xFF) - { - // this is a data block - description = "Data Block " + (blockSize - 2) + "bytes"; - block.AddMetaData(BlockDescriptorTitle.Data_Bytes, (blockSize - 2).ToString() + " Bytes"); - } - else - { - // other type - description = $"#{blockdata[0]:X2} block, {blockSize} bytes"; - //description += (crc != 0) ? $", crc bad (#{crcFile:X2}!=#{crcValue:X2})" : ", crc ok"; - block.AddMetaData(BlockDescriptorTitle.Undefined, description); - } - */ - } - - // update metadata - switch (dataBlockType) - { - case DataBlockType.Standard: - case DataBlockType.Turbo: - - if (dataBlockType == DataBlockType.Standard) - block.BlockDescription = BlockType.Standard_Speed_Data_Block; - if (dataBlockType == DataBlockType.Turbo) - block.BlockDescription = BlockType.Turbo_Speed_Data_Block; - - block.AddMetaData(BlockDescriptorTitle.Pilot_Pulse_Length, pilotToneLength + " T-States"); - block.AddMetaData(BlockDescriptorTitle.Pilot_Pulse_Count, pilotCount + " Pulses"); - block.AddMetaData(BlockDescriptorTitle.First_Sync_Length, sync1PulseLength + " T-States"); - block.AddMetaData(BlockDescriptorTitle.Second_Sync_Length, sync2PulseLength + " T-States"); - break; - - case DataBlockType.Pure: - block.BlockDescription = BlockType.Pure_Data_Block; - break; - } - - block.AddMetaData(BlockDescriptorTitle.Zero_Bit_Length, bit0PulseLength + " T-States"); - block.AddMetaData(BlockDescriptorTitle.One_Bit_Length, bit1PulseLength + " T-States"); - block.AddMetaData(BlockDescriptorTitle.Data_Length, blockSize + " Bytes"); - block.AddMetaData(BlockDescriptorTitle.Bits_In_Last_Byte, bitsInLastByte + " Bits"); - block.AddMetaData(BlockDescriptorTitle.Pause_After_Data, pauseAfterBlock + " ms"); - - // calculate period information - List dataPeriods = new List(); - - // generate pilot pulses - - if (pilotCount > 0) - { - for (int i = 0; i < pilotCount; i++) - { - dataPeriods.Add(pilotToneLength); + // this is a full byte + bitCount = 8; } - // add syncro pulses - dataPeriods.Add(sync1PulseLength); - dataPeriods.Add(sync2PulseLength); - } + // get the byte to be processed + var currByte = data[_position]; - int pos = 0; - - // add bit0 and bit1 periods - for (int i = 0; i < blockSize - 1; i++, pos++) - { - for (byte b = 0x80; b != 0; b >>= 1) + // do the bits + int currBitLength = 0; + while (bitCount > 0) { - if ((blockdata[i] & b) != 0) - dataPeriods.Add(bit1PulseLength); + if ((currByte & 0x80) != 0) + { + // this is a '1' + currBitLength = bit1PulseLength; + } else - dataPeriods.Add(bit0PulseLength); - if ((blockdata[i] & b) != 0) - dataPeriods.Add(bit1PulseLength); - else - dataPeriods.Add(bit0PulseLength); + { + // this is a '0' + currBitLength = bit0PulseLength; + } + + // play two pulses (a whole period) to generate an edge + t.DataPeriods.Add(currBitLength); + ToggleSignal(); + t.DataLevels.Add(signal); + t.PulseDescription.Add("Data Pulse 1/2 (Byte: " + blockLen + " / Bit: " + bitCount + ")"); + + t.DataPeriods.Add(currBitLength); + ToggleSignal(); + t.DataLevels.Add(signal); + t.PulseDescription.Add("Data Pulse 2/2 (Byte: " + blockLen + " / Bit: " + bitCount + ")"); + + currByte <<= 1; + bitCount--; } + + blockLen--; + _position++; } - // add the last byte - for (byte c = 0x80; c != (byte)(0x80 >> bitsInLastByte); c >>= 1) - { - if ((blockdata[pos] & c) != 0) - dataPeriods.Add(bit1PulseLength); - else - dataPeriods.Add(bit0PulseLength); - if ((blockdata[pos] & c) != 0) - dataPeriods.Add(bit1PulseLength); - else - dataPeriods.Add(bit0PulseLength); - } - - // add block pause if pause is not 0 - if (pauseAfterBlock != 0) - { - block.PauseInMS = pauseAfterBlock; - //int actualPause = pauseAfterBlock * 3500; - //dataPeriods.Add(actualPause); - } - - // add to the tapedatablock object - block.DataPeriods = dataPeriods; - - // add the raw data - block.BlockData = blockdata; - - return block; + // handle pause after block + DoPause(); } /// - /// Used to process either a standard or turbo data block + /// Generates pause data /// - private TapeDataBlock DecodeDataBlock - ( - TapeDataBlock block, - byte[] blockData, - DataBlockType dataBlockType, - int pauseAfterBlock, - int pilotToneLength = 2168, - int sync1PulseLength = 667, - int sync2PulseLength = 735, - int bit0PulseLength = 855, - int bit1PulseLength = 1710, - int bitsInLastByte = 8 - ) + private void DoPause() { - - // pilot count needs to be ascertained from flag byte - int pilotCount; - if (blockData[0] < 128) - pilotCount = 8063; - else - pilotCount = 3223; - - // now we can decode - var nBlock = DecodeDataBlock - ( - block, - blockData, - dataBlockType, - pauseAfterBlock, - pilotCount, - pilotToneLength, - sync1PulseLength, - sync2PulseLength, - bit0PulseLength, - bit1PulseLength, - bitsInLastByte - ); - - - return nBlock; - } - - /// - /// If neccessary a seperate PAUSE block will be created - /// - private void CreatePauseBlock(TapeDataBlock original) - { - if (original.PauseInMS > 0) + if (pauseLen > 0) { - TapeDataBlock pBlock = new TapeDataBlock(); - pBlock.DataPeriods = new List(); - pBlock.BlockDescription = BlockType.PAUSE_BLOCK; - pBlock.PauseInMS = 0; - var pauseInTStates = TranslatePause(original.PauseInMS); - - pBlock.AddMetaData(BlockDescriptorTitle.Block_ID, pauseInTStates + " cycles"); - - int by1000 = pauseInTStates / 70000; - int rem1000 = pauseInTStates % 70000; - - if (by1000 > 1) - { - pBlock.DataPeriods.Add(35000); - pBlock.DataPeriods.Add(pauseInTStates - 35000); - } - else - { - pBlock.DataPeriods.Add(pauseInTStates); - pBlock.DataPeriods.Add(0); - } - - _datacorder.DataBlocks.Add(pBlock); + t.DataPeriods.Add(t.PauseInTStates); + ToggleSignal(); + t.DataLevels.Add(signal); + t.PulseDescription.Add("Pause after block: " + t.PauseInTStates); } } } - - public enum DataBlockType - { - Standard, - Turbo, - Pure - } } diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeDataBlock.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeDataBlock.cs index 80ddb7e31e..adb8605455 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeDataBlock.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeDataBlock.cs @@ -9,6 +9,12 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum /// public class TapeDataBlock { + public TapeDataBlock() + { + DataPeriods = new List(); + DataLevels = new List(); + } + /// /// Either the TZX block ID, or -1 in the case of non-tzx blocks /// @@ -77,6 +83,13 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum /// public List DataPeriods = new List(); + /// + /// List containing the pulse levels (in relation to the pulse timing values) + /// + public List DataLevels = new List(); + + public List PulseDescription = new List(); + public bool InitialPulseLevel; /// @@ -91,7 +104,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum } /// - /// The defined post-block pause + /// The defined post-block pause in MS /// private int _pauseInMS; public int PauseInMS @@ -100,6 +113,15 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum set => _pauseInMS = value; } + /// + /// The defined post-block pause in T-States + /// + private int _pauseInTStates; + public int PauseInTStates + { + get { return _pauseInTStates; } + set { _pauseInTStates = value; } + } /// /// Returns the data periods as an array @@ -226,6 +248,10 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum Pulse_Length, Pulse_Count, + TStatesPerSample, + + Archive_Info, + Custom_Info, Text_Description, Title, Publisher, diff --git a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/WAV/WavConverter.cs b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/WAV/WavConverter.cs index 687312a0ca..6c3a8959bb 100644 --- a/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/WAV/WavConverter.cs +++ b/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/WAV/WavConverter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum @@ -85,7 +86,8 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum WavStreamReader reader = new WavStreamReader(stream); - int rate = (69888 * 50) / reader.Header.sampleRate; + //int rate = (int)(((double)69888 * (double)50) / (double)reader.Header.sampleRate); + int rate = (int)((double)(3500000) / (double)reader.Header.sampleRate); int smpCounter = 0; int state = reader.ReadNext(); @@ -94,6 +96,9 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum t.BlockDescription = BlockType.WAV_Recording; t.BlockID = 0; t.DataPeriods = new List(); + t.DataLevels = new List(); + + bool currLevel = false; for (int i = 0; i < reader.Count; i++) { @@ -101,16 +106,33 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum smpCounter++; if ((state < 0 && sample < 0) || (state >= 0 && sample >= 0)) continue; - t.DataPeriods.Add(smpCounter * rate); + t.DataPeriods.Add((int)(((double)smpCounter * (double)rate) / (double)0.9838560885608856)); + currLevel = !currLevel; + t.DataLevels.Add(currLevel); smpCounter = 0; state = sample; } // add closing period t.DataPeriods.Add((69888 * 50) / 10); + currLevel = false; + t.DataLevels.Add(currLevel); // add to datacorder _datacorder.DataBlocks.Add(t); + + StringBuilder export = new StringBuilder(); + foreach (var b in _datacorder.DataBlocks) + { + for (int i = 0; i < b.DataPeriods.Count(); i++) + { + export.Append(b.DataPeriods[i].ToString()); + export.Append("\t\t"); + export.AppendLine(b.DataLevels[i].ToString()); + } + } + + string o = export.ToString(); } } }