From b0b87d05fde4d0f613cb2bf6343359cc7797d10c Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Sat, 14 Jan 2023 22:24:25 -0800 Subject: [PATCH] Add PLJ builders/wrappers/printing --- BurnOutSharp.Builders/PlayJ.cs | 328 ++++++++++++++++++++++++ BurnOutSharp.Models/PlayJ/Playlist.cs | 6 +- BurnOutSharp.Wrappers/PlayJAudioFile.cs | 284 ++++++++++++++++++++ README.md | 2 +- Test/Printer.cs | 6 + 5 files changed, 622 insertions(+), 4 deletions(-) create mode 100644 BurnOutSharp.Builders/PlayJ.cs create mode 100644 BurnOutSharp.Wrappers/PlayJAudioFile.cs diff --git a/BurnOutSharp.Builders/PlayJ.cs b/BurnOutSharp.Builders/PlayJ.cs new file mode 100644 index 00000000..cb2afc7b --- /dev/null +++ b/BurnOutSharp.Builders/PlayJ.cs @@ -0,0 +1,328 @@ +using System.IO; +using System.Text; +using BurnOutSharp.Models.PlayJ; +using BurnOutSharp.Utilities; +using static BurnOutSharp.Models.PlayJ.Constants; + +namespace BurnOutSharp.Builders +{ + public class PlayJ + { + #region Byte Data + + /// + /// Parse a byte array into a PlayJ playlist + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled playlist on success, null on error + public static Playlist ParsePlaylist(byte[] data, int offset) + { + // If the data is invalid + if (data == null) + return null; + + // If the offset is out of bounds + if (offset < 0 || offset >= data.Length) + return null; + + // Create a memory stream and parse that + MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); + return ParsePlaylist(dataStream); + } + + /// + /// Parse a byte array into a PlayJ audio file + /// + /// Byte array to parse + /// Offset into the byte array + /// Filled audio file on success, null on error + public static AudioFile ParseAudioFile(byte[] data, int offset) + { + // If the data is invalid + if (data == null) + return null; + + // If the offset is out of bounds + if (offset < 0 || offset >= data.Length) + return null; + + // Create a memory stream and parse that + MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); + return ParseAudioFile(dataStream); + } + + #endregion + + #region Stream Data + + /// + /// Parse a Stream into a PlayJ playlist + /// + /// Stream to parse + /// Filled playlist on success, null on error + public static Playlist ParsePlaylist(Stream data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + // If the offset is out of bounds + if (data.Position < 0 || data.Position >= data.Length) + return null; + + // Cache the current offset + int initialOffset = (int)data.Position; + + // Create a new playlist to fill + var playlist = new Playlist(); + + #region Playlist Header + + // Try to parse the playlist header + var playlistHeader = ParsePlaylistHeader(data); + if (playlistHeader == null) + return null; + + // Set the playlist header + playlist.Header = playlistHeader; + + #endregion + + #region Audio Files + + // Create the audio files array + playlist.AudioFiles = new AudioFile[playlistHeader.TrackCount]; + + // Try to parse the audio files + for (int i = 0; i < playlist.AudioFiles.Length; i++) + { + long currentOffset = data.Position; + var entryHeader = ParseAudioFile(data, currentOffset); + if (entryHeader == null) + return null; + + playlist.AudioFiles[i] = entryHeader; + } + + #endregion + + return playlist; + } + + /// + /// Parse a Stream into a PlayJ audio file + /// + /// Stream to parse + /// Offset to adjust all seeking by + /// Filled audio file on success, null on error + public static AudioFile ParseAudioFile(Stream data, long adjust = 0) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + // If the offset is out of bounds + if (data.Position < 0 || data.Position >= data.Length) + return null; + + // Cache the current offset + int initialOffset = (int)data.Position; + + // Create a new audio file to fill + var audioFile = new AudioFile(); + + #region Entry Header + + // Try to parse the entry header + var entryHeader = ParseEntryHeader(data); + if (entryHeader == null) + return null; + + // Set the entry header + audioFile.Header = entryHeader; + + #endregion + + #region Unknown Block 1 + + // Get the unknown block 1 offset + long offset = entryHeader.UnknownOffset1 + adjust; + if (offset < 0 || offset >= data.Length) + return null; + + // Seek to the unknown block 1 + data.Seek(offset, SeekOrigin.Begin); + + // Try to parse the unknown block 1 + var unknownBlock1 = ParseUnknownBlock1(data); + if (unknownBlock1 == null) + return null; + + // Set the unknown block 1 + audioFile.UnknownBlock1 = unknownBlock1; + + #endregion + + #region Unknown Value 2 + + // Get the unknown value 2 offset + offset = entryHeader.UnknownOffset2 + adjust; + if (offset < 0 || offset >= data.Length) + return null; + + // Seek to the unknown value 2 + data.Seek(offset, SeekOrigin.Begin); + + // Set the unknown value 2 + audioFile.UnknownValue2 = data.ReadUInt32();; + + #endregion + + #region Unknown Block 3 + + // Get the unknown block 3 offset + offset = entryHeader.UnknownOffset3 + adjust; + if (offset < 0 || offset >= data.Length) + return null; + + // Seek to the unknown block 3 + data.Seek(offset, SeekOrigin.Begin); + + // Try to parse the unknown block 3 + var unknownBlock3 = ParseUnknownBlock3(data); + if (unknownBlock3 == null) + return null; + + // Set the unknown block 3 + audioFile.UnknownBlock3 = unknownBlock3; + + #endregion + + return audioFile; + } + + /// + /// Parse a Stream into a playlist header + /// + /// Stream to parse + /// Filled playlist header on success, null on error + private static PlaylistHeader ParsePlaylistHeader(Stream data) + { + // TODO: Use marshalling here instead of building + PlaylistHeader playlistHeader = new PlaylistHeader(); + + playlistHeader.TrackCount = data.ReadUInt32(); + playlistHeader.Data = data.ReadBytes(52); + + return playlistHeader; + } + + /// + /// Parse a Stream into an entry header + /// + /// Stream to parse + /// Filled entry header on success, null on error + private static EntryHeader ParseEntryHeader(Stream data) + { + // Cache the current offset + long initialOffset = data.Position; + + // TODO: Use marshalling here instead of building + EntryHeader entryHeader = new EntryHeader(); + + entryHeader.Signature = data.ReadUInt32(); + if (entryHeader.Signature != SignatureUInt32) + return null; + + // Only V1 is supported currently + entryHeader.Version = data.ReadUInt32(); + if (entryHeader.Version != 0x00000000) + return null; + + entryHeader.TrackID = data.ReadUInt32(); + entryHeader.UnknownOffset1 = data.ReadUInt32(); + entryHeader.UnknownOffset2 = data.ReadUInt32(); + entryHeader.UnknownOffset3 = data.ReadUInt32(); + entryHeader.Unknown1 = data.ReadUInt32(); + entryHeader.Unknown2 = data.ReadUInt32(); + entryHeader.Year = data.ReadUInt32(); + entryHeader.TrackNumber = data.ReadByteValue(); + entryHeader.Subgenre = (Subgenre)data.ReadByteValue(); + entryHeader.Duration = data.ReadUInt32(); + + entryHeader.TrackLength = data.ReadUInt16(); + byte[] track = data.ReadBytes(entryHeader.TrackLength); + if (track != null) + entryHeader.Track = Encoding.ASCII.GetString(track); + + entryHeader.ArtistLength = data.ReadUInt16(); + byte[] artist = data.ReadBytes(entryHeader.ArtistLength); + if (artist != null) + entryHeader.Artist = Encoding.ASCII.GetString(artist); + + entryHeader.AlbumLength = data.ReadUInt16(); + byte[] album = data.ReadBytes(entryHeader.AlbumLength); + if (album != null) + entryHeader.Album = Encoding.ASCII.GetString(album); + + entryHeader.WriterLength = data.ReadUInt16(); + byte[] writer = data.ReadBytes(entryHeader.WriterLength); + if (writer != null) + entryHeader.Writer = Encoding.ASCII.GetString(writer); + + entryHeader.PublisherLength = data.ReadUInt16(); + byte[] publisher = data.ReadBytes(entryHeader.PublisherLength); + if (publisher != null) + entryHeader.Publisher = Encoding.ASCII.GetString(publisher); + + entryHeader.LabelLength = data.ReadUInt16(); + byte[] label = data.ReadBytes(entryHeader.LabelLength); + if (label != null) + entryHeader.Label = Encoding.ASCII.GetString(label); + + if (data.Position - initialOffset < entryHeader.UnknownOffset1) + { + entryHeader.CommentsLength = data.ReadUInt16(); + byte[] comments = data.ReadBytes(entryHeader.CommentsLength); + if (comments != null) + entryHeader.Comments = Encoding.ASCII.GetString(comments); + } + + return entryHeader; + } + + /// + /// Parse a Stream into an unknown block 1 + /// + /// Stream to parse + /// Filled unknown block 1 on success, null on error + private static UnknownBlock1 ParseUnknownBlock1(Stream data) + { + // TODO: Use marshalling here instead of building + UnknownBlock1 unknownBlock1 = new UnknownBlock1(); + + unknownBlock1.Length = data.ReadUInt16(); + unknownBlock1.Data = data.ReadBytes(unknownBlock1.Length); + + return unknownBlock1; + } + + /// + /// Parse a Stream into an unknown block 3 + /// + /// Stream to parse + /// Filled unknown block 3 on success, null on error + private static UnknownBlock3 ParseUnknownBlock3(Stream data) + { + // TODO: Use marshalling here instead of building + UnknownBlock3 unknownBlock3 = new UnknownBlock3(); + + // No-op because we don't even know the length + + return unknownBlock3; + } + + #endregion + } +} diff --git a/BurnOutSharp.Models/PlayJ/Playlist.cs b/BurnOutSharp.Models/PlayJ/Playlist.cs index 57557003..713b7cca 100644 --- a/BurnOutSharp.Models/PlayJ/Playlist.cs +++ b/BurnOutSharp.Models/PlayJ/Playlist.cs @@ -8,11 +8,11 @@ namespace BurnOutSharp.Models.PlayJ /// /// Playlist header /// - public PlaylistHeader PlaylistHeader { get; set; } + public PlaylistHeader Header { get; set; } /// - /// Entry headers + /// Embedded audio files / headers /// - public EntryHeader[] EntryHeaders { get; set; } + public AudioFile[] AudioFiles { get; set; } } } \ No newline at end of file diff --git a/BurnOutSharp.Wrappers/PlayJAudioFile.cs b/BurnOutSharp.Wrappers/PlayJAudioFile.cs new file mode 100644 index 00000000..6804151e --- /dev/null +++ b/BurnOutSharp.Wrappers/PlayJAudioFile.cs @@ -0,0 +1,284 @@ +using System; +using System.IO; +using System.Text; + +namespace BurnOutSharp.Wrappers +{ + public class PlayJAudioFile : WrapperBase + { + #region Pass-Through Properties + + #region Entry Header + + /// + public uint Signature => _audioFile.Header.Signature; + + /// + public uint Version => _audioFile.Header.Version; + + /// + public uint TrackID => _audioFile.Header.TrackID; + + /// + public uint UnknownOffset1 => _audioFile.Header.UnknownOffset1; + + /// + public uint UnknownOffset2 => _audioFile.Header.UnknownOffset2; + + /// + public uint UnknownOffset3 => _audioFile.Header.UnknownOffset3; + + /// + public uint Unknown1 => _audioFile.Header.Unknown1; + + /// + public uint Unknown2 => _audioFile.Header.Unknown2; + + /// + public uint Year => _audioFile.Header.Year; + + /// + public byte TrackNumber => _audioFile.Header.TrackNumber; + + /// + public Models.PlayJ.Subgenre Subgenre => _audioFile.Header.Subgenre; + + /// + public uint Duration => _audioFile.Header.Duration; + + /// + public ushort TrackLength => _audioFile.Header.TrackLength; + + /// + public string Track => _audioFile.Header.Track; + + /// + public ushort ArtistLength => _audioFile.Header.ArtistLength; + + /// + public string Artist => _audioFile.Header.Artist; + + /// + public ushort AlbumLength => _audioFile.Header.AlbumLength; + + /// + public string Album => _audioFile.Header.Album; + + /// + public ushort WriterLength => _audioFile.Header.WriterLength; + + /// + public string Writer => _audioFile.Header.Writer; + + /// + public ushort PublisherLength => _audioFile.Header.PublisherLength; + + /// + public string Publisher => _audioFile.Header.Publisher; + + /// + public ushort LabelLength => _audioFile.Header.LabelLength; + + /// + public string Label => _audioFile.Header.Label; + + /// + public ushort CommentsLength => _audioFile.Header.CommentsLength; + + /// + public string Comments => _audioFile.Header.Comments; + + #endregion + + #region Unknown Block 1 + + /// + public ushort UB1_Length => _audioFile.UnknownBlock1.Length; + + /// + public byte[] UB1_Data => _audioFile.UnknownBlock1.Data; + + #endregion + + #region Unknown Value 2 + + /// + public uint UnknownValue2 => _audioFile.UnknownValue2; + + #endregion + + #region Unknown Block 3 + + /// + public byte[] UB3_Data => _audioFile.UnknownBlock3.Data; + + #endregion + + #endregion + + #region Instance Variables + + /// + /// Internal representation of the archive + /// + private Models.PlayJ.AudioFile _audioFile; + + #endregion + + #region Constructors + + /// + /// Private constructor + /// + private PlayJAudioFile() { } + + /// + /// Create a PlayJ audio file from a byte array and offset + /// + /// Byte array representing the archive + /// Offset within the array to parse + /// A PlayJ audio file wrapper on success, null on failure + public static PlayJAudioFile Create(byte[] data, int offset) + { + // If the data is invalid + if (data == null) + return null; + + // If the offset is out of bounds + if (offset < 0 || offset >= data.Length) + return null; + + // Create a memory stream and use that + MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset); + return Create(dataStream); + } + + /// + /// Create a PlayJ audio file from a Stream + /// + /// Stream representing the archive + /// A PlayJ audio file wrapper on success, null on failure + public static PlayJAudioFile Create(Stream data) + { + // If the data is invalid + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + return null; + + var audioFile = Builders.PlayJ.ParseAudioFile(data); + if (audioFile == null) + return null; + + var wrapper = new PlayJAudioFile + { + _audioFile = audioFile, + _dataSource = DataSource.Stream, + _streamData = data, + }; + return wrapper; + } + + #endregion + + #region Printing + + /// + public override StringBuilder PrettyPrint() + { + StringBuilder builder = new StringBuilder(); + + builder.AppendLine("PlayJ Audio File Information:"); + builder.AppendLine("-------------------------"); + builder.AppendLine(); + + PrintEntryHeader(builder); + PrintUnknownBlock1(builder); + PrintUnknownValue2(builder); + PrintUnknownBlock3(builder); + + return builder; + } + + /// + /// Print entry header information + /// + /// StringBuilder to append information to + private void PrintEntryHeader(StringBuilder builder) + { + builder.AppendLine(" Entry Header Information:"); + builder.AppendLine(" -------------------------"); + builder.AppendLine($" Signature: {Signature} (0x{Signature:X})"); + builder.AppendLine($" Version: {Version} (0x{Version:X})"); + builder.AppendLine($" Track ID: {TrackID} (0x{TrackID:X})"); + builder.AppendLine($" Unknown offset 1: {UnknownOffset1} (0x{UnknownOffset1:X})"); + builder.AppendLine($" Unknown offset 2: {UnknownOffset2} (0x{UnknownOffset2:X})"); + builder.AppendLine($" Unknown offset 3: {UnknownOffset3} (0x{UnknownOffset3:X})"); + builder.AppendLine($" Unknown 1: {Unknown1} (0x{Unknown1:X})"); + builder.AppendLine($" Unknown 2: {Unknown2} (0x{Unknown2:X})"); + builder.AppendLine($" Year: {Year} (0x{Year:X})"); + builder.AppendLine($" Track number: {TrackNumber} (0x{TrackNumber:X})"); + builder.AppendLine($" Subgenre: {Subgenre} (0x{Subgenre:X})"); + builder.AppendLine($" Duration in seconds: {Duration} (0x{Duration:X})"); + builder.AppendLine($" Track length: {TrackLength} (0x{TrackLength:X})"); + builder.AppendLine($" Track: {Track ?? "[NULL]"}"); + builder.AppendLine($" Artist length: {ArtistLength} (0x{ArtistLength:X})"); + builder.AppendLine($" Artist: {Artist ?? "[NULL]"}"); + builder.AppendLine($" Album length: {AlbumLength} (0x{AlbumLength:X})"); + builder.AppendLine($" Album: {Album ?? "[NULL]"}"); + builder.AppendLine($" Writer length: {WriterLength} (0x{WriterLength:X})"); + builder.AppendLine($" Writer: {Writer ?? "[NULL]"}"); + builder.AppendLine($" Publisher length: {PublisherLength} (0x{PublisherLength:X})"); + builder.AppendLine($" Publisher: {Publisher ?? "[NULL]"}"); + builder.AppendLine($" Label length: {LabelLength} (0x{LabelLength:X})"); + builder.AppendLine($" Label: {Label ?? "[NULL]"}"); + builder.AppendLine($" Comments length: {CommentsLength} (0x{CommentsLength:X})"); + builder.AppendLine($" Comments: {Comments ?? "[NULL]"}"); + builder.AppendLine(); + } + + /// + /// Print unknown block 1 information + /// + /// StringBuilder to append information to + private void PrintUnknownBlock1(StringBuilder builder) + { + builder.AppendLine(" Unknown Block 1 Information:"); + builder.AppendLine(" -------------------------"); + builder.AppendLine($" Length: {UB1_Length} (0x{UB1_Length:X})"); + builder.AppendLine($" Data: {BitConverter.ToString(UB1_Data ?? new byte[0]).Replace('-', ' ')}"); + builder.AppendLine(); + } + + /// + /// Print unknown value 2 information + /// + /// StringBuilder to append information to + private void PrintUnknownValue2(StringBuilder builder) + { + builder.AppendLine(" Unknown Value 2 Information:"); + builder.AppendLine(" -------------------------"); + builder.AppendLine($" Value: {UnknownValue2} (0x{UnknownValue2:X})"); + builder.AppendLine(); + } + + /// + /// Print unknown block 3 information + /// + /// StringBuilder to append information to + private void PrintUnknownBlock3(StringBuilder builder) + { + builder.AppendLine(" Unknown Block 3 Information:"); + builder.AppendLine(" -------------------------"); + builder.AppendLine($" Data: {BitConverter.ToString(UB3_Data ?? new byte[0]).Replace('-', ' ')}"); + builder.AppendLine(); + } + +#if NET6_0_OR_GREATER + + /// + public override string ExportJSON() => System.Text.Json.JsonSerializer.Serialize(_audioFile, _jsonSerializerOptions); + +#endif + + #endregion + } +} \ No newline at end of file diff --git a/README.md b/README.md index 8b7ca96c..400aae56 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ Below is a list of container formats that are supported in some way: | Nintendo CIA archive | Yes | Yes | No | | | Nintendo DS/DSi cart image | Yes | Yes | No | | | PKZIP and derived files (ZIP, etc.) | No | Yes | Yes | Via `SharpCompress` | -| PlayJ audio file (PLJ) | No | Yes | No | | +| PlayJ audio file (PLJ) | Yes* | Yes | No | Undocumented file format, many fields printed | | Portable Executable | Yes | Yes | No* | Some packed executables are supported | | Quantum archive (Q) | Yes | No | No | | | RAR archive (RAR) | No | Yes | Yes | Via `SharpCompress` | diff --git a/Test/Printer.cs b/Test/Printer.cs index 3734482b..96280d33 100644 --- a/Test/Printer.cs +++ b/Test/Printer.cs @@ -195,6 +195,12 @@ namespace Test wrapper = PAK.Create(stream); break; + // PLJ + case SupportedFileType.PLJ: + wrapperName = "PlayJ audio file"; + wrapper = PlayJAudioFile.Create(stream); + break; + // Quantum case SupportedFileType.Quantum: wrapperName = "Quantum archive";