using System; using System.Collections.Generic; using RedBookPlayer.Models.Hardware.Karaoke; namespace RedBookPlayer.Models.Hardware { /// /// Represents a single packet of subcode data /// /// internal class SubchannelPacket { public byte Command { get; private set; } public SubchannelInstruction Instruction { get; private set; } public byte[] ParityQ { get; private set; } = new byte[2]; public byte[] Data { get; private set; } = new byte[16]; public byte[] ParityP { get; private set; } = new byte[4]; /// /// Create a new subchannel packet from a byte array /// public SubchannelPacket(byte[] bytes) { if(bytes == null || bytes.Length != 24) return; this.Command = bytes[0]; this.Instruction = (SubchannelInstruction)bytes[1]; Array.Copy(bytes, 2, this.ParityQ, 0, 2); Array.Copy(bytes, 4, this.Data, 0, 16); Array.Copy(bytes, 20, this.ParityP, 0, 4); } #region Standard Handling /// /// Convert the data into separate named subchannels /// public Dictionary ConvertData() { if(this.Data == null || this.Data.Length != 16) return null; // Create the output dictionary for the formatted data Dictionary formattedData = new Dictionary { ['P'] = new byte[2], ['Q'] = new byte[2], ['R'] = new byte[2], ['S'] = new byte[2], ['T'] = new byte[2], ['U'] = new byte[2], ['V'] = new byte[2], ['W'] = new byte[2], }; // Loop through all bytes in the subchannel data and populate int index = -1; for(int i = 0; i < 16; i++) { // Get the modulo value of the current byte int modValue = i % 8; if(modValue == 0) index++; // Retrieve the next byte byte b = this.Data[i]; // Set the respective bit in the new byte data formattedData['P'][index] |= (byte)(HasBitSet(b, 7) ? 1 << (7 - modValue) : 0); formattedData['Q'][index] |= (byte)(HasBitSet(b, 6) ? 1 << (7 - modValue) : 0); formattedData['R'][index] |= (byte)(HasBitSet(b, 5) ? 1 << (7 - modValue) : 0); formattedData['S'][index] |= (byte)(HasBitSet(b, 4) ? 1 << (7 - modValue) : 0); formattedData['T'][index] |= (byte)(HasBitSet(b, 3) ? 1 << (7 - modValue) : 0); formattedData['U'][index] |= (byte)(HasBitSet(b, 2) ? 1 << (7 - modValue) : 0); formattedData['V'][index] |= (byte)(HasBitSet(b, 1) ? 1 << (7 - modValue) : 0); formattedData['W'][index] |= (byte)(HasBitSet(b, 0) ? 1 << (7 - modValue) : 0); } return formattedData; } /// /// Check if a bit is set in a byte /// /// Byte value to check /// Index of the bit to check /// True if the bit was set, false otherwise private bool HasBitSet(byte value, int bitIndex) => (value & (1 << bitIndex)) != 0; #endregion #region CD+G Handling /// /// Determine if a packet is CD+G data /// public bool IsCDGPacket() { byte lowerSixBits = (byte)(this.Command & 0x3F); return lowerSixBits == 0x09; } /// /// Read packet data according to the instruction, if possible /// /// Supported object created from data, null on error public object ReadData() { if(!IsCDGPacket()) return null; return (this.Instruction) switch { SubchannelInstruction.MemoryPreset => new MemPreset(this.Data), SubchannelInstruction.BorderPreset => new BorderPreset(this.Data), SubchannelInstruction.TileBlockNormal => new TileBlock(this.Data), SubchannelInstruction.ScrollPreset => new Scroll(this.Data), SubchannelInstruction.ScrollCopy => new Scroll(this.Data), SubchannelInstruction.DefineTransparentColor => null, // Undefined in documentation SubchannelInstruction.LoadColorTableLower => new LoadCLUT(this.Data), SubchannelInstruction.LoadColorTableUpper => new LoadCLUT(this.Data), SubchannelInstruction.TileBlockXOR => new TileBlock(this.Data), _ => null, }; } #endregion } }