mirror of
https://github.com/aaru-dps/RedBookPlayer.git
synced 2025-12-16 11:14:39 +00:00
@@ -85,6 +85,27 @@ namespace RedBookPlayer.Models
|
||||
All,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine how to scroll
|
||||
/// </summary>
|
||||
public enum ScrollCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// No scrolling
|
||||
/// </summary>
|
||||
NoScroll = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Scroll 6 pixels in the positive direction (right/down)
|
||||
/// </summary>
|
||||
Positive = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Scroll 6 pixels in the negative direction (left/up)
|
||||
/// </summary>
|
||||
Negative = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine how to handle different sessions
|
||||
/// </summary>
|
||||
@@ -100,4 +121,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,
|
||||
}
|
||||
}
|
||||
26
RedBookPlayer.Models/Hardware/Karaoke/BorderPreset.cs
Normal file
26
RedBookPlayer.Models/Hardware/Karaoke/BorderPreset.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
360
RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs
Normal file
360
RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs
Normal file
@@ -0,0 +1,360 @@
|
||||
using System;
|
||||
|
||||
namespace RedBookPlayer.Models.Hardware.Karaoke
|
||||
{
|
||||
/// <see cref="https://jbum.com/cdg_revealed.html"/>
|
||||
internal class KaraokeDisplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Display data as a 2-dimensional byte array
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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]
|
||||
/// </remarks>
|
||||
public byte[,] DisplayData { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current 16-entry color table
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public short[] ColorTable { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Color currently defined as transparent
|
||||
/// </summary>
|
||||
public byte TransparentColor { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new, blank karaoke display
|
||||
/// </summary>
|
||||
public KaraokeDisplay()
|
||||
{
|
||||
this.DisplayData = new byte[300,216];
|
||||
this.ColorTable = new short[16];
|
||||
this.TransparentColor = 0x00;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print the current color map to console
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process a subchannel packet and update the display as necessary
|
||||
/// </summary>
|
||||
/// <param name="packet">Subchannel packet data to process</param>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Set the screen to a particular color
|
||||
/// </summary>
|
||||
/// <param name="memPreset">MemPreset with the new data</param>
|
||||
/// <param name="consistentData">True if all subchannel data is present, false otherwise</param>
|
||||
/// <remarks>
|
||||
/// 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 <see cref="DisplayData"/> for more details.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the border to a particular color
|
||||
/// </summary>
|
||||
/// <param name="borderPreset">BorderPreset with the new data</param>
|
||||
/// <remarks>
|
||||
/// The area that is considered the border is unclear. Please see the remarks
|
||||
/// on <see cref="DisplayData"/> for more details.
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load a block of pixels with a certain pattern
|
||||
/// </summary>
|
||||
/// <param name="tileBlock">TileBlock with the pattern data</param>
|
||||
/// <param name="xor">
|
||||
/// If true, the color values are combined with the color values
|
||||
/// that are already onscreen using the XOR operator
|
||||
/// </param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scroll the display according to the instruction
|
||||
/// </summary>
|
||||
/// <param name="scroll">Scroll with the new data</param>
|
||||
/// <param name="copy">True if data wraps around on scroll, false if filled by a solid color</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load either the upper or lower half of the color table
|
||||
/// </summary>
|
||||
/// <param name="tableData">Color table data to load</param>
|
||||
/// <param name="upper">True for colors 8-15, false for colors 0-7</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
23
RedBookPlayer.Models/Hardware/Karaoke/LoadCLUT.cs
Normal file
23
RedBookPlayer.Models/Hardware/Karaoke/LoadCLUT.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
RedBookPlayer.Models/Hardware/Karaoke/MemPreset.cs
Normal file
30
RedBookPlayer.Models/Hardware/Karaoke/MemPreset.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
RedBookPlayer.Models/Hardware/Karaoke/Scroll.cs
Normal file
42
RedBookPlayer.Models/Hardware/Karaoke/Scroll.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
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; }
|
||||
|
||||
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];
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
RedBookPlayer.Models/Hardware/Karaoke/TileBlock.cs
Normal file
38
RedBookPlayer.Models/Hardware/Karaoke/TileBlock.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// </summary>
|
||||
private readonly object _readingImage = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Internal representation of a Karaoke (CD+G) display
|
||||
/// </summary>
|
||||
private readonly KaraokeDisplay _karaokeDisplay = new KaraokeDisplay();
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
@@ -1333,36 +1339,79 @@ namespace RedBookPlayer.Models.Hardware
|
||||
#region Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Reformat raw subchannel data for multiple sectors
|
||||
/// Parse multiple subchannels into object data
|
||||
/// </summary>
|
||||
/// <param name="subchannelData">Raw subchannel data to format</param>
|
||||
/// <returns>Dictionary mapping subchannel to formatted data</returns>
|
||||
public Dictionary<char, byte[]> ConvertSubchannels(byte[] subchannelData)
|
||||
/// <returns>List of subchannel object data</returns>
|
||||
private List<SubchannelData> ParseSubchannels(byte[] subchannelData)
|
||||
{
|
||||
if(subchannelData == null || subchannelData.Length % 96 != 0)
|
||||
return null;
|
||||
|
||||
// Prepare the output formatted data
|
||||
int modValue = subchannelData.Length / 96;
|
||||
Dictionary<char, byte[]> formattedData = new Dictionary<char, byte[]>
|
||||
{
|
||||
['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<SubchannelData>();
|
||||
|
||||
// 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<char, byte[]> singleData = singleSubchannel.ConvertData();
|
||||
parsedSubchannelData.Add(singleSubchannel);
|
||||
}
|
||||
|
||||
return parsedSubchannelData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reformat raw subchannel data for multiple sectors
|
||||
/// </summary>
|
||||
/// <param name="subchannelData">Raw subchannel data to format</param>
|
||||
/// <returns>Dictionary mapping subchannel to formatted data</returns>
|
||||
Dictionary<char, byte[]> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reformat subchannel object data for multiple sectors
|
||||
/// </summary>
|
||||
/// <param name="subchannelData">Subchannel object data to format</param>
|
||||
/// <returns>Dictionary mapping subchannel to formatted data</returns>
|
||||
private Dictionary<char, byte[]> ConvertSubchannels(List<SubchannelData> subchannelData)
|
||||
{
|
||||
if(subchannelData == null)
|
||||
return null;
|
||||
|
||||
// Prepare the output formatted data
|
||||
Dictionary<char, byte[]> formattedData = new Dictionary<char, byte[]>
|
||||
{
|
||||
['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<char, byte[]> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process subchannel object data
|
||||
/// </summary>
|
||||
/// <param name="subchannelData">Subchannel object data to format</param>
|
||||
private void ProcessKaraokeData(List<SubchannelData> 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
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
/// <summary>
|
||||
@@ -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
|
||||
|
||||
/// <summary>
|
||||
/// Convert the data into separate named subchannels
|
||||
/// </summary>
|
||||
@@ -85,5 +91,20 @@ 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;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user