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

361 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using ATL.Logging;
using static ATL.AudioData.AudioDataManager;
using Commons;
using System.Text;
using System.IO.Compression;
using static ATL.ChannelsArrangements;
using static ATL.TagData;
using System.Globalization;
using System.Linq;
namespace ATL.AudioData.IO
{
/// <summary>
/// Class for Video Game Music files (Master System, Game Gear, SG1000, Genesis) manipulation (extensions : .VGM)
/// According to file format v1.70
///
/// Implementation notes :
/// 1/ GD3 tag format is directly implemented in here, since it is not a "real" standard and is only used for VGM files
///
/// 2/ Gzipped files are currently supported in read-only mode (i.e. ATL cannot write metadata to a GYM file containing gzipped data)
/// </summary>
class VGM : MetaDataIO, IAudioDataIO
{
private static readonly byte[] VGM_SIGNATURE = Utils.Latin1Encoding.GetBytes("Vgm ");
private static readonly byte[] GD3_SIGNATURE = Utils.Latin1Encoding.GetBytes("Gd3 ");
private const int VGM_HEADER_SIZE = 256;
private const int LOOP_COUNT_DEFAULT = 1; // Default loop count
private const int FADEOUT_DURATION_DEFAULT = 10000; // Default fadeout duration, in milliseconds (10s)
private const int RECORDING_RATE_DEFAULT = 60; // Default playback rate for v1.00 files
// Standard fields
private int gd3TagOffset;
private SizeInfo sizeInfo;
// ---------- INFORMATIVE INTERFACE IMPLEMENTATIONS & MANDATORY OVERRIDES
// AudioDataIO
public int SampleRate { get; private set; }
public bool IsVBR => false;
public Format AudioFormat
{
get;
}
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()
{
return TO_BUILTIN;
}
protected override MetaDataIOFactory.TagType getImplementedTagType()
{
return MetaDataIOFactory.TagType.NATIVE;
}
protected override Field getFrameMapping(string zone, string ID, byte tagVersion)
{
throw new NotImplementedException();
}
public override string EncodeDate(DateTime date)
{
// Shitty convention for Year <-> DateTime conversion
if (1 == date.Month && 1 == date.Day)
return date.Year.ToString();
else
// According to GD3 spec for release date
return date.ToString("yyyy/MM/dd", CultureInfo.InvariantCulture);
}
// ---------- CONSTRUCTORS & INITIALIZERS
private void resetData()
{
// Reset variables
SampleRate = 44100; // Default value for all VGM files, according to v1.70 spec
BitRate = 0;
Duration = 0;
gd3TagOffset = 0;
AudioDataOffset = -1;
AudioDataSize = 0;
ResetData();
}
public VGM(string filePath, Format format)
{
this.FileName = filePath;
AudioFormat = format;
resetData();
}
public static bool IsValidHeader(byte[] data)
{
return StreamUtils.ArrBeginsWith(data, VGM_SIGNATURE);
}
// === PRIVATE METHODS ===
private bool readHeader(BufferedBinaryReader source, ReadTagParams readTagParams)
{
int nbLoops = LOOP_COUNT_DEFAULT;
int recordingRate = RECORDING_RATE_DEFAULT;
byte[] headerSignature = source.ReadBytes(VGM_SIGNATURE.Length);
if (IsValidHeader(headerSignature))
{
AudioDataOffset = source.Position;
source.Seek(4, SeekOrigin.Current); // EOF offset
int version = source.ReadInt32();
source.Seek(8, SeekOrigin.Current); // Clocks
gd3TagOffset = source.ReadInt32();
if (gd3TagOffset > 0)
{
gd3TagOffset += (int)source.Position - 4;
AudioDataSize = gd3TagOffset;
}
else
AudioDataSize = sizeInfo.FileSize;
if (/*gd3TagOffset > 0 && */readTagParams.PrepareForWriting)
{
if (gd3TagOffset > VGM_HEADER_SIZE)
{
structureHelper.AddZone(gd3TagOffset, (int)sizeInfo.FileSize - gd3TagOffset);
structureHelper.AddIndex(source.Position - 4, gd3TagOffset, true);
}
else
{
structureHelper.AddZone(sizeInfo.FileSize, 0);
structureHelper.AddIndex(source.Position - 4, (int)sizeInfo.FileSize, true);
}
}
var nbSamples = source.ReadInt32();
source.Seek(4, SeekOrigin.Current); // Loop offset
var loopNbSamples = source.ReadInt32();
if (version >= 0x00000101)
{
recordingRate = source.ReadInt32();
}
if (version >= 0x00000160)
{
source.Seek(0x7E, SeekOrigin.Begin);
nbLoops -= source.ReadSByte(); // Loop base
}
if (version >= 0x00000151)
{
source.Seek(0x7F, SeekOrigin.Begin);
nbLoops *= source.ReadByte(); // Loop modifier
}
Duration = nbSamples * 1000.0 / SampleRate + nbLoops * (loopNbSamples * 1000.0 / SampleRate);
if (Settings.GYM_VGM_playbackRate > 0)
{
Duration *= Settings.GYM_VGM_playbackRate / (double)recordingRate;
}
if (nbLoops > 0) Duration += FADEOUT_DURATION_DEFAULT;
BitRate = (sizeInfo.FileSize - VGM_HEADER_SIZE) * 8 / Duration; // TODO - use unpacked size if applicable, and not raw file size
return true;
}
else
{
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "Not a VGM file");
return false;
}
}
private void readGd3Tag(BufferedBinaryReader source, int offset)
{
source.Seek(offset, SeekOrigin.Begin);
if (GD3_SIGNATURE.SequenceEqual(source.ReadBytes(GD3_SIGNATURE.Length)))
{
source.Seek(4, SeekOrigin.Current); // Version number
source.Seek(4, SeekOrigin.Current); // Length
var str = StreamUtils.ReadNullTerminatedString(source, Encoding.Unicode);
tagData.IntegrateValue(Field.TITLE, str);
str = StreamUtils.ReadNullTerminatedString(source, Encoding.Unicode); // Title (japanese)
tagData.AdditionalFields.Add(new MetaFieldInfo(getImplementedTagType(), "TITLE_J", str));
str = StreamUtils.ReadNullTerminatedString(source, Encoding.Unicode); // Game name (english)
tagData.IntegrateValue(Field.ALBUM, str);
str = StreamUtils.ReadNullTerminatedString(source, Encoding.Unicode); // Game name (japanese)
tagData.AdditionalFields.Add(new MetaFieldInfo(getImplementedTagType(), "GAME_J", str));
str = StreamUtils.ReadNullTerminatedString(source, Encoding.Unicode); // System name (english)
tagData.AdditionalFields.Add(new MetaFieldInfo(getImplementedTagType(), "SYSTEM", str));
str = StreamUtils.ReadNullTerminatedString(source, Encoding.Unicode); // System name (japanese)
tagData.AdditionalFields.Add(new MetaFieldInfo(getImplementedTagType(), "SYSTEM_J", str));
str = StreamUtils.ReadNullTerminatedString(source, Encoding.Unicode); // Author (english)
tagData.IntegrateValue(Field.ARTIST, str);
str = StreamUtils.ReadNullTerminatedString(source, Encoding.Unicode); // Author (japanese)
tagData.AdditionalFields.Add(new MetaFieldInfo(getImplementedTagType(), "AUTHOR_J", str));
str = StreamUtils.ReadNullTerminatedString(source, Encoding.Unicode); // Release date
tagData.IntegrateValue(Field.RECORDING_DATE, str);
str = StreamUtils.ReadNullTerminatedString(source, Encoding.Unicode); // Dumper
tagData.AdditionalFields.Add(new MetaFieldInfo(getImplementedTagType(), "DUMPER", str));
str = StreamUtils.ReadNullTerminatedString(source, Encoding.Unicode); // Notes
tagData.IntegrateValue(Field.COMMENT, str);
}
else
{
LogDelegator.GetLogDelegate()(Log.LV_WARNING, "Not a GD3 footer");
}
}
// === 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)
{
resetData();
BufferedBinaryReader reader = new BufferedBinaryReader(source);
reader.Seek(0, SeekOrigin.Begin);
BufferedBinaryReader usedSource = reader;
byte[] headerSignature = reader.ReadBytes(2);
reader.Seek(0, SeekOrigin.Begin);
if (headerSignature[0] == 0x1f && headerSignature[1] == 0x8b) // File is GZIP-compressed
{
if (readTagParams.PrepareForWriting)
{
LogDelegator.GetLogDelegate()(Log.LV_ERROR, "Writing metadata to gzipped VGM files is not supported yet.");
return false;
}
using GZipStream gzStream = new GZipStream(reader, CompressionMode.Decompress);
var memStream = new MemoryStream();
StreamUtils.CopyStream(gzStream, memStream);
memStream.Seek(0, SeekOrigin.Begin);
usedSource = new BufferedBinaryReader(memStream);
}
if (readHeader(usedSource, readTagParams) && gd3TagOffset > VGM_HEADER_SIZE)
{
tagExists = true;
readGd3Tag(usedSource, gd3TagOffset);
}
return true;
}
// Write GD3 tag
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)
{
byte[] endString = new byte[] { 0, 0 };
int result = 11; // 11 field to write
Encoding unicodeEncoder = Encoding.Unicode;
w.Write(GD3_SIGNATURE);
w.Write(0x00000100); // Version number
var sizePos = w.BaseStream.Position;
w.Write(0);
w.Write(unicodeEncoder.GetBytes(tag[Field.TITLE]));
w.Write(endString); // Strings must be null-terminated
var str = "";
if (AdditionalFields.ContainsKey("TITLE_J")) str = AdditionalFields["TITLE_J"];
w.Write(unicodeEncoder.GetBytes(str));
w.Write(endString);
w.Write(unicodeEncoder.GetBytes(tag[Field.ALBUM]));
w.Write(endString);
str = "";
if (AdditionalFields.ContainsKey("GAME_J")) str = AdditionalFields["GAME_J"];
w.Write(unicodeEncoder.GetBytes(str));
w.Write(endString);
str = "";
if (AdditionalFields.ContainsKey("SYSTEM")) str = AdditionalFields["SYSTEM"];
w.Write(unicodeEncoder.GetBytes(str));
w.Write(endString);
str = "";
if (AdditionalFields.ContainsKey("SYSTEM_J")) str = AdditionalFields["SYSTEM_J"];
w.Write(unicodeEncoder.GetBytes(str));
w.Write(endString);
w.Write(unicodeEncoder.GetBytes(tag[Field.ARTIST]));
w.Write(endString);
str = "";
if (AdditionalFields.ContainsKey("AUTHOR_J")) str = AdditionalFields["AUTHOR_J"];
w.Write(unicodeEncoder.GetBytes(str));
w.Write(endString);
w.Write(unicodeEncoder.GetBytes(EncodeDate(Date)));
w.Write(endString);
str = "";
if (AdditionalFields.ContainsKey("DUMPER")) str = AdditionalFields["DUMPER"];
w.Write(unicodeEncoder.GetBytes(str));
w.Write(endString);
w.Write(unicodeEncoder.GetBytes(tag[Field.COMMENT]));
w.Write(endString);
w.Write(endString); // Is supposed to be there, according to sample files
int size = (int)(w.BaseStream.Position - sizePos - 4);
w.BaseStream.Seek(sizePos, SeekOrigin.Begin);
w.Write(size);
return result;
}
}
}