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

488 lines
18 KiB
C#

using System;
using System.IO;
using System.Text;
using static ATL.ChannelsArrangements;
using System.Collections.Generic;
using System.Globalization;
using static ATL.TagData;
using System.Threading.Tasks;
using static ATL.AudioData.FileStructureHelper;
using System.Linq;
using Commons;
namespace ATL.AudioData.IO
{
/// <summary>
/// Class for Audible Formats 2 to 4 files manipulation (extensions : .AA)
///
/// Implementation notes
///
/// - Only the editing of existing zones has been tested, not the adding of new zones (e.g. tagging a tagless AA, adding a picture to a pictureless AA)
/// due to the lack of empty test files
///
/// </summary>
partial class AA : MetaDataIO, IAudioDataIO
{
public const int AA_MAGIC_NUMBER = 1469084982;
public const int TOC_HEADER_TERMINATOR = 1;
public const int TOC_CONTENT_TAGS = 2;
public const int TOC_AUDIO = 10;
public const int TOC_COVER_ART = 11;
public const string CODEC_MP332 = "mp332";
public const string CODEC_ACELP85 = "acelp85";
public const string CODEC_ACELP16 = "acelp16";
public const string ZONE_TOC = "toc";
public const string ZONE_TAGS = "2";
public const string ZONE_IMAGE = "11";
// Mapping between MP4 frame codes and ATL frame codes
private static readonly Dictionary<string, Field> frameMapping = new Dictionary<string, Field>() {
{ "title", Field.TITLE },
{ "parent_title", Field.ALBUM},
{ "narrator", Field.ARTIST },
{ "description", Field.COMMENT},
{ "pubdate", Field.PUBLISHING_DATE},
{ "provider", Field.PUBLISHER},
{ "author", Field.COMPOSER },
{ "long_description", Field.GENERAL_DESCRIPTION},
{ "copyright", Field.COPYRIGHT },
};
private string codec;
private long tocOffset;
private long tocSize;
private readonly Format audioFormat;
private IDictionary<int, TocEntry> toc;
private sealed class TocEntry
{
public readonly long TocOffset;
public readonly int Section;
public readonly uint Offset;
public readonly uint Size;
public TocEntry(long tocOffset, int section, uint offset, uint size)
{
TocOffset = tocOffset;
Section = section;
Offset = offset;
Size = size;
}
public override string ToString()
{
return "[" + Section + "] @" + Offset + " (" + Size + ")";
}
}
// ---------- INFORMATIVE INTERFACE IMPLEMENTATIONS & MANDATORY OVERRIDES
/// <inheritdoc/>
public bool IsVBR => false;
/// <inheritdoc/>
public Format AudioFormat
{
get
{
Format f = new Format(audioFormat);
if (codec.Length > 0)
f.Name = f.Name + " (" + codec + ")";
else
f.Name += " (Unknown)";
return f;
}
}
/// <inheritdoc/>
public int CodecFamily => AudioDataIOFactory.CF_LOSSY;
/// <inheritdoc/>
public double BitRate
{
get
{
switch (codec)
{
case CODEC_MP332:
return 32 / 8.0;
case CODEC_ACELP16:
return 16 / 8.0;
case CODEC_ACELP85:
return 8.5 / 8.0;
default:
return 1;
}
}
}
/// <inheritdoc/>
public double Duration => getDuration();
/// <inheritdoc/>
public int SampleRate
{
get
{
return codec switch
{
CODEC_MP332 => 22050,
CODEC_ACELP16 => 16000,
CODEC_ACELP85 => 8500,
_ => 1
};
}
}
/// <inheritdoc/>
public int BitDepth => -1; // Irrelevant for lossy formats
/// <inheritdoc/>
public string FileName { get; }
/// <inheritdoc/>
public List<MetaDataIOFactory.TagType> GetSupportedMetas()
{
return new List<MetaDataIOFactory.TagType> { MetaDataIOFactory.TagType.NATIVE };
}
/// <inheritdoc/>
public ChannelsArrangement ChannelsArrangement => MONO;
/// <inheritdoc/>
public long AudioDataOffset { get; set; }
/// <inheritdoc/>
public long AudioDataSize { get; set; }
// MetaDataIO
/// <inheritdoc/>
protected override int getDefaultTagOffset()
{
return TO_BUILTIN;
}
/// <inheritdoc/>
protected override MetaDataIOFactory.TagType getImplementedTagType()
{
return MetaDataIOFactory.TagType.NATIVE;
}
/// <inheritdoc/>
protected override Field getFrameMapping(string zone, string ID, byte tagVersion)
{
Field supportedMetaId = Field.NO_FIELD;
if (frameMapping.TryGetValue(ID, out var value)) supportedMetaId = value;
return supportedMetaId;
}
/// <inheritdoc/>
protected override bool isLittleEndian => false;
/// <inheritdoc/>
public override string EncodeDate(DateTime date)
{
return date.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture).ToUpper();
}
// ---------- CONSTRUCTORS & INITIALIZERS
protected void resetData()
{
codec = "";
tocOffset = 0;
tocSize = 0;
toc?.Clear();
AudioDataOffset = -1;
AudioDataSize = 0;
}
public AA(string fileName, Format format)
{
this.FileName = fileName;
audioFormat = format;
resetData();
}
public static bool IsValidHeader(byte[] data)
{
// Bytes 4 to 7
byte[] usefulData = new byte[4];
Array.Copy(data, 4, usefulData, 0, 4);
return AA_MAGIC_NUMBER == StreamUtils.DecodeBEInt32(usefulData);
}
// ********************** Private functions & procedures *********************
// Calculate duration time
private double getDuration()
{
if (Utils.ApproxEquals(BitRate,0)) return 0;
return AudioDataSize / (BitRate * 1000);
}
// Read header data
private bool readHeader(BufferedBinaryReader source)
{
byte[] buffer = new byte[8];
source.Read(buffer, 0, buffer.Length);
if (!IsValidHeader(buffer)) return false;
uint fileSize = StreamUtils.DecodeBEUInt32(buffer);
tagExists = true;
AudioDataOffset = source.Position - 4;
tocOffset = source.Position;
toc = readToc(source);
tocSize = source.Position - tocOffset;
foreach (var entry in toc)
{
structureHelper.AddZone(entry.Value.Offset, (int)entry.Value.Size, entry.Key.ToString(), isSectionDeletable(entry.Key));
structureHelper.AddIndex(entry.Value.TocOffset + 4, entry.Value.Offset, false, entry.Key.ToString());
structureHelper.AddSize(entry.Value.TocOffset + 8, entry.Value.Size, entry.Key.ToString());
switch (entry.Key)
{
case TOC_AUDIO:
AudioDataOffset = entry.Value.Offset;
AudioDataSize = entry.Value.Size;
break;
case TOC_CONTENT_TAGS:
case TOC_COVER_ART:
structureHelper.AddSize(0, fileSize, entry.Key.ToString());
break;
}
}
// Save TOC as a zone for future editing
structureHelper.AddZone(tocOffset, tocSize, ZONE_TOC, false);
structureHelper.AddSize(0, fileSize, ZONE_TOC);
return true;
}
// The table of contents describes the layout of the file as triples of integers (<section>, <offset>, <length>)
private static IDictionary<int, TocEntry> readToc(BufferedBinaryReader s)
{
IDictionary<int, TocEntry> result = new Dictionary<int, TocEntry>();
int nbTocEntries = StreamUtils.DecodeBEInt32(s.ReadBytes(4));
s.Seek(4, SeekOrigin.Current); // Even FFMPeg doesn't know what this integer is
for (int i = 0; i < nbTocEntries; i++)
{
long offset = s.Position;
int section = StreamUtils.DecodeBEInt32(s.ReadBytes(4));
uint tocEntryOffset = StreamUtils.DecodeBEUInt32(s.ReadBytes(4));
uint tocEntrySize = StreamUtils.DecodeBEUInt32(s.ReadBytes(4));
result[section] = new TocEntry(offset, section, tocEntryOffset, tocEntrySize);
}
return result;
}
private static bool isSectionDeletable(int sectionId)
{
return TOC_CONTENT_TAGS == sectionId || TOC_COVER_ART == sectionId;
}
private void readTags(BufferedBinaryReader source, long offset, ReadTagParams readTagParams)
{
source.Seek(offset, SeekOrigin.Begin);
int nbTags = StreamUtils.DecodeBEInt32(source.ReadBytes(4));
for (int i = 0; i < nbTags; i++)
{
source.Seek(1, SeekOrigin.Current); // No idea what this byte is
int keyLength = StreamUtils.DecodeBEInt32(source.ReadBytes(4));
int valueLength = StreamUtils.DecodeBEInt32(source.ReadBytes(4));
string key = Encoding.UTF8.GetString(source.ReadBytes(keyLength));
string value = Encoding.UTF8.GetString(source.ReadBytes(valueLength)).Trim();
SetMetaField(key, value, readTagParams.ReadAllMetaFrames);
if ("codec".Equals(key)) codec = value;
}
}
private void readCover(BufferedBinaryReader source, long offset, PictureInfo.PIC_TYPE pictureType)
{
source.Seek(offset, SeekOrigin.Begin);
int pictureSize = StreamUtils.DecodeBEInt32(source.ReadBytes(4));
int picOffset = StreamUtils.DecodeBEInt32(source.ReadBytes(4));
structureHelper.AddIndex(source.Position - 4, (uint)picOffset, false, ZONE_IMAGE);
source.Seek(picOffset, SeekOrigin.Begin);
PictureInfo picInfo = PictureInfo.fromBinaryData(source, pictureSize, pictureType, getImplementedTagType(), TOC_COVER_ART);
tagData.Pictures.Add(picInfo);
}
private void readChapters(BufferedBinaryReader source, long offset, long size)
{
source.Seek(offset, SeekOrigin.Begin);
if (null == tagData.Chapters) tagData.Chapters = new List<ChapterInfo>(); else tagData.Chapters.Clear();
double cumulatedDuration = 0;
int idx = 1;
while (source.Position < offset + size)
{
uint chapterSize = StreamUtils.DecodeBEUInt32(source.ReadBytes(4));
uint chapterOffset = StreamUtils.DecodeBEUInt32(source.ReadBytes(4));
structureHelper.AddZone(chapterOffset, (int)chapterSize, "chp" + idx, false); // AA chapters are embedded into the audio chunk; they are _not_ deletable
structureHelper.AddIndex(source.Position - 4, chapterOffset, false, "chp" + idx);
ChapterInfo chapter = new ChapterInfo
{
Title = "Chapter " + idx++, // Chapters have no title metatada in the AA format
StartTime = (uint)Math.Round(cumulatedDuration)
};
cumulatedDuration += chapterSize / (BitRate * 1000);
chapter.EndTime = (uint)Math.Round(cumulatedDuration);
tagData.Chapters.Add(chapter);
source.Seek(chapterSize, SeekOrigin.Current);
}
}
// Read data from file
public bool Read(Stream source, AudioDataManager.SizeInfo sizeInfo, ReadTagParams readTagParams)
{
return read(source, readTagParams);
}
protected override bool read(Stream source, ReadTagParams readTagParams)
{
BufferedBinaryReader reader = new BufferedBinaryReader(source);
ResetData();
if (!readHeader(reader)) return false;
if (toc.ContainsKey(TOC_CONTENT_TAGS))
{
readTags(reader, toc[TOC_CONTENT_TAGS].Offset, readTagParams);
}
if (toc.ContainsKey(TOC_COVER_ART) && readTagParams.ReadPictures)
{
readCover(reader, toc[TOC_COVER_ART].Offset, PictureInfo.PIC_TYPE.Generic);
}
readChapters(reader, toc[TOC_AUDIO].Offset, toc[TOC_AUDIO].Size);
return true;
}
protected override int write(TagData tag, Stream s, string zone)
{
int result = -1; // Default : leave as is
if (zone.Equals(ZONE_TAGS))
{
long nbTagsOffset = s.Position;
s.Write(StreamUtils.EncodeInt32(0)); // Number of tags; will be rewritten at the end of the method
// Mapped textual fields
IDictionary<Field, string> map = tag.ToMap();
foreach (Field frameType in map.Keys)
{
if (map[frameType].Length > 0) // No frame with empty value
{
foreach (string str in frameMapping.Keys)
{
if (frameType == frameMapping[str])
{
string value = formatBeforeWriting(frameType, tag, map);
writeTagField(s, str, value);
result++;
break;
}
}
}
}
// Other textual fields
foreach (MetaFieldInfo fieldInfo in tag.AdditionalFields)
{
if ((fieldInfo.TagType.Equals(MetaDataIOFactory.TagType.ANY) || fieldInfo.TagType.Equals(getImplementedTagType())) && !fieldInfo.MarkedForDeletion)
{
writeTagField(s, fieldInfo.NativeFieldCode, FormatBeforeWriting(fieldInfo.Value));
result++;
}
}
s.Seek(nbTagsOffset, SeekOrigin.Begin);
s.Write(StreamUtils.EncodeBEInt32(result)); // Number of tags
}
if (zone.Equals(ZONE_IMAGE))
{
result = 0;
foreach (PictureInfo picInfo in tag.Pictures)
{
// Picture has either to be supported, or to come from the right tag standard
bool doWritePicture = !picInfo.PicType.Equals(PictureInfo.PIC_TYPE.Unsupported);
if (!doWritePicture) doWritePicture = (getImplementedTagType() == picInfo.TagType);
// It also has not to be marked for deletion
doWritePicture = doWritePicture && (!picInfo.MarkedForDeletion);
if (doWritePicture)
{
writePictureFrame(s, picInfo.PictureData);
return 1; // Stop here; there can only be one picture in an AA file
}
}
}
return result;
}
private static void writeTagField(Stream s, string key, string value)
{
s.WriteByte(0); // Unknown byte; always zero
byte[] keyB = Encoding.UTF8.GetBytes(key);
byte[] valueB = Encoding.UTF8.GetBytes(value);
s.Write(StreamUtils.EncodeBEInt32(keyB.Length));
s.Write(StreamUtils.EncodeBEInt32(valueB.Length));
StreamUtils.WriteBytes(s, keyB);
StreamUtils.WriteBytes(s, valueB);
}
private static void writePictureFrame(Stream s, byte[] pictureData)
{
s.Write(StreamUtils.EncodeBEInt32(pictureData.Length));
s.Write(StreamUtils.EncodeInt32(0)); // Pic data absolute offset; to be rewritten later
StreamUtils.WriteBytes(s, pictureData);
}
// Specific implementation for rewriting of the TOC after zone removal
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public override async Task<bool> RemoveAsync(Stream s)
{
bool result = await base.RemoveAsync(s);
if (result)
{
int newTocSize = writeCoreToc(s);
await finalizeFileAsync(s, newTocSize);
}
return result;
}
private int writeCoreToc(Stream s)
{
s.Seek(tocOffset, SeekOrigin.Begin);
var span = new Span<byte>(new byte[4]);
BufferedBinaryReader br = new BufferedBinaryReader(s);
IDictionary<int, TocEntry> newToc = readToc(br);
List<TocEntry> finalToc = newToc.Values.Where(e => !isSectionDeletable(e.Section)).ToList();
int deltaBytes = (newToc.Count - finalToc.Count) * 12;
s.Seek(tocOffset, SeekOrigin.Begin);
StreamUtils.WriteBEInt32(s, finalToc.Count, span);
s.Seek(4, SeekOrigin.Current); // Skip unfathomable byte
// Rewrite table of contents (<section>, <offset>, <length>)
foreach (TocEntry entry in finalToc)
{
StreamUtils.WriteBEInt32(s, entry.Section, span);
StreamUtils.WriteBEUInt32(s, entry.Offset, span);
StreamUtils.WriteBEUInt32(s, entry.Size, span);
}
int newTocSize = (int)(s.Position - tocOffset);
// Process TOC resizing
structureHelper.RewriteHeaders(s, null, -deltaBytes, ACTION.Edit, ZONE_TOC);
return newTocSize;
}
// Remove unused data
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private async Task finalizeFileAsync(Stream s, long newTocSize)
{
await StreamUtils.ShortenStreamAsync(s, tocOffset + tocSize, (uint)(tocSize - newTocSize));
}
}
}