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