678 lines
20 KiB
C#
678 lines
20 KiB
C#
using System;
|
|
using System.Drawing;
|
|
using System.Drawing.Imaging;
|
|
using System.IO;
|
|
|
|
namespace CdgLib
|
|
{
|
|
public class CDGFile : IDisposable
|
|
{
|
|
private const int COLOUR_TABLE_SIZE = 16;
|
|
|
|
// To detect redundant calls
|
|
private bool disposedValue;
|
|
|
|
#region " IDisposable Support "
|
|
|
|
// This code added by Visual Basic to correctly implement the disposable pattern.
|
|
public void Dispose()
|
|
{
|
|
// Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above.
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
#endregion
|
|
|
|
// IDisposable
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (!disposedValue)
|
|
{
|
|
if (disposing)
|
|
{
|
|
m_pStream.Close();
|
|
}
|
|
m_pStream = null;
|
|
m_pSurface = null;
|
|
}
|
|
disposedValue = true;
|
|
}
|
|
|
|
#region "Constants"
|
|
|
|
//CDG Command Code
|
|
|
|
private const byte CDG_COMMAND = 0x9;
|
|
//CDG Instruction Codes
|
|
private const int CDG_INST_MEMORY_PRESET = 1;
|
|
private const int CDG_INST_BORDER_PRESET = 2;
|
|
private const int CDG_INST_TILE_BLOCK = 6;
|
|
private const int CDG_INST_SCROLL_PRESET = 20;
|
|
private const int CDG_INST_SCROLL_COPY = 24;
|
|
private const int CDG_INST_DEF_TRANSP_COL = 28;
|
|
private const int CDG_INST_LOAD_COL_TBL_LO = 30;
|
|
private const int CDG_INST_LOAD_COL_TBL_HIGH = 31;
|
|
|
|
private const int CDG_INST_TILE_BLOCK_XOR = 38;
|
|
//Bitmask for all CDG fields
|
|
private const byte CDG_MASK = 0x3f;
|
|
private const int CDG_PACKET_SIZE = 24;
|
|
private const int TILE_HEIGHT = 12;
|
|
|
|
private const int TILE_WIDTH = 6;
|
|
//This is the size of the display as defined by the CDG specification.
|
|
//The pixels in this region can be painted, and scrolling operations
|
|
//rotate through this number of pixels.
|
|
public const int CDG_FULL_WIDTH = 300;
|
|
|
|
public const int CDG_FULL_HEIGHT = 216;
|
|
//This is the size of the screen that is actually intended to be
|
|
//visible. It is the center area of CDG_FULL.
|
|
private const int CDG_DISPLAY_WIDTH = 294;
|
|
|
|
private const int CDG_DISPLAY_HEIGHT = 204;
|
|
|
|
#endregion
|
|
|
|
#region "Private Declarations"
|
|
|
|
private readonly byte[,] m_pixelColours = new byte[CDG_FULL_HEIGHT, CDG_FULL_WIDTH];
|
|
private readonly int[] m_colourTable = new int[COLOUR_TABLE_SIZE];
|
|
private int m_presetColourIndex;
|
|
private int m_borderColourIndex;
|
|
|
|
private int m_transparentColour;
|
|
private int m_hOffset;
|
|
|
|
private int m_vOffset;
|
|
private CdgFileIoStream m_pStream;
|
|
private ISurface m_pSurface;
|
|
private long m_positionMs;
|
|
|
|
private long m_duration;
|
|
|
|
private Bitmap mImage;
|
|
|
|
#endregion
|
|
|
|
#region "Properties"
|
|
|
|
public bool Transparent { get; set; }
|
|
|
|
public Image RgbImage
|
|
{
|
|
get
|
|
{
|
|
Stream temp = new MemoryStream();
|
|
try
|
|
{
|
|
var i = 0;
|
|
for (var ri = 0; ri <= CDG_FULL_HEIGHT - 1; ri++)
|
|
{
|
|
for (var ci = 0; ci <= CDG_FULL_WIDTH - 1; ci++)
|
|
{
|
|
var ARGBInt = m_pSurface.rgbData[ri, ci];
|
|
var myByte = new byte[4];
|
|
myByte = BitConverter.GetBytes(ARGBInt);
|
|
temp.Write(myByte, 0, 4);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//Do nothing (empty bitmap will be returned)
|
|
}
|
|
var myBitmap = GraphicUtil.StreamToBitmap(ref temp, CDG_FULL_WIDTH, CDG_FULL_HEIGHT);
|
|
if (Transparent)
|
|
{
|
|
myBitmap.MakeTransparent(myBitmap.GetPixel(1, 1));
|
|
}
|
|
return myBitmap;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region "Public Methods"
|
|
|
|
//Png Export
|
|
public void SavePng(string filename)
|
|
{
|
|
RgbImage.Save(filename, ImageFormat.Png);
|
|
}
|
|
|
|
//New
|
|
public CDGFile(string cdgFileName)
|
|
{
|
|
m_pStream = new CdgFileIoStream();
|
|
m_pStream.Open(cdgFileName);
|
|
m_pSurface = new ISurface();
|
|
if (m_pStream != null && m_pSurface != null)
|
|
{
|
|
reset();
|
|
m_duration = m_pStream.Getsize() / CDG_PACKET_SIZE * 1000 / 300;
|
|
}
|
|
}
|
|
|
|
public long getTotalDuration()
|
|
{
|
|
return m_duration;
|
|
}
|
|
|
|
public bool renderAtPosition(long ms)
|
|
{
|
|
var pack = new CdgPacket();
|
|
long numPacks = 0;
|
|
var res = true;
|
|
|
|
if (m_pStream == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ms < m_positionMs)
|
|
{
|
|
if (m_pStream.Seek(0, SeekOrigin.Begin) < 0)
|
|
return false;
|
|
m_positionMs = 0;
|
|
}
|
|
|
|
//duration of one packet is 1/300 seconds (4 packets per sector, 75 sectors per second)
|
|
|
|
numPacks = ms - m_positionMs;
|
|
numPacks /= 10;
|
|
|
|
m_positionMs += numPacks * 10;
|
|
numPacks *= 3;
|
|
|
|
//TODO: double check logic due to inline while loop fucntionality
|
|
//AndAlso m_pSurface.rgbData Is Nothing
|
|
while (numPacks > 0)
|
|
{
|
|
res = readPacket(ref pack);
|
|
processPacket(ref pack);
|
|
numPacks -= 1;
|
|
}
|
|
|
|
render();
|
|
return res;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region "Private Methods"
|
|
|
|
private void reset()
|
|
{
|
|
Array.Clear(m_pixelColours, 0, m_pixelColours.Length);
|
|
Array.Clear(m_colourTable, 0, m_colourTable.Length);
|
|
|
|
m_presetColourIndex = 0;
|
|
m_borderColourIndex = 0;
|
|
m_transparentColour = 0;
|
|
m_hOffset = 0;
|
|
m_vOffset = 0;
|
|
|
|
m_duration = 0;
|
|
m_positionMs = 0;
|
|
|
|
//clear surface
|
|
if (m_pSurface.rgbData != null)
|
|
{
|
|
Array.Clear(m_pSurface.rgbData, 0, m_pSurface.rgbData.Length);
|
|
}
|
|
}
|
|
|
|
private bool readPacket(ref CdgPacket pack)
|
|
{
|
|
if (m_pStream == null || m_pStream.Eof())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var read = 0;
|
|
|
|
read += m_pStream.Read(ref pack.command, 1);
|
|
read += m_pStream.Read(ref pack.instruction, 1);
|
|
read += m_pStream.Read(ref pack.parityQ, 2);
|
|
read += m_pStream.Read(ref pack.data, 16);
|
|
read += m_pStream.Read(ref pack.parityP, 4);
|
|
|
|
return read == 24;
|
|
}
|
|
|
|
|
|
private void processPacket(ref CdgPacket pack)
|
|
{
|
|
var inst_code = 0;
|
|
|
|
if ((pack.command[0] & CDG_MASK) == CDG_COMMAND)
|
|
{
|
|
inst_code = pack.instruction[0] & CDG_MASK;
|
|
switch (inst_code)
|
|
{
|
|
case CDG_INST_MEMORY_PRESET:
|
|
memoryPreset(ref pack);
|
|
|
|
|
|
break;
|
|
case CDG_INST_BORDER_PRESET:
|
|
borderPreset(ref pack);
|
|
|
|
|
|
break;
|
|
case CDG_INST_TILE_BLOCK:
|
|
tileBlock(ref pack, false);
|
|
|
|
|
|
break;
|
|
case CDG_INST_SCROLL_PRESET:
|
|
scroll(ref pack, false);
|
|
|
|
|
|
break;
|
|
case CDG_INST_SCROLL_COPY:
|
|
scroll(ref pack, true);
|
|
|
|
|
|
break;
|
|
case CDG_INST_DEF_TRANSP_COL:
|
|
defineTransparentColour(ref pack);
|
|
|
|
|
|
break;
|
|
case CDG_INST_LOAD_COL_TBL_LO:
|
|
loadColorTable(ref pack, 0);
|
|
|
|
|
|
break;
|
|
case CDG_INST_LOAD_COL_TBL_HIGH:
|
|
loadColorTable(ref pack, 1);
|
|
|
|
|
|
break;
|
|
case CDG_INST_TILE_BLOCK_XOR:
|
|
tileBlock(ref pack, true);
|
|
|
|
|
|
break;
|
|
default:
|
|
//Ignore the unsupported commands
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private void memoryPreset(ref CdgPacket pack)
|
|
{
|
|
var colour = 0;
|
|
var ri = 0;
|
|
var ci = 0;
|
|
var repeat = 0;
|
|
|
|
colour = pack.data[0] & 0xf;
|
|
repeat = pack.data[1] & 0xf;
|
|
|
|
//Our new interpretation of CD+G Revealed is that memory preset
|
|
//commands should also change the border
|
|
m_presetColourIndex = colour;
|
|
m_borderColourIndex = colour;
|
|
|
|
//we have a reliable data stream, so the repeat command
|
|
//is executed only the first time
|
|
|
|
|
|
if (repeat == 0)
|
|
{
|
|
//Note that this may be done before any load colour table
|
|
//commands by some CDGs. So the load colour table itself
|
|
//actual recalculates the RGB values for all pixels when
|
|
//the colour table changes.
|
|
|
|
//Set the preset colour for every pixel. Must be stored in
|
|
//the pixel colour table indeces array
|
|
|
|
for (ri = 0; ri <= CDG_FULL_HEIGHT - 1; ri++)
|
|
{
|
|
for (ci = 0; ci <= CDG_FULL_WIDTH - 1; ci++)
|
|
{
|
|
m_pixelColours[ri, ci] = (byte)colour;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private void borderPreset(ref CdgPacket pack)
|
|
{
|
|
var colour = 0;
|
|
var ri = 0;
|
|
var ci = 0;
|
|
|
|
colour = pack.data[0] & 0xf;
|
|
m_borderColourIndex = colour;
|
|
|
|
//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).
|
|
|
|
for (ri = 0; ri <= CDG_FULL_HEIGHT - 1; ri++)
|
|
{
|
|
for (ci = 0; ci <= 5; ci++)
|
|
{
|
|
m_pixelColours[ri, ci] = (byte)colour;
|
|
}
|
|
|
|
for (ci = CDG_FULL_WIDTH - 6; ci <= CDG_FULL_WIDTH - 1; ci++)
|
|
{
|
|
m_pixelColours[ri, ci] = (byte)colour;
|
|
}
|
|
}
|
|
|
|
for (ci = 6; ci <= CDG_FULL_WIDTH - 7; ci++)
|
|
{
|
|
for (ri = 0; ri <= 11; ri++)
|
|
{
|
|
m_pixelColours[ri, ci] = (byte)colour;
|
|
}
|
|
|
|
for (ri = CDG_FULL_HEIGHT - 12; ri <= CDG_FULL_HEIGHT - 1; ri++)
|
|
{
|
|
m_pixelColours[ri, ci] = (byte)colour;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private void loadColorTable(ref CdgPacket pack, int table)
|
|
{
|
|
for (var i = 0; i <= 7; i++)
|
|
{
|
|
//[---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
|
|
|
|
var byte0 = pack.data[2 * i];
|
|
var byte1 = pack.data[2 * i + 1];
|
|
var red = (byte0 & 0x3f) >> 2;
|
|
var green = ((byte0 & 0x3) << 2) | ((byte1 & 0x3f) >> 4);
|
|
var blue = byte1 & 0xf;
|
|
|
|
red *= 17;
|
|
green *= 17;
|
|
blue *= 17;
|
|
|
|
if (m_pSurface != null)
|
|
{
|
|
m_colourTable[i + table * 8] = m_pSurface.MapRGBColour(red, green, blue);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private void tileBlock(ref CdgPacket pack, bool bXor)
|
|
{
|
|
var colour0 = 0;
|
|
var colour1 = 0;
|
|
var column_index = 0;
|
|
var row_index = 0;
|
|
var myByte = 0;
|
|
var pixel = 0;
|
|
var xor_col = 0;
|
|
var currentColourIndex = 0;
|
|
var new_col = 0;
|
|
|
|
colour0 = pack.data[0] & 0xf;
|
|
colour1 = pack.data[1] & 0xf;
|
|
row_index = (pack.data[2] & 0x1f) * 12;
|
|
column_index = (pack.data[3] & 0x3f) * 6;
|
|
|
|
if (row_index > CDG_FULL_HEIGHT - TILE_HEIGHT)
|
|
return;
|
|
if (column_index > CDG_FULL_WIDTH - TILE_WIDTH)
|
|
return;
|
|
|
|
//Set the pixel array for each of the pixels in the 12x6 tile.
|
|
//Normal = Set the colour to either colour0 or colour1 depending
|
|
//on whether the pixel value is 0 or 1.
|
|
//XOR = XOR the colour with the colour index currently there.
|
|
|
|
|
|
for (var i = 0; i <= 11; i++)
|
|
{
|
|
myByte = pack.data[4 + i] & 0x3f;
|
|
for (var j = 0; j <= 5; j++)
|
|
{
|
|
pixel = (myByte >> (5 - j)) & 0x1;
|
|
if (bXor)
|
|
{
|
|
//Tile Block XOR
|
|
if (pixel == 0)
|
|
{
|
|
xor_col = colour0;
|
|
}
|
|
else
|
|
{
|
|
xor_col = colour1;
|
|
}
|
|
|
|
//Get the colour index currently at this location, and xor with it
|
|
currentColourIndex = m_pixelColours[row_index + i, column_index + j];
|
|
new_col = currentColourIndex ^ xor_col;
|
|
}
|
|
else
|
|
{
|
|
if (pixel == 0)
|
|
{
|
|
new_col = colour0;
|
|
}
|
|
else
|
|
{
|
|
new_col = colour1;
|
|
}
|
|
}
|
|
|
|
//Set the pixel with the new colour. We set both the surfarray
|
|
//containing actual RGB values, as well as our array containing
|
|
//the colour indexes into our colour table.
|
|
m_pixelColours[row_index + i, column_index + j] = (byte)new_col;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void defineTransparentColour(ref CdgPacket pack)
|
|
{
|
|
m_transparentColour = pack.data[0] & 0xf;
|
|
}
|
|
|
|
|
|
private void scroll(ref CdgPacket pack, bool copy)
|
|
{
|
|
var colour = 0;
|
|
var hScroll = 0;
|
|
var vScroll = 0;
|
|
var hSCmd = 0;
|
|
var hOffset = 0;
|
|
var vSCmd = 0;
|
|
var vOffset = 0;
|
|
var vScrollPixels = 0;
|
|
var hScrollPixels = 0;
|
|
|
|
//Decode the scroll command parameters
|
|
colour = pack.data[0] & 0xf;
|
|
hScroll = pack.data[1] & 0x3f;
|
|
vScroll = pack.data[2] & 0x3f;
|
|
|
|
hSCmd = (hScroll & 0x30) >> 4;
|
|
hOffset = hScroll & 0x7;
|
|
vSCmd = (vScroll & 0x30) >> 4;
|
|
vOffset = vScroll & 0xf;
|
|
|
|
|
|
m_hOffset = hOffset < 5 ? hOffset : 5;
|
|
m_vOffset = vOffset < 11 ? vOffset : 11;
|
|
|
|
//Scroll Vertical - Calculate number of pixels
|
|
|
|
vScrollPixels = 0;
|
|
if (vSCmd == 2)
|
|
{
|
|
vScrollPixels = -12;
|
|
}
|
|
else if (vSCmd == 1)
|
|
{
|
|
vScrollPixels = 12;
|
|
}
|
|
|
|
//Scroll Horizontal- Calculate number of pixels
|
|
|
|
hScrollPixels = 0;
|
|
if (hSCmd == 2)
|
|
{
|
|
hScrollPixels = -6;
|
|
}
|
|
else if (hSCmd == 1)
|
|
{
|
|
hScrollPixels = 6;
|
|
}
|
|
|
|
if (hScrollPixels == 0 && vScrollPixels == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
//Perform the actual scroll.
|
|
|
|
var temp = new byte[CDG_FULL_HEIGHT + 1, CDG_FULL_WIDTH + 1];
|
|
var vInc = vScrollPixels + CDG_FULL_HEIGHT;
|
|
var hInc = hScrollPixels + CDG_FULL_WIDTH;
|
|
var ri = 0;
|
|
//row index
|
|
var ci = 0;
|
|
//column index
|
|
|
|
for (ri = 0; ri <= CDG_FULL_HEIGHT - 1; ri++)
|
|
{
|
|
for (ci = 0; ci <= CDG_FULL_WIDTH - 1; ci++)
|
|
{
|
|
temp[(ri + vInc) % CDG_FULL_HEIGHT, (ci + hInc) % CDG_FULL_WIDTH] = m_pixelColours[ri, ci];
|
|
}
|
|
}
|
|
|
|
|
|
//if copy is false, we were supposed to fill in the new pixels
|
|
//with a new colour. Go back and do that now.
|
|
|
|
|
|
if (copy == false)
|
|
{
|
|
if (vScrollPixels > 0)
|
|
{
|
|
for (ci = 0; ci <= CDG_FULL_WIDTH - 1; ci++)
|
|
{
|
|
for (ri = 0; ri <= vScrollPixels - 1; ri++)
|
|
{
|
|
temp[ri, ci] = (byte)colour;
|
|
}
|
|
}
|
|
}
|
|
else if (vScrollPixels < 0)
|
|
{
|
|
for (ci = 0; ci <= CDG_FULL_WIDTH - 1; ci++)
|
|
{
|
|
for (ri = CDG_FULL_HEIGHT + vScrollPixels; ri <= CDG_FULL_HEIGHT - 1; ri++)
|
|
{
|
|
temp[ri, ci] = (byte)colour;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (hScrollPixels > 0)
|
|
{
|
|
for (ci = 0; ci <= hScrollPixels - 1; ci++)
|
|
{
|
|
for (ri = 0; ri <= CDG_FULL_HEIGHT - 1; ri++)
|
|
{
|
|
temp[ri, ci] = (byte)colour;
|
|
}
|
|
}
|
|
}
|
|
else if (hScrollPixels < 0)
|
|
{
|
|
for (ci = CDG_FULL_WIDTH + hScrollPixels; ci <= CDG_FULL_WIDTH - 1; ci++)
|
|
{
|
|
for (ri = 0; ri <= CDG_FULL_HEIGHT - 1; ri++)
|
|
{
|
|
temp[ri, ci] = (byte)colour;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Now copy the temporary buffer back to our array
|
|
|
|
for (ri = 0; ri <= CDG_FULL_HEIGHT - 1; ri++)
|
|
{
|
|
for (ci = 0; ci <= CDG_FULL_WIDTH - 1; ci++)
|
|
{
|
|
m_pixelColours[ri, ci] = temp[ri, ci];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private void render()
|
|
{
|
|
if (m_pSurface == null)
|
|
return;
|
|
for (var ri = 0; ri <= CDG_FULL_HEIGHT - 1; ri++)
|
|
{
|
|
for (var ci = 0; ci <= CDG_FULL_WIDTH - 1; ci++)
|
|
{
|
|
if (ri < TILE_HEIGHT || ri >= CDG_FULL_HEIGHT - TILE_HEIGHT || ci < TILE_WIDTH ||
|
|
ci >= CDG_FULL_WIDTH - TILE_WIDTH)
|
|
{
|
|
m_pSurface.rgbData[ri, ci] = m_colourTable[m_borderColourIndex];
|
|
}
|
|
else
|
|
{
|
|
m_pSurface.rgbData[ri, ci] = m_colourTable[m_pixelColours[ri + m_vOffset, ci + m_hOffset]];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|
|
|
|
|
|
namespace CdgLib
|
|
{
|
|
public class CdgPacket
|
|
{
|
|
public byte[] command = new byte[1];
|
|
public byte[] data = new byte[16];
|
|
public byte[] instruction = new byte[1];
|
|
public byte[] parityP = new byte[4];
|
|
public byte[] parityQ = new byte[2];
|
|
}
|
|
}
|
|
|
|
namespace CdgLib
|
|
{
|
|
public class ISurface
|
|
{
|
|
public int[,] rgbData = new int[CDGFile.CDG_FULL_HEIGHT, CDGFile.CDG_FULL_WIDTH];
|
|
|
|
public int MapRGBColour(int red, int green, int blue)
|
|
{
|
|
return Color.FromArgb(red, green, blue).ToArgb();
|
|
}
|
|
}
|
|
}
|