387 lines
13 KiB
C#
387 lines
13 KiB
C#
using Li.Utilities;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Linq.Expressions;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace GameBuilder.Cue
|
|
{
|
|
public class CueReader : IDisposable
|
|
{
|
|
public int FirstDataTrackNo
|
|
{
|
|
get
|
|
{
|
|
return getFirstDataTrackNo();
|
|
}
|
|
}
|
|
|
|
|
|
private CueTrack[] tracks = new CueTrack[99];
|
|
private Dictionary<int, CueStream> openTracks;
|
|
|
|
public int GetTotalTracks()
|
|
{
|
|
int totalTracks = 0;
|
|
for(int i = 0; i < tracks.Length; i++)
|
|
if (tracks[i] is not null) totalTracks++;
|
|
return totalTracks;
|
|
}
|
|
|
|
public static byte BinaryDecimalToDecimal(int i)
|
|
{
|
|
return Convert.ToByte(Convert.ToInt32(10 * (i - i % 16) / 16 + i % 16));
|
|
}
|
|
public static byte DecimalToBinaryDecimal(int i)
|
|
{
|
|
return Convert.ToByte(Convert.ToInt32((i % 10) + 16 * ((i / 10) % 10)));
|
|
}
|
|
public static int IdxToSectorRel(DiscIndex index)
|
|
{
|
|
int offset = (((index.Mrel * 60) + index.Srel) * 75 + index.Frel);
|
|
return offset;
|
|
}
|
|
public static int IdxToSector(DiscIndex index)
|
|
{
|
|
int offset = (((index.m * 60) + index.s) * 75 + index.f);
|
|
return offset;
|
|
}
|
|
|
|
public static DiscIndex SectorToIdx(int sector, byte index=1)
|
|
{
|
|
DiscIndex idx = new DiscIndex(index);
|
|
|
|
int x = sector;
|
|
int f = sector % 75;
|
|
x = x - f;
|
|
x = Convert.ToInt32(Math.Floor(Convert.ToDouble(x) / 75.0));
|
|
int s = x % 60;
|
|
int m = Convert.ToInt32(Math.Floor(Convert.ToDouble(x) / 60.0));
|
|
|
|
idx.Mrel = Convert.ToInt16(m);
|
|
idx.Srel = Convert.ToInt16(s);
|
|
idx.Frel = Convert.ToInt16(f);
|
|
|
|
return idx;
|
|
}
|
|
|
|
private int getFirstDataTrackNo()
|
|
{
|
|
foreach (CueTrack track in tracks)
|
|
if (track is not null)
|
|
if (track.TrackType == TrackType.TRACK_MODE2_2352) return track.TrackNo;
|
|
|
|
// no non-data tracks?
|
|
return 1;
|
|
}
|
|
private void setTrackNumber(int trackNo, ref CueTrack? track)
|
|
{
|
|
tracks[trackNo - 1] = track;
|
|
}
|
|
public CueTrack GetTrackNumber(int trackNo)
|
|
{
|
|
return tracks[trackNo - 1];
|
|
}
|
|
private int findTrackSz(int trackNo)
|
|
{
|
|
CueTrack track = GetTrackNumber(trackNo);
|
|
// total iso (size / sector size)
|
|
int startSector = IdxToSector(track.TrackIndex[1]);
|
|
int fileSectorSz = Convert.ToInt32(track.binFileSz / track.SectorSz);
|
|
int endSector = Convert.ToInt32(startSector + fileSectorSz);
|
|
|
|
// find first track to start after this one ..
|
|
for (int i = 0; i < tracks.Length; i++)
|
|
{
|
|
CueTrack? cTrack = tracks[i];
|
|
if (cTrack is not null)
|
|
{
|
|
if (cTrack.TrackNo <= track.TrackNo) continue;
|
|
int sector = IdxToSector(cTrack.TrackIndex[0]);
|
|
|
|
if (sector < endSector) endSector = sector;
|
|
}
|
|
}
|
|
|
|
int sectorsLength = (endSector - startSector);
|
|
return sectorsLength;
|
|
}
|
|
|
|
public CueStream OpenTrack(int trackNo)
|
|
{
|
|
if (!openTracks.ContainsKey(trackNo))
|
|
{
|
|
CueTrack track = GetTrackNumber(trackNo);
|
|
int sectorStart = IdxToSectorRel(track.TrackIndex[1]);
|
|
int sectorLen = findTrackSz(trackNo);
|
|
|
|
CueStream trackBin = new CueStream(File.OpenRead(track.binFileName), sectorStart * track.SectorSz, sectorLen * track.SectorSz);
|
|
openTracks[trackNo] = trackBin;
|
|
return trackBin;
|
|
}
|
|
else
|
|
{
|
|
CueStream openTrack = openTracks[trackNo];
|
|
if (!openTrack.IsClosed)
|
|
{
|
|
openTracks.Remove(trackNo);
|
|
return OpenTrack(trackNo);
|
|
}
|
|
else
|
|
{
|
|
openTracks[trackNo].Seek(0x00, SeekOrigin.Begin);
|
|
return openTracks[trackNo];
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private int getLastTrackNo()
|
|
{
|
|
int trackNo = 0;
|
|
for (int i = 0; i < tracks.Length; i++)
|
|
{
|
|
if (tracks[i] is null) continue;
|
|
trackNo = tracks[i].TrackNo;
|
|
}
|
|
return trackNo;
|
|
}
|
|
private int getTotalSectorSz()
|
|
{
|
|
int sectors = 0;
|
|
HashSet<string> countedBins = new HashSet<string>();
|
|
|
|
for(int i = 0; i < tracks.Length; i++)
|
|
{
|
|
if (tracks[i] is null) continue;
|
|
if (!countedBins.Contains(tracks[i].binFileName))
|
|
{
|
|
countedBins.Add(tracks[i].binFileName);
|
|
sectors += Convert.ToInt32(tracks[i].binFileSz / CueTrack.MODE2_SECTOR_SZ);
|
|
}
|
|
}
|
|
return sectors;
|
|
}
|
|
|
|
private bool haveAudioTracks
|
|
{
|
|
get
|
|
{
|
|
for (int i = 0; i < tracks.Length; i++)
|
|
{
|
|
if (tracks[i] is null) continue;
|
|
if (tracks[i].TrackType == TrackType.TRACK_CDDA) return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private byte[] createDummyTracks()
|
|
{
|
|
// every psn ps1 game have "A0" track that points to sector 6000 (MSF 01 20 00)
|
|
byte[] tocA0Entry = new byte[10] { 0x41, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x20, 0x00 };
|
|
|
|
// And an A1 track (determines how many tracks there are)
|
|
byte[] tocA1Entry = new byte[10] { 0x41, 0x00, 0xA1, 0x00, 0x00, 0x00, 0x00, DecimalToBinaryDecimal(GetTotalTracks()), 0x00, 0x00 };
|
|
|
|
// the A2 track is a bit more complicated ..
|
|
int totalSectors = getTotalSectorSz();
|
|
DiscIndex idx = SectorToIdx(totalSectors);
|
|
idx.Sdelta = 2;
|
|
|
|
byte[] tocA2Entry = new byte[10] { 0x41, 0x00, 0xA2, 0x00, 0x00, 0x00, 0x00, idx.M, idx.S, idx.F };
|
|
|
|
if (GetTrackNumber(getLastTrackNo()).TrackType == TrackType.TRACK_CDDA)
|
|
{
|
|
tocA2Entry[0x00] = 0x01;
|
|
tocA1Entry[0x00] = 0x01;
|
|
}
|
|
|
|
byte[] tocDummy = new byte[10 * 3];
|
|
Array.ConstrainedCopy(tocA0Entry, 0, tocDummy, 0, 10);
|
|
Array.ConstrainedCopy(tocA1Entry, 0, tocDummy, 10, 10);
|
|
Array.ConstrainedCopy(tocA2Entry, 0, tocDummy, 20, 10);
|
|
|
|
return tocDummy;
|
|
}
|
|
|
|
private int getTrackSectorOnDisc(int trackNo)
|
|
{
|
|
int absolutePosition = 0;
|
|
for(int i = 0; i < tracks.Length; i++)
|
|
{
|
|
if (tracks[i] is null) continue;
|
|
if (tracks[i].TrackNo == trackNo) break;
|
|
|
|
absolutePosition += findTrackSz(tracks[i].TrackNo);
|
|
}
|
|
return absolutePosition;
|
|
}
|
|
|
|
private void fixUpMsf()
|
|
{
|
|
|
|
Dictionary<string, int> positions = new Dictionary<string, int>();
|
|
int totalPosition = 0;
|
|
|
|
for (int i = 0; i < tracks.Length; i++)
|
|
{
|
|
if (tracks[i] is null) continue;
|
|
|
|
if (!positions.ContainsKey(tracks[i].binFileName))
|
|
{
|
|
positions[tracks[i].binFileName] = totalPosition;
|
|
double sz = Convert.ToDouble(tracks[i].binFileSz) / Convert.ToDouble(tracks[i].SectorSz);
|
|
|
|
totalPosition += Convert.ToInt32(sz);
|
|
}
|
|
|
|
}
|
|
|
|
for (int i = 0; i < tracks.Length; i++)
|
|
{
|
|
if (tracks[i] is null) continue;
|
|
int pos = positions[tracks[i].binFileName];
|
|
|
|
DiscIndex idx = SectorToIdx(pos);
|
|
// pregap not included on first track
|
|
|
|
if (tracks[i].TrackNo == 1) tracks[i].TrackIndex[0].Sdelta = 0;
|
|
|
|
// add pregap
|
|
idx.Mdelta = Convert.ToInt16(tracks[i].TrackIndex[1].Mrel - tracks[i].TrackIndex[0].Mrel);
|
|
idx.Sdelta = Convert.ToInt16(tracks[i].TrackIndex[1].Srel - tracks[i].TrackIndex[0].Srel);
|
|
idx.Fdelta = Convert.ToInt16(tracks[i].TrackIndex[1].Frel - tracks[i].TrackIndex[0].Frel);
|
|
|
|
tracks[i].TrackIndex[0].Mdelta = idx.m;
|
|
tracks[i].TrackIndex[0].Sdelta = idx.s;
|
|
tracks[i].TrackIndex[0].Fdelta = idx.f;
|
|
|
|
// index is always ofset by 2 thou
|
|
idx.Sdelta = 2;
|
|
tracks[i].TrackIndex[1].Mdelta = idx.m;
|
|
tracks[i].TrackIndex[1].Sdelta = idx.s;
|
|
tracks[i].TrackIndex[1].Fdelta = idx.f;
|
|
}
|
|
}
|
|
|
|
public byte[] CreateToc()
|
|
{
|
|
using (MemoryStream toc = new MemoryStream())
|
|
{
|
|
StreamUtil tocUtil = new StreamUtil(toc);
|
|
tocUtil.WriteBytes(createDummyTracks());
|
|
|
|
for (int trackNo = 0; trackNo < tracks.Length; trackNo++)
|
|
{
|
|
if (tracks[trackNo] is not null)
|
|
{
|
|
tocUtil.WriteBytes(tracks[trackNo].ToTocEntry());
|
|
}
|
|
}
|
|
|
|
int remain = Convert.ToInt32(0x3F0 - toc.Length);
|
|
tocUtil.WritePadding(0x00, remain);
|
|
|
|
toc.Seek(0x00, SeekOrigin.Begin);
|
|
return toc.ToArray();
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach(CueStream openTrack in openTracks.Values)
|
|
{
|
|
if(openTrack.IsClosed)
|
|
openTrack.Close();
|
|
}
|
|
openTracks.Clear();
|
|
}
|
|
|
|
private string getFilename(string str)
|
|
{
|
|
if (!str.Contains(' ')) throw new Exception("cue specifies no bin file.");
|
|
if (!str.Contains('"')) return str.Split(' ')[1];
|
|
|
|
int start = str.IndexOf('"');
|
|
str = str.Substring(start + 1);
|
|
|
|
|
|
int end = str.IndexOf('"');
|
|
str = str.Substring(0, end);
|
|
return str;
|
|
}
|
|
public CueReader(string cueFile)
|
|
{
|
|
openTracks = new Dictionary<int, CueStream>();
|
|
for (int trackNo = 0; trackNo < tracks.Length; trackNo++) tracks[trackNo] = null;
|
|
|
|
using (TextReader cueReader = File.OpenText(cueFile))
|
|
{
|
|
CueTrack? curTrack = null;
|
|
|
|
for (string? cueData = cueReader.ReadLine();
|
|
cueData is not null;
|
|
cueData = cueReader.ReadLine())
|
|
{
|
|
cueData = cueData.Trim().Replace("\r", "").Replace("\n", "");
|
|
string[] cueLn = cueData.Split(' ');
|
|
|
|
if (cueLn[0] == "INDEX")
|
|
{
|
|
if (curTrack is null) throw new Exception("tried to create new index, when track was null");
|
|
|
|
int indexNumber = Convert.ToByte(int.Parse(cueLn[1]));
|
|
string[] msf = cueLn[2].Split(':');
|
|
|
|
curTrack.TrackIndex[indexNumber].Mrel = Convert.ToByte(Int32.Parse(msf[0]));
|
|
curTrack.TrackIndex[indexNumber].Srel = Convert.ToByte(Int32.Parse(msf[1]));
|
|
curTrack.TrackIndex[indexNumber].Frel = Convert.ToByte(Int32.Parse(msf[2]));
|
|
|
|
setTrackNumber(curTrack.TrackNo, ref curTrack);
|
|
}
|
|
else if (cueLn[0] == "TRACK")
|
|
{
|
|
if (curTrack is null) throw new Exception("tried to create new track, when track was null");
|
|
|
|
if (curTrack.TrackNo != 0xFF)
|
|
{
|
|
setTrackNumber(curTrack.TrackNo, ref curTrack);
|
|
curTrack = new CueTrack(curTrack.binFileName);
|
|
}
|
|
|
|
curTrack.TrackNo = Convert.ToByte(int.Parse(cueLn[1]));
|
|
if (cueLn[2] == "MODE2/2352")
|
|
curTrack.TrackType = TrackType.TRACK_MODE2_2352;
|
|
else if (cueLn[2] == "AUDIO")
|
|
curTrack.TrackType = TrackType.TRACK_CDDA;
|
|
setTrackNumber(curTrack.TrackNo, ref curTrack);
|
|
}
|
|
else if (cueLn[0] == "FILE")
|
|
{
|
|
if (curTrack != null) setTrackNumber(curTrack.TrackNo, ref curTrack);
|
|
|
|
// parse out filename..
|
|
string binFileName = getFilename(cueData);
|
|
string? folderContainingCue = Path.GetDirectoryName(cueFile);
|
|
|
|
if (folderContainingCue != null)
|
|
binFileName = Path.Combine(folderContainingCue, binFileName);
|
|
|
|
if(!File.Exists(binFileName))
|
|
binFileName = Path.ChangeExtension(cueFile, ".bin");
|
|
|
|
if (!File.Exists(binFileName)) throw new FileNotFoundException("unable to find bin file.");
|
|
|
|
curTrack = new CueTrack(binFileName);
|
|
}
|
|
}
|
|
}
|
|
|
|
fixUpMsf();
|
|
}
|
|
}
|
|
}
|