diff --git a/RedBookPlayer.Models/Enums.cs b/RedBookPlayer.Models/Enums.cs index 5619e11..d76f859 100644 --- a/RedBookPlayer.Models/Enums.cs +++ b/RedBookPlayer.Models/Enums.cs @@ -85,6 +85,27 @@ namespace RedBookPlayer.Models All, } + /// + /// Determine how to scroll + /// + public enum ScrollCommand + { + /// + /// No scrolling + /// + NoScroll = 0, + + /// + /// Scroll 6 pixels in the positive direction (right/down) + /// + Positive = 1, + + /// + /// Scroll 6 pixels in the negative direction (left/up) + /// + Negative = 2, + } + /// /// Determine how to handle different sessions /// @@ -100,4 +121,56 @@ namespace RedBookPlayer.Models /// FirstSessionOnly = 1, } + + /// + /// Known set of subchannel instructions + /// + /// + public enum SubchannelInstruction : byte + { + /// + /// Set the screen to a particular color. + /// + MemoryPreset = 1, + + /// + /// Set the border of the screen to a particular color. + /// + BorderPreset = 2, + + /// + /// Load a 12 x 6, 2 color tile and display it normally. + /// + TileBlockNormal = 6, + + /// + /// Scroll the image, filling in the new area with a color. + /// + ScrollPreset = 20, + + /// + /// Scroll the image, rotating the bits back around. + /// + ScrollCopy = 24, + + /// + /// Define a specific color as being transparent. + /// + DefineTransparentColor = 28, + + /// + /// Load in the lower 8 entries of the color table. + /// + LoadColorTableLower = 30, + + /// + /// Load in the upper 8 entries of the color table. + /// + LoadColorTableUpper = 31, + + /// + /// Load a 12 x 6, 2 color tile and display it using the XOR method. + /// + TileBlockXOR = 38, + } } \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/Karaoke/BorderPreset.cs b/RedBookPlayer.Models/Hardware/Karaoke/BorderPreset.cs new file mode 100644 index 0000000..cd44d96 --- /dev/null +++ b/RedBookPlayer.Models/Hardware/Karaoke/BorderPreset.cs @@ -0,0 +1,26 @@ +using System; + +namespace RedBookPlayer.Models.Hardware.Karaoke +{ + /// + internal class BorderPreset + { + // Only lower 4 bits are used, mask with 0x0F + public byte Color { get; private set; } + + public byte[] Filler { get; private set; } = new byte[15]; + + /// + /// Interpret subchannel packet data as Border Preset + /// + public BorderPreset(byte[] bytes) + { + if(bytes == null || bytes.Length != 16) + return; + + this.Color = (byte)(bytes[0] & 0x0F); + + Array.Copy(bytes, 1, this.Filler, 0, 15); + } + } +} \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs b/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs new file mode 100644 index 0000000..37e81fe --- /dev/null +++ b/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs @@ -0,0 +1,360 @@ +using System; + +namespace RedBookPlayer.Models.Hardware.Karaoke +{ + /// + internal class KaraokeDisplay + { + /// + /// Display data as a 2-dimensional byte array + /// + /// + /// Coordinate (0,0) is the upper left corner of the display + /// Coordinate (299, 215) is the lower right corner of the display + /// + // CONFLICTING INFO: + /// + /// In the top part of the document, it states: + /// In the CD+G system, 16 color graphics are displayed on a raster field which is + /// 300 x 216 pixels in size. The middle 294 x 204 area is within the TV's + /// "safe area", and that is where the graphics are displayed. The outer border is + /// set to a solid color. The colors are stored in a 16 entry color lookup table. + /// + /// And in the bottom part of the document around CDG_BorderPreset, it states: + /// Color refers to a color to clear the screen to. The border area of the screen + /// should be cleared to this color. The border area is the area contained with a + /// rectangle defined by (0,0,300,216) minus the interior pixels which are contained + /// within a rectangle defined by (6,12,294,204). + /// + /// With both of these in mind, does that mean that the "drawable" area is: + /// a) (3, 6, 297, 210) [Dimensions of 294 x 204] + /// b) (6, 12, 294, 204) [Dimensions of 288 x 192] + /// + public byte[,] DisplayData { get; private set; } + + /// + /// Current 16-entry color table + /// + /// + /// Each color entry has the following format: + /// + /// [---high byte---] [---low byte----] + /// 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + /// X X r r r r g g X X g g b b b b + /// + /// Note that P and Q channel bits need to be masked off (they are marked + /// here with Xs. + /// + public short[] ColorTable { get; private set; } + + /// + /// Color currently defined as transparent + /// + public byte TransparentColor { get; private set; } + + /// + /// Create a new, blank karaoke display + /// + public KaraokeDisplay() + { + this.DisplayData = new byte[300,216]; + this.ColorTable = new short[16]; + this.TransparentColor = 0x00; + } + + /// + /// Print the current color map to console + /// + public void DebugPrintScreen() + { + string screenDump = string.Empty; + + for(int y = 0; y < 216; y++) + { + for(int x = 0; x < 300; x++) + screenDump += $"{this.DisplayData[x,y]:X}"; + + screenDump += Environment.NewLine; + } + } + + /// + /// Process a subchannel packet and update the display as necessary + /// + /// Subchannel packet data to process + public void ProcessData(SubchannelPacket packet) + { + if(!packet.IsCDGPacket()) + return; + + switch(packet.Instruction) + { + case SubchannelInstruction.MemoryPreset: + var memoryPreset = new MemPreset(packet.Data); + SetScreenColor(memoryPreset); + break; + + case SubchannelInstruction.BorderPreset: + var borderPreset = new BorderPreset(packet.Data); + SetBorderColor(borderPreset); + break; + + case SubchannelInstruction.TileBlockNormal: + var tileBlockNormal = new TileBlock(packet.Data); + LoadTileBlock(tileBlockNormal, false); + break; + + case SubchannelInstruction.ScrollPreset: + var scrollPreset = new Scroll(packet.Data); + ScrollDisplay(scrollPreset, false); + break; + + case SubchannelInstruction.ScrollCopy: + var scrollCopy = new Scroll(packet.Data); + ScrollDisplay(scrollCopy, true); + break; + + case SubchannelInstruction.DefineTransparentColor: + var transparentColor = new BorderPreset(packet.Data); + this.TransparentColor = transparentColor.Color; + break; + + case SubchannelInstruction.LoadColorTableLower: + var loadColorTableLower = new LoadCLUT(packet.Data); + LoadColorTable(loadColorTableLower, false); + break; + + case SubchannelInstruction.LoadColorTableUpper: + var loadColorTableUpper = new LoadCLUT(packet.Data); + LoadColorTable(loadColorTableUpper, true); + break; + + case SubchannelInstruction.TileBlockXOR: + var tileBlockXor = new TileBlock(packet.Data); + LoadTileBlock(tileBlockXor, true); + break; + }; + } + + #region Command Processors + + /// + /// Set the screen to a particular color + /// + /// MemPreset with the new data + /// True if all subchannel data is present, false otherwise + /// + /// It is unclear if this is supposed to set the entire screen to the same color or + /// if it is only setting the interior pixels. To err on the side of caution, this sets + /// the viewable area only. + /// + /// The area that is considered the border is unclear. Please see the remarks + /// on for more details. + /// + private void SetScreenColor(MemPreset memPreset, bool consistentData = false) + { + if(memPreset == null) + return; + + // Skip in a consistent data case + if(consistentData && memPreset.Repeat > 0) + return; + + for(int x = 3; x < 297; x++) + for(int y = 6; y < 210; y++) + this.DisplayData[x,y] = memPreset.Color; + } + + /// + /// Set the border to a particular color + /// + /// BorderPreset with the new data + /// + /// The area that is considered the border is unclear. Please see the remarks + /// on for more details. + /// + private void SetBorderColor(BorderPreset borderPreset) + { + if(borderPreset == null) + return; + + for(int x = 0; x < 3; x++) + for(int y = 0; y < 216; y++) + this.DisplayData[x,y] = borderPreset.Color; + + for(int x = 297; x < 300; x++) + for(int y = 0; y < 216; y++) + this.DisplayData[x,y] = borderPreset.Color; + + for(int x = 0; x < 300; x++) + for(int y = 0; y < 6; y++) + this.DisplayData[x,y] = borderPreset.Color; + + for(int x = 0; x < 300; x++) + for(int y = 210; y < 216; y++) + this.DisplayData[x,y] = borderPreset.Color; + } + + /// + /// Load a block of pixels with a certain pattern + /// + /// TileBlock with the pattern data + /// + /// If true, the color values are combined with the color values + /// that are already onscreen using the XOR operator + /// + private void LoadTileBlock(TileBlock tileBlock, bool xor) + { + if(tileBlock == null) + return; + + // Extract out the "bitmap" into a byte pattern + byte[,] pattern = new byte[12,6]; + for(int i = 0; i < tileBlock.TilePixels.Length; i++) + { + byte b = tileBlock.TilePixels[i]; + pattern[i,0] = (byte)(b & (1 << 0)); + pattern[i,1] = (byte)(b & (1 << 1)); + pattern[i,2] = (byte)(b & (1 << 2)); + pattern[i,3] = (byte)(b & (1 << 3)); + pattern[i,4] = (byte)(b & (1 << 4)); + pattern[i,5] = (byte)(b & (1 << 5)); + } + + // Now load the bitmap starting in the correct place + for(int x = 0; x < 12; x++) + for(int y = 0; y < 6; y++) + { + int adjustedX = x + tileBlock.Column; + int adjustedY = y + tileBlock.Row; + byte colorIndex = pattern[x,y] == 0 ? tileBlock.Color0 : tileBlock.Color1; + + if(xor) + this.DisplayData[adjustedX, adjustedY] = (byte)(colorIndex ^ this.DisplayData[adjustedX, adjustedY]); + else + this.DisplayData[adjustedX, adjustedY] = colorIndex; + } + } + + /// + /// Scroll the display according to the instruction + /// + /// Scroll with the new data + /// True if data wraps around on scroll, false if filled by a solid color + private void ScrollDisplay(Scroll scroll, bool copy) + { + if(scroll == null || scroll.HScrollOffset < 0 || scroll.VScrollOffset < 0) + return; + + // Derive the scroll values based on offsets + int hOffsetTotal = 6 + scroll.HScrollOffset; + int vOffsetTotal = 12 + scroll.VScrollOffset; + + // If we're scrolling horizontally + if(scroll.HScrollCommand == ScrollCommand.Positive + || (scroll.HScrollCommand == ScrollCommand.NoScroll && scroll.HScrollOffset > 0)) + { + for(int y = 0; y < 216; y++) + { + byte[] overflow = new byte[hOffsetTotal]; + + for(int x = 299; x >= 0; x--) + { + if(x + hOffsetTotal >= 300) + overflow[(x + hOffsetTotal) % 300] = this.DisplayData[x,y]; + else + this.DisplayData[x + hOffsetTotal, y] = this.DisplayData[x,y]; + } + + // Fill in the now-empty pixels + for(int i = 0; i < hOffsetTotal; i++) + this.DisplayData[i,y] = copy ? overflow[i] : scroll.Color; + } + } + else if(scroll.HScrollCommand == ScrollCommand.Negative) + { + for(int y = 0; y < 216; y++) + { + byte[] overflow = new byte[hOffsetTotal]; + + for(int x = 0; x < 300; x++) + { + if(x - hOffsetTotal < 0) + overflow[x] = this.DisplayData[x,y]; + else + this.DisplayData[x - hOffsetTotal, y] = this.DisplayData[x,y]; + } + + // Fill in the now-empty pixels + for(int i = 299; i > 299 - hOffsetTotal; i++) + this.DisplayData[i,y] = copy ? overflow[i] : scroll.Color; + } + } + + // If we're scrolling vertically + if(scroll.VScrollCommand == ScrollCommand.Positive + || (scroll.VScrollCommand == ScrollCommand.NoScroll && scroll.VScrollOffset > 0)) + { + for(int x = 0; x < 300; x++) + { + byte[] overflow = new byte[vOffsetTotal]; + + for(int y = 215; y >= 0; y--) + { + if(y + vOffsetTotal >= 216) + overflow[(y + vOffsetTotal) % 216] = this.DisplayData[x,y]; + else + this.DisplayData[x, y + vOffsetTotal] = this.DisplayData[x,y]; + } + + // Fill in the now-empty pixels + for(int i = 0; i < vOffsetTotal; i++) + this.DisplayData[x,i] = copy ? overflow[i] : scroll.Color; + } + } + else if(scroll.VScrollCommand == ScrollCommand.Negative) + { + for(int x = 0; x < 300; x++) + { + byte[] overflow = new byte[vOffsetTotal]; + + for(int y = 0; y < 216; y++) + { + if(y - vOffsetTotal < 0) + overflow[y] = this.DisplayData[x,y]; + else + this.DisplayData[x, y - vOffsetTotal] = this.DisplayData[x,y]; + } + + // Fill in the now-empty pixels + for(int i = 215; i > 215 - vOffsetTotal; i++) + this.DisplayData[x,i] = copy ? overflow[i] : scroll.Color; + } + } + } + + /// + /// Load either the upper or lower half of the color table + /// + /// Color table data to load + /// True for colors 8-15, false for colors 0-7 + private void LoadColorTable(LoadCLUT tableData, bool upper) + { + if(tableData == null) + return; + + // Load the color table data directly + int start = upper ? 8 : 0; + this.ColorTable[start] = tableData.ColorSpec[0]; + this.ColorTable[start + 1] = tableData.ColorSpec[1]; + this.ColorTable[start + 2] = tableData.ColorSpec[2]; + this.ColorTable[start + 3] = tableData.ColorSpec[3]; + this.ColorTable[start + 4] = tableData.ColorSpec[4]; + this.ColorTable[start + 5] = tableData.ColorSpec[5]; + this.ColorTable[start + 6] = tableData.ColorSpec[6]; + this.ColorTable[start + 7] = tableData.ColorSpec[7]; + } + + #endregion + } +} \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/Karaoke/LoadCLUT.cs b/RedBookPlayer.Models/Hardware/Karaoke/LoadCLUT.cs new file mode 100644 index 0000000..e563a6e --- /dev/null +++ b/RedBookPlayer.Models/Hardware/Karaoke/LoadCLUT.cs @@ -0,0 +1,23 @@ +using System; + +namespace RedBookPlayer.Models.Hardware.Karaoke +{ + /// + internal class LoadCLUT + { + // AND with 0x3F3F to clear P and Q channel + public short[] ColorSpec { get; private set; } = new short[8]; + + /// + /// Interpret subchannel packet data as Load Color Lookup Table + /// + public LoadCLUT(byte[] bytes) + { + if(bytes == null || bytes.Length != 16) + return; + + for(int i = 0; i < 8; i++) + this.ColorSpec[i] = (short)(BitConverter.ToInt16(bytes, 2 * i) & 0x3F3F); + } + } +} \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/Karaoke/MemPreset.cs b/RedBookPlayer.Models/Hardware/Karaoke/MemPreset.cs new file mode 100644 index 0000000..3d26dd7 --- /dev/null +++ b/RedBookPlayer.Models/Hardware/Karaoke/MemPreset.cs @@ -0,0 +1,30 @@ +using System; + +namespace RedBookPlayer.Models.Hardware.Karaoke +{ + /// + internal class MemPreset + { + // Only lower 4 bits are used, mask with 0x0F + public byte Color { get; private set; } + + // Only lower 4 bits are used, mask with 0x0F + public byte Repeat { get; private set; } + + public byte[] Filler { get; private set; } = new byte[14]; + + /// + /// Interpret subchannel packet data as Memory Preset + /// + public MemPreset(byte[] bytes) + { + if(bytes == null || bytes.Length != 16) + return; + + this.Color = (byte)(bytes[0] & 0x0F); + this.Repeat = (byte)(bytes[1] & 0x0F); + + Array.Copy(bytes, 2, this.Filler, 0, 14); + } + } +} \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/Karaoke/Scroll.cs b/RedBookPlayer.Models/Hardware/Karaoke/Scroll.cs new file mode 100644 index 0000000..a3279c8 --- /dev/null +++ b/RedBookPlayer.Models/Hardware/Karaoke/Scroll.cs @@ -0,0 +1,42 @@ +using System; + +namespace RedBookPlayer.Models.Hardware.Karaoke +{ + /// + internal class Scroll + { + // Only lower 4 bits are used, mask with 0x0F + public byte Color { get; private set; } + + // Only lower 6 bits are used, mask with 0x3F + public byte HScroll { get; private set; } + + public ScrollCommand HScrollCommand => (ScrollCommand)((this.HScroll & 0x30) >> 4); + + public int HScrollOffset => this.HScroll & 0x07; + + // Only lower 6 bits are used, mask with 0x3F + public byte VScroll { get; private set; } + + public ScrollCommand VScrollCommand => (ScrollCommand)((this.VScroll & 0x30) >> 4); + + public int VScrollOffset => this.VScroll & 0x07; + + public byte[] Filler { get; private set; } = new byte[13]; + + /// + /// Interpret subchannel packet data as Scroll + /// + public Scroll(byte[] bytes) + { + if(bytes == null || bytes.Length != 16) + return; + + this.Color = (byte)(bytes[0] & 0x0F); + this.HScroll = (byte)(bytes[1] & 0x3F); + this.VScroll = (byte)(bytes[2] & 0x3F); + + Array.Copy(bytes, 3, this.Filler, 0, 13); + } + } +} \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/Karaoke/TileBlock.cs b/RedBookPlayer.Models/Hardware/Karaoke/TileBlock.cs new file mode 100644 index 0000000..3abad38 --- /dev/null +++ b/RedBookPlayer.Models/Hardware/Karaoke/TileBlock.cs @@ -0,0 +1,38 @@ +namespace RedBookPlayer.Models.Hardware.Karaoke +{ + /// + internal class TileBlock + { + // Only lower 4 bits are used, mask with 0x0F + public byte Color0 { get; private set; } + + // Only lower 4 bits are used, mask with 0x0F + public byte Color1 { get; private set; } + + // Only lower 5 bits are used, mask with 0x1F + public byte Row { get; private set; } + + // Only lower 6 bits are used, mask with 0x3F + public byte Column { get; private set; } + + // Only lower 6 bits of each byte are used + public byte[] TilePixels { get; private set; } = new byte[12]; + + /// + /// Interpret subchannel packet data as Tile Block + /// + public TileBlock(byte[] bytes) + { + if(bytes == null || bytes.Length != 16) + return; + + this.Color0 = (byte)(bytes[0] & 0x0F); + this.Color1 = (byte)(bytes[1] & 0x0F); + this.Row = (byte)(bytes[2] & 0x1F); + this.Column = (byte)(bytes[3] & 0x3F); + + for(int i = 0; i < 12; i++) + this.TilePixels[i] = (byte)(bytes[4 + i] & 0x3F); + } + } +} \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/Player.cs b/RedBookPlayer.Models/Hardware/Player.cs index 639cc6c..653b03e 100644 --- a/RedBookPlayer.Models/Hardware/Player.cs +++ b/RedBookPlayer.Models/Hardware/Player.cs @@ -10,6 +10,7 @@ using ReactiveUI; using RedBookPlayer.Models.Audio; using RedBookPlayer.Models.Discs; using RedBookPlayer.Models.Factories; +using RedBookPlayer.Models.Hardware.Karaoke; namespace RedBookPlayer.Models.Hardware { @@ -333,6 +334,11 @@ namespace RedBookPlayer.Models.Hardware /// private readonly object _readingImage = new object(); + /// + /// Internal representation of a Karaoke (CD+G) display + /// + private readonly KaraokeDisplay _karaokeDisplay = new KaraokeDisplay(); + #endregion /// @@ -1333,36 +1339,79 @@ namespace RedBookPlayer.Models.Hardware #region Helpers /// - /// Reformat raw subchannel data for multiple sectors + /// Parse multiple subchannels into object data /// /// Raw subchannel data to format - /// Dictionary mapping subchannel to formatted data - public Dictionary ConvertSubchannels(byte[] subchannelData) + /// List of subchannel object data + private List ParseSubchannels(byte[] subchannelData) { if(subchannelData == null || subchannelData.Length % 96 != 0) return null; - // Prepare the output formatted data - int modValue = subchannelData.Length / 96; - Dictionary formattedData = new Dictionary - { - ['P'] = new byte[8 * modValue], - ['Q'] = new byte[8 * modValue], - ['R'] = new byte[8 * modValue], - ['S'] = new byte[8 * modValue], - ['T'] = new byte[8 * modValue], - ['U'] = new byte[8 * modValue], - ['V'] = new byte[8 * modValue], - ['W'] = new byte[8 * modValue], - }; + // Create the list of objects to return + var parsedSubchannelData = new List(); // Read in 96-byte chunks + int modValue = subchannelData.Length / 96; for(int i = 0; i < modValue; i++) { byte[] buffer = new byte[96]; Array.Copy(subchannelData, i * 96, buffer, 0, 96); var singleSubchannel = new SubchannelData(buffer); - Dictionary singleData = singleSubchannel.ConvertData(); + parsedSubchannelData.Add(singleSubchannel); + } + + return parsedSubchannelData; + } + + /// + /// Reformat raw subchannel data for multiple sectors + /// + /// Raw subchannel data to format + /// Dictionary mapping subchannel to formatted data + Dictionary ConvertSubchannels(byte[] subchannelData) + { + if(subchannelData == null || subchannelData.Length % 96 != 0) + return null; + + // Parse the subchannel data, if possible + var parsedSubchannelData = ParseSubchannels(subchannelData); + + return ConvertSubchannels(parsedSubchannelData); + } + + /// + /// Reformat subchannel object data for multiple sectors + /// + /// Subchannel object data to format + /// Dictionary mapping subchannel to formatted data + private Dictionary ConvertSubchannels(List subchannelData) + { + if(subchannelData == null) + return null; + + // Prepare the output formatted data + Dictionary formattedData = new Dictionary + { + ['P'] = new byte[8 * subchannelData.Count], + ['Q'] = new byte[8 * subchannelData.Count], + ['R'] = new byte[8 * subchannelData.Count], + ['S'] = new byte[8 * subchannelData.Count], + ['T'] = new byte[8 * subchannelData.Count], + ['U'] = new byte[8 * subchannelData.Count], + ['V'] = new byte[8 * subchannelData.Count], + ['W'] = new byte[8 * subchannelData.Count], + }; + + // Read in each object + for(int i = 0; i < subchannelData.Count; i++) + { + if(subchannelData[i] == null) + continue; + + Dictionary singleData = subchannelData[i].ConvertData(); + if(singleData == null) + continue; Array.Copy(singleData['P'], 0, formattedData['P'], 8 * i, 8); Array.Copy(singleData['Q'], 0, formattedData['Q'], 8 * i, 8); @@ -1377,6 +1426,37 @@ namespace RedBookPlayer.Models.Hardware return formattedData; } + /// + /// Process subchannel object data + /// + /// Subchannel object data to format + private void ProcessKaraokeData(List subchannelData) + { + if(subchannelData == null) + return; + + // Process each subchannel data object in order + foreach(var subchannelDataObj in subchannelData) + { + if(subchannelDataObj == null) + continue; + + // Check that the packets are valid + var packets = subchannelDataObj.Packets; + if(packets == null) + continue; + + // Each packet has to be separately run + foreach(var packet in packets) + { + if(packet == null || !packet.IsCDGPacket()) + continue; + + _karaokeDisplay.ProcessData(packet); + } + } + } + #endregion } } \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/SubchannelPacket.cs b/RedBookPlayer.Models/Hardware/SubchannelPacket.cs index 2051439..b7368f1 100644 --- a/RedBookPlayer.Models/Hardware/SubchannelPacket.cs +++ b/RedBookPlayer.Models/Hardware/SubchannelPacket.cs @@ -10,9 +10,13 @@ namespace RedBookPlayer.Models.Hardware internal class SubchannelPacket { public byte Command { get; private set; } - public byte Instruction { 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]; /// @@ -24,13 +28,15 @@ namespace RedBookPlayer.Models.Hardware return; this.Command = bytes[0]; - this.Instruction = bytes[1]; + 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 /// @@ -85,5 +91,20 @@ namespace RedBookPlayer.Models.Hardware /// 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; + } + + #endregion } } \ No newline at end of file