Subchannel interpretation code (nw)

This commit is contained in:
Matt Nadareski
2021-11-29 16:32:36 -08:00
parent 7b0615a792
commit 34d985d481
7 changed files with 255 additions and 2 deletions

View File

@@ -100,4 +100,56 @@ namespace RedBookPlayer.Models
/// </summary>
FirstSessionOnly = 1,
}
/// <summary>
/// Known set of subchannel instructions
/// </summary>
/// <see cref="https://jbum.com/cdg_revealed.html"/>
public enum SubchannelInstruction : byte
{
/// <summary>
/// Set the screen to a particular color.
/// </summary>
MemoryPreset = 1,
/// <summary>
/// Set the border of the screen to a particular color.
/// </summary>
BorderPreset = 2,
/// <summary>
/// Load a 12 x 6, 2 color tile and display it normally.
/// </summary>
TileBlockNormal = 6,
/// <summary>
/// Scroll the image, filling in the new area with a color.
/// </summary>
ScrollPreset = 20,
/// <summary>
/// Scroll the image, rotating the bits back around.
/// </summary>
ScrollCopy = 24,
/// <summary>
/// Define a specific color as being transparent.
/// </summary>
DefineTransparentColor = 28,
/// <summary>
/// Load in the lower 8 entries of the color table.
/// </summary>
LoadColorTableLower = 30,
/// <summary>
/// Load in the upper 8 entries of the color table.
/// </summary>
LoadColorTableUpper = 31,
/// <summary>
/// Load a 12 x 6, 2 color tile and display it using the XOR method.
/// </summary>
TileBlockXOR = 38,
}
}

View File

@@ -0,0 +1,26 @@
using System;
namespace RedBookPlayer.Models.Hardware.Karaoke
{
/// <see cref="https://jbum.com/cdg_revealed.html"/>
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];
/// <summary>
/// Interpret subchannel packet data as Border Preset
/// </summary>
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);
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
namespace RedBookPlayer.Models.Hardware.Karaoke
{
/// <see cref="https://jbum.com/cdg_revealed.html"/>
internal class LoadCLUT
{
// AND with 0x3F3F to clear P and Q channel
public short[] ColorSpec { get; private set; } = new short[8];
/// <summary>
/// Interpret subchannel packet data as Load Color Lookup Table
/// </summary>
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);
}
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
namespace RedBookPlayer.Models.Hardware.Karaoke
{
/// <see cref="https://jbum.com/cdg_revealed.html"/>
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];
/// <summary>
/// Interpret subchannel packet data as Memory Preset
/// </summary>
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);
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
namespace RedBookPlayer.Models.Hardware.Karaoke
{
/// <see cref="https://jbum.com/cdg_revealed.html"/>
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];
/// <summary>
/// Interpret subchannel packet data as Scroll
/// </summary>
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);
}
}
}

View File

@@ -0,0 +1,40 @@
namespace RedBookPlayer.Models.Hardware.Karaoke
{
/// <see cref="https://jbum.com/cdg_revealed.html"/>
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];
/// <summary>
/// Interpret subchannel packet data as Tile Block
/// </summary>
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);
}
}
}
}

View File

@@ -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];
/// <summary>
@@ -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
/// <summary>
/// Convert the data into separate named subchannels
/// </summary>
@@ -85,5 +92,44 @@ namespace RedBookPlayer.Models.Hardware
/// <param name="bitIndex">Index of the bit to check</param>
/// <returns>True if the bit was set, false otherwise</returns>
private bool HasBitSet(byte value, int bitIndex) => (value & (1 << bitIndex)) != 0;
#endregion
#region CD+G Handling
/// <summary>
/// Determine if a packet is CD+G data
/// </summary>
public bool IsCDGPacket()
{
byte lowerSixBits = (byte)(this.Command & 0x3F);
return lowerSixBits == 0x09;
}
/// <summary>
/// Read packet data according to the instruction, if possible
/// </summary>
/// <returns>Supported object created from data, null on error</returns>
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
}
}