From 3e44c115029f92ee8c566b88f0d5c1cfb2a54c4b Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Mon, 29 Nov 2021 21:12:50 -0800 Subject: [PATCH] Start wiring up some CD+G instructions --- .../Hardware/Karaoke/KaraokeDisplay.cs | 208 ++++++++++++++++++ RedBookPlayer.Models/Hardware/Player.cs | 38 +++- .../Hardware/SubchannelPacket.cs | 25 --- 3 files changed, 241 insertions(+), 30 deletions(-) create mode 100644 RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs diff --git a/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs b/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs new file mode 100644 index 0000000..9479671 --- /dev/null +++ b/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs @@ -0,0 +1,208 @@ +namespace RedBookPlayer.Models.Hardware.Karaoke +{ + /// + internal class KaraokeDisplay + { + /// + /// Display data as a 2-dimensional byte array + /// + /// + // 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 8-entry color table + /// + /// + /// In practice, this should be 8 colors, probably similar to the CGA palette, + /// including the "bright" or "dark" variant (possibly mapping from "high" to "low"). + /// + public byte[] 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 byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }; + this.TransparentColor = 0x00; + } + + /// + /// 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); + break; + case SubchannelInstruction.ScrollCopy: + var scrollCopy = new Scroll(packet.Data); + break; + case SubchannelInstruction.DefineTransparentColor: + var transparentColor = new BorderPreset(packet.Data); + this.TransparentColor = transparentColor.Color; + break; + case SubchannelInstruction.LoadColorTableLower: + var loadColorTableLower = new LoadCLUT(packet.Data); + break; + case SubchannelInstruction.LoadColorTableUpper: + var loadColorTableUpper = new LoadCLUT(packet.Data); + break; + case SubchannelInstruction.TileBlockXOR: + var tileBlockXor = new TileBlock(packet.Data); + LoadTileBlock(tileBlockXor, true); + break; + }; + } + + /// + /// 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 + /// + /// BorderPreset with the new 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; + int colorIndex = pattern[x,y] == 0 ? tileBlock.Color0 : tileBlock.Color1; + + if(xor) + this.DisplayData[adjustedX, adjustedY] = (byte)(this.ColorTable[colorIndex] ^ this.DisplayData[adjustedX, adjustedY]); + else + this.DisplayData[adjustedX, adjustedY] = this.ColorTable[colorIndex]; + } + } + } +} \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/Player.cs b/RedBookPlayer.Models/Hardware/Player.cs index d48bc1c..a2eef8a 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 { @@ -336,11 +337,7 @@ namespace RedBookPlayer.Models.Hardware /// /// Internal representation of a Karaoke (CD+G) display /// - /// - /// Uses a 300x216 display capable of 16 colors - /// See https://jbum.com//cdg_revealed.html for more details - /// - private readonly byte[,] _karaokeDisplay = new byte[300,216]; + private readonly KaraokeDisplay _karaokeDisplay = new KaraokeDisplay(); #endregion @@ -1428,6 +1425,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 9242a29..b7368f1 100644 --- a/RedBookPlayer.Models/Hardware/SubchannelPacket.cs +++ b/RedBookPlayer.Models/Hardware/SubchannelPacket.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using RedBookPlayer.Models.Hardware.Karaoke; namespace RedBookPlayer.Models.Hardware { @@ -106,30 +105,6 @@ namespace RedBookPlayer.Models.Hardware 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