decode HCA audio stream in background task.

parallel decoding disabled for now (see HcaReader.ConvertParallel).
This commit is contained in:
morkt 2016-03-04 06:18:11 +04:00
parent dbb3384e02
commit 48b6747c4c

View File

@ -24,11 +24,14 @@
//
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Text;
using GameRes.Utility;
using System.Threading;
using System.Threading.Tasks;
namespace GameRes.Formats.Cri
{
@ -48,14 +51,7 @@ namespace GameRes.Formats.Cri
public override SoundInput TryOpen (Stream file)
{
using (var reader = new HcaReader (file, DefaultKey.Item1, DefaultKey.Item2))
{
var data = reader.Unpack (ConversionFormat.IeeeFloat);
var input = new MemoryStream (data);
var sound = new RawPcmInput (input, reader.Format);
file.Dispose();
return sound;
}
return new HcaInput (file, ConversionFormat.IeeeFloat, DefaultKey);
}
}
@ -65,12 +61,136 @@ namespace GameRes.Formats.Cri
IeeeFloat = 3,
};
internal class HcaInput : SoundInput
{
HcaReader m_reader;
long m_position;
int m_bitrate;
Array[] m_decoded_blocks;
int m_decoded_block_size;
public override long Position
{
get { return m_position; }
set { m_position = value; }
}
public override bool CanSeek { get { return true; } }
public override string SourceFormat { get { return "hca"; } }
public override int SourceBitrate { get { return m_bitrate; } }
public HcaInput (Stream file, ConversionFormat target, Tuple<uint, uint> key) : base (file)
{
m_reader = new HcaReader (file, key.Item1, key.Item2);
m_reader.InitConversion (target);
Format = m_reader.Format;
m_bitrate = (int)(Format.SamplesPerSecond * m_reader.BlockSize / (0x80 * Format.Channels));
m_decoded_block_size = (int)(0x80 * Format.Channels * Format.BitsPerSample);
PcmSize = m_reader.BlockCount * m_decoded_block_size;
m_decoded_blocks = new Array[m_reader.BlockCount];
InitBackgroundReader (target);
}
public override int Read (byte[] buffer, int offset, int count)
{
long block_pos;
int block_index = (int)Math.DivRem (m_position, m_decoded_block_size, out block_pos);
if (block_index < 0 || block_index > m_decoded_blocks.Length)
return 0;
int total_read = 0;
while (block_index < m_decoded_blocks.Length && count > 0)
{
if (null == m_decoded_blocks[block_index])
FillBlock (block_index);
if (null == m_decoded_blocks[block_index])
break;
int available = Math.Min (count, m_decoded_block_size - (int)block_pos);
Buffer.BlockCopy (m_decoded_blocks[block_index], (int)block_pos, buffer, offset, available);
m_position += available;
total_read += available;
count -= available;
if (0 == count)
break;
offset += available;
block_pos = 0;
++block_index;
}
return total_read;
}
void InitBackgroundReader (ConversionFormat target)
{
m_block_queue = new BlockingCollection<Tuple<int, Array>>();
m_cancel_source = new CancellationTokenSource();
var token = m_cancel_source.Token;
if (ConversionFormat.IeeeFloat == target)
m_conversion_task = Task.Factory.StartNew (() => m_reader.ConvertParallel (m_block_queue, HcaReader.PackSampleFloat, token), token);
else
m_conversion_task = Task.Factory.StartNew (() => m_reader.ConvertParallel (m_block_queue, HcaReader.PackSample16, token), token);
}
BlockingCollection<Tuple<int, Array>> m_block_queue;
Task m_conversion_task;
CancellationTokenSource m_cancel_source;
void FillBlock (int block_index)
{
var token = m_cancel_source.Token;
while (!m_block_queue.IsCompleted && null == m_decoded_blocks[block_index])
{
var block = m_block_queue.Take (token);
if (block.Item1 >= m_decoded_blocks.Length)
throw new IndexOutOfRangeException();
m_decoded_blocks[block.Item1] = block.Item2;
}
}
#region IDisposable Members
bool _hca_disposed = false;
protected override void Dispose (bool disposing)
{
if (!_hca_disposed)
{
if (disposing)
{
if (m_cancel_source != null)
{
if (!m_block_queue.IsAddingCompleted)
m_cancel_source.Cancel();
try
{
m_conversion_task.Wait();
}
catch
{
// ignore exceptions
}
finally
{
m_conversion_task.Dispose();
m_block_queue.Dispose();
m_cancel_source.Dispose();
}
}
m_reader.Dispose();
}
_hca_disposed = true;
base.Dispose (disposing);
}
}
#endregion
}
internal sealed class HcaReader : IDisposable
{
WaveFormat m_format;
byte[] m_input;
public WaveFormat Format { get { return m_format; } }
public uint BlockCount { get { return m_fmt_block_count.Value; } }
public int BlockSize { get { return m_comp.BlockSize; } }
public HcaReader (Stream input, uint key1, uint key2)
{
@ -78,19 +198,20 @@ namespace GameRes.Formats.Cri
ParseHeader (file);
m_ath = new AthTable (m_ath_type.Value, m_format.SamplesPerSecond);
m_cipher = new Cipher (m_ciph_type.Value, key1, key2);
m_block = new ThreadLocal<byte[]> (() => new byte[m_comp.BlockSize]);
InitBuffer (input);
}
delegate void SampleConverter (float f, BinaryWriter output);
delegate void SampleWriter (float f, BinaryWriter output);
static readonly Dictionary<ConversionFormat, ushort> FormatBpsMap = new Dictionary<ConversionFormat, ushort> {
{ ConversionFormat.Pcm, 16 },
{ ConversionFormat.IeeeFloat, 32 },
};
static readonly Dictionary<ConversionFormat, SampleConverter> ConversionMap = new Dictionary<ConversionFormat, SampleConverter> {
{ ConversionFormat.Pcm, PackSample16 },
{ ConversionFormat.IeeeFloat, PackSampleFloat },
static readonly Dictionary<ConversionFormat, SampleWriter> ConversionMap = new Dictionary<ConversionFormat, SampleWriter> {
{ ConversionFormat.Pcm, (f, output) => output.Write (PackSample16 (f)) },
{ ConversionFormat.IeeeFloat, (f, output) => output.Write (PackSampleFloat (f)) },
};
int m_version;
@ -102,20 +223,26 @@ namespace GameRes.Formats.Cri
float m_rva_volume = 1.0f;
AthTable m_ath;
Cipher m_cipher;
Channel[] m_channel;
byte[] m_block;
ThreadLocal<Channel[]> m_channel;
ThreadLocal<byte[]> m_block;
public byte[] Unpack (ConversionFormat target)
{
InitWaveFormat (target);
m_block = new byte[m_comp.BlockSize];
var output = new byte[m_fmt_block_count.Value * 0x400 * m_format.BlockAlign];
var output = new byte[BlockCount * 0x400 * m_format.BlockAlign];
var convert_sample = ConversionMap[target];
using (var mem = new MemoryStream (output))
using (var writer = new BinaryWriter (mem))
Decode (writer, ConversionMap[target]);
ConvertSequential (f => convert_sample (f, writer));
return output;
}
public void InitConversion (ConversionFormat target)
{
InitWaveFormat (target);
}
void InitWaveFormat (ConversionFormat target)
{
m_format.FormatTag = (ushort)target;
@ -259,80 +386,127 @@ namespace GameRes.Formats.Cri
}
}
}
m_channel = new Channel[m_format.Channels];
for (int i = 0; i < m_channel.Length; ++i)
{
m_channel[i] = new Channel (r[i], m_comp.R[5], m_comp.R[6]);
}
m_channel = new ThreadLocal<Channel[]> (() => {
var channels = new Channel[m_format.Channels];
for (int i = 0; i < channels.Length; ++i)
{
channels[i] = new Channel (r[i], m_comp.R[5], m_comp.R[6]);
}
return channels;
});
}
void Decode (BinaryWriter output, SampleConverter pack_sample)
void ConvertSequential (Action<float> pack_sample)
{
int block_offset = m_data_offset;
for (uint i = 0; i < m_fmt_block_count; ++i)
foreach (var block_offset in GetBlockOffsets())
{
if (block_offset + m_comp.BlockSize > m_input.Length)
throw new EndOfStreamException();
Buffer.BlockCopy (m_input, block_offset, m_block, 0, m_comp.BlockSize);
block_offset += m_comp.BlockSize;
Buffer.BlockCopy (m_input, block_offset, m_block.Value, 0, m_comp.BlockSize);
DecodeBlock();
for (int j = 0; j < 8; ++j)
for (int k = 0; k < 0x80; ++k)
for (int c = 0; c < m_channel.Length; ++c)
for (int c = 0; c < m_format.Channels; ++c)
{
float f = m_channel[c].Samples[j,k] * m_rva_volume;
pack_sample (f, output);
float f = m_channel.Value[c].Samples[j,k] * m_rva_volume;
pack_sample (f);
}
}
}
public void ConvertParallel<SampleType> (BlockingCollection<Tuple<int, Array>> output, Func<float, SampleType> convert_sample, CancellationToken token)
{
try
{
// despite the fact that parallel decoding is considerably faster (roughly x[number of cores])
// it hurts playback badly due to locks inside BlockingCollection.Take()
// Parallel.ForEach (Enumerable.Range (0, (int)BlockCount), block_num =>
foreach (int block_num in Enumerable.Range (0, (int)BlockCount))
{
int block_offset = m_data_offset + block_num * m_comp.BlockSize;
if (block_offset + m_comp.BlockSize > m_input.Length)
throw new EndOfStreamException();
token.ThrowIfCancellationRequested();
Buffer.BlockCopy (m_input, block_offset, m_block.Value, 0, m_comp.BlockSize);
DecodeBlock();
token.ThrowIfCancellationRequested();
var decoded = new SampleType[0x400 * m_format.Channels];
int i = 0;
for (int j = 0; j < 8; ++j)
for (int k = 0; k < 0x80; ++k)
for (int c = 0; c < m_format.Channels; ++c)
{
float f = m_channel.Value[c].Samples[j,k] * m_rva_volume;
decoded[i++] = convert_sample (f);
}
output.Add (new Tuple<int, Array> (block_num, decoded), token);
}
}
finally
{
output.CompleteAdding();
}
}
IEnumerable<int> GetBlockOffsets ()
{
int block_offset = m_data_offset;
for (int i = 0; i < m_fmt_block_count; ++i)
{
yield return block_offset;
block_offset += m_comp.BlockSize;
}
}
void DecodeBlock ()
{
if (CheckSum (m_block) != 0)
var block = m_block.Value;
if (CheckSum (block) != 0)
throw new InvalidFormatException ("Data checksum mismatch");
m_cipher.Decipher (m_block);
using (var input = new MemoryStream (m_block, 0, m_block.Length-2))
m_cipher.Decipher (block);
using (var input = new MemoryStream (block, 0, block.Length-2))
using (var bits = new HsaBitStream (input))
{
if(0xFFFF == bits.GetBits (16))
if (0xFFFF != bits.GetBits (16))
return;
var decoder = m_channel.Value;
int t = bits.GetBits (9) << 8;
t -= bits.GetBits (7);
for (int i = 0; i < decoder.Length; ++i)
decoder[i].Decode1 (bits, m_comp.R9, t, m_ath.Table);
for (int i = 0; i < 8; ++i)
{
int t = bits.GetBits (9) << 8;
t -= bits.GetBits (7);
for (int i = 0; i < m_format.Channels; ++i)
m_channel[i].Decode1 (bits, m_comp.R9, t, m_ath.Table);
for (int i = 0; i < 8; ++i)
{
for (int j = 0; j < m_channel.Length; ++j)
m_channel[j].Decode2 (bits);
for (int j = 0; j < m_channel.Length; ++j)
m_channel[j].Decode3 (m_comp.R9, m_comp.R[7], m_comp.R[6] + m_comp.R[5], m_comp.R[4]);
for (int j = 0; j < m_channel.Length-1; ++j)
m_channel[j].Decode4 (i, m_comp.R[4]-m_comp.R[5], m_comp.R[5], m_comp.R[6], m_channel[j+1]);
for (int j = 0; j < m_channel.Length; ++j)
m_channel[j].Decode5 (i);
}
for (int j = 0; j < decoder.Length; ++j)
decoder[j].Decode2 (bits);
for (int j = 0; j < decoder.Length; ++j)
decoder[j].Decode3 (m_comp.R9, m_comp.R[7], m_comp.R[6] + m_comp.R[5], m_comp.R[4]);
for (int j = 0; j < decoder.Length-1; ++j)
decoder[j].Decode4 (i, m_comp.R[4]-m_comp.R[5], m_comp.R[5], m_comp.R[6], decoder[j+1]);
for (int j = 0; j < decoder.Length; ++j)
decoder[j].Decode5 (i);
}
}
}
static void PackSampleFloat (float f, BinaryWriter output)
public static float PackSampleFloat (float f)
{
if (f > 1)
f = 1;
else if (f < -1)
f = -1;
output.Write (f);
return f;
}
static void PackSample16 (float f, BinaryWriter output)
public static short PackSample16 (float f)
{
int v = (int)(f * 0x7FFF);
if (v > 0x7FFF)
v = 0x7FFF;
else if (v < -0x7FFF)
v = -0x7FFF;
output.Write ((short)v);
int s = (int)(f * 0x7FFF);
if (s > 0x7FFF)
s = 0x7FFF;
else if (s < -0x7FFF)
s = -0x7FFF;
return (short)s;
}
static uint ReadSignature (BigEndianReader input)
@ -982,6 +1156,8 @@ namespace GameRes.Formats.Cri
{
if (!_disposed)
{
m_channel.Dispose();
m_block.Dispose();
_disposed = true;
}
}