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

672 lines
28 KiB
C#

using ATL.Logging;
using Commons;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using static ATL.AudioData.AudioDataManager;
using static ATL.ChannelsArrangements;
using System.Linq;
using static ATL.TagData;
namespace ATL.AudioData.IO
{
/// <summary>
/// Class for Audio Interchange File Format files manipulation (extension : .AIF, .AIFF, .AIFC)
///
/// Implementation notes
///
/// 1/ Annotations being somehow deprecated (Cf. specs "Use of this chunk is discouraged within FORM AIFC. The more refined Comments Chunk should be used instead"),
/// any data read from an ANNO chunk will be written as a COMT chunk when updating the file (ANNO chunks will be deleted in the process).
///
/// 2/ Embedded MIDI detection, parsing and writing is not supported
///
/// 3/ Instrument detection, parsing and writing is not supported
/// </summary>
class AIFF : MetaDataIO, IAudioDataIO, IMetaDataEmbedder
{
#pragma warning disable S1144 // Unused private types or members should be removed
#pragma warning disable IDE0051 // Remove unused private members
public static readonly byte[] AIFF_CONTAINER_ID = Utils.Latin1Encoding.GetBytes("FORM");
private const string FORMTYPE_AIFF = "AIFF";
private const string FORMTYPE_AIFC = "AIFC";
private const string COMPRESSION_NONE = "NONE";
private const string COMPRESSION_NONE_LE = "sowt";
private const string CHUNKTYPE_COMMON = "COMM";
private const string CHUNKTYPE_SOUND = "SSND";
private const string CHUNKTYPE_MARKER = "MARK";
private const string CHUNKTYPE_INSTRUMENT = "INST";
private const string CHUNKTYPE_COMMENTS = "COMT";
private const string CHUNKTYPE_NAME = "NAME";
private const string CHUNKTYPE_AUTHOR = "AUTH";
private const string CHUNKTYPE_COPYRIGHT = "(c) ";
private const string CHUNKTYPE_ANNOTATION = "ANNO"; // Use in discouraged by specs in favour of COMT
private const string CHUNKTYPE_ID3TAG = "ID3 ";
#pragma warning restore IDE0051 // Remove unused private members
#pragma warning restore S1144 // Unused private types or members should be removed
// AIFx timestamp are defined as "the number of seconds since January 1, 1904"
private static readonly DateTime timestampBase = new DateTime(1904, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public class CommentData
{
public uint Timestamp;
public short MarkerId;
}
private struct ChunkHeader
{
public string ID;
public int Size;
}
// Private declarations
private int bits;
private string compression;
private readonly FileStructureHelper id3v2StructureHelper = new FileStructureHelper(false);
// Mapping between AIFx frame codes and ATL frame codes
private static readonly IDictionary<string, Field> frameMapping = new Dictionary<string, Field>
{
{ CHUNKTYPE_NAME, Field.TITLE },
{ CHUNKTYPE_AUTHOR, Field.ARTIST },
{ CHUNKTYPE_COPYRIGHT, Field.COPYRIGHT }
};
// Version code
public byte VersionID { get; private set; }
// ---------- INFORMATIVE INTERFACE IMPLEMENTATIONS & MANDATORY OVERRIDES
// IAudioDataIO
public bool IsVBR => false;
public Format AudioFormat
{
get;
}
public int CodecFamily => compression.Equals(COMPRESSION_NONE) || compression.Equals(COMPRESSION_NONE_LE) ? AudioDataIOFactory.CF_LOSSLESS : AudioDataIOFactory.CF_LOSSY;
public string FileName { get; }
public int SampleRate { get; private set; }
public double BitRate { get; private set; }
public int BitDepth => bits > 0 ? bits : -1;
public double Duration { get; private set; }
public ChannelsArrangement ChannelsArrangement { get; private set; }
public List<MetaDataIOFactory.TagType> GetSupportedMetas()
{
return new List<MetaDataIOFactory.TagType> { MetaDataIOFactory.TagType.ID3V2, MetaDataIOFactory.TagType.NATIVE };
}
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;
}
public override byte FieldCodeFixedLength => 4;
protected override bool isLittleEndian => false;
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.TryGetValue(ID, out var value)) supportedMetaId = value;
return supportedMetaId;
}
// IMetaDataEmbedder
public long HasEmbeddedID3v2 { get; private set; }
public uint ID3v2EmbeddingHeaderSize => 8;
public FileStructureHelper.Zone Id3v2Zone => id3v2StructureHelper.GetZone(CHUNKTYPE_ID3TAG);
// ---------- CONSTRUCTORS & INITIALIZERS
private void resetData()
{
Duration = 0;
BitRate = 0;
id3v2StructureHelper.Clear();
bits = 0;
SampleRate = 0;
VersionID = 0;
HasEmbeddedID3v2 = -1;
AudioDataOffset = -1;
AudioDataSize = 0;
ResetData();
}
public AIFF(string filePath, Format format)
{
this.FileName = filePath;
AudioFormat = format;
resetData();
}
// ---------- SUPPORT METHODS
/// <summary>
/// Reads ID and size of a local chunk and returns them in a dedicated structure _without_ reading nor skipping data
/// </summary>
/// <param name="source">Source where to read header information</param>
/// <param name="limit">Maximum absolute position to search to</param>
/// <param name="previousChunkId">ID of the previous chunk</param>
/// <returns>Local chunk header information</returns>
private ChunkHeader seekNextChunkHeader(BufferedBinaryReader source, long limit, string previousChunkId)
{
ChunkHeader header = new ChunkHeader();
byte[] aByte = new byte[1];
int previousChunkSizeCorrection = 0;
source.Read(aByte, 0, 1);
// In case previous chunk has a padding byte, seek a suitable first character for an ID
if (aByte[0] != 40 && !char.IsLetter((char)aByte[0]) && source.Position <= limit)
{
previousChunkSizeCorrection++;
if (source.Position < limit) source.Read(aByte, 0, 1);
}
// Update zone size (remove and replace zone with updated size)
if (previousChunkId.Length > 0 && previousChunkSizeCorrection > 0)
{
FileStructureHelper sHelper = previousChunkId == CHUNKTYPE_ID3TAG ? id3v2StructureHelper : structureHelper;
FileStructureHelper.Zone previousZone = sHelper.GetZone(previousChunkId);
if (previousZone != null)
{
previousZone.Size += previousChunkSizeCorrection;
sHelper.RemoveZone(previousChunkId);
sHelper.AddZone(previousZone);
}
}
// Write actual tag size
if (source.Position < limit)
{
source.Seek(-1, SeekOrigin.Current);
// Chunk ID
header.ID = Utils.Latin1Encoding.GetString(source.ReadBytes(4));
// Chunk size
header.Size = StreamUtils.DecodeBEInt32(source.ReadBytes(4));
}
else
{
header.ID = "";
}
return header;
}
public static bool IsValidHeader(byte[] data)
{
return StreamUtils.ArrBeginsWith(data, AIFF_CONTAINER_ID);
}
public bool Read(Stream source, SizeInfo sizeInfo, ReadTagParams readTagParams)
{
return read(source, readTagParams);
}
protected override bool read(Stream source, ReadTagParams readTagParams)
{
resetData();
BufferedBinaryReader reader = new BufferedBinaryReader(source);
reader.Seek(0, SeekOrigin.Begin);
// Magic number check
if (!IsValidHeader(reader.ReadBytes(4))) return false;
// Container chunk size
long containerChunkPos = reader.Position;
int containerChunkSize = StreamUtils.DecodeBEInt32(reader.ReadBytes(4));
if (containerChunkPos + containerChunkSize + 4 != reader.Length)
{
LogDelegator.GetLogDelegate()(Log.LV_WARNING, "Header size is incoherent with file size");
}
// Form type
string format = Utils.Latin1Encoding.GetString(reader.ReadBytes(4));
// AIFF / AIFC format check
if (!format.Equals(FORMTYPE_AIFF) && !format.Equals(FORMTYPE_AIFC)) return false;
StringBuilder commentStr = new StringBuilder("");
long soundChunkPosition = 0;
long soundChunkSize = 0; // Header size included
bool nameFound = false;
bool authorFound = false;
bool copyrightFound = false;
bool commentsFound = false;
long limit = Math.Min(containerChunkPos + containerChunkSize + 4, reader.Length);
int annotationIndex = 0;
int commentIndex = 0;
string chunkId = "";
while (reader.Position < limit)
{
ChunkHeader header = seekNextChunkHeader(reader, limit, chunkId);
chunkId = header.ID;
var position = reader.Position;
switch (header.ID)
{
case CHUNKTYPE_COMMON:
{
short channels = StreamUtils.DecodeBEInt16(reader.ReadBytes(2));
ChannelsArrangement = channels switch
{
1 => MONO,
2 => STEREO,
3 => ISO_3_0_0,
4 => ISO_2_2_0, // // Specs actually allow both 2/2.0 and LRCS
6 => LRLcRcCS,
_ => UNKNOWN
};
uint numSampleFrames = StreamUtils.DecodeBEUInt32(reader.ReadBytes(4));
bits = StreamUtils.DecodeBEInt16(reader.ReadBytes(2)); // This sample size is for uncompressed data only
byte[] byteArray = reader.ReadBytes(10);
Array.Reverse(byteArray);
double aSampleRate = StreamUtils.ExtendedToDouble(byteArray);
if (format.Equals(FORMTYPE_AIFC))
{
compression = Utils.Latin1Encoding.GetString(reader.ReadBytes(4));
}
else // AIFF <=> no compression
{
compression = COMPRESSION_NONE;
}
if (aSampleRate > 0)
{
SampleRate = (int)Math.Round(aSampleRate);
Duration = numSampleFrames * 1000.0 / SampleRate;
if (!compression.Equals(COMPRESSION_NONE)) // Sample size is specific to selected compression method
{
switch (compression.ToLower())
{
case "fl32":
bits = 32;
break;
case "fl64":
bits = 64;
break;
case "alaw":
case "ulaw":
bits = 8;
break;
}
}
if (Duration > 0) BitRate = bits * numSampleFrames * ChannelsArrangement.NbChannels / Duration;
}
break;
}
case CHUNKTYPE_SOUND:
soundChunkPosition = reader.Position - 8;
soundChunkSize = header.Size + 8;
AudioDataOffset = soundChunkPosition;
AudioDataSize = soundChunkSize;
break;
case CHUNKTYPE_NAME:
case CHUNKTYPE_AUTHOR:
case CHUNKTYPE_COPYRIGHT:
{
structureHelper.AddZone(reader.Position - 8, header.Size + 8, header.ID);
structureHelper.AddSize(containerChunkPos, containerChunkSize, header.ID);
tagExists = true;
switch (header.ID)
{
case CHUNKTYPE_NAME:
nameFound = true;
break;
case CHUNKTYPE_AUTHOR:
authorFound = true;
break;
case CHUNKTYPE_COPYRIGHT:
copyrightFound = true;
break;
}
SetMetaField(header.ID, Utils.Latin1Encoding.GetString(reader.ReadBytes(header.Size)), readTagParams.ReadAllMetaFrames);
break;
}
case CHUNKTYPE_ANNOTATION:
{
annotationIndex++;
chunkId = header.ID + annotationIndex;
structureHelper.AddZone(reader.Position - 8, header.Size + 8, header.ID + annotationIndex);
structureHelper.AddSize(containerChunkPos, containerChunkSize, header.ID + annotationIndex);
if (commentStr.Length > 0) commentStr.Append(Settings.InternalValueSeparator);
commentStr.Append(Utils.Latin1Encoding.GetString(reader.ReadBytes(header.Size)));
tagExists = true;
break;
}
case CHUNKTYPE_COMMENTS:
{
commentIndex++;
chunkId = header.ID + commentIndex;
structureHelper.AddZone(reader.Position - 8, header.Size + 8, header.ID + commentIndex);
structureHelper.AddSize(containerChunkPos, containerChunkSize, header.ID + commentIndex);
tagExists = true;
commentsFound = true;
ushort numComs = StreamUtils.DecodeBEUInt16(reader.ReadBytes(2));
for (int i = 0; i < numComs; i++)
{
CommentData cmtData = new CommentData
{
Timestamp = StreamUtils.DecodeBEUInt32(reader.ReadBytes(4)),
MarkerId = StreamUtils.DecodeBEInt16(reader.ReadBytes(2))
};
// Comments length
ushort comLength = StreamUtils.DecodeBEUInt16(reader.ReadBytes(2));
MetaFieldInfo comment = new MetaFieldInfo(getImplementedTagType(), header.ID + commentIndex)
{
Value = Utils.Latin1Encoding.GetString(reader.ReadBytes(comLength)),
SpecificData = cmtData
};
tagData.AdditionalFields.Add(comment);
// Only read general purpose comments, not those linked to a marker
if (0 == cmtData.MarkerId)
{
if (commentStr.Length > 0) commentStr.Append(Settings.InternalValueSeparator);
commentStr.Append(comment.Value);
}
}
break;
}
case CHUNKTYPE_ID3TAG:
HasEmbeddedID3v2 = reader.Position;
// Zone is already added by Id3v2.Read
id3v2StructureHelper.AddZone(HasEmbeddedID3v2 - 8, header.Size + 8, CHUNKTYPE_ID3TAG);
id3v2StructureHelper.AddSize(containerChunkPos, containerChunkSize, CHUNKTYPE_ID3TAG);
break;
}
reader.Position = position + header.Size;
} // Loop through file
tagData.IntegrateValue(Field.COMMENT, commentStr.ToString().Replace("\0", " ").Trim());
if (-1 == HasEmbeddedID3v2)
{
HasEmbeddedID3v2 = 0; // Switch status to "tried to read, but nothing found"
if (readTagParams.PrepareForWriting)
{
id3v2StructureHelper.AddZone(soundChunkPosition + soundChunkSize, 0, CHUNKTYPE_ID3TAG);
id3v2StructureHelper.AddSize(containerChunkPos, containerChunkSize, CHUNKTYPE_ID3TAG);
}
}
// Add zone placeholders for future tag writing
if (readTagParams.PrepareForWriting)
{
if (!nameFound)
{
structureHelper.AddZone(soundChunkPosition, 0, CHUNKTYPE_NAME);
structureHelper.AddSize(containerChunkPos, containerChunkSize, CHUNKTYPE_NAME);
}
if (!authorFound)
{
structureHelper.AddZone(soundChunkPosition, 0, CHUNKTYPE_AUTHOR);
structureHelper.AddSize(containerChunkPos, containerChunkSize, CHUNKTYPE_AUTHOR);
}
if (!copyrightFound)
{
structureHelper.AddZone(soundChunkPosition, 0, CHUNKTYPE_COPYRIGHT);
structureHelper.AddSize(containerChunkPos, containerChunkSize, CHUNKTYPE_COPYRIGHT);
}
if (!commentsFound)
{
structureHelper.AddZone(soundChunkPosition, 0, CHUNKTYPE_COMMENTS);
structureHelper.AddSize(containerChunkPos, containerChunkSize, CHUNKTYPE_COMMENTS);
}
}
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, zone);
}
private static int write(TagData tag, BinaryWriter w, string zone)
{
int result = 0;
switch (zone)
{
case CHUNKTYPE_NAME:
{
if (tag[Field.TITLE].Length > 0)
{
w.Write(Utils.Latin1Encoding.GetBytes(zone));
long sizePos = w.BaseStream.Position;
w.Write(0); // Placeholder for field size that will be rewritten at the end of the method
byte[] strBytes = Utils.Latin1Encoding.GetBytes(tag[Field.TITLE]);
w.Write(strBytes);
// Add the extra padding byte if needed
long finalPos = w.BaseStream.Position;
long paddingSize = (finalPos - sizePos) % 2;
if (paddingSize > 0) w.BaseStream.WriteByte(0);
// Write actual tag size
w.BaseStream.Seek(sizePos, SeekOrigin.Begin);
w.Write(StreamUtils.EncodeBEInt32(strBytes.Length));
result++;
}
break;
}
case CHUNKTYPE_AUTHOR:
{
if (tag[Field.ARTIST].Length > 0)
{
w.Write(Utils.Latin1Encoding.GetBytes(zone));
long sizePos = w.BaseStream.Position;
w.Write(0); // Placeholder for field size that will be rewritten at the end of the method
byte[] strBytes = Utils.Latin1Encoding.GetBytes(tag[Field.ARTIST]);
w.Write(strBytes);
// Add the extra padding byte if needed
long finalPos = w.BaseStream.Position;
long paddingSize = (finalPos - sizePos) % 2;
if (paddingSize > 0) w.BaseStream.WriteByte(0);
// Write actual tag size
w.BaseStream.Seek(sizePos, SeekOrigin.Begin);
w.Write(StreamUtils.EncodeBEInt32(strBytes.Length));
result++;
}
break;
}
case CHUNKTYPE_COPYRIGHT:
{
if (tag[Field.COPYRIGHT].Length > 0)
{
w.Write(Utils.Latin1Encoding.GetBytes(zone));
long sizePos = w.BaseStream.Position;
w.Write(0); // Placeholder for field size that will be rewritten at the end of the method
byte[] strBytes = Utils.Latin1Encoding.GetBytes(tag[Field.COPYRIGHT]);
w.Write(strBytes);
// Add the extra padding byte if needed
long finalPos = w.BaseStream.Position;
long paddingSize = (finalPos - sizePos) % 2;
if (paddingSize > 0) w.BaseStream.WriteByte(0);
// Write actual tag size
w.BaseStream.Seek(sizePos, SeekOrigin.Begin);
w.Write(StreamUtils.EncodeBEInt32(strBytes.Length));
result++;
}
break;
}
default:
{
if (zone.StartsWith(CHUNKTYPE_ANNOTATION))
{
// Do not write anything, this field is deprecated (Cf. specs "Use of this chunk is discouraged within FORM AIFC. The more refined Comments Chunk should be used instead")
}
else if (zone.StartsWith(CHUNKTYPE_COMMENTS))
{
bool applicable = tag[Field.COMMENT].Length > 0;
if (!applicable && tag.AdditionalFields.Count > 0)
{
foreach (MetaFieldInfo fieldInfo in tag.AdditionalFields)
{
applicable = fieldInfo.NativeFieldCode.StartsWith(CHUNKTYPE_COMMENTS);
if (applicable) break;
}
}
if (applicable)
{
ushort numComments = 0;
w.Write(Utils.Latin1Encoding.GetBytes(CHUNKTYPE_COMMENTS));
long sizePos = w.BaseStream.Position;
w.Write(0); // Placeholder for 'chunk size' field that will be rewritten at the end of the method
w.Write((ushort)0); // Placeholder for 'number of comments' field that will be rewritten at the end of the method
// First write generic comments (those linked to the Comment field)
string[] comments = tag[Field.COMMENT].Split(Settings.InternalValueSeparator);
foreach (string s in comments)
{
writeCommentChunk(w, null, s);
numComments++;
}
// Then write comments linked to a Marker ID
if (tag.AdditionalFields != null && tag.AdditionalFields.Count > 0)
{
foreach (var fieldInfo in tag.AdditionalFields.Where(fieldInfo => fieldInfo.NativeFieldCode.StartsWith(CHUNKTYPE_COMMENTS)).Where(fieldInfo => ((CommentData)fieldInfo.SpecificData).MarkerId != 0))
{
writeCommentChunk(w, fieldInfo);
numComments++;
}
}
long dataEndPos = w.BaseStream.Position;
// Add the extra padding byte if needed
long finalPos = w.BaseStream.Position;
long paddingSize = (finalPos - sizePos) % 2;
if (paddingSize > 0) w.BaseStream.WriteByte(0);
// Write actual tag size
w.BaseStream.Seek(sizePos, SeekOrigin.Begin);
w.Write(StreamUtils.EncodeBEInt32((int)(dataEndPos - sizePos - 4)));
w.Write(StreamUtils.EncodeBEUInt16(numComments));
result++;
}
}
break;
}
}
return result;
}
private static void writeCommentChunk(BinaryWriter w, MetaFieldInfo info, string comment = "")
{
byte[] commentData;
if (null == info) // Plain string
{
w.Write(StreamUtils.EncodeBEUInt32(encodeTimestamp(DateTime.Now)));
w.Write((short)0);
commentData = Utils.Latin1Encoding.GetBytes(comment);
}
else
{
w.Write(StreamUtils.EncodeBEUInt32(((CommentData)info.SpecificData).Timestamp));
w.Write(StreamUtils.EncodeBEInt16(((CommentData)info.SpecificData).MarkerId));
commentData = Utils.Latin1Encoding.GetBytes(info.Value);
}
w.Write(StreamUtils.EncodeBEUInt16((ushort)commentData.Length));
w.Write(commentData);
}
// AIFx timestamps are "the number of seconds since January 1, 1904"
private static uint encodeTimestamp(DateTime when)
{
return (uint)Math.Round((when.Ticks - timestampBase.Ticks) * 1.0 / TimeSpan.TicksPerSecond);
}
public void WriteID3v2EmbeddingHeader(Stream s, long tagSize)
{
StreamUtils.WriteBytes(s, Utils.Latin1Encoding.GetBytes(CHUNKTYPE_ID3TAG));
s.Write(StreamUtils.EncodeBEInt32((int)tagSize));
}
public void WriteID3v2EmbeddingFooter(Stream s, long tagSize)
{
if (tagSize % 2 > 0) s.WriteByte(0);
}
}
}