From 34d985d4819954fbb43560b7a5c00e0b2fde1632 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Mon, 29 Nov 2021 16:32:36 -0800 Subject: [PATCH 1/9] Subchannel interpretation code (nw) --- RedBookPlayer.Models/Enums.cs | 52 +++++++++++++++++++ .../Hardware/Karaoke/BorderPreset.cs | 26 ++++++++++ .../Hardware/Karaoke/LoadCLUT.cs | 25 +++++++++ .../Hardware/Karaoke/MemPreset.cs | 30 +++++++++++ .../Hardware/Karaoke/Scroll.cs | 34 ++++++++++++ .../Hardware/Karaoke/TileBlock.cs | 40 ++++++++++++++ .../Hardware/SubchannelPacket.cs | 50 +++++++++++++++++- 7 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 RedBookPlayer.Models/Hardware/Karaoke/BorderPreset.cs create mode 100644 RedBookPlayer.Models/Hardware/Karaoke/LoadCLUT.cs create mode 100644 RedBookPlayer.Models/Hardware/Karaoke/MemPreset.cs create mode 100644 RedBookPlayer.Models/Hardware/Karaoke/Scroll.cs create mode 100644 RedBookPlayer.Models/Hardware/Karaoke/TileBlock.cs 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 From 78ef6d8511b9054b5d7a127c934e59b88548c8a3 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Mon, 29 Nov 2021 16:50:19 -0800 Subject: [PATCH 2/9] Rework subchannel helper methods --- RedBookPlayer.Models/Hardware/Player.cs | 76 +++++++++++++++++++------ 1 file changed, 59 insertions(+), 17 deletions(-) diff --git a/RedBookPlayer.Models/Hardware/Player.cs b/RedBookPlayer.Models/Hardware/Player.cs index 639cc6c..c21e634 100644 --- a/RedBookPlayer.Models/Hardware/Player.cs +++ b/RedBookPlayer.Models/Hardware/Player.cs @@ -1333,36 +1333,78 @@ 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 + private 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); From 15b39e8cfffd1402ae1eeca4152fcef92adb17d7 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Mon, 29 Nov 2021 16:56:36 -0800 Subject: [PATCH 3/9] Add currently-unused karaoke display internally --- RedBookPlayer.Models/Hardware/Player.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/RedBookPlayer.Models/Hardware/Player.cs b/RedBookPlayer.Models/Hardware/Player.cs index c21e634..d48bc1c 100644 --- a/RedBookPlayer.Models/Hardware/Player.cs +++ b/RedBookPlayer.Models/Hardware/Player.cs @@ -333,6 +333,15 @@ namespace RedBookPlayer.Models.Hardware /// private readonly object _readingImage = new object(); + /// + /// 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]; + #endregion /// From 3e44c115029f92ee8c566b88f0d5c1cfb2a54c4b Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Mon, 29 Nov 2021 21:12:50 -0800 Subject: [PATCH 4/9] 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 From 1880d49ee573fe906b1bc50195e47978690a0b8e Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Mon, 29 Nov 2021 22:16:23 -0800 Subject: [PATCH 5/9] Add most of scrolling logic; add notes --- RedBookPlayer.Models/Enums.cs | 21 +++ .../Hardware/Karaoke/KaraokeDisplay.cs | 129 +++++++++++++++++- .../Hardware/Karaoke/Scroll.cs | 8 ++ 3 files changed, 157 insertions(+), 1 deletion(-) diff --git a/RedBookPlayer.Models/Enums.cs b/RedBookPlayer.Models/Enums.cs index 4a4d52d..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 /// diff --git a/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs b/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs index 9479671..e310a5e 100644 --- a/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs +++ b/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs @@ -33,6 +33,8 @@ namespace RedBookPlayer.Models.Hardware.Karaoke /// /// 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"). + /// The current interpretation of this may be incorrect, as the internal color table + /// may actually include all 16 RGB values immediately. /// public byte[] ColorTable { get; private set; } @@ -76,9 +78,11 @@ namespace RedBookPlayer.Models.Hardware.Karaoke 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); @@ -86,9 +90,11 @@ namespace RedBookPlayer.Models.Hardware.Karaoke break; case SubchannelInstruction.LoadColorTableLower: var loadColorTableLower = new LoadCLUT(packet.Data); + // TODO: Load color table data break; case SubchannelInstruction.LoadColorTableUpper: var loadColorTableUpper = new LoadCLUT(packet.Data); + // TODO: Load color table data break; case SubchannelInstruction.TileBlockXOR: var tileBlockXor = new TileBlock(packet.Data); @@ -167,7 +173,7 @@ namespace RedBookPlayer.Models.Hardware.Karaoke /// /// Load a block of pixels with a certain pattern /// - /// BorderPreset with the new data + /// TileBlock with the pattern data /// /// If true, the color values are combined with the color values /// that are already onscreen using the XOR operator @@ -204,5 +210,126 @@ namespace RedBookPlayer.Models.Hardware.Karaoke this.DisplayData[adjustedX, adjustedY] = this.ColorTable[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 + /// + /// Based on the documentation, there's a bit of ambiguity how the Offset fields are used. + /// The current best understanding is that the offset can be combined with any scroll command + /// to add (or subtract) between 0 and 5 pixels in a particular axis. + /// TODO: Offsets are not currently implemented in the code below + /// + private void ScrollDisplay(Scroll scroll, bool copy) + { + if(scroll == null) + return; + + // If we're scrolling horizontally + if(scroll.HScrollCommand == ScrollCommand.Positive) + { + for(int y = 0; y < 216; y++) + { + byte[] overflow = new byte[6]; + for(int x = 299; x >= 0; x--) + { + if(x + 6 >= 300) + overflow[(x + 6) % 300] = this.DisplayData[x,y]; + else + this.DisplayData[x + 6, y] = this.DisplayData[x,y]; + } + + // Fill in the now-empty pixels + this.DisplayData[0,y] = copy ? overflow[0] : this.ColorTable[scroll.Color]; + this.DisplayData[1,y] = copy ? overflow[1] : this.ColorTable[scroll.Color]; + this.DisplayData[2,y] = copy ? overflow[2] : this.ColorTable[scroll.Color]; + this.DisplayData[3,y] = copy ? overflow[3] : this.ColorTable[scroll.Color]; + this.DisplayData[4,y] = copy ? overflow[4] : this.ColorTable[scroll.Color]; + this.DisplayData[5,y] = copy ? overflow[5] : this.ColorTable[scroll.Color]; + } + } + else if(scroll.HScrollCommand == ScrollCommand.Negative) + { + for(int y = 0; y < 216; y++) + { + byte[] overflow = new byte[6]; + for(int x = 0; x < 300; x++) + { + if(x - 6 < 0) + overflow[x] = this.DisplayData[x,y]; + else + this.DisplayData[x - 6, y] = this.DisplayData[x,y]; + } + + // Fill in the now-empty pixels + this.DisplayData[294,y] = copy ? overflow[0] : this.ColorTable[scroll.Color]; + this.DisplayData[295,y] = copy ? overflow[1] : this.ColorTable[scroll.Color]; + this.DisplayData[296,y] = copy ? overflow[2] : this.ColorTable[scroll.Color]; + this.DisplayData[297,y] = copy ? overflow[3] : this.ColorTable[scroll.Color]; + this.DisplayData[298,y] = copy ? overflow[4] : this.ColorTable[scroll.Color]; + this.DisplayData[299,y] = copy ? overflow[5] : this.ColorTable[scroll.Color]; + } + } + + // If we're scrolling vertically + if(scroll.VScrollCommand == ScrollCommand.Positive) + { + for(int x = 0; x < 300; x++) + { + byte[] overflow = new byte[12]; + for(int y = 215; y >= 0; y--) + { + if(y + 12 >= 216) + overflow[(y + 12) % 216] = this.DisplayData[x,y]; + else + this.DisplayData[x, y + 12] = this.DisplayData[x,y]; + } + + // Fill in the now-empty pixels + this.DisplayData[x,0] = copy ? overflow[0] : this.ColorTable[scroll.Color]; + this.DisplayData[x,1] = copy ? overflow[1] : this.ColorTable[scroll.Color]; + this.DisplayData[x,2] = copy ? overflow[2] : this.ColorTable[scroll.Color]; + this.DisplayData[x,3] = copy ? overflow[3] : this.ColorTable[scroll.Color]; + this.DisplayData[x,4] = copy ? overflow[4] : this.ColorTable[scroll.Color]; + this.DisplayData[x,5] = copy ? overflow[5] : this.ColorTable[scroll.Color]; + this.DisplayData[x,6] = copy ? overflow[6] : this.ColorTable[scroll.Color]; + this.DisplayData[x,7] = copy ? overflow[7] : this.ColorTable[scroll.Color]; + this.DisplayData[x,8] = copy ? overflow[8] : this.ColorTable[scroll.Color]; + this.DisplayData[x,9] = copy ? overflow[9] : this.ColorTable[scroll.Color]; + this.DisplayData[x,10] = copy ? overflow[10] : this.ColorTable[scroll.Color]; + this.DisplayData[x,11] = copy ? overflow[11] : this.ColorTable[scroll.Color]; + } + } + else if(scroll.VScrollCommand == ScrollCommand.Negative) + { + for(int x = 0; x < 300; x++) + { + byte[] overflow = new byte[12]; + for(int y = 0; y < 216; y++) + { + if(y - 12 < 0) + overflow[y] = this.DisplayData[x,y]; + else + this.DisplayData[x, y - 12] = this.DisplayData[x,y]; + } + + // Fill in the now-empty pixels + this.DisplayData[x,204] = copy ? overflow[0] : this.ColorTable[scroll.Color]; + this.DisplayData[x,205] = copy ? overflow[1] : this.ColorTable[scroll.Color]; + this.DisplayData[x,206] = copy ? overflow[2] : this.ColorTable[scroll.Color]; + this.DisplayData[x,207] = copy ? overflow[3] : this.ColorTable[scroll.Color]; + this.DisplayData[x,208] = copy ? overflow[4] : this.ColorTable[scroll.Color]; + this.DisplayData[x,209] = copy ? overflow[5] : this.ColorTable[scroll.Color]; + this.DisplayData[x,210] = copy ? overflow[6] : this.ColorTable[scroll.Color]; + this.DisplayData[x,211] = copy ? overflow[7] : this.ColorTable[scroll.Color]; + this.DisplayData[x,212] = copy ? overflow[8] : this.ColorTable[scroll.Color]; + this.DisplayData[x,213] = copy ? overflow[9] : this.ColorTable[scroll.Color]; + this.DisplayData[x,214] = copy ? overflow[10] : this.ColorTable[scroll.Color]; + this.DisplayData[x,215] = copy ? overflow[11] : this.ColorTable[scroll.Color]; + } + } + } } } \ No newline at end of file diff --git a/RedBookPlayer.Models/Hardware/Karaoke/Scroll.cs b/RedBookPlayer.Models/Hardware/Karaoke/Scroll.cs index 9a9bd20..a3279c8 100644 --- a/RedBookPlayer.Models/Hardware/Karaoke/Scroll.cs +++ b/RedBookPlayer.Models/Hardware/Karaoke/Scroll.cs @@ -11,9 +11,17 @@ namespace RedBookPlayer.Models.Hardware.Karaoke // 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]; /// From 52ecacba7c0e9cd8cbce8b8fcb273e181fe4c3c4 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Mon, 29 Nov 2021 22:44:16 -0800 Subject: [PATCH 6/9] Implement color table loading --- .../Hardware/Karaoke/KaraokeDisplay.cs | 122 +++++++++++------- 1 file changed, 74 insertions(+), 48 deletions(-) diff --git a/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs b/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs index e310a5e..2c8b545 100644 --- a/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs +++ b/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs @@ -28,15 +28,19 @@ namespace RedBookPlayer.Models.Hardware.Karaoke public byte[,] DisplayData { get; private set; } /// - /// Current 8-entry color table + /// Current 16-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"). - /// The current interpretation of this may be incorrect, as the internal color table - /// may actually include all 16 RGB values immediately. + /// 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 byte[] ColorTable { get; private set; } + public short[] ColorTable { get; private set; } /// /// Color currently defined as transparent @@ -49,7 +53,7 @@ namespace RedBookPlayer.Models.Hardware.Karaoke public KaraokeDisplay() { this.DisplayData = new byte[300,216]; - this.ColorTable = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }; + this.ColorTable = new short[16]; this.TransparentColor = 0x00; } @@ -90,11 +94,11 @@ namespace RedBookPlayer.Models.Hardware.Karaoke break; case SubchannelInstruction.LoadColorTableLower: var loadColorTableLower = new LoadCLUT(packet.Data); - // TODO: Load color table data + LoadColorTable(loadColorTableLower, false); break; case SubchannelInstruction.LoadColorTableUpper: var loadColorTableUpper = new LoadCLUT(packet.Data); - // TODO: Load color table data + LoadColorTable(loadColorTableUpper, true); break; case SubchannelInstruction.TileBlockXOR: var tileBlockXor = new TileBlock(packet.Data); @@ -202,12 +206,12 @@ namespace RedBookPlayer.Models.Hardware.Karaoke { int adjustedX = x + tileBlock.Column; int adjustedY = y + tileBlock.Row; - int colorIndex = pattern[x,y] == 0 ? tileBlock.Color0 : tileBlock.Color1; + byte colorIndex = pattern[x,y] == 0 ? tileBlock.Color0 : tileBlock.Color1; if(xor) - this.DisplayData[adjustedX, adjustedY] = (byte)(this.ColorTable[colorIndex] ^ this.DisplayData[adjustedX, adjustedY]); + this.DisplayData[adjustedX, adjustedY] = (byte)(colorIndex ^ this.DisplayData[adjustedX, adjustedY]); else - this.DisplayData[adjustedX, adjustedY] = this.ColorTable[colorIndex]; + this.DisplayData[adjustedX, adjustedY] = colorIndex; } } @@ -242,12 +246,12 @@ namespace RedBookPlayer.Models.Hardware.Karaoke } // Fill in the now-empty pixels - this.DisplayData[0,y] = copy ? overflow[0] : this.ColorTable[scroll.Color]; - this.DisplayData[1,y] = copy ? overflow[1] : this.ColorTable[scroll.Color]; - this.DisplayData[2,y] = copy ? overflow[2] : this.ColorTable[scroll.Color]; - this.DisplayData[3,y] = copy ? overflow[3] : this.ColorTable[scroll.Color]; - this.DisplayData[4,y] = copy ? overflow[4] : this.ColorTable[scroll.Color]; - this.DisplayData[5,y] = copy ? overflow[5] : this.ColorTable[scroll.Color]; + this.DisplayData[0,y] = copy ? overflow[0] : scroll.Color; + this.DisplayData[1,y] = copy ? overflow[1] : scroll.Color; + this.DisplayData[2,y] = copy ? overflow[2] : scroll.Color; + this.DisplayData[3,y] = copy ? overflow[3] : scroll.Color; + this.DisplayData[4,y] = copy ? overflow[4] : scroll.Color; + this.DisplayData[5,y] = copy ? overflow[5] : scroll.Color; } } else if(scroll.HScrollCommand == ScrollCommand.Negative) @@ -264,12 +268,12 @@ namespace RedBookPlayer.Models.Hardware.Karaoke } // Fill in the now-empty pixels - this.DisplayData[294,y] = copy ? overflow[0] : this.ColorTable[scroll.Color]; - this.DisplayData[295,y] = copy ? overflow[1] : this.ColorTable[scroll.Color]; - this.DisplayData[296,y] = copy ? overflow[2] : this.ColorTable[scroll.Color]; - this.DisplayData[297,y] = copy ? overflow[3] : this.ColorTable[scroll.Color]; - this.DisplayData[298,y] = copy ? overflow[4] : this.ColorTable[scroll.Color]; - this.DisplayData[299,y] = copy ? overflow[5] : this.ColorTable[scroll.Color]; + this.DisplayData[294,y] = copy ? overflow[0] : scroll.Color; + this.DisplayData[295,y] = copy ? overflow[1] : scroll.Color; + this.DisplayData[296,y] = copy ? overflow[2] : scroll.Color; + this.DisplayData[297,y] = copy ? overflow[3] : scroll.Color; + this.DisplayData[298,y] = copy ? overflow[4] : scroll.Color; + this.DisplayData[299,y] = copy ? overflow[5] : scroll.Color; } } @@ -288,18 +292,18 @@ namespace RedBookPlayer.Models.Hardware.Karaoke } // Fill in the now-empty pixels - this.DisplayData[x,0] = copy ? overflow[0] : this.ColorTable[scroll.Color]; - this.DisplayData[x,1] = copy ? overflow[1] : this.ColorTable[scroll.Color]; - this.DisplayData[x,2] = copy ? overflow[2] : this.ColorTable[scroll.Color]; - this.DisplayData[x,3] = copy ? overflow[3] : this.ColorTable[scroll.Color]; - this.DisplayData[x,4] = copy ? overflow[4] : this.ColorTable[scroll.Color]; - this.DisplayData[x,5] = copy ? overflow[5] : this.ColorTable[scroll.Color]; - this.DisplayData[x,6] = copy ? overflow[6] : this.ColorTable[scroll.Color]; - this.DisplayData[x,7] = copy ? overflow[7] : this.ColorTable[scroll.Color]; - this.DisplayData[x,8] = copy ? overflow[8] : this.ColorTable[scroll.Color]; - this.DisplayData[x,9] = copy ? overflow[9] : this.ColorTable[scroll.Color]; - this.DisplayData[x,10] = copy ? overflow[10] : this.ColorTable[scroll.Color]; - this.DisplayData[x,11] = copy ? overflow[11] : this.ColorTable[scroll.Color]; + this.DisplayData[x,0] = copy ? overflow[0] : scroll.Color; + this.DisplayData[x,1] = copy ? overflow[1] : scroll.Color; + this.DisplayData[x,2] = copy ? overflow[2] : scroll.Color; + this.DisplayData[x,3] = copy ? overflow[3] : scroll.Color; + this.DisplayData[x,4] = copy ? overflow[4] : scroll.Color; + this.DisplayData[x,5] = copy ? overflow[5] : scroll.Color; + this.DisplayData[x,6] = copy ? overflow[6] : scroll.Color; + this.DisplayData[x,7] = copy ? overflow[7] : scroll.Color; + this.DisplayData[x,8] = copy ? overflow[8] : scroll.Color; + this.DisplayData[x,9] = copy ? overflow[9] : scroll.Color; + this.DisplayData[x,10] = copy ? overflow[10] : scroll.Color; + this.DisplayData[x,11] = copy ? overflow[11] : scroll.Color; } } else if(scroll.VScrollCommand == ScrollCommand.Negative) @@ -316,20 +320,42 @@ namespace RedBookPlayer.Models.Hardware.Karaoke } // Fill in the now-empty pixels - this.DisplayData[x,204] = copy ? overflow[0] : this.ColorTable[scroll.Color]; - this.DisplayData[x,205] = copy ? overflow[1] : this.ColorTable[scroll.Color]; - this.DisplayData[x,206] = copy ? overflow[2] : this.ColorTable[scroll.Color]; - this.DisplayData[x,207] = copy ? overflow[3] : this.ColorTable[scroll.Color]; - this.DisplayData[x,208] = copy ? overflow[4] : this.ColorTable[scroll.Color]; - this.DisplayData[x,209] = copy ? overflow[5] : this.ColorTable[scroll.Color]; - this.DisplayData[x,210] = copy ? overflow[6] : this.ColorTable[scroll.Color]; - this.DisplayData[x,211] = copy ? overflow[7] : this.ColorTable[scroll.Color]; - this.DisplayData[x,212] = copy ? overflow[8] : this.ColorTable[scroll.Color]; - this.DisplayData[x,213] = copy ? overflow[9] : this.ColorTable[scroll.Color]; - this.DisplayData[x,214] = copy ? overflow[10] : this.ColorTable[scroll.Color]; - this.DisplayData[x,215] = copy ? overflow[11] : this.ColorTable[scroll.Color]; + this.DisplayData[x,204] = copy ? overflow[0] : scroll.Color; + this.DisplayData[x,205] = copy ? overflow[1] : scroll.Color; + this.DisplayData[x,206] = copy ? overflow[2] : scroll.Color; + this.DisplayData[x,207] = copy ? overflow[3] : scroll.Color; + this.DisplayData[x,208] = copy ? overflow[4] : scroll.Color; + this.DisplayData[x,209] = copy ? overflow[5] : scroll.Color; + this.DisplayData[x,210] = copy ? overflow[6] : scroll.Color; + this.DisplayData[x,211] = copy ? overflow[7] : scroll.Color; + this.DisplayData[x,212] = copy ? overflow[8] : scroll.Color; + this.DisplayData[x,213] = copy ? overflow[9] : scroll.Color; + this.DisplayData[x,214] = copy ? overflow[10] : scroll.Color; + this.DisplayData[x,215] = copy ? overflow[11] : 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]; + } } } \ No newline at end of file From a72d79a438f1a432fffa211430540f8a21d5bb36 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Mon, 29 Nov 2021 22:55:57 -0800 Subject: [PATCH 7/9] Re-implement scrolling with offsets this time --- .../Hardware/Karaoke/KaraokeDisplay.cs | 96 ++++++++----------- 1 file changed, 38 insertions(+), 58 deletions(-) diff --git a/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs b/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs index 2c8b545..a8e03ab 100644 --- a/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs +++ b/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs @@ -220,118 +220,98 @@ namespace RedBookPlayer.Models.Hardware.Karaoke /// /// Scroll with the new data /// True if data wraps around on scroll, false if filled by a solid color - /// - /// Based on the documentation, there's a bit of ambiguity how the Offset fields are used. - /// The current best understanding is that the offset can be combined with any scroll command - /// to add (or subtract) between 0 and 5 pixels in a particular axis. - /// TODO: Offsets are not currently implemented in the code below - /// private void ScrollDisplay(Scroll scroll, bool copy) { if(scroll == null) 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) + if(scroll.HScrollCommand == ScrollCommand.Positive + || (scroll.HScrollCommand == ScrollCommand.NoScroll && scroll.HScrollOffset > 0)) { for(int y = 0; y < 216; y++) { - byte[] overflow = new byte[6]; + byte[] overflow = new byte[hOffsetTotal]; for(int x = 299; x >= 0; x--) { - if(x + 6 >= 300) - overflow[(x + 6) % 300] = this.DisplayData[x,y]; + if(x + hOffsetTotal >= 300) + overflow[(x + hOffsetTotal) % 300] = this.DisplayData[x,y]; else - this.DisplayData[x + 6, y] = this.DisplayData[x,y]; + this.DisplayData[x + hOffsetTotal, y] = this.DisplayData[x,y]; } // Fill in the now-empty pixels - this.DisplayData[0,y] = copy ? overflow[0] : scroll.Color; - this.DisplayData[1,y] = copy ? overflow[1] : scroll.Color; - this.DisplayData[2,y] = copy ? overflow[2] : scroll.Color; - this.DisplayData[3,y] = copy ? overflow[3] : scroll.Color; - this.DisplayData[4,y] = copy ? overflow[4] : scroll.Color; - this.DisplayData[5,y] = copy ? overflow[5] : scroll.Color; + 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[6]; + byte[] overflow = new byte[hOffsetTotal]; for(int x = 0; x < 300; x++) { - if(x - 6 < 0) + if(x - hOffsetTotal < 0) overflow[x] = this.DisplayData[x,y]; else - this.DisplayData[x - 6, y] = this.DisplayData[x,y]; + this.DisplayData[x - hOffsetTotal, y] = this.DisplayData[x,y]; } // Fill in the now-empty pixels - this.DisplayData[294,y] = copy ? overflow[0] : scroll.Color; - this.DisplayData[295,y] = copy ? overflow[1] : scroll.Color; - this.DisplayData[296,y] = copy ? overflow[2] : scroll.Color; - this.DisplayData[297,y] = copy ? overflow[3] : scroll.Color; - this.DisplayData[298,y] = copy ? overflow[4] : scroll.Color; - this.DisplayData[299,y] = copy ? overflow[5] : scroll.Color; + 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) + if(scroll.VScrollCommand == ScrollCommand.Positive + || (scroll.VScrollCommand == ScrollCommand.NoScroll && scroll.VScrollOffset > 0)) { for(int x = 0; x < 300; x++) { - byte[] overflow = new byte[12]; + byte[] overflow = new byte[vOffsetTotal]; for(int y = 215; y >= 0; y--) { - if(y + 12 >= 216) - overflow[(y + 12) % 216] = this.DisplayData[x,y]; + if(y + vOffsetTotal >= 216) + overflow[(y + vOffsetTotal) % 216] = this.DisplayData[x,y]; else - this.DisplayData[x, y + 12] = this.DisplayData[x,y]; + this.DisplayData[x, y + vOffsetTotal] = this.DisplayData[x,y]; } // Fill in the now-empty pixels - this.DisplayData[x,0] = copy ? overflow[0] : scroll.Color; - this.DisplayData[x,1] = copy ? overflow[1] : scroll.Color; - this.DisplayData[x,2] = copy ? overflow[2] : scroll.Color; - this.DisplayData[x,3] = copy ? overflow[3] : scroll.Color; - this.DisplayData[x,4] = copy ? overflow[4] : scroll.Color; - this.DisplayData[x,5] = copy ? overflow[5] : scroll.Color; - this.DisplayData[x,6] = copy ? overflow[6] : scroll.Color; - this.DisplayData[x,7] = copy ? overflow[7] : scroll.Color; - this.DisplayData[x,8] = copy ? overflow[8] : scroll.Color; - this.DisplayData[x,9] = copy ? overflow[9] : scroll.Color; - this.DisplayData[x,10] = copy ? overflow[10] : scroll.Color; - this.DisplayData[x,11] = copy ? overflow[11] : scroll.Color; + 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[12]; + byte[] overflow = new byte[vOffsetTotal]; for(int y = 0; y < 216; y++) { - if(y - 12 < 0) + if(y - vOffsetTotal < 0) overflow[y] = this.DisplayData[x,y]; else - this.DisplayData[x, y - 12] = this.DisplayData[x,y]; + this.DisplayData[x, y - vOffsetTotal] = this.DisplayData[x,y]; } // Fill in the now-empty pixels - this.DisplayData[x,204] = copy ? overflow[0] : scroll.Color; - this.DisplayData[x,205] = copy ? overflow[1] : scroll.Color; - this.DisplayData[x,206] = copy ? overflow[2] : scroll.Color; - this.DisplayData[x,207] = copy ? overflow[3] : scroll.Color; - this.DisplayData[x,208] = copy ? overflow[4] : scroll.Color; - this.DisplayData[x,209] = copy ? overflow[5] : scroll.Color; - this.DisplayData[x,210] = copy ? overflow[6] : scroll.Color; - this.DisplayData[x,211] = copy ? overflow[7] : scroll.Color; - this.DisplayData[x,212] = copy ? overflow[8] : scroll.Color; - this.DisplayData[x,213] = copy ? overflow[9] : scroll.Color; - this.DisplayData[x,214] = copy ? overflow[10] : scroll.Color; - this.DisplayData[x,215] = copy ? overflow[11] : scroll.Color; + for(int i = 215; i > 215 - vOffsetTotal; i++) + { + this.DisplayData[x,i] = copy ? overflow[i] : scroll.Color; + } } } } From 2ab1be864da2e1b8021991f2186bc6268c78dc4d Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Tue, 30 Nov 2021 10:23:50 -0800 Subject: [PATCH 8/9] Add debug print for KaraokeDisplay --- .../Hardware/Karaoke/KaraokeDisplay.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs b/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs index a8e03ab..051b177 100644 --- a/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs +++ b/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs @@ -1,3 +1,5 @@ +using System; + namespace RedBookPlayer.Models.Hardware.Karaoke { /// @@ -7,6 +9,9 @@ namespace RedBookPlayer.Models.Hardware.Karaoke /// 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: @@ -57,6 +62,23 @@ namespace RedBookPlayer.Models.Hardware.Karaoke 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 /// @@ -106,7 +128,9 @@ namespace RedBookPlayer.Models.Hardware.Karaoke break; }; } - + + #region Command Processors + /// /// Set the screen to a particular color /// @@ -337,5 +361,7 @@ namespace RedBookPlayer.Models.Hardware.Karaoke this.ColorTable[start + 6] = tableData.ColorSpec[6]; this.ColorTable[start + 7] = tableData.ColorSpec[7]; } + + #endregion } } \ No newline at end of file From 2efd0fa40c24136fdeb675dbb83606848ff19b30 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Fri, 11 Feb 2022 13:23:33 -0800 Subject: [PATCH 9/9] Address review comments --- .../Hardware/Karaoke/KaraokeDisplay.cs | 75 +++++++++---------- .../Hardware/Karaoke/LoadCLUT.cs | 2 - .../Hardware/Karaoke/TileBlock.cs | 2 - RedBookPlayer.Models/Hardware/Player.cs | 3 +- 4 files changed, 36 insertions(+), 46 deletions(-) diff --git a/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs b/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs index 051b177..37e81fe 100644 --- a/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs +++ b/RedBookPlayer.Models/Hardware/Karaoke/KaraokeDisplay.cs @@ -68,12 +68,11 @@ namespace RedBookPlayer.Models.Hardware.Karaoke 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; } @@ -94,34 +93,42 @@ namespace RedBookPlayer.Models.Hardware.Karaoke 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); @@ -154,10 +161,8 @@ namespace RedBookPlayer.Models.Hardware.Karaoke return; for(int x = 3; x < 297; x++) - for(int y = 6; y < 210; y++) - { - this.DisplayData[x,y] = memPreset.Color; - } + for(int y = 6; y < 210; y++) + this.DisplayData[x,y] = memPreset.Color; } /// @@ -174,28 +179,20 @@ namespace RedBookPlayer.Models.Hardware.Karaoke return; for(int x = 0; x < 3; x++) - for(int y = 0; y < 216; y++) - { - this.DisplayData[x,y] = borderPreset.Color; - } + 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 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 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; - } + for(int y = 210; y < 216; y++) + this.DisplayData[x,y] = borderPreset.Color; } /// @@ -226,17 +223,17 @@ namespace RedBookPlayer.Models.Hardware.Karaoke // 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; + 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; - } + if(xor) + this.DisplayData[adjustedX, adjustedY] = (byte)(colorIndex ^ this.DisplayData[adjustedX, adjustedY]); + else + this.DisplayData[adjustedX, adjustedY] = colorIndex; + } } /// @@ -246,7 +243,7 @@ namespace RedBookPlayer.Models.Hardware.Karaoke /// True if data wraps around on scroll, false if filled by a solid color private void ScrollDisplay(Scroll scroll, bool copy) { - if(scroll == null) + if(scroll == null || scroll.HScrollOffset < 0 || scroll.VScrollOffset < 0) return; // Derive the scroll values based on offsets @@ -260,6 +257,7 @@ namespace RedBookPlayer.Models.Hardware.Karaoke for(int y = 0; y < 216; y++) { byte[] overflow = new byte[hOffsetTotal]; + for(int x = 299; x >= 0; x--) { if(x + hOffsetTotal >= 300) @@ -270,9 +268,7 @@ namespace RedBookPlayer.Models.Hardware.Karaoke // 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) @@ -280,6 +276,7 @@ namespace RedBookPlayer.Models.Hardware.Karaoke for(int y = 0; y < 216; y++) { byte[] overflow = new byte[hOffsetTotal]; + for(int x = 0; x < 300; x++) { if(x - hOffsetTotal < 0) @@ -290,9 +287,7 @@ namespace RedBookPlayer.Models.Hardware.Karaoke // Fill in the now-empty pixels for(int i = 299; i > 299 - hOffsetTotal; i++) - { this.DisplayData[i,y] = copy ? overflow[i] : scroll.Color; - } } } @@ -303,6 +298,7 @@ namespace RedBookPlayer.Models.Hardware.Karaoke for(int x = 0; x < 300; x++) { byte[] overflow = new byte[vOffsetTotal]; + for(int y = 215; y >= 0; y--) { if(y + vOffsetTotal >= 216) @@ -313,9 +309,7 @@ namespace RedBookPlayer.Models.Hardware.Karaoke // 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) @@ -323,6 +317,7 @@ namespace RedBookPlayer.Models.Hardware.Karaoke for(int x = 0; x < 300; x++) { byte[] overflow = new byte[vOffsetTotal]; + for(int y = 0; y < 216; y++) { if(y - vOffsetTotal < 0) @@ -333,9 +328,7 @@ namespace RedBookPlayer.Models.Hardware.Karaoke // Fill in the now-empty pixels for(int i = 215; i > 215 - vOffsetTotal; i++) - { this.DisplayData[x,i] = copy ? overflow[i] : scroll.Color; - } } } } diff --git a/RedBookPlayer.Models/Hardware/Karaoke/LoadCLUT.cs b/RedBookPlayer.Models/Hardware/Karaoke/LoadCLUT.cs index c4ca6e6..e563a6e 100644 --- a/RedBookPlayer.Models/Hardware/Karaoke/LoadCLUT.cs +++ b/RedBookPlayer.Models/Hardware/Karaoke/LoadCLUT.cs @@ -17,9 +17,7 @@ namespace RedBookPlayer.Models.Hardware.Karaoke 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/TileBlock.cs b/RedBookPlayer.Models/Hardware/Karaoke/TileBlock.cs index 93018aa..3abad38 100644 --- a/RedBookPlayer.Models/Hardware/Karaoke/TileBlock.cs +++ b/RedBookPlayer.Models/Hardware/Karaoke/TileBlock.cs @@ -32,9 +32,7 @@ namespace RedBookPlayer.Models.Hardware.Karaoke 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 a2eef8a..653b03e 100644 --- a/RedBookPlayer.Models/Hardware/Player.cs +++ b/RedBookPlayer.Models/Hardware/Player.cs @@ -1369,13 +1369,14 @@ namespace RedBookPlayer.Models.Hardware /// /// Raw subchannel data to format /// Dictionary mapping subchannel to formatted data - private Dictionary ConvertSubchannels(byte[] subchannelData) + 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); }