diff --git a/RedBookPlayer.Models/Enums.cs b/RedBookPlayer.Models/Enums.cs
index 5619e11..4a4d52d 100644
--- a/RedBookPlayer.Models/Enums.cs
+++ b/RedBookPlayer.Models/Enums.cs
@@ -100,4 +100,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/LoadCLUT.cs b/RedBookPlayer.Models/Hardware/Karaoke/LoadCLUT.cs
new file mode 100644
index 0000000..c4ca6e6
--- /dev/null
+++ b/RedBookPlayer.Models/Hardware/Karaoke/LoadCLUT.cs
@@ -0,0 +1,25 @@
+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..9a9bd20
--- /dev/null
+++ b/RedBookPlayer.Models/Hardware/Karaoke/Scroll.cs
@@ -0,0 +1,34 @@
+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; }
+
+ // Only lower 6 bits are used, mask with 0x3F
+ public byte VScroll { get; private set; }
+
+ 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..93018aa
--- /dev/null
+++ b/RedBookPlayer.Models/Hardware/Karaoke/TileBlock.cs
@@ -0,0 +1,40 @@
+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/SubchannelPacket.cs b/RedBookPlayer.Models/Hardware/SubchannelPacket.cs
index 2051439..9242a29 100644
--- a/RedBookPlayer.Models/Hardware/SubchannelPacket.cs
+++ b/RedBookPlayer.Models/Hardware/SubchannelPacket.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using RedBookPlayer.Models.Hardware.Karaoke;
namespace RedBookPlayer.Models.Hardware
{
@@ -10,9 +11,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 +29,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 +92,44 @@ 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;
+ }
+
+ ///
+ /// 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
}
}
\ No newline at end of file