1
0
mirror of https://github.com/SineVector241/VoiceCraft-MCBE_Proximity_Chat.git synced 2024-11-24 14:36:13 +00:00
VoiceCraft-MCBE_Proximity_Chat/ATL/AudioData/IO/MPEGplus.cs
2024-07-13 11:16:08 +10:00

314 lines
10 KiB
C#

using Commons;
using System;
using System.Collections.Generic;
using System.IO;
using static ATL.AudioData.AudioDataManager;
using static ATL.ChannelsArrangements;
namespace ATL.AudioData.IO
{
/// <summary>
/// Class for MusePack / MPEGplus files manipulation (extensions : .MPC, .MP+)
/// </summary>
class MPEGplus : IAudioDataIO
{
// Sample frequencies
private static readonly int[] MPP_SAMPLERATES = new int[] { 44100, 48000, 37800, 32000 };
// ID code for stream version > 6
private const long STREAM_VERSION_7_ID = 0x4D502B07; // 'MP+' + #7
private const long STREAM_VERSION_71_ID = 0x4D502B17; // 'MP+' + #23
private const long STREAM_VERSION_8_ID = 0x4D50434B; // 'MPCK'
private int frameCount;
private SizeInfo sizeInfo;
// File header data - for internal use
private sealed class HeaderRecord
{
public readonly byte[] ByteArray = new byte[32]; // Data as byte array
public readonly int[] IntegerArray = new int[8]; // Data as integer array
public static int GetVersion(byte[] data)
{
if (data.Length < 4) return 0;
int dataAsInt = StreamUtils.DecodeBEInt32(data);
// Get MPEGplus stream version
if (STREAM_VERSION_7_ID == dataAsInt) return 70;
else if (STREAM_VERSION_71_ID == dataAsInt) return 71;
else if (STREAM_VERSION_8_ID == dataAsInt) return 80;
else
{
switch (data[1] % 32 / 2) // Int division
{
case 3: return 40;
case 7: return 50;
case 11: return 60;
default: return 0;
}
}
}
public void computeVersion()
{
Version = GetVersion(ByteArray);
}
public int Version { get; private set; } = 0;
}
// ---------- INFORMATIVE INTERFACE IMPLEMENTATIONS & MANDATORY OVERRIDES
public bool IsVBR => true;
public Format AudioFormat
{
get;
}
public int CodecFamily => AudioDataIOFactory.CF_LOSSY;
public string FileName { get; }
public double BitRate { get; private set; }
public int BitDepth => -1; // Irrelevant for lossy formats
public double Duration { get; private set; }
public ChannelsArrangement ChannelsArrangement { get; private set; }
public int SampleRate { get; private set; }
/// <inheritdoc/>
public List<MetaDataIOFactory.TagType> GetSupportedMetas()
{
return new List<MetaDataIOFactory.TagType>
{
MetaDataIOFactory.TagType.ID3V2, MetaDataIOFactory.TagType.APE, MetaDataIOFactory.TagType.ID3V1
};
}
public long AudioDataOffset { get; set; }
public long AudioDataSize { get; set; }
// ---------- CONSTRUCTORS & INITIALIZERS
private void resetData()
{
frameCount = 0;
SampleRate = 0;
AudioDataOffset = -1;
AudioDataSize = 0;
}
public MPEGplus(string filePath, Format format)
{
this.FileName = filePath;
AudioFormat = format;
resetData();
}
// ---------- SUPPORT METHODS
public static bool IsValidHeader(byte[] data)
{
return HeaderRecord.GetVersion(data) > 60; // <v7 not auto-detected (no specs available)
}
private bool readHeader(Stream source, ref HeaderRecord header)
{
bool result = true;
source.Seek(sizeInfo.ID3v2Size, SeekOrigin.Begin);
// Read header and get file size
source.Read(header.ByteArray, 0, header.ByteArray.Length);
// if transfer is not complete
byte[] temp = new byte[4];
for (int i = 0; i < header.IntegerArray.Length; i++)
{
Array.Copy(header.ByteArray, i * 4, temp, 0, 4);
header.IntegerArray[i] = StreamUtils.DecodeInt32(temp);
}
header.computeVersion();
// If VS8 file, looks for the (mandatory) stream header packet
if (80 == header.Version)
{
string packetKey;
bool headerFound = false;
// Let's go back right after the 32-bit version marker
source.Seek(sizeInfo.ID3v2Size + 4, SeekOrigin.Begin);
byte[] buffer = new byte[2];
while (!headerFound)
{
long initialPos = source.Position;
source.Read(buffer, 0, 2);
packetKey = Utils.Latin1Encoding.GetString(buffer);
readVariableSizeInteger(source); // Packet size (unused)
// SV8 stream header packet
if (packetKey.Equals("SH"))
{
AudioDataOffset = initialPos;
// Skip CRC-32 and stream version
source.Seek(5, SeekOrigin.Current);
long sampleCount = readVariableSizeInteger(source);
readVariableSizeInteger(source); // Skip beginning silence
source.Read(buffer, 0, 1);// Sample frequency (3) + Max used bands (5)
SampleRate = MPP_SAMPLERATES[(buffer[0] & 0b11100000) >> 5]; // First 3 bits
source.Read(buffer, 0, 1); // Channel count (4) + Mid/Side Stereo used (1) + Audio block frames (3)
int channelCount = (buffer[0] & 0b11110000) >> 4; // First 4 bits
bool isMidSideStereo = (buffer[0] & 0b00001000) > 0; // First 4 bits
if (isMidSideStereo) ChannelsArrangement = JOINT_STEREO_MID_SIDE;
else ChannelsArrangement = GuessFromChannelNumber(channelCount);
// MPC has variable bitrate; only MPC versions < 7 display fixed bitrate
Duration = sampleCount * 1000.0 / SampleRate;
BitRate = calculateAverageBitrate(Duration);
headerFound = true;
}
// Continue searching for header
source.Seek(initialPos + 2, SeekOrigin.Begin);
}
}
else
{
AudioDataOffset = sizeInfo.ID3v2Size;
}
AudioDataSize = sizeInfo.FileSize - sizeInfo.APESize - sizeInfo.ID3v1Size - AudioDataOffset;
return result;
}
/* Get samplerate from header
Note: this is the same byte where profile is stored
*/
private static int getSV7SampleRate(HeaderRecord header)
{
if (header.Version > 50)
{
return MPP_SAMPLERATES[header.ByteArray[10] & 3];
}
else
{
return 44100; // Fixed to 44.1 Khz before SV5
}
}
private static ChannelsArrangement getSV7ChannelsArrangement(HeaderRecord header)
{
ChannelsArrangement result;
if ((70 == header.Version) || (71 == header.Version))
// Get channel mode for stream version 7
if ((header.ByteArray[11] % 128) < 64) result = STEREO;
else result = JOINT_STEREO; // TODO - could actually be either intensity stereo or mid/side stereo; however that code is obscure....
else
// Get channel mode for stream version 4-6
if (0 == (header.ByteArray[2] % 128)) result = STEREO;
else result = JOINT_STEREO;
return result;
}
private static int getSV7FrameCount(HeaderRecord header)
{
int result;
if (40 == header.Version) result = header.IntegerArray[1] >> 16;
else if ((50 <= header.Version) && (header.Version <= 71))
{
result = header.IntegerArray[1];
}
else result = 0;
return result;
}
private double getSV7BitRate()
{
return calculateAverageBitrate(getSV7Duration());
}
private double calculateAverageBitrate(double duration)
{
double result = 0;
long CompressedSize;
CompressedSize = sizeInfo.FileSize - sizeInfo.TotalTagSize;
if (duration > 0) result = Math.Round(CompressedSize * 8.0 / duration);
return result;
}
private double getSV7Duration()
{
// Calculate duration time
if (SampleRate > 0) return (frameCount * 1152.0 * 1000.0 / SampleRate);
else return 0;
}
public bool Read(Stream source, SizeInfo sizeInfo, MetaDataIO.ReadTagParams readTagParams)
{
HeaderRecord header = new HeaderRecord();
bool result;
this.sizeInfo = sizeInfo;
resetData();
// Load header from file to variable
result = readHeader(source, ref header);
// Process data if loaded and file valid
if (result && (sizeInfo.FileSize > 0) && (header.Version > 0))
{
if (header.Version < 80)
{
// Fill properties with SV7 header data
SampleRate = getSV7SampleRate(header);
ChannelsArrangement = getSV7ChannelsArrangement(header);
frameCount = getSV7FrameCount(header);
BitRate = getSV7BitRate();
Duration = getSV7Duration();
}
else
{
// SV8 data already read
}
}
return result;
}
// Specific to MPC SV8
// See specifications
private static long readVariableSizeInteger(Stream source)
{
long result = 0;
byte b = 128;
byte[] buffer = new byte[1];
// Data is coded with a Big-endian, 7-byte variable-length record
while ((b & 128) > 0)
{
source.Read(buffer, 0, 1);
b = buffer[0];
result = (result << 7) + (b & 127); // Big-endian
}
return result;
}
}
}