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/PSF.cs
2024-07-13 11:16:08 +10:00

513 lines
18 KiB
C#

using System;
using System.IO;
using System.Collections.Generic;
using static ATL.AudioData.AudioDataManager;
using Commons;
using System.Text;
using static ATL.ChannelsArrangements;
using static ATL.TagData;
using System.Threading.Tasks;
namespace ATL.AudioData.IO
{
/// <summary>
/// Class for Portable Sound Format files manipulation (extensions : .PSF, .PSF1, .PSF2,
/// .MINIPSF, .MINIPSF1, .MINIPSF2, .SSF, .MINISSF, .DSF, .MINIDSF, .GSF, .MINIGSF, .QSF, .MINISQF)
/// According to Neil Corlett's specifications v. 1.6
/// </summary>
partial class PSF : MetaDataIO, IAudioDataIO
{
// Format Type Names
public const string PSF_FORMAT_UNKNOWN = "Unknown";
public const string PSF_FORMAT_PSF1 = "Playstation";
public const string PSF_FORMAT_PSF2 = "Playstation 2";
public const string PSF_FORMAT_SSF = "Saturn";
public const string PSF_FORMAT_DSF = "Dreamcast";
public const string PSF_FORMAT_USF = "Nintendo 64";
public const string PSF_FORMAT_QSF = "Capcom QSound";
// Tag predefined fields
public const string TAG_LENGTH = "length";
public const string TAG_FADE = "fade";
private static readonly byte[] PSF_FORMAT_TAG = Utils.Latin1Encoding.GetBytes("PSF");
private const string TAG_HEADER = "[TAG]";
private const uint HEADER_LENGTH = 16;
private const byte LINE_FEED = 0x0A;
private const byte SPACE = 0x20;
private const int PSF_DEFAULT_DURATION = 180000; // 3 minutes
private byte version;
private SizeInfo sizeInfo;
private readonly Format audioFormat;
// Mapping between PSF frame codes and ATL frame codes
private static readonly IDictionary<string, Field> frameMapping = new Dictionary<string, Field>
{
{ "title", Field.TITLE },
{ "game", Field.ALBUM }, // Small innocent semantic shortcut
{ "artist", Field.ARTIST },
{ "copyright", Field.COPYRIGHT },
{ "comment", Field.COMMENT },
{ "year", Field.RECORDING_YEAR },
{ "genre", Field.GENRE },
{ "rating", Field.RATING } // Does not belong to the predefined standard PSF tags
};
// Frames that are required for playback
private static readonly IList<string> playbackFrames = new List<string>
{
"volume",
"length",
"fade",
"filedir",
"filename",
"fileext"
};
// ---------- INFORMATIVE INTERFACE IMPLEMENTATIONS & MANDATORY OVERRIDES
// AudioDataIO
public int SampleRate { get; private set; }
public bool IsVBR => false;
public Format AudioFormat
{
get
{
Format f = new Format(audioFormat);
f.Name = f.Name + " (" + subformat() + ")";
return f;
}
}
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 };
}
public long AudioDataOffset { get; set; }
public long AudioDataSize { get; set; }
// IMetaDataIO
protected override int getDefaultTagOffset() => TO_BUILTIN;
protected override MetaDataIOFactory.TagType getImplementedTagType() => MetaDataIOFactory.TagType.NATIVE;
protected override Field getFrameMapping(string zone, string ID, byte tagVersion)
{
Field supportedMetaId = Field.NO_FIELD;
// Finds the ATL field identifier according to the ID3v2 version
if (frameMapping.ContainsKey(ID.ToLower())) supportedMetaId = frameMapping[ID.ToLower()];
return supportedMetaId;
}
// === PRIVATE STRUCTURES/SUBCLASSES ===
private sealed class PSFHeader
{
public byte[] FormatTag = new byte[3]; // Format tag (should be PSF_FORMAT_TAG)
public byte VersionByte; // Version mark
public uint ReservedAreaLength; // Length of reserved area (bytes)
public uint CompressedProgramLength; // Length of compressed program (bytes)
public void Reset()
{
FormatTag = new byte[3];
VersionByte = 0;
ReservedAreaLength = 0;
CompressedProgramLength = 0;
}
}
private sealed class PSFTag
{
public string TagHeader; // Tag header (should be TAG_HEADER)
public int size;
public void Reset()
{
TagHeader = "";
size = 0;
}
}
// ---------- CONSTRUCTORS & INITIALIZERS
private void resetData()
{
SampleRate = 44100; // Seems to be de facto value for all PSF files, even though spec doesn't say anything about it
version = 0;
BitRate = 0;
Duration = 0;
AudioDataOffset = -1;
AudioDataSize = 0;
ResetData();
}
public PSF(string filePath, Format format)
{
this.FileName = filePath;
this.audioFormat = format;
resetData();
}
// ---------- SUPPORT METHODS
private string subformat()
{
switch (version)
{
case 0x01: return PSF_FORMAT_PSF1;
case 0x02: return PSF_FORMAT_PSF2;
case 0x11: return PSF_FORMAT_SSF;
case 0x12: return PSF_FORMAT_DSF;
case 0x21: return PSF_FORMAT_USF;
case 0x41: return PSF_FORMAT_QSF;
default: return PSF_FORMAT_UNKNOWN;
}
}
public static bool IsValidHeader(byte[] data)
{
return StreamUtils.ArrBeginsWith(data, PSF_FORMAT_TAG);
}
private static bool readHeader(Stream source, ref PSFHeader header)
{
byte[] buffer = new byte[4];
source.Read(header.FormatTag, 0, 3);
if (IsValidHeader(header.FormatTag))
{
source.Read(buffer, 0, 1);
header.VersionByte = buffer[0];
source.Read(buffer, 0, 4);
header.ReservedAreaLength = StreamUtils.DecodeUInt32(buffer);
source.Read(buffer, 0, 4);
header.CompressedProgramLength = StreamUtils.DecodeUInt32(buffer);
return true;
}
else
{
return false;
}
}
private static string readPSFLine(Stream source, Encoding encoding)
{
long lineStart = source.Position;
long lineEnd;
bool hasEOL = true;
if (StreamUtils.FindSequence(source, new byte[] { LINE_FEED }))
{
lineEnd = source.Position;
}
else
{
lineEnd = source.Length;
hasEOL = false;
}
source.Seek(lineStart, SeekOrigin.Begin);
byte[] data = new byte[lineEnd - lineStart];
source.Read(data, 0, data.Length);
for (int i = 0; i < data.Length; i++) if (data[i] < SPACE) data[i] = SPACE; // According to spec : "All characters 0x01-0x20 are considered whitespace"
return encoding.GetString(data, 0, data.Length - (hasEOL ? 1 : 0)).Trim(); // -1 because we don't want to include LINE_FEED in the result
}
private bool readTag(Stream source, ref PSFTag tag, ReadTagParams readTagParams)
{
long initialPosition = source.Position;
Encoding encoding = Utils.Latin1Encoding;
byte[] buffer = new byte[5];
source.Read(buffer, 0, buffer.Length);
tag.TagHeader = Utils.Latin1Encoding.GetString(buffer);
if (TAG_HEADER == tag.TagHeader)
{
string s = readPSFLine(source, encoding);
string lastKey = "";
string lastValue = "";
bool lengthFieldFound = false;
while (s != "")
{
var equalIndex = s.IndexOf("=", StringComparison.Ordinal);
if (equalIndex != -1)
{
var keyStr = s.Substring(0, equalIndex).Trim();
var lowKeyStr = keyStr.ToLower();
var valueStr = s.Substring(equalIndex + 1, s.Length - (equalIndex + 1)).Trim();
if (lowKeyStr.Equals("utf8") && valueStr.Equals("1")) encoding = Encoding.UTF8;
if (lowKeyStr.Equals(TAG_LENGTH) || lowKeyStr.Equals(TAG_FADE))
{
if (lowKeyStr.Equals(TAG_LENGTH)) lengthFieldFound = true;
Duration += parsePSFDuration(valueStr);
}
// PSF specifics : a field appearing more than once is the same field, with values spanning over multiple lines
if (lastKey.Equals(keyStr))
{
lastValue += Environment.NewLine + valueStr;
}
else
{
SetMetaField(lastKey, lastValue, readTagParams.ReadAllMetaFrames);
lastValue = valueStr;
}
lastKey = keyStr;
}
s = readPSFLine(source, encoding);
} // Metadata lines
SetMetaField(lastKey, lastValue, readTagParams.ReadAllMetaFrames);
// PSF files without any 'length' tag take default duration, regardless of 'fade' value
if (!lengthFieldFound) Duration = PSF_DEFAULT_DURATION;
tag.size = (int)(source.Position - initialPosition);
if (readTagParams.PrepareForWriting)
{
structureHelper.AddZone(initialPosition, tag.size);
}
return true;
}
else
{
return false;
}
}
private static double parsePSFDuration(string durationStr)
{
string hStr = "";
string mStr = "";
string sStr = "";
string dStr = "";
double result = 0;
// decimal
var sepIndex = durationStr.LastIndexOf('.');
if (-1 == sepIndex) sepIndex = durationStr.LastIndexOf(',');
if (-1 != sepIndex)
{
sepIndex++;
dStr = durationStr.Substring(sepIndex, durationStr.Length - sepIndex);
durationStr = durationStr[..Math.Max(0, sepIndex - 1)];
}
// seconds
sepIndex = durationStr.LastIndexOf(':');
sepIndex++;
sStr = durationStr.Substring(sepIndex, durationStr.Length - sepIndex);
durationStr = durationStr[..Math.Max(0, sepIndex - 1)];
// minutes
if (durationStr.Length > 0)
{
sepIndex = durationStr.LastIndexOf(':');
sepIndex++;
mStr = durationStr.Substring(sepIndex, durationStr.Length - sepIndex);
durationStr = durationStr[..Math.Max(0, sepIndex - 1)];
}
// hours
if (durationStr.Length > 0)
{
sepIndex = durationStr.LastIndexOf(':');
sepIndex++;
hStr = durationStr.Substring(sepIndex, durationStr.Length - sepIndex);
}
if (dStr != "") result += (int.Parse(dStr) * 100);
if (sStr != "") result += (int.Parse(sStr) * 1000);
if (mStr != "") result += (int.Parse(mStr) * 60000);
if (hStr != "") result += (int.Parse(hStr) * 3600000);
return result;
}
// === 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)
{
PSFHeader header = new PSFHeader();
PSFTag tag = new PSFTag();
header.Reset();
tag.Reset();
resetData();
if (!readHeader(source, ref header)) throw new InvalidDataException("Not a PSF file");
AudioDataOffset = 0;
if (source.Length > HEADER_LENGTH + header.CompressedProgramLength + header.ReservedAreaLength)
{
source.Seek((long)(4 + header.CompressedProgramLength + header.ReservedAreaLength), SeekOrigin.Current);
if (!readTag(source, ref tag, readTagParams)) throw new InvalidDataException("Not a PSF tag");
tagExists = true;
}
AudioDataSize = sizeInfo.FileSize - tag.size;
version = header.VersionByte;
BitRate = AudioDataSize * 8 / Duration;
return true;
}
protected override int write(TagData tag, Stream s, string zone)
{
using (BinaryWriter w = new BinaryWriter(s, Encoding.UTF8, true)) return write(tag, w);
}
private int write(TagData tag, BinaryWriter w)
{
int result = 0;
// Keep these in memory to prevent setting them twice using AdditionalFields
var writtenFieldCodes = new HashSet<string>();
w.Write(Utils.Latin1Encoding.GetBytes(TAG_HEADER));
// Announce UTF-8 support
w.Write(Utils.Latin1Encoding.GetBytes("utf8=1"));
w.Write(LINE_FEED);
IDictionary<Field, string> map = tag.ToMap();
// Supported textual fields
foreach (Field frameType in map.Keys)
{
foreach (string s in frameMapping.Keys)
{
if (frameType == frameMapping[s])
{
if (map[frameType].Length > 0) // No frame with empty value
{
writeTextFrame(w, s, map[frameType]);
writtenFieldCodes.Add(s.ToUpper());
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.NativeFieldCode.Equals("utf8") // utf8 already written
&& !writtenFieldCodes.Contains(fieldInfo.NativeFieldCode.ToUpper())
)
{
writeTextFrame(w, fieldInfo.NativeFieldCode, FormatBeforeWriting(fieldInfo.Value));
result++;
}
}
// Remove the last end-of-line character
w.BaseStream.SetLength(w.BaseStream.Length - 1);
return result;
}
private static void writeTextFrame(BinaryWriter writer, string frameCode, string text)
{
string[] textLines;
if (text.Contains(Environment.NewLine))
{
// Split a multiple-line value into multiple frames with the same code
textLines = text.Split(Environment.NewLine.ToCharArray());
}
else
{
textLines = new[] { text };
}
foreach (string s in textLines)
{
writer.Write(Utils.Latin1Encoding.GetBytes(frameCode));
writer.Write('=');
writer.Write(Encoding.UTF8.GetBytes(s));
writer.Write(LINE_FEED);
}
}
// 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();
s.Seek(sizeInfo.ID3v2Size, SeekOrigin.Begin);
return await WriteAsync(s, tag);
}
private TagData prepareRemove()
{
TagData result = new TagData();
foreach (Field b in frameMapping.Values)
{
result.IntegrateValue(b, "");
}
foreach (MetaFieldInfo fieldInfo in GetAdditionalFields())
{
var fieldCode = fieldInfo.NativeFieldCode.ToLower();
if (!fieldCode.StartsWith('_') && !playbackFrames.Contains(fieldCode))
{
MetaFieldInfo emptyFieldInfo = new MetaFieldInfo(fieldInfo);
emptyFieldInfo.MarkedForDeletion = true;
result.AdditionalFields.Add(emptyFieldInfo);
}
}
return result;
}
}
}