mirror of
https://github.com/SineVector241/VoiceCraft-MCBE_Proximity_Chat.git
synced 2024-11-20 10:27:45 +00:00
510 lines
19 KiB
C#
510 lines
19 KiB
C#
using ATL.AudioData.IO;
|
|
using ATL.Logging;
|
|
using Commons;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using static ATL.AudioData.MetaDataIOFactory;
|
|
|
|
namespace ATL.AudioData
|
|
{
|
|
/// <summary>
|
|
/// Handles high-level basic operations on the given audio file, calling Metadata readers when needed
|
|
/// </summary>
|
|
public partial class AudioDataManager
|
|
{
|
|
// Settings to use when opening any FileStream
|
|
// NB : These settings are optimal according to performance tests on the dev environment
|
|
private static int bufferSize = 2048;
|
|
private static FileOptions fileOptions = FileOptions.RandomAccess;
|
|
|
|
/// <summary>
|
|
/// Set file options to use when opening any FileStream
|
|
/// </summary>
|
|
/// <param name="options">FileOptions to use when opening any FileStream</param>
|
|
public static void SetFileOptions(FileOptions options)
|
|
{
|
|
fileOptions = options;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set I/O buffer size to use when opening any FileStream
|
|
/// </summary>
|
|
/// <param name="bufSize">I/O buffer size to use when opening any FileStream</param>
|
|
public static void SetBufferSize(int bufSize)
|
|
{
|
|
bufferSize = bufSize;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Contains various useful information about the size of an audio file and its components
|
|
/// </summary>
|
|
public class SizeInfo
|
|
{
|
|
private readonly IDictionary<TagType, long> TagSizes = new Dictionary<TagType, long>();
|
|
private long audioDataSize = -1;
|
|
|
|
/// <summary>
|
|
/// Reset all data
|
|
/// </summary>
|
|
public void ResetData() { FileSize = 0; TagSizes.Clear(); }
|
|
|
|
/// <summary>
|
|
/// Set the size for the given TagType, in bytes
|
|
/// </summary>
|
|
/// <param name="type">Tag type to set the size for</param>
|
|
/// <param name="size">Size to set (bytes)</param>
|
|
public void SetSize(TagType type, long size)
|
|
{
|
|
TagSizes[type] = size;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Size of the ID3v1 tag (bytes)
|
|
/// </summary>
|
|
public long ID3v1Size => TagSizes.ContainsKey(TagType.ID3V1) ? TagSizes[TagType.ID3V1] : 0;
|
|
|
|
/// <summary>
|
|
/// Size of the ID3v2 tag (bytes)
|
|
/// </summary>
|
|
public long ID3v2Size => TagSizes.ContainsKey(TagType.ID3V2) ? TagSizes[TagType.ID3V2] : 0;
|
|
/// <summary>
|
|
/// Size of the APE tag (bytes)
|
|
/// </summary>
|
|
public long APESize => TagSizes.ContainsKey(TagType.APE) ? TagSizes[TagType.APE] : 0;
|
|
|
|
/// <summary>
|
|
/// Size of the native tag (bytes)
|
|
/// </summary>
|
|
public long NativeSize => TagSizes.ContainsKey(TagType.NATIVE) ? TagSizes[TagType.NATIVE] : 0;
|
|
/// <summary>
|
|
/// Total size of all tags (bytes)
|
|
/// </summary>
|
|
public long TotalTagSize => ID3v1Size + ID3v2Size + APESize + NativeSize;
|
|
|
|
/// <summary>
|
|
/// Size of the entire file (bytes)
|
|
/// </summary>
|
|
public long FileSize { get; set; }
|
|
/// <summary>
|
|
/// Offset of the audio data (bytes)
|
|
/// </summary>
|
|
public long AudioDataOffset { get; set; } = -1;
|
|
/// <summary>
|
|
/// Size of the audio data (bytes)
|
|
/// </summary>
|
|
public long AudioDataSize
|
|
{
|
|
get
|
|
{
|
|
if (audioDataSize <= 0) return FileSize - TotalTagSize;
|
|
else return audioDataSize;
|
|
}
|
|
set => audioDataSize = value;
|
|
}
|
|
}
|
|
|
|
private IMetaDataIO iD3v1 = new ID3v1();
|
|
private IMetaDataIO iD3v2 = new ID3v2();
|
|
private IMetaDataIO aPEtag = new APEtag();
|
|
private IMetaDataIO nativeTag;
|
|
|
|
private readonly IAudioDataIO audioDataIO;
|
|
private readonly Stream stream;
|
|
|
|
private readonly SizeInfo sizeInfo = new SizeInfo();
|
|
|
|
|
|
private string fileName => audioDataIO.FileName;
|
|
|
|
/// <summary>
|
|
/// ID3v1 tag data
|
|
/// </summary>
|
|
public IMetaDataIO ID3v1 => iD3v1;
|
|
|
|
/// <summary>
|
|
/// ID3v2 tag data
|
|
/// </summary>
|
|
public IMetaDataIO ID3v2 => iD3v2;
|
|
|
|
/// <summary>
|
|
/// APE tag data
|
|
/// </summary>
|
|
public IMetaDataIO APEtag => aPEtag;
|
|
|
|
/// <summary>
|
|
/// Native tag data
|
|
/// </summary>
|
|
public IMetaDataIO NativeTag => nativeTag;
|
|
|
|
/// <summary>
|
|
/// Offset of audio data (bytes)
|
|
/// </summary>
|
|
public long AudioDataOffset => sizeInfo.AudioDataOffset;
|
|
|
|
/// <summary>
|
|
/// Size of audio data (bytes)
|
|
/// </summary>
|
|
public long AudioDataSize => sizeInfo.AudioDataSize;
|
|
|
|
/// <summary>
|
|
/// Create a new instance using the given IAudioDataIO and the given IProgress
|
|
/// </summary>
|
|
/// <param name="audioDataReader">Audio data reader to use</param>
|
|
internal AudioDataManager(IAudioDataIO audioDataReader)
|
|
{
|
|
this.audioDataIO = audioDataReader;
|
|
this.stream = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a new instance using the given IAudioDataIO, the given data Stream and the given IProgress
|
|
/// </summary>
|
|
/// <param name="audioDataReader">Audio data reader to use</param>
|
|
/// <param name="stream">Data stream to use</param>
|
|
internal AudioDataManager(IAudioDataIO audioDataReader, Stream stream)
|
|
{
|
|
this.audioDataIO = audioDataReader;
|
|
this.stream = stream;
|
|
}
|
|
|
|
|
|
// ====================== METHODS =========================
|
|
|
|
private void resetData()
|
|
{
|
|
sizeInfo.ResetData();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicate whether the current audio file contains a tag from the given type
|
|
/// </summary>
|
|
/// <param name="type">Tag type whose presence to check</param>
|
|
/// <returns>True if the current audio file contains a tag of the given type; false if not</returns>
|
|
public bool hasMeta(TagType type)
|
|
{
|
|
return type switch
|
|
{
|
|
TagType.ID3V1 => iD3v1 is { Exists: true },
|
|
TagType.ID3V2 => iD3v2 is { Exists: true },
|
|
TagType.APE => aPEtag is { Exists: true },
|
|
TagType.NATIVE => nativeTag is { Exists: true },
|
|
_ => false
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicate whether the current file supports native tagging
|
|
/// </summary>
|
|
/// <returns>True if the current file supports native tagging; false if it doesn't</returns>
|
|
public bool HasNativeMeta()
|
|
{
|
|
return isMetaSupported(TagType.NATIVE);
|
|
}
|
|
|
|
private bool isMetaSupported(TagType meta)
|
|
{
|
|
return audioDataIO.GetSupportedMetas().Contains(meta);
|
|
}
|
|
|
|
/// <summary>
|
|
/// List the available tag types of the current file
|
|
/// </summary>
|
|
/// <returns>List of tag types available in the current file</returns>
|
|
public ISet<TagType> getAvailableMetas()
|
|
{
|
|
ISet<TagType> result = new HashSet<TagType>();
|
|
foreach (var tagType in from TagType tagType in Enum.GetValues(typeof(TagType))
|
|
where hasMeta(tagType)
|
|
select tagType)
|
|
{
|
|
result.Add(tagType);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// List the tag types supported by the format of the current file
|
|
/// </summary>
|
|
/// <returns>Tag types supported by the format of the current file</returns>
|
|
public ISet<TagType> getSupportedMetas()
|
|
{
|
|
ISet<TagType> result = new HashSet<TagType>();
|
|
foreach (var tagType in from TagType tagType in Enum.GetValues(typeof(TagType))
|
|
where isMetaSupported(tagType)
|
|
select tagType)
|
|
{
|
|
result.Add(tagType);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// List the tag types recommended for the format of the current file
|
|
/// </summary>
|
|
/// <returns>Tag types recommended for the format of the current file</returns>
|
|
public ISet<TagType> getRecommendedMetas()
|
|
{
|
|
ISet<TagType> result = new HashSet<TagType>();
|
|
var supportedMetas = audioDataIO.GetSupportedMetas();
|
|
if (supportedMetas.Count <= 0) return result;
|
|
|
|
if (1 == supportedMetas.Count) result.Add(supportedMetas[0]);
|
|
else
|
|
{
|
|
if (audioDataIO is OptimFrog) result.Add(TagType.APE); // TODO this is ugly
|
|
else
|
|
{
|
|
var id3v2Exists = supportedMetas.Contains(TagType.ID3V2);
|
|
foreach (var meta in supportedMetas.Where(meta => meta != TagType.ID3V1))
|
|
{
|
|
if (meta == TagType.ID3V2 || meta == TagType.NATIVE) result.Add(meta); // Default preference go to these
|
|
if (meta == TagType.APE && !id3v2Exists) result.Add(meta); // If no ID3v2 support at all
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return metadata from the given tag type from the current file
|
|
/// </summary>
|
|
/// <param name="type">Tag type to retrieve metadata from</param>
|
|
/// <returns>Metadata I/O for the given tag type</returns>
|
|
public IMetaDataIO getMeta(TagType type)
|
|
{
|
|
if (type.Equals(TagType.ID3V1)) return iD3v1;
|
|
if (type.Equals(TagType.ID3V2)) return iD3v2;
|
|
if (type.Equals(TagType.APE)) return aPEtag;
|
|
if (type.Equals(TagType.NATIVE) && nativeTag != null) return nativeTag;
|
|
return new DummyTag();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set the given metadata to the current file
|
|
/// NB : Operates on RAM; doesn't save the file on disk. To do so, use UpdateTagInFile
|
|
/// </summary>
|
|
/// <param name="meta">Metadata to set</param>
|
|
public void setMeta(IMetaDataIO meta)
|
|
{
|
|
if (meta is ID3v1)
|
|
{
|
|
iD3v1 = meta;
|
|
sizeInfo.SetSize(TagType.ID3V1, iD3v1.Size);
|
|
}
|
|
else if (meta is ID3v2)
|
|
{
|
|
iD3v2 = meta;
|
|
sizeInfo.SetSize(TagType.ID3V2, iD3v2.Size);
|
|
}
|
|
else if (meta is APEtag)
|
|
{
|
|
aPEtag = meta;
|
|
sizeInfo.SetSize(TagType.APE, aPEtag.Size);
|
|
}
|
|
else
|
|
{
|
|
nativeTag = meta;
|
|
sizeInfo.SetSize(TagType.NATIVE, nativeTag.Size);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read all metadata from the current file
|
|
/// </summary>
|
|
/// <param name="readEmbeddedPictures">True if embedded pictures should be read; false if not (faster, less memory)</param>
|
|
/// <param name="readAllMetaFrames">True if all frames, including "Additional fields" should be read; false if only fields published in IMetaDataIO should be read</param>
|
|
/// <returns>True if the operation succeeds; false if an issue happened (in that case, the problem is logged on screen + in a Log)</returns>
|
|
public bool ReadFromFile(bool readEmbeddedPictures = false, bool readAllMetaFrames = false)
|
|
{
|
|
bool result;
|
|
LogDelegator.GetLocateDelegate()(fileName);
|
|
|
|
resetData();
|
|
|
|
try
|
|
{
|
|
// Open file, read first block of data and search for a frame
|
|
Stream s = stream ?? new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, fileOptions);
|
|
try
|
|
{
|
|
result = read(s, readEmbeddedPictures, readAllMetaFrames);
|
|
}
|
|
finally
|
|
{
|
|
if (null == stream) s.Close();
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Utils.TraceException(e);
|
|
result = false;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update metadata of current file and save it to disk
|
|
/// Pre-requisite : ReadFromFile must have been called before
|
|
/// </summary>
|
|
/// <param name="theTag">Metadata to save</param>
|
|
/// <param name="tagType">TagType to save the given metadata with</param>
|
|
/// <param name="writeProgress">ProgressManager to report with (optional)</param>
|
|
/// <returns>True if the operation succeeds; false if an issue happened (in that case, the problem is logged on screen + in a Log)</returns>
|
|
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
|
public async Task<bool> UpdateTagInFileAsync(TagData theTag, TagType tagType, ProgressManager writeProgress = null)
|
|
{
|
|
bool result = true;
|
|
LogDelegator.GetLocateDelegate()(fileName);
|
|
theTag.DurationMs = audioDataIO.Duration;
|
|
|
|
if (isMetaSupported(tagType))
|
|
{
|
|
try
|
|
{
|
|
var theMetaIO = getMeta(tagType);
|
|
|
|
var s = stream ?? new FileStream(fileName, FileMode.Open, FileAccess.ReadWrite, FileShare.None, bufferSize, fileOptions | FileOptions.Asynchronous);
|
|
try
|
|
{
|
|
// If current file can embed metadata, do a 1st pass to detect embedded metadata position
|
|
handleEmbedder(s, theMetaIO);
|
|
|
|
ProgressToken<float> progress = writeProgress?.CreateProgressToken();
|
|
result = await theMetaIO.WriteAsync(s, theTag, progress);
|
|
if (result) setMeta(theMetaIO);
|
|
}
|
|
finally
|
|
{
|
|
if (null == stream) s.Close();
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Utils.TraceException(e);
|
|
result = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LogDelegator.GetLogDelegate()(Log.LV_DEBUG, "Tag type " + tagType + " not supported");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private void handleEmbedder(Stream r, IMetaDataIO theMetaIO)
|
|
{
|
|
if (audioDataIO is IMetaDataEmbedder embedder)
|
|
{
|
|
MetaDataIO.ReadTagParams readTagParams = new MetaDataIO.ReadTagParams(false, false);
|
|
readTagParams.PrepareForWriting = true;
|
|
|
|
audioDataIO.Read(r, sizeInfo, readTagParams);
|
|
theMetaIO.SetEmbedder(embedder);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove the tagging from the given type (i.e. the whole technical structure, not only values) from the current file
|
|
/// </summary>
|
|
/// <param name="tagType">Type of the tagging to be removed</param>
|
|
/// <param name="progressManager">ProgressManager to report with (optional)</param>
|
|
/// <returns>True if the operation succeeds; false if an issue happened (in that case, the problem is logged on screen + in a Log)</returns>
|
|
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
|
public async Task<bool> RemoveTagFromFileAsync(TagType tagType, ProgressManager progressManager = null)
|
|
{
|
|
bool result;
|
|
LogDelegator.GetLocateDelegate()(fileName);
|
|
|
|
try
|
|
{
|
|
var s = stream ?? new FileStream(fileName, FileMode.Open, FileAccess.ReadWrite, FileShare.None, bufferSize, fileOptions | FileOptions.Asynchronous);
|
|
try
|
|
{
|
|
result = read(s, false, false, true);
|
|
|
|
IMetaDataIO metaIO = getMeta(tagType);
|
|
if (metaIO.Exists) await metaIO.RemoveAsync(s);
|
|
}
|
|
finally
|
|
{
|
|
if (null == stream) s.Close();
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Utils.TraceException(e);
|
|
result = false;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private bool read(Stream source, bool readEmbeddedPictures = false, bool readAllMetaFrames = false, bool prepareForWriting = false)
|
|
{
|
|
sizeInfo.ResetData();
|
|
|
|
sizeInfo.FileSize = source.Length;
|
|
MetaDataIO.ReadTagParams readTagParams = new MetaDataIO.ReadTagParams(readEmbeddedPictures, readAllMetaFrames);
|
|
readTagParams.PrepareForWriting = prepareForWriting;
|
|
|
|
return read(source, readTagParams);
|
|
}
|
|
|
|
private bool read(Stream source, MetaDataIO.ReadTagParams readTagParams)
|
|
{
|
|
if (isMetaSupported(TagType.ID3V1) && iD3v1.Read(source, readTagParams))
|
|
{
|
|
sizeInfo.SetSize(TagType.ID3V1, iD3v1.Size);
|
|
}
|
|
// No embedded ID3v2 tag => supported tag is the standard version of ID3v2
|
|
if (isMetaSupported(TagType.ID3V2) && !(audioDataIO is IMetaDataEmbedder) && iD3v2.Read(source, readTagParams))
|
|
{
|
|
sizeInfo.SetSize(TagType.ID3V2, iD3v2.Size);
|
|
}
|
|
if (isMetaSupported(TagType.APE) && aPEtag.Read(source, readTagParams))
|
|
{
|
|
sizeInfo.SetSize(TagType.APE, aPEtag.Size);
|
|
}
|
|
|
|
bool result;
|
|
if (isMetaSupported(TagType.NATIVE) && audioDataIO is IMetaDataIO)
|
|
{
|
|
nativeTag = (IMetaDataIO)audioDataIO;
|
|
result = audioDataIO.Read(source, sizeInfo, readTagParams);
|
|
|
|
if (result) sizeInfo.SetSize(TagType.NATIVE, nativeTag.Size);
|
|
}
|
|
else
|
|
{
|
|
readTagParams.ReadTag = false;
|
|
result = audioDataIO.Read(source, sizeInfo, readTagParams);
|
|
}
|
|
|
|
if (audioDataIO is IMetaDataEmbedder embedder) // Embedded ID3v2 tag detected while reading
|
|
{
|
|
if (embedder.HasEmbeddedID3v2 > 0)
|
|
{
|
|
readTagParams.Offset = embedder.HasEmbeddedID3v2;
|
|
if (iD3v2.Read(source, readTagParams)) sizeInfo.SetSize(TagType.ID3V2, iD3v2.Size);
|
|
}
|
|
else
|
|
{
|
|
iD3v2.Clear();
|
|
}
|
|
}
|
|
|
|
sizeInfo.AudioDataOffset = audioDataIO.AudioDataOffset;
|
|
if (audioDataIO.AudioDataSize > 0) sizeInfo.AudioDataSize = audioDataIO.AudioDataSize;
|
|
|
|
return result;
|
|
}
|
|
}
|
|
}
|