1
0
mirror of https://github.com/SineVector241/VoiceCraft-MCBE_Proximity_Chat.git synced 2024-11-20 10:27:45 +00:00
VoiceCraft-MCBE_Proximity_Chat/ATL/AudioData/IO/SPC.cs
2024-07-13 11:16:08 +10:00

651 lines
26 KiB
C#

using System;
using System.IO;
using System.Collections.Generic;
using static ATL.AudioData.AudioDataManager;
using Commons;
using static ATL.ChannelsArrangements;
using static ATL.TagData;
using System.Threading.Tasks;
using static System.Int32;
namespace ATL.AudioData.IO
{
/// <summary>
/// Class for SPC700 files manipulation (extensions : .SPC)
/// According to file format v0.30; inspired by the SNESamp source (ID666.cpp)
/// </summary>
partial class SPC : MetaDataIO, IAudioDataIO
{
private const string ZONE_EXTENDED = "extended";
private const string ZONE_HEADER = "header";
private static readonly byte[] SPC_FORMAT_TAG = Utils.Latin1Encoding.GetBytes("SNES-SPC700 Sound File Data");
private const string XTENDED_TAG = "xid6";
#pragma warning disable S1144 // Unused private types or members should be removed
#pragma warning disable IDE0051 // Remove unused private members
private const int REGISTERS_LENGTH = 9;
private const int AUDIODATA_LENGTH = 65792;
private const int SPC_RAW_LENGTH = 66048;
private const int HEADER_TEXT = 0;
private const int HEADER_BINARY = 1;
private const bool PREFER_BIN = false;
private const int SPC_DEFAULT_DURATION = 180000; // 3 minutes
// Sub-chunk ID's / Metadata
private const byte XID6_SONG = 0x01; //see ReadMe.Txt for format information
private const byte XID6_GAME = 0x02;
private const byte XID6_ARTIST = 0x03;
private const byte XID6_DUMPER = 0x04;
private const byte XID6_DATE = 0x05;
private const byte XID6_EMU = 0x06;
private const byte XID6_CMNTS = 0x07;
private const byte XID6_OST = 0x10;
private const byte XID6_DISC = 0x11;
private const byte XID6_TRACK = 0x12;
private const byte XID6_PUB = 0x13;
private const byte XID6_COPY = 0x14;
// Sub-chunk ID's / Playback data
private const byte XID6_INTRO = 0x30;
private const byte XID6_LOOP = 0x31;
private const byte XID6_END = 0x32;
private const byte XID6_FADE = 0x33;
private const byte XID6_MUTE = 0x34;
private const byte XID6_LOOPX = 0x35;
private const byte XID6_AMP = 0x36;
// Artificial IDs for fields stored in header
private const byte HEADER_TITLE = 0xA0;
private const byte HEADER_ALBUM = 0xA1;
private const byte HEADER_DUMPERNAME = 0xA2;
private const byte HEADER_COMMENT = 0xA3;
private const byte HEADER_DUMPDATE = 0xA4;
private const byte HEADER_SONGLENGTH = 0xA5;
private const byte HEADER_FADE = 0xA6;
private const byte HEADER_ARTIST = 0xA7;
//Data types
private const byte XID6_TVAL = 0x00; // int16
private const byte XID6_TSTR = 0x01; // ANSI string
private const byte XID6_TINT = 0x04; // int32
//Timer stuff
private const int XID6_MAXTICKS = 383999999; //Max ticks possible for any field (99:59.99 * 64k)
private const int XID6_TICKSMIN = 3840000; //Number of ticks in a minute (60 * 64k)
private const int XID6_TICKSSEC = 64000; //Number of ticks in a second
private const int XID6_TICKSMS = 64; //Number of ticks in a millisecond
private const int XID6_MAXLOOP = 9; //Max loop times
#pragma warning restore IDE0051 // Remove unused private members
#pragma warning restore S1144 // Unused private types or members should be removed
// Standard fields
private SizeInfo sizeInfo;
// Mapping between SPC extended frame codes and ATL frame codes
private static readonly IDictionary<byte, Field> extendedFrameMapping = new Dictionary<byte, Field>
{
{ XID6_SONG, Field.TITLE },
{ XID6_GAME, Field.ALBUM }, // Small innocent semantic shortcut
{ XID6_ARTIST, Field.ARTIST },
{ XID6_CMNTS, Field.COMMENT },
{ XID6_COPY, Field.RECORDING_YEAR }, // Actual field name is "Copyright year", which makes that legit
{ XID6_TRACK, Field.TRACK_NUMBER },
{ XID6_DISC, Field.DISC_NUMBER },
{ XID6_PUB, Field.PUBLISHER }
};
// Mapping between SPC header frame codes and ATL frame codes
private static readonly IDictionary<byte, Field> headerFrameMapping = new Dictionary<byte, Field>
{
{ HEADER_TITLE, Field.TITLE },
{ HEADER_ALBUM, Field.ALBUM },
{ HEADER_ARTIST, Field.ARTIST },
{ HEADER_COMMENT, Field.COMMENT }
};
// Frames that are required for playback
private static readonly IList<byte> playbackFrames = new List<byte>
{
XID6_INTRO,
XID6_LOOP,
XID6_END,
XID6_FADE,
XID6_MUTE,
XID6_LOOPX,
XID6_AMP,
HEADER_SONGLENGTH,
HEADER_FADE
};
// Mapping between SPC frame codes and frame data types that aren't type 1
private static readonly IDictionary<byte, byte> extendedFrameTypes = new Dictionary<byte, byte>()
{
{ XID6_DATE, 4 },
{ XID6_EMU, 0 },
{ XID6_DISC, 0 },
{ XID6_TRACK, 0 },
{ XID6_COPY, 0 },
{ XID6_INTRO, 4 },
{ XID6_LOOP, 4 },
{ XID6_END, 4 },
{ XID6_FADE, 4 },
{ XID6_MUTE, 0 },
{ XID6_LOOPX, 0 },
{ XID6_AMP, 4 }
};
// ---------- INFORMATIVE INTERFACE IMPLEMENTATIONS & MANDATORY OVERRIDES
// AudioDataIO
public int SampleRate { get; private set; }
public bool IsVBR => false;
public Format AudioFormat
{
get;
}
public int CodecFamily => AudioDataIOFactory.CF_SEQ_WAV;
public string FileName { get; }
public double BitRate { get; private set; }
public int BitDepth => -1; // Irrelevant for that format
public double Duration { get; private set; }
public ChannelsArrangement ChannelsArrangement => STEREO;
/// <inheritdoc/>
public List<MetaDataIOFactory.TagType> GetSupportedMetas()
{
return new List<MetaDataIOFactory.TagType> { MetaDataIOFactory.TagType.NATIVE, MetaDataIOFactory.TagType.APE };
}
public long AudioDataOffset { get; set; }
public long AudioDataSize { get; set; }
// IMetaDataIO
protected override int getDefaultTagOffset()
{
return TO_BUILTIN;
}
protected override MetaDataIOFactory.TagType getImplementedTagType()
{
return MetaDataIOFactory.TagType.NATIVE;
}
protected override Field getFrameMapping(string zone, string ID, byte tagVersion)
{
Field supportedMetaId = Field.NO_FIELD;
byte ID_b = byte.Parse(ID);
// Finds the ATL field identifier
if (ZONE_EXTENDED.Equals(zone) && extendedFrameMapping.TryGetValue(ID_b, out var value)) supportedMetaId = value;
else if (ZONE_HEADER.Equals(zone) && headerFrameMapping.TryGetValue(ID_b, out var value1)) supportedMetaId = value1;
return supportedMetaId;
}
// === PRIVATE STRUCTURES/SUBCLASSES ===
private sealed class SpcHeader
{
public const int TAG_IN_HEADER = 26;
public long Size;
public byte TagInHeader; // Set to TAG_IN_HEADER if header contains ID666 info
public void Reset()
{
Size = 0;
}
}
private sealed class SpcExTags
{
public string FormatTag; // Extended info tag (should be XTENDED_TAG)
public uint Size; // Chunk size
public void Reset()
{
FormatTag = "";
Size = 0;
}
}
// ---------- CONSTRUCTORS & INITIALIZERS
private void resetData()
{
// Reset variables
SampleRate = 32000; // Seems to be de facto value for all SPC files, even though spec doesn't say anything about it
BitRate = 0;
Duration = SPC_DEFAULT_DURATION;
ResetData();
}
public SPC(string filePath, Format format)
{
this.FileName = filePath;
AudioFormat = format;
resetData();
}
// === PRIVATE METHODS ===
public static bool IsValidHeader(byte[] data)
{
return StreamUtils.ArrBeginsWith(data, SPC_FORMAT_TAG);
}
private static bool readHeader(Stream source, ref SpcHeader header)
{
source.Seek(0, SeekOrigin.Begin);
long initialPosition = source.Position;
byte[] buffer = new byte[SPC_FORMAT_TAG.Length];
source.Read(buffer, 0, buffer.Length);
if (IsValidHeader(buffer))
{
source.Seek(8, SeekOrigin.Current); // Remainder of header tag (version marker vX.XX + 2 bytes)
source.Read(buffer, 0, 2);
header.TagInHeader = buffer[0];
header.Size = source.Position - initialPosition;
return true;
}
else
{
return false;
}
}
private void readHeaderTags(Stream source, ref SpcHeader header, ReadTagParams readTagParams)
{
byte[] buffer = new byte[32];
long initialPosition = source.Position;
source.Read(buffer, 0, 32);
SetMetaField(HEADER_TITLE.ToString(), Utils.Latin1Encoding.GetString(buffer).Replace("\0", "").Trim(), readTagParams.ReadAllMetaFrames, ZONE_HEADER);
source.Read(buffer, 0, 32);
SetMetaField(HEADER_ALBUM.ToString(), Utils.Latin1Encoding.GetString(buffer).Replace("\0", "").Trim(), readTagParams.ReadAllMetaFrames, ZONE_HEADER);
source.Read(buffer, 0, 16);
SetMetaField(HEADER_DUMPERNAME.ToString(), Utils.Latin1Encoding.GetString(buffer).Replace("\0", "").Trim(), readTagParams.ReadAllMetaFrames, ZONE_HEADER);
source.Read(buffer, 0, 32);
SetMetaField(HEADER_COMMENT.ToString(), Utils.Latin1Encoding.GetString(buffer).Replace("\0", "").Trim(), readTagParams.ReadAllMetaFrames, ZONE_HEADER);
byte[] date = new byte[11];
byte[] song = new byte[3];
byte[] fade = new byte[5];
// NB : Dump date is used to determine if the tag is binary or text-based.
// It won't be recorded as a property of TSPC
source.Read(date, 0, date.Length);
source.Read(song, 0, song.Length);
source.Read(fade, 0, fade.Length);
int dateRes = isText(date);
int songRes = isText(song);
int fadeRes = isText(fade);
bool bin = true;
if (songRes != -1 && fadeRes != -1) // No time, or time is text
{
if (dateRes > 0) //If date is text, then tag is text
{
bin = false;
}
else if (0 == dateRes) //No date
{
bin = PREFER_BIN; //Times could still be binary (ex. 56 bin = '8' txt)
}
else if (-1 == dateRes) //Date contains invalid characters
{
bin = true;
for (int i = 4; i < 8; i++)
{
bin = bin && 0 == date[i];
}
}
}
else
{
bin = true;
}
int fadeVal;
int songVal;
if (bin)
{
fadeVal =
fade[0] * 0x000001 +
fade[1] * 0x0000FF +
fade[2] * 0x00FF00 +
fade[3] * 0xFF0000;
if (fadeVal > 59999) fadeVal = 59999;
songVal = song[0] * 0x01 + song[1] * 0x10;
if (songVal > 959) songVal = 959;
source.Seek(-1, SeekOrigin.Current); // We're one byte ahead
SetMetaField(HEADER_FADE.ToString(), Utils.Latin1Encoding.GetString(fade), readTagParams.ReadAllMetaFrames, ZONE_HEADER);
}
else
{
fadeVal = TrackUtils.ExtractTrackNumber(Utils.Latin1Encoding.GetString(fade));
songVal = TrackUtils.ExtractTrackNumber(Utils.Latin1Encoding.GetString(song));
SetMetaField(HEADER_FADE.ToString(), Utils.Latin1Encoding.GetString(fade), readTagParams.ReadAllMetaFrames, ZONE_HEADER);
}
SetMetaField(HEADER_DUMPDATE.ToString(), Utils.Latin1Encoding.GetString(date), readTagParams.ReadAllMetaFrames, ZONE_HEADER);
SetMetaField(HEADER_SONGLENGTH.ToString(), Utils.Latin1Encoding.GetString(song), readTagParams.ReadAllMetaFrames, ZONE_HEADER);
// if fadeval > 0 alone, the fade is applied on the default 3:00 duration without extending it
if (songVal > 0) Duration = fadeVal + songVal;
source.Read(buffer, 0, 32);
SetMetaField(HEADER_ARTIST.ToString(), Utils.Latin1Encoding.GetString(buffer).Replace("\0", "").Trim(), readTagParams.ReadAllMetaFrames, ZONE_HEADER);
header.Size += source.Position - initialPosition;
if (readTagParams.PrepareForWriting)
{
structureHelper.AddZone(initialPosition, (int)(source.Position - initialPosition), ZONE_HEADER);
}
}
private static int isText(byte[] data)
{
int c = 0;
while (c < data.Length && ((data[c] >= 0x30 && data[c] <= 0x39) || 0x2F == data[c])) c++; // 0x2F = '/' (date separator)
if (c == data.Length || data[c] == 0)
return c;
else
return -1;
}
private void readExtendedData(Stream source, ref SpcExTags footer, ReadTagParams readTagParams)
{
long initialPosition = source.Position;
byte[] buffer = new byte[4];
source.Read(buffer, 0, buffer.Length);
footer.FormatTag = Utils.Latin1Encoding.GetString(buffer);
if (XTENDED_TAG == footer.FormatTag)
{
tagExists = true;
source.Read(buffer, 0, buffer.Length);
footer.Size = StreamUtils.DecodeUInt32(buffer);
string strData = "";
int intData = 0;
long ticks = 0;
long dataPosition = source.Position;
while (source.Position < dataPosition + footer.Size - 4)
{
source.Read(buffer, 0, 2);
var ID = buffer[0];
var type = buffer[1];
source.Read(buffer, 0, 2);
var size = StreamUtils.DecodeUInt16(buffer);
switch (type)
{
case XID6_TVAL:
// Value is stored into the Size field
if (ID == XID6_TRACK) // Specific case : upper byte is the number 0-99, lower byte is an optional ASCII character
{
intData = size >> 8;
strData = intData.ToString();
byte optionalChar = (byte)(size & 0x00FF);
if (optionalChar > 0x20) // Character is displayable
{
strData += (char)optionalChar;
}
}
else
{
intData = size;
strData = intData.ToString();
}
break;
case XID6_TSTR:
intData = 0;
byte[] strDatab = new byte[size];
source.Read(strDatab, 0, size);
strData = Utils.Latin1Encoding.GetString(strDatab).Replace("\0", "").Trim();
while (source.Position < source.Length && 0 == source.ReadByte()) ; // Skip parasite ending zeroes
if (source.Position < source.Length) source.Seek(-1, SeekOrigin.Current);
break;
case XID6_TINT:
source.Read(buffer, 0, 4);
intData = StreamUtils.DecodeInt32(buffer);
strData = intData.ToString();
break;
}
if (XID6_LOOP == ID) ticks += Math.Min(XID6_MAXTICKS, intData);
else if (XID6_LOOPX == ID) ticks *= Math.Min(XID6_MAXLOOP, (int)size);
else if (XID6_INTRO == ID) ticks += Math.Min(XID6_MAXTICKS, intData);
else if (XID6_END == ID) ticks += Math.Min(XID6_MAXTICKS, intData);
else if (XID6_FADE == ID) ticks += Math.Min(XID6_MAXTICKS, intData);
SetMetaField(ID.ToString(), strData, readTagParams.ReadAllMetaFrames, ZONE_EXTENDED);
}
if (ticks > 0) Duration = Math.Round((double)ticks / XID6_TICKSSEC);
if (readTagParams.PrepareForWriting)
{
structureHelper.AddZone(initialPosition, (int)(source.Position - initialPosition), ZONE_EXTENDED);
}
}
}
// === PUBLIC METHODS ===
public bool Read(Stream source, SizeInfo sizeInfo, ReadTagParams readTagParams)
{
this.sizeInfo = sizeInfo;
return read(source, readTagParams);
}
protected override bool read(Stream source, ReadTagParams readTagParams)
{
bool result = true;
SpcHeader header = new SpcHeader();
SpcExTags footer = new SpcExTags();
header.Reset();
footer.Reset();
resetData();
source.Seek(sizeInfo.ID3v2Size, SeekOrigin.Begin);
if (!readHeader(source, ref header)) throw new InvalidDataException("Not a SPC file");
// Reads the header tag
if (SpcHeader.TAG_IN_HEADER == header.TagInHeader)
{
tagExists = true;
source.Seek(REGISTERS_LENGTH, SeekOrigin.Current);
readHeaderTags(source, ref header, readTagParams);
}
AudioDataOffset = source.Position;
// Reads extended tag
if (source.Length > SPC_RAW_LENGTH)
{
source.Seek(SPC_RAW_LENGTH, SeekOrigin.Begin);
readExtendedData(source, ref footer, readTagParams);
}
else
{
if (readTagParams.PrepareForWriting)
{
structureHelper.AddZone(SPC_RAW_LENGTH, 0, ZONE_EXTENDED);
}
}
AudioDataSize = sizeInfo.FileSize - header.Size - footer.Size;
BitRate = AudioDataSize * 8 / Duration;
return result;
}
protected override int write(TagData tag, Stream s, string zone)
{
int result = 0;
var buffer = new Span<byte>(new byte[4]);
if (zone.Equals(ZONE_HEADER))
{
StreamUtils.WriteBytes(s, Utils.Latin1Encoding.GetBytes(Utils.BuildStrictLengthString(tag[Field.TITLE], 32, '\0')));
StreamUtils.WriteBytes(s, Utils.Latin1Encoding.GetBytes(Utils.BuildStrictLengthString(tag[Field.ALBUM], 32, '\0')));
StreamUtils.WriteBytes(s, Utils.Latin1Encoding.GetBytes(Utils.BuildStrictLengthString(AdditionalFields[HEADER_DUMPERNAME.ToString()], 16, '\0')));
StreamUtils.WriteBytes(s, Utils.Latin1Encoding.GetBytes(Utils.BuildStrictLengthString(tag[Field.COMMENT], 32, '\0')));
StreamUtils.WriteBytes(s, Utils.Latin1Encoding.GetBytes(AdditionalFields[HEADER_DUMPDATE.ToString()]));
StreamUtils.WriteBytes(s, Utils.Latin1Encoding.GetBytes(AdditionalFields[HEADER_SONGLENGTH.ToString()]));
StreamUtils.WriteBytes(s, Utils.Latin1Encoding.GetBytes(AdditionalFields[HEADER_FADE.ToString()]));
StreamUtils.WriteBytes(s, Utils.Latin1Encoding.GetBytes(Utils.BuildStrictLengthString(tag[Field.ARTIST], 32, '\0')));
result = 8;
}
else if (zone.Equals(ZONE_EXTENDED))
{
// SPC specific : are only allowed to appear in extended metadata fields that
// - either do not exist in header
// - or have been truncated when written in header
Utils.Latin1Encoding.GetBytes(XTENDED_TAG.AsSpan(), buffer);
s.Write(buffer);
long sizePos = s.Position;
StreamUtils.WriteInt32(s, 0, buffer); // Size placeholder; to be rewritten with actual value at the end of the method
IDictionary<Field, string> map = tag.ToMap();
// Supported textual fields
foreach (Field frameType in map.Keys)
{
foreach (byte b in extendedFrameMapping.Keys)
{
if (frameType == extendedFrameMapping[b])
{
if (map[frameType].Length > 0 && canBeWrittenInExtendedMetadata(frameType, map[frameType])) // No frame with empty value
{
writeSubChunk(s, b, map[frameType], buffer);
result++;
}
break;
}
}
}
// Other textual fields
foreach (MetaFieldInfo fieldInfo in tag.AdditionalFields)
{
if ((fieldInfo.TagType.Equals(MetaDataIOFactory.TagType.ANY) || fieldInfo.TagType.Equals(getImplementedTagType())) && !fieldInfo.MarkedForDeletion && !fieldInfo.Zone.Equals(ZONE_HEADER) && fieldInfo.Value.Length > 0)
{
writeSubChunk(s, byte.Parse(fieldInfo.NativeFieldCode), FormatBeforeWriting(fieldInfo.Value), buffer);
result++;
}
}
int size = (int)(s.Position - sizePos);
s.Seek(sizePos, SeekOrigin.Begin);
StreamUtils.WriteInt32(s, size, buffer);
}
return result;
}
private static bool canBeWrittenInExtendedMetadata(Field frameType, string value)
{
if (frameType == Field.TITLE || frameType == Field.ALBUM || frameType == Field.COMMENT || frameType == Field.ARTIST)
{
return value.Length > 32;
}
return true;
}
private static void writeSubChunk(Stream stream, byte frameCode, string text, Span<byte> buffer)
{
stream.WriteByte(frameCode);
byte type = 1;
if (extendedFrameTypes.TryGetValue(frameCode, out var frameType)) type = frameType;
stream.WriteByte(type);
switch (type)
{
case XID6_TVAL:
if (frameCode == XID6_TRACK) // Specific case : upper byte is the number 0-99, lower byte is an optional ASCII character
{
byte trackValue = (byte)Math.Min((ushort)0xFF, TrackUtils.ExtractTrackNumber(text));
stream.WriteByte(0); // Optional char support is not implemented
stream.WriteByte(trackValue);
}
else
{
StreamUtils.WriteUInt16(stream, ushort.Parse(text), buffer); // Value is directly written as an ushort into the length field
}
break;
case XID6_TSTR:
if (text.Length > 255) text = text[..255];
else if (text.Length < 3) text = Utils.BuildStrictLengthString(text, 3, ' ');
byte[] textBinary = Utils.Latin1Encoding.GetBytes(text);
StreamUtils.WriteUInt16(stream, (ushort)(textBinary.Length + 1), buffer);
StreamUtils.WriteBytes(stream, textBinary);
stream.WriteByte(0);
break;
case XID6_TINT:
StreamUtils.WriteUInt16(stream, 4, buffer);
StreamUtils.WriteInt32(stream, Parse(text), buffer);
break;
}
}
// Specific implementation for conservation of fields that are required for playback
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public override async Task<bool> RemoveAsync(Stream s)
{
TagData tag = prepareRemove();
return await WriteAsync(s, tag);
}
private TagData prepareRemove()
{
TagData result = new TagData();
foreach (Field b in extendedFrameMapping.Values)
{
result.IntegrateValue(b, "");
}
byte fieldCode;
foreach (MetaFieldInfo fieldInfo in GetAdditionalFields())
{
fieldCode = byte.Parse(fieldInfo.NativeFieldCode);
if (!playbackFrames.Contains(fieldCode))
{
MetaFieldInfo emptyFieldInfo = new MetaFieldInfo(fieldInfo);
emptyFieldInfo.MarkedForDeletion = true;
result.AdditionalFields.Add(emptyFieldInfo);
}
}
return result;
}
}
}