417 lines
14 KiB
C#

//! \file OggStream.cs
//! \date Sat Apr 08 01:43:58 2017
//! \brief libogg partial implementation.
//
// Copyright (C) 2017 by morkt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//
using System;
using System.IO;
using System.Text;
using GameRes.Utility;
namespace GameRes.Formats.Vorbis
{
internal sealed class OggBitStream : IDisposable
{
LsbBitStream m_input;
public OggBitStream (OggPacket input)
{
// certainly an overhead to create a new stream for every packet, but it's so convenient
var buf = new MemoryStream (input.Packet);
m_input = new LsbBitStream (buf);
}
/// <summary>Read <paramref name="count"/> bits from a stream.</summary>
/// <returns>-1 if there was not enough bits in a stream</returns>
public int ReadBits (int count)
{
if (count <= 24)
return m_input.GetBits (count);
else if (count > 32)
throw new ArgumentOutOfRangeException ("count", "Attempted to read more than 32 bits from OggBitStream.");
int lo = m_input.GetBits (24);
return m_input.GetBits (count - 24) << 24 | lo;
}
/// <summary>Read 8-bit integer from bitstream.</summary>
/// <returns>-1 if there was not enough bits in a stream</returns>
public int ReadByte ()
{
return ReadBits (8);
}
/// <summary>Read 8-bit integer from bitstream.</summary>
/// <exception cref="EndOfStreamException">Thrown if there's not enough bits in a stream.</exception>
public byte ReadUInt8 ()
{
int b = ReadBits (8);
if (-1 == b)
throw new EndOfStreamException();
return (byte)b;
}
/// <summary>Read 32-bit integer from bitstream.</summary>
/// <exception cref="EndOfStreamException">Thrown if there's not enough bits in a stream.</exception>
public int ReadInt32 ()
{
int lo = ReadBits (16);
int hi = ReadBits (16);
if (-1 == lo || -1 == hi)
throw new EndOfStreamException();
return hi << 16 | lo;
}
/// <summary>Attempt to read <paramref name="count"/> bytes from stream.</summary>
/// <exception cref="EndOfStreamException">Thrown if there's not enough bytes in a bitstream.</exception>
public byte[] ReadBytes (int count)
{
var buf = new byte[count];
for (int i = 0; i < count; ++i)
buf[i] = ReadUInt8();
return buf;
}
bool m_disposed = false;
public void Dispose ()
{
if (!m_disposed)
{
m_input.Dispose();
m_disposed = true;
}
}
}
// struct ogg_packet
// https://xiph.org/ogg/doc/libogg/ogg_packet.html
internal class OggPacket
{
public byte[] Packet;
public bool BoS;
public bool EoS;
public long GranulePos;
public long PacketNo;
public void SetPacket (long packet_no, byte[] packet)
{
PacketNo = packet_no;
Packet = packet;
}
}
// struct ogg_stream_state
// https://xiph.org/ogg/doc/libogg/ogg_stream_state.html
internal class OggStreamState
{
byte[] BodyData; // bytes from packet bodies
int BodyStorage; // storage elements allocated
int BodyFill; // elements stored; fill mark
int BodyReturned; // elements of fill returned
int[] LacingVals; // The values that will go to the segment table granulepos values for headers.
long[] GranuleVals; // Not compact this way, but it is simple coupled to the lacing fifo.
int LacingStorage;
int LacingFill;
byte[] Header; // working space for header encode
int HeaderFill;
bool EoS; // set when we have buffered the last packet in the logical bitstream
bool BoS; // set after we've written the initial page of a logical bitstream
int SerialNo;
int PageNo;
long PacketNo; // sequence number for decode; the framing knows where there's a hole in the data,
// but we need coupling so that the codec (which is in a seperate abstraction
// layer) also knows about the gap
long GranulePos;
// https://xiph.org/ogg/doc/libogg/ogg_stream_init.html
public OggStreamState (int serial_no)
{
BodyStorage = 0x4000;
LacingStorage = 0x400;
BodyData = new byte[BodyStorage];
LacingVals = new int[LacingStorage];
GranuleVals = new long[LacingStorage];
Header = new byte[282];
SerialNo = serial_no;
}
public void Clear ()
{
BodyStorage = 0;
BodyFill = 0;
BodyReturned = 0;
LacingStorage = 0;
LacingFill = 0;
HeaderFill = 0;
EoS = false;
BoS = false;
SerialNo = 0;
PageNo = 0;
PacketNo = 0;
GranulePos = 0;
}
public bool PacketIn (OggPacket op)
{
int bytes = op.Packet.Length;
int lacing_vals = bytes / 255 + 1;
if (BodyReturned > 0)
{
// advance packet data according to the body_returned pointer.
// We had to keep it around to return a pointer into the buffer last call.
BodyFill -= BodyReturned;
if (BodyFill > 0)
Buffer.BlockCopy (BodyData, BodyReturned, BodyData, 0, BodyFill);
BodyReturned = 0;
}
// make sure we have the buffer storage
if(!BodyExpand (bytes) || !LacingExpand (lacing_vals))
return false;
// Copy in the submitted packet.
Buffer.BlockCopy (op.Packet, 0, BodyData, BodyFill, op.Packet.Length);
BodyFill += op.Packet.Length;
// Store lacing vals for this packet
int i;
for (i = 0; i < lacing_vals-1; ++i)
{
LacingVals[LacingFill + i] = 0xFF;
GranuleVals[LacingFill + i] = GranulePos;
}
LacingVals[LacingFill + i] = bytes % 0xFF;
GranulePos = GranuleVals[LacingFill+i] = GranulePos;
// flag the first segment as the beginning of the packet
LacingVals[LacingFill] |= 0x100;
LacingFill += lacing_vals;
PacketNo++;
EoS = op.EoS;
return true;
}
public void Write (Stream output)
{
var page = new OggPage();
while (PageOut (page))
{
output.Write (page.Header, 0, page.HeaderLength);
output.Write (page.Body, page.BodyStart, page.BodyLength);
}
}
public void Flush (Stream output)
{
var page = new OggPage();
while (Flush (page, true, 0x1000))
{
output.Write (page.Header, 0, page.HeaderLength);
output.Write (page.Body, page.BodyStart, page.BodyLength);
}
}
public bool PageOut (OggPage page)
{
bool force = EoS && (LacingFill > 0) || (LacingFill > 0 && !BoS);
return Flush (page, force, 0x1000);
}
bool BodyExpand (int needed)
{
if (BodyStorage - needed <= BodyFill)
{
if (BodyStorage > int.MaxValue - needed)
{
Clear();
return false;
}
int body_storage = BodyStorage + needed;
if (body_storage < int.MaxValue - 1024)
body_storage += 1024;
Array.Resize (ref BodyData, body_storage);
BodyStorage = body_storage;
}
return true;
}
bool LacingExpand (int needed)
{
if (LacingStorage - needed <= LacingFill)
{
if (LacingStorage > int.MaxValue - needed)
{
Clear();
return false;
}
int lacing_storage = LacingStorage + needed;
if (lacing_storage < int.MaxValue - 32)
lacing_storage += 32;
Array.Resize (ref LacingVals, lacing_storage);
Array.Resize (ref GranuleVals, lacing_storage);
LacingStorage = lacing_storage;
}
return true;
}
bool Flush (OggPage og, bool force, int fill)
{
int maxvals = Math.Min (LacingFill, 0xFF);
if (0 == maxvals)
return false;
// construct a page
// decide how many segments to include
int vals = 0;
int acc = 0;
long granule_pos = -1;
// If this is the initial header case, the first page must only include
// the initial header packet
if (!BoS) // 'initial header page' case
{
granule_pos = 0;
for (vals = 0; vals < maxvals; vals++)
{
if ((LacingVals[vals] & 0xFF) < 0xFF)
{
vals++;
break;
}
}
}
else
{
int packets_done = 0;
int packet_just_done = 0;
for (vals = 0; vals < maxvals; vals++)
{
if (acc > fill && packet_just_done >= 4)
{
force = true;
break;
}
acc += LacingVals[vals] & 0xFF;
if ((LacingVals[vals] & 0xFF) < 0xFF)
{
granule_pos = GranuleVals[vals];
packet_just_done = ++packets_done;
}
else
packet_just_done = 0;
}
if (0xFF == vals)
force = true;
}
if (!force)
return false;
// construct the header in temp storage
Encoding.ASCII.GetBytes ("OggS", 0, 4, Header, 0);
// stream structure version
Header[4] = 0;
// continued packet flag?
Header[5] = 0;
if ((LacingVals[0] & 0x100) == 0)
Header[5] |= 1;
// first page flag?
if (!BoS)
Header[5] |= 2;
// last page flag?
if (EoS && LacingFill == vals)
Header[5] |= 4;
BoS = true;
// 64 bits of PCM position
LittleEndian.Pack (granule_pos, Header, 6);
// 32 bits of stream serial number
LittleEndian.Pack (SerialNo, Header, 14);
// 32 bits of page counter (we have both counter and page header because this
// val can roll over)
if (-1 == PageNo)
PageNo = 0;
LittleEndian.Pack (PageNo, Header, 18);
++PageNo;
int bytes = 0;
// segment table
Header[26] = (byte)vals;
for (int i = 0; i < vals; ++i)
bytes += Header[i+27] = (byte)LacingVals[i];
// set pointers in the ogg_page struct
og.Header = Header;
og.HeaderLength = HeaderFill = vals + 27;
og.Body = BodyData;
og.BodyStart = BodyReturned;
og.BodyLength = bytes;
// advance the lacing data and set the body_returned pointer
LacingFill -= vals;
Array.Copy (LacingVals, vals, LacingVals, 0, LacingFill);
Array.Copy (GranuleVals, vals, GranuleVals, 0, LacingFill);
BodyReturned += bytes;
// calculate the checksum
og.SetChecksum();
return true;
}
}
// struct ogg_page
// https://xiph.org/ogg/doc/libogg/ogg_page.html
internal class OggPage
{
public byte[] Header;
public int HeaderLength;
public byte[] Body;
public int BodyStart;
public int BodyLength;
public void SetChecksum ()
{
Header[22] = Header[23] = Header[24] = Header[25] = 0;
uint crc = Crc32Normal.UpdateCrc (0, Header, 0, HeaderLength);
crc = Crc32Normal.UpdateCrc (crc, Body, BodyStart, BodyLength);
LittleEndian.Pack (crc, Header, 22);
}
}
}