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

535 lines
22 KiB
C#

using System;
using System.IO;
using System.Collections.Generic;
using static ATL.AudioData.FileStructureHelper;
using static ATL.AudioData.IO.MetaDataIO;
using static ATL.AudioData.IO.FileSurgeon;
using static ATL.ChannelsArrangements;
using static ATL.AudioData.FlacHelper;
using System.Text;
using System.Threading.Tasks;
using Commons;
namespace ATL.AudioData.IO
{
/// <summary>
/// Class for Free Lossless Audio Codec files manipulation (extension : .FLAC)
/// </summary>
partial class FLAC : VorbisTagHolder, IMetaDataIO, IAudioDataIO
{
#pragma warning disable S1144 // Unused private types or members should be removed
#pragma warning disable IDE0051 // Remove unused private members
private const byte META_STREAMINFO = 0;
private const byte META_PADDING = 1;
private const byte META_APPLICATION = 2;
private const byte META_SEEKTABLE = 3;
private const byte META_VORBIS_COMMENT = 4;
private const byte META_CUESHEET = 5;
private const byte META_PICTURE = 6;
#pragma warning restore IDE0051 // Remove unused private members
#pragma warning restore S1144 // Unused private types or members should be removed
public static readonly byte[] FLAC_ID = Utils.Latin1Encoding.GetBytes("fLaC");
private const byte FLAG_LAST_METADATA_BLOCK = 0x80;
private FlacHeader header;
private AudioDataManager.SizeInfo sizeInfo;
IList<Zone> zones; // TODO - That's one hint of why interactions with VorbisTag need to be redesigned...
// Initial offset of the padding block; used to handle padding the smart way when rewriting data
private long initialPaddingOffset, initialPaddingSize;
// Physical info
private byte bitsPerSample;
private long samples;
/// <summary>
/// Write-time vars (TODO - find a better place than the whole class scope)
/// </summary>
// Save a snapshot of the initial embedded pictures for processing purposes
private IList<PictureInfo> initialPictures;
// Indexes of currently processed existing and target embedded pictures
private int existingPictureIndex;
private int targetPictureIndex;
// Handling of the 'isLast' bit
private long latestBlockOffset = -1;
private byte latestBlockType;
// ---------- INFORMATIVE INTERFACE IMPLEMENTATIONS & MANDATORY OVERRIDES
// IAudioDataIO
public int SampleRate { get; private set; }
public bool IsVBR => false;
/// <inheritdoc/>
public override IList<Format> MetadataFormats
{
get
{
IList<Format> result = base.MetadataFormats;
result[0].Name += " (FLAC)";
result[0].ID += AudioFormat.ID;
return result;
}
}
/// <inheritdoc/>
public string FileName { get; }
/// <inheritdoc/>
public double BitRate => Math.Round((double)(sizeInfo.FileSize - AudioDataOffset) * 8 / Duration);
/// <inheritdoc/>
public int BitDepth => bitsPerSample;
/// <inheritdoc/>
public double Duration => getDuration();
/// <inheritdoc/>
public ChannelsArrangement ChannelsArrangement { get; private set; }
/// <inheritdoc/>
public Format AudioFormat
{
get;
}
/// <inheritdoc/>
public int CodecFamily => AudioDataIOFactory.CF_LOSSLESS;
/// <inheritdoc/>
public long AudioDataOffset { get; set; }
/// <inheritdoc/>
public long AudioDataSize { get; set; }
public List<MetaDataIOFactory.TagType> GetSupportedMetas()
{
// Native is for VorbisTag
return new List<MetaDataIOFactory.TagType> { MetaDataIOFactory.TagType.NATIVE, MetaDataIOFactory.TagType.ID3V2 };
}
// ---------- CONSTRUCTORS & INITIALIZERS
protected void resetData()
{
// Audio data
SampleRate = 0;
bitsPerSample = 0;
samples = 0;
initialPaddingOffset = -1;
initialPaddingSize = 0;
AudioDataOffset = -1;
AudioDataSize = 0;
}
public FLAC(string path, Format format) : base(false, false, false, false)
{
FileName = path;
AudioFormat = format;
resetData();
}
// ---------- SUPPORT METHODS
// Check for right FLAC file data
private bool isValid()
{
if (header == null) return false;
return header.IsValid() &&
ChannelsArrangement.NbChannels > 0 &&
SampleRate > 0 &&
bitsPerSample > 0 &&
samples > 0;
}
private double getDuration()
{
if (isValid() && SampleRate > 0) return samples * 1000.0 / SampleRate;
return 0;
}
/// <inheritdoc/>
public bool Read(Stream source, AudioDataManager.SizeInfo sizeInfo, ReadTagParams readTagParams)
{
this.sizeInfo = sizeInfo;
return Read(source, readTagParams);
}
// TODO : support for CUESHEET block
/// <inheritdoc/>
public bool Read(Stream source, ReadTagParams readTagParams)
{
bool result = false;
initialPaddingOffset = -1;
initialPaddingSize = 0;
int blockIndex;
bool paddingFound = false;
long blockEndOffset = -1;
source.Seek(sizeInfo.ID3v2Size, SeekOrigin.Begin);
header = readHeader(source);
// Process data if loaded and header valid
if (header.IsValid())
{
ChannelsArrangement = header.getChannelsArrangement();
SampleRate = header.SampleRate;
bitsPerSample = header.BitsPerSample;
samples = header.NbSamples;
if (header.MetadataExists)
{
blockIndex = 0;
vorbisTag.Clear();
if (readTagParams.PrepareForWriting)
{
if (null == zones) zones = new List<Zone>(); else zones.Clear();
blockEndOffset = source.Position;
}
byte[] metaDataBlockHeader = new byte[4];
bool isLast;
do // Read all metadata blocks
{
source.Read(metaDataBlockHeader, 0, 4);
isLast = (metaDataBlockHeader[0] & FLAG_LAST_METADATA_BLOCK) > 0; // last flag ( first bit == 1 )
blockIndex++;
var blockLength = StreamUtils.DecodeBEUInt24(metaDataBlockHeader, 1);
var blockType = (byte)(metaDataBlockHeader[0] & 0x7F);
var position = source.Position;
if (blockType == META_VORBIS_COMMENT) // Vorbis metadata
{
if (readTagParams.PrepareForWriting) zones.Add(new Zone(blockType + "." + zones.Count, position - 4, (int)blockLength + 4, Array.Empty<byte>(), true, blockType));
vorbisTag.Read(source, readTagParams);
}
else if (blockType == META_PADDING && !paddingFound) // Padding block (skip any other padding block)
{
if (readTagParams.PrepareForWriting) zones.Add(new Zone(PADDING_ZONE_NAME, position - 4, (int)blockLength + 4, Array.Empty<byte>(), true, blockType));
initialPaddingSize = blockLength;
initialPaddingOffset = position;
paddingFound = true;
source.Seek(blockLength, SeekOrigin.Current);
}
else if (blockType == META_PICTURE) // Picture (NB: as per FLAC specs, pictures must be embedded at the FLAC level, not in the VorbisComment !)
{
if (readTagParams.PrepareForWriting) zones.Add(new Zone(blockType + "." + zones.Count, position - 4, (int)blockLength + 4, Array.Empty<byte>(), true, blockType));
vorbisTag.ReadPicture(source, readTagParams);
}
else // Unhandled block; needs to be zoned anyway to be able to manage the 'isLast' flag at write-time
{
if (readTagParams.PrepareForWriting) zones.Add(new Zone(blockType + "." + zones.Count, position - 4, (int)blockLength + 4, Array.Empty<byte>(), true, blockType));
}
if (blockType < 7)
{
source.Seek(position + blockLength, SeekOrigin.Begin);
blockEndOffset = position + blockLength;
}
else
{
// Abnormal header : incorrect size and/or misplaced last-metadata-block flag
break;
}
}
while (!isLast);
if (readTagParams.PrepareForWriting)
{
bool vorbisTagFound = false;
bool pictureFound = false;
foreach (Zone zone in zones)
{
if (zone.Flag == META_PICTURE) pictureFound = true;
else if (zone.Flag == META_VORBIS_COMMENT) vorbisTagFound = true;
}
if (!vorbisTagFound) zones.Add(new Zone(META_VORBIS_COMMENT + "." + zones.Count, blockEndOffset, 0, Array.Empty<byte>(), true, META_VORBIS_COMMENT));
if (!pictureFound) zones.Add(new Zone(META_PICTURE + "." + zones.Count, blockEndOffset, 0, Array.Empty<byte>(), true, META_PICTURE));
// Padding must be the last block for it to correctly absorb size variations of the other blocks
if (!paddingFound && Settings.AddNewPadding) zones.Add(new Zone(PADDING_ZONE_NAME, blockEndOffset, 0, Array.Empty<byte>(), true, META_PADDING));
}
}
}
if (isValid())
{
AudioDataOffset = source.Position;
AudioDataSize = sizeInfo.FileSize - sizeInfo.APESize - sizeInfo.ID3v1Size - AudioDataOffset;
result = true;
}
return result;
}
// NB : This only works if writeVorbisTag is called _before_ writePictures, since tagData fusion is done by vorbisTag.Write
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public async Task<bool> WriteAsync(Stream s, TagData tag, ProgressToken<float> writeProgress = null)
{
Tuple<bool, TagData> results = prepareWrite(s, tag);
FileSurgeon surgeon = new FileSurgeon(null, null, MetaDataIOFactory.TagType.NATIVE, MetaDataIO.TO_BUILTIN, writeProgress);
await surgeon.RewriteZonesAsync(s, new FileSurgeon.WriteDelegate(write), zones, results.Item2, results.Item1);
postWrite(s);
return true;
}
private Tuple<bool, TagData> prepareWrite(Stream r, TagData tag)
{
// Read all the fields in the existing tag (including unsupported fields)
ReadTagParams readTagParams = new ReadTagParams(true, true);
readTagParams.PrepareForWriting = true;
bool tagExists = Read(r, readTagParams);
// Save a snapshot of the initial embedded pictures for processing purposes
existingPictureIndex = 0;
targetPictureIndex = 0;
initialPictures = vorbisTag.EmbeddedPictures;
// Prepare picture data with freshly read vorbisTag
TagData dataToWrite = new TagData();
dataToWrite.Pictures = vorbisTag.EmbeddedPictures;
dataToWrite.IntegrateValues(tag, true, false); // Merge existing information + new tag information except additional fields which will be merged by VorbisComment
adjustPictureZones(dataToWrite.Pictures);
return Tuple.Create(tagExists, dataToWrite);
}
private void postWrite(Stream w)
{
// Set the 'isLast' bit on the actual last block
w.Seek(latestBlockOffset, SeekOrigin.Begin);
w.WriteByte((byte)(latestBlockType | FLAG_LAST_METADATA_BLOCK));
}
private WriteResult write(Stream s, TagData tag, Zone zone)
{
using BinaryWriter w = new BinaryWriter(s, Encoding.UTF8, true);
return write(tag, w, zone);
}
private WriteResult write(TagData tag, BinaryWriter w, Zone zone)
{
WriteResult result;
if (zone.Name.StartsWith(META_VORBIS_COMMENT + ".")) result = writeVorbisCommentBlock(w.BaseStream, tag, vorbisTag);
else if (zone.Name.Equals(PADDING_ZONE_NAME)) result = writePaddingBlock(w, tag.DataSizeDelta);
else if (zone.Name.StartsWith(META_PICTURE + ".")) result = processPictureBlock(w, initialPictures, tag.Pictures, ref existingPictureIndex, ref targetPictureIndex);
else // Unhandled field - write raw header without 'isLast' bit and let the rest as it is
{
w.Write(zone.Flag);
result = new WriteResult(WriteMode.OVERWRITE, 1);
}
// Remember the latest block position
if (result.WrittenFields > 0)
{
latestBlockOffset = zone.Offset + tag.DataSizeDelta;
latestBlockType = zone.Flag;
}
return result;
}
/// <summary>
/// Adjust the number of picture zones to match the actual number of pictures to be written
/// </summary>
/// <param name="picturesToWrite">List of pictures to be written</param>
private void adjustPictureZones(IList<PictureInfo> picturesToWrite)
{
int nbExistingPictures = 0;
int lastPictureZoneIndex = -1;
long lastPictureZoneOffset = -1;
for (int i = 0; i < zones.Count; i++)
{
if (META_PICTURE == zones[i].Flag)
{
nbExistingPictures++;
lastPictureZoneIndex = i;
lastPictureZoneOffset = zones[i].Offset;
}
}
// Insert additional picture zones after the current ones (not at the end of the zones list to avoid adding data after the padding block)
if (nbExistingPictures < picturesToWrite.Count)
{
for (int i = 0; i < picturesToWrite.Count - nbExistingPictures; i++)
zones.Insert(lastPictureZoneIndex + 1, new Zone(META_PICTURE + "." + zones.Count, lastPictureZoneOffset, 0, Array.Empty<byte>(), true, META_PICTURE));
}
}
public static WriteResult writeVorbisCommentBlock(Stream w, TagData tag, VorbisTag vorbisTag, bool isLastMetaBlock = false)
{
byte toWrite = META_VORBIS_COMMENT;
if (isLastMetaBlock) toWrite |= 0x80;
w.Write(new byte[] { toWrite }, 0, 1);
var sizePos = w.Position;
w.Write(new byte[] { 0, 0, 0 }, 0, 3); // Placeholder for 24-bit integer that will be rewritten at the end of the method
var dataPos = w.Position;
int writtenFields = vorbisTag.Write(w, tag);
var finalPos = w.Position;
w.Seek(sizePos, SeekOrigin.Begin);
w.Write(StreamUtils.EncodeBEUInt24((uint)(finalPos - dataPos)), 0, 3);
w.Seek(finalPos, SeekOrigin.Begin);
return new WriteResult(WriteMode.REPLACE, writtenFields);
}
private WriteResult writePaddingBlock(BinaryWriter w, long cumulativeDelta, bool isLastMetaBlock = false)
{
long paddingSizeToWrite = TrackUtils.ComputePaddingSize(initialPaddingOffset, initialPaddingSize, -cumulativeDelta);
if (paddingSizeToWrite > 0)
{
byte toWrite = META_PADDING;
if (isLastMetaBlock) toWrite |= 0x80;
w.Write(toWrite);
w.Write(StreamUtils.EncodeBEUInt24((uint)paddingSizeToWrite));
for (int i = 0; i < paddingSizeToWrite; i++) w.Write((byte)0);
return new WriteResult(WriteMode.REPLACE, 1);
}
return new WriteResult(WriteMode.REPLACE, 0);
}
/// <summary>
/// Process picture block at the index 'targetPictureIndex'
/// Three outcomes :
/// 1/ Target picture cannot be written => block is marked for deletion
/// 2/ Target picture can be written and is identical to existing picture at the same position => block is left as it is
/// 3/ Target picture can be written and is different to existing picture at the same position => target picture is written
/// </summary>
/// <param name="w">Writer to be used</param>
/// <param name="existingPictures">List of existing pictures on the file</param>
/// <param name="picturesToWrite">List of pictures to write</param>
/// <param name="existingPictureIndex">Current index of existing pictures in use in the main write loop</param>
/// <param name="targetPictureIndex">Current index of target pictures in use in the main write loop</param>
/// <returns></returns>
private static WriteResult processPictureBlock(BinaryWriter w, IList<PictureInfo> existingPictures, IList<PictureInfo> picturesToWrite, ref int existingPictureIndex, ref int targetPictureIndex)
{
bool doWritePicture = false;
PictureInfo pictureToWrite = null;
while (!doWritePicture && picturesToWrite.Count > targetPictureIndex)
{
pictureToWrite = picturesToWrite[targetPictureIndex++];
// Picture has either to be supported, or to come from the right tag standard
doWritePicture = !pictureToWrite.PicType.Equals(PictureInfo.PIC_TYPE.Unsupported);
if (!doWritePicture) doWritePicture = MetaDataIOFactory.TagType.NATIVE.Equals(pictureToWrite.TagType);
// It also has not to be marked for deletion
doWritePicture = doWritePicture && (!pictureToWrite.MarkedForDeletion);
}
if (doWritePicture)
{
bool pictureExists = false;
// Check if the picture to write is already there ('neutral update' use case)
if (existingPictures.Count > existingPictureIndex)
{
PictureInfo existingPic = existingPictures[existingPictureIndex++];
pictureExists = existingPic.ComputePicHash() == pictureToWrite.ComputePicHash(); // No need to rewrite an identical pic
}
if (!pictureExists) return new WriteResult(WriteMode.REPLACE, writePictureBlock(w, pictureToWrite));
else
{
w.Write(META_PICTURE);
return new WriteResult(WriteMode.OVERWRITE, 1);
}
}
else return new WriteResult(WriteMode.REPLACE, 0); // Nothing else to write; existing picture blocks are erased
}
private static int writePictureBlock(BinaryWriter w, PictureInfo picture, bool isLastMetaBlock = false)
{
byte toWrite = META_PICTURE;
if (isLastMetaBlock) toWrite |= 0x80;
w.Write(toWrite);
var sizePos = w.BaseStream.Position;
w.Write(new byte[] { 0, 0, 0 }); // Placeholder for 24-bit integer that will be rewritten at the end of the method
var dataPos = w.BaseStream.Position;
VorbisTag.WritePicture(w, picture.PictureData, picture.MimeType, picture.PicType.Equals(PictureInfo.PIC_TYPE.Unsupported) ? picture.NativePicCode : ID3v2.EncodeID3v2PictureType(picture.PicType), picture.Description);
var finalPos = w.BaseStream.Position;
w.BaseStream.Seek(sizePos, SeekOrigin.Begin);
w.Write(StreamUtils.EncodeBEUInt24((uint)(finalPos - dataPos)));
w.BaseStream.Seek(finalPos, SeekOrigin.Begin);
return 1;
}
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public async Task<bool> RemoveAsync(Stream s)
{
long cumulativeDelta = 0;
// Handling of the 'isLast' bit
latestBlockOffset = -1;
latestBlockType = 0;
foreach (var zone in zones)
{
if (zone.Offset > -1 && zone.Size > zone.CoreSignature.Length)
{
if (zone.Flag == META_PADDING || zone.Flag == META_PICTURE || zone.Flag == META_VORBIS_COMMENT)
{
await StreamUtils.ShortenStreamAsync(s, zone.Offset + zone.Size - cumulativeDelta, (uint)(zone.Size - zone.CoreSignature.Length));
vorbisTag.Clear();
cumulativeDelta += zone.Size - zone.CoreSignature.Length;
}
else
{
latestBlockOffset = zone.Offset - cumulativeDelta;
latestBlockType = zone.Flag;
s.Seek(latestBlockOffset, SeekOrigin.Begin);
s.WriteByte(latestBlockType);
}
}
}
setIsLast(s);
return true;
}
// Set the 'isLast' bit on the actual last block
private void setIsLast(Stream s)
{
if (latestBlockOffset > -1)
{
s.Seek(latestBlockOffset, SeekOrigin.Begin);
s.WriteByte((byte)(latestBlockType | FLAG_LAST_METADATA_BLOCK));
}
}
public void SetEmbedder(IMetaDataEmbedder embedder)
{
throw new NotImplementedException();
}
public void Clear()
{
vorbisTag.Clear();
}
}
}