implemented 'UnityFS' archives and FSB5 audio.

This commit is contained in:
morkt 2017-04-11 00:19:55 +04:00
parent 31a01f2e5d
commit 3028c27a6d
9 changed files with 2808 additions and 1 deletions

View File

@ -7,7 +7,7 @@
<ProjectGuid>{60054FD9-4472-4BB4-9E3D-2F80D3D22468}</ProjectGuid> <ProjectGuid>{60054FD9-4472-4BB4-9E3D-2F80D3D22468}</ProjectGuid>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder> <AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>GameRes.Experimental</RootNamespace> <RootNamespace>GameRes.Extra</RootNamespace>
<AssemblyName>ArcExtra</AssemblyName> <AssemblyName>ArcExtra</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
@ -81,6 +81,13 @@
<Compile Include="CellWorks\ArcDB.cs" /> <Compile Include="CellWorks\ArcDB.cs" />
<Compile Include="Opus\AudioOPUS.cs" /> <Compile Include="Opus\AudioOPUS.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Unity\ArcUnityFS.cs" />
<Compile Include="Unity\Asset.cs" />
<Compile Include="Unity\AssetReader.cs" />
<Compile Include="Unity\AudioFSB5.cs" />
<Compile Include="Unity\BundleStream.cs" />
<Compile Include="Unity\OggStream.cs" />
<Compile Include="Unity\Vorbis.cs" />
<Compile Include="WebP\ImageWEBP.cs" /> <Compile Include="WebP\ImageWEBP.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -97,6 +104,7 @@
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Unity\strings.dat" />
<None Include="x64\libwebp.dll"> <None Include="x64\libwebp.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>

View File

@ -0,0 +1,302 @@
//! \file ArcUnityFS.cs
//! \date Tue Apr 04 22:27:22 2017
//! \brief Unity asset archive.
//
// 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.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Text;
using GameRes.Compression;
using GameRes.Utility;
namespace GameRes.Formats.Unity
{
[Export(typeof(ArchiveFormat))]
public class UnityFSOpener : ArchiveFormat
{
public override string Tag { get { return "UNITY/FS"; } }
public override string Description { get { return "Unity game engine asset archive"; } }
public override uint Signature { get { return 0x74696E55; } } // 'UnityFS'
public override bool IsHierarchic { get { return true; } }
public override bool CanWrite { get { return false; } }
public UnityFSOpener ()
{
Extensions = new string[] { "" };
}
public override ArcFile TryOpen (ArcView file)
{
if (!file.View.AsciiEqual (0, "UnityFS\0"))
return null;
int arc_version = Binary.BigEndian (file.View.ReadInt32 (8));
if (arc_version != 6)
return null;
long data_offset;
byte[] index_data;
using (var input = file.CreateStream())
{
input.Position = 0xC;
input.ReadCString (Encoding.UTF8);
input.ReadCString (Encoding.UTF8);
long file_size = Binary.BigEndian (input.ReadInt64());
int packed_index_size = Binary.BigEndian (input.ReadInt32());
int index_size = Binary.BigEndian (input.ReadInt32());
int flags = Binary.BigEndian (input.ReadInt32());
long index_offset;
if (0 == (flags & 0x80))
{
index_offset = input.Position;
data_offset = index_offset + packed_index_size;
}
else
{
index_offset = file_size - packed_index_size;
data_offset = input.Position;
}
input.Position = index_offset;
var packed = input.ReadBytes (packed_index_size);
switch (flags & 0x3F)
{
case 0:
index_data = packed;
break;
case 1:
index_data = UnpackLzma (packed, index_size);
break;
case 3:
index_data = new byte[index_size];
Lz4Compressor.DecompressBlock (packed, packed.Length, index_data, index_data.Length);
break;
default:
return null;
}
}
var index = new AssetDeserializer (file, data_offset);
using (var input = new BinMemoryStream (index_data))
index.Parse (input);
var dir = index.LoadObjects();
return new UnityBundle (file, this, dir, index.Segments);
}
public override Stream OpenEntry (ArcFile arc, Entry entry)
{
var uarc = (UnityBundle)arc;
var input = new BundleStream (uarc.File, uarc.Segments);
return new StreamRegion (input, entry.Offset, entry.Size);
}
internal static byte[] UnpackLzma (byte[] input, int unpacked_size)
{
throw new NotImplementedException();
}
}
internal class BundleEntry : Entry
{
public uint Flags;
}
internal class AssetEntry : Entry
{
public BundleEntry Bundle;
public UnityObject AssetObject;
}
internal class UnityBundle : ArcFile
{
public readonly List<BundleSegment> Segments;
public UnityBundle (ArcView arc, ArchiveFormat impl, ICollection<Entry> dir, List<BundleSegment> segments)
: base (arc, impl, dir)
{
Segments = segments;
}
}
internal class BundleSegment
{
public long Offset;
public uint PackedSize;
public long UnpackedOffset;
public uint UnpackedSize;
public int Compression;
public bool IsCompressed { get { return Compression != 0; } }
}
internal class AssetDeserializer
{
readonly ArcView m_file;
readonly long m_data_offset;
List<BundleSegment> m_segments;
List<BundleEntry> m_bundles;
public List<BundleSegment> Segments { get { return m_segments; } }
public AssetDeserializer (ArcView file, long data_offset)
{
m_file = file;
m_data_offset = data_offset;
}
public void Parse (IBinaryStream index)
{
index.Position = 16;
int segment_count = Binary.BigEndian (index.ReadInt32());
m_segments = new List<BundleSegment> (segment_count);
long packed_offset = m_data_offset;
long unpacked_offset = 0;
for (int i = 0; i < segment_count; ++i)
{
var segment = new BundleSegment();
segment.Offset = packed_offset;
segment.UnpackedOffset = unpacked_offset;
segment.UnpackedSize = Binary.BigEndian (index.ReadUInt32());
segment.PackedSize = Binary.BigEndian (index.ReadUInt32());
segment.Compression = Binary.BigEndian (index.ReadUInt16());
m_segments.Add (segment);
packed_offset += segment.PackedSize;
unpacked_offset += segment.UnpackedSize;
}
int count = Binary.BigEndian (index.ReadInt32());
m_bundles = new List<BundleEntry> (count);
for (int i = 0; i < count; ++i)
{
var entry = new BundleEntry();
entry.Offset = Binary.BigEndian (index.ReadInt64());
entry.Size = (uint)Binary.BigEndian (index.ReadInt64());
entry.Flags = Binary.BigEndian (index.ReadUInt32());
entry.Name = index.ReadCString (Encoding.UTF8);
m_bundles.Add (entry);
}
}
public List<Entry> LoadObjects ()
{
var dir = new List<Entry>();
using (var stream = new BundleStream (m_file, m_segments))
{
foreach (BundleEntry bundle in m_bundles)
{
if (bundle.Name.EndsWith (".resource"))
continue;
using (var asset_stream = new StreamRegion (stream, bundle.Offset, bundle.Size, true))
using (var reader = new AssetReader (asset_stream, bundle.Name))
{
var asset = new Asset();
asset.Load (reader);
var object_dir = ParseAsset (bundle, asset, stream);
dir.AddRange (object_dir);
}
}
if (0 == dir.Count)
dir.AddRange (m_bundles);
}
return dir;
}
IEnumerable<Entry> ParseAsset (BundleEntry bundle, Asset asset, Stream file)
{
Dictionary<long, string> id_map = null;
var bundle_types = asset.Tree.TypeTrees.Where (t => t.Value.Type == "AssetBundle").Select (t => t.Key);
if (bundle_types.Any())
{
// try to read entry names from AssetBundle object
int bundle_type_id = bundle_types.First();
var asset_bundle = asset.Objects.FirstOrDefault (x => x.TypeId == bundle_type_id);
if (asset_bundle != null)
{
id_map = asset.ReadAssetBundle (file, asset_bundle);
}
}
if (null == id_map)
id_map = new Dictionary<long, string>();
foreach (var obj in asset.Objects)
{
string type = obj.Type;
AssetEntry entry = null;
if ("AudioClip" == type)
{
entry = ReadAudioClip (file, obj, asset);
}
if (null == entry)
{
entry = new AssetEntry {
Type = type,
Bundle = bundle,
AssetObject = obj,
Offset = obj.Offset,
Size = obj.Size,
};
}
string name;
if (!id_map.TryGetValue (obj.PathId, out name))
name = obj.PathId.ToString ("X16");
else
name = ShortenPath (name);
entry.Name = name;
yield return entry;
}
}
AssetEntry ReadAudioClip (Stream input, UnityObject obj, Asset asset)
{
using (var stream = new StreamRegion (input, obj.Offset, obj.Size, true))
using (var reader = new AssetReader (stream, ""))
{
reader.SetupReaders (asset.Format, asset.IsLittleEndian);
var clip = new AudioClip();
clip.Load (reader);
var bundle_name = Path.GetFileName (clip.m_Source);
var bundle = m_bundles.FirstOrDefault (b => b.Name == bundle_name);
if (null == bundle)
return null;
return new AssetEntry {
Type = "audio",
Bundle = bundle,
AssetObject = obj,
Offset = bundle.Offset + clip.m_Offset,
Size = (uint)clip.m_Size,
};
}
}
/// <summary>
/// Shorten asset path to contain only the bottom directory component.
/// </summary>
static string ShortenPath (string name)
{
int slash_pos = name.LastIndexOf ('/');
if (-1 == slash_pos)
return name;
slash_pos = name.LastIndexOf ('/', slash_pos-1);
if (-1 == slash_pos)
return name;
return name.Substring (slash_pos+1);
}
}
}

454
Experimental/Unity/Asset.cs Normal file
View File

@ -0,0 +1,454 @@
//! \file Asset.cs
//! \date Wed Apr 05 18:58:07 2017
//! \brief Unity asset class.
//
// Based on the [UnityPack](https://github.com/HearthSim/UnityPack)
//
// Copyright (c) Jerome Leclanche
//
// C# port 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using GameRes.Utility;
namespace GameRes.Formats.Unity
{
internal class Asset
{
int m_header_size;
int m_format;
uint m_data_offset;
bool m_is_little_endian;
UnityTypeData m_tree = new UnityTypeData();
Dictionary<long, int> m_adds;
List<AssetRef> m_refs;
Dictionary<int, TypeTree> m_types = new Dictionary<int, TypeTree>();
Dictionary<long, UnityObject> m_objects = new Dictionary<long, UnityObject>();
public int Format { get { return m_format; } }
public bool IsLittleEndian { get { return m_is_little_endian; } }
public long DataOffset { get { return m_data_offset; } }
public UnityTypeData Tree { get { return m_tree; } }
public IEnumerable<UnityObject> Objects { get { return m_objects.Values; } }
public void Load (AssetReader input)
{
m_header_size = input.ReadInt32();
input.ReadUInt32(); // file_size
m_format = input.ReadInt32();
m_data_offset = input.ReadUInt32();
if (m_format >= 9)
m_is_little_endian = 0 == input.ReadInt32();
input.SetupReaders (m_format, m_is_little_endian);
m_tree.Load (input);
bool long_ids = Format >= 14;
if (Format >= 7 && Format < 14)
long_ids = 0 != input.ReadInt32();
input.SetupReadId (long_ids);
int obj_count = input.ReadInt32();
for (int i = 0; i < obj_count; ++i)
{
input.Align();
var obj = new UnityObject (this);
obj.Load (input);
RegisterObject (obj);
}
if (Format >= 11)
{
int count = input.ReadInt32();
m_adds = new Dictionary<long, int> (count);
for (int i = 0; i < count; ++i)
{
input.Align();
var id = input.ReadId();
m_adds[id] = input.ReadInt32();
}
}
if (Format >= 6)
{
int count = input.ReadInt32();
m_refs = new List<AssetRef> (count);
for (int i = 0; i < count; ++i)
{
var asset_ref = AssetRef.Load (input);
m_refs.Add (asset_ref);
}
}
input.ReadCString();
}
void RegisterObject (UnityObject obj)
{
if (m_tree.TypeTrees.ContainsKey (obj.TypeId))
{
m_types[obj.TypeId] = m_tree.TypeTrees[obj.TypeId];
}
else if (!m_types.ContainsKey (obj.TypeId))
{
/*
var trees = TypeTree.Default (this).TypeTrees;
if (trees.ContainsKey (obj.ClassId))
{
m_types[obj.TypeId] = trees[obj.ClassId];
}
else
*/
{
Trace.WriteLine ("Unknown type id", obj.ClassId.ToString());
m_types[obj.TypeId] = null;
}
throw new ApplicationException (string.Format ("Unknwon type id {0}", obj.ClassId));
}
if (m_objects.ContainsKey (obj.PathId))
throw new ApplicationException (string.Format ("Duplicate asset object {0} (PathId: {1})", obj, obj.PathId));
m_objects[obj.PathId] = obj;
}
public Dictionary<long, string> ReadAssetBundle (Stream input, UnityObject bundle)
{
using (var stream = new StreamRegion (input, bundle.Offset, bundle.Size, true))
using (var reader = new AssetReader (stream, ""))
{
reader.SetupReaders (m_format, m_is_little_endian);
var name = reader.ReadString(); // m_Name
reader.Align();
var id_map = new Dictionary<long, string>();
id_map[bundle.PathId] = name;
int count = reader.ReadInt32(); // m_PreloadTable
for (int i = 0; i < count; ++i)
{
reader.ReadInt32(); // m_FileID
reader.ReadInt64(); // m_PathID
}
count = reader.ReadInt32(); // m_Container
for (int i = 0; i < count; ++i)
{
name = reader.ReadString();
reader.Align();
reader.ReadInt32(); // preloadIndex
reader.ReadInt32(); // preloadSize
reader.ReadInt32(); // m_FileID
long id = reader.ReadInt64();
id_map[id] = name;
}
return id_map;
}
}
}
internal class AssetRef
{
public string AssetPath;
public Guid Guid;
public int Type;
public string FilePath;
public object Asset;
public static AssetRef Load (AssetReader reader)
{
var r = new AssetRef();
r.AssetPath = reader.ReadCString();
r.Guid = new Guid (reader.ReadBytes (16));
r.Type = reader.ReadInt32();
r.FilePath = reader.ReadCString();
r.Asset = null;
return r;
}
}
internal class UnityObject
{
public Asset Asset;
public long PathId;
public long Offset;
public uint Size;
public int TypeId;
public int ClassId;
public bool IsDestroyed;
public UnityObject (Asset owner)
{
Asset = owner;
}
public void Load (AssetReader reader)
{
PathId = reader.ReadId();
Offset = reader.ReadUInt32() + Asset.DataOffset;
Size = reader.ReadUInt32();
if (Asset.Format < 17)
{
TypeId = reader.ReadInt32();
ClassId = reader.ReadInt16();
}
else
{
var type_id = reader.ReadInt32();
var class_id = Asset.Tree.ClassIds[type_id];
TypeId = class_id;
ClassId = class_id;
}
if (Asset.Format <= 10)
IsDestroyed = reader.ReadInt16() != 0;
if (Asset.Format >= 11 && Asset.Format < 17)
reader.ReadInt16();
if (Asset.Format >= 15 && Asset.Format < 17)
reader.ReadByte();
}
public string Type {
get {
var type_tree = Asset.Tree.TypeTrees;
if (type_tree.ContainsKey (TypeId))
return type_tree[TypeId].Type;
return string.Format ("[TypeId:{0}]", TypeId);
}
}
public override string ToString ()
{
return string.Format ("<{0} {1}>", Type, ClassId);
}
}
internal class TypeTree
{
int m_format;
List<TypeTree> m_children = new List<TypeTree>();
public int Version;
public bool IsArray;
public string Type;
public string Name;
public int Size;
public uint Index;
public int Flags;
public IList<TypeTree> Children { get { return m_children; } }
static readonly string Null = "(null)";
static readonly Lazy<byte[]> StringsDat = new Lazy<byte[]> (() => LoadResource ("strings.dat"));
public TypeTree (int format)
{
m_format = format;
}
public void Load (AssetReader reader)
{
if (10 == m_format || m_format >= 12)
LoadBlob (reader);
else
LoadRaw (reader);
}
void LoadRaw (AssetReader reader)
{
throw new NotImplementedException();
}
byte[] m_data;
void LoadBlob (AssetReader reader)
{
int count = reader.ReadInt32();
int buffer_bytes = reader.ReadInt32();
var node_data = reader.ReadBytes (24 * count);
m_data = reader.ReadBytes (buffer_bytes);
var parents = new Stack<TypeTree>();
parents.Push (this);
using (var buf = new BinMemoryStream (node_data))
{
for (int i = 0; i < count; ++i)
{
int version = buf.ReadInt16();
int depth = buf.ReadUInt8();
TypeTree current;
if (0 == depth)
{
current = this;
}
else
{
while (parents.Count > depth)
parents.Pop();
current = new TypeTree (m_format);
parents.Peek().Children.Add (current);
parents.Push (current);
}
current.Version = version;
current.IsArray = buf.ReadUInt8() != 0;
current.Type = GetString (buf.ReadInt32());
current.Name = GetString (buf.ReadInt32());
current.Size = buf.ReadInt32();
current.Index = buf.ReadUInt32();
current.Flags = buf.ReadInt32();
}
}
}
string GetString (int offset)
{
byte[] strings;
if (offset < 0)
{
offset &= 0x7FFFFFFF;
strings = StringsDat.Value;
}
else if (offset < m_data.Length)
strings = m_data;
else
return Null;
return Binary.GetCString (strings, offset, strings.Length-offset, Encoding.UTF8);
}
internal static Stream OpenResource (string name)
{
var qualified_name = ".Unity." + name;
var assembly = Assembly.GetExecutingAssembly();
var res_name = assembly.GetManifestResourceNames().Single (r => r.EndsWith (qualified_name));
Stream stream = assembly.GetManifestResourceStream (res_name);
if (null == stream)
throw new FileNotFoundException ("Resource not found.", name);
return stream;
}
internal static byte[] LoadResource (string name)
{
using (var stream = OpenResource (name))
{
var res = new byte[stream.Length];
stream.Read (res, 0, res.Length);
return res;
}
}
}
internal class UnityTypeData
{
List<int> m_class_ids = new List<int> ();
Dictionary<int, byte[]> m_hashes = new Dictionary<int, byte[]> ();
Dictionary<int, TypeTree> m_type_trees = new Dictionary<int, TypeTree> ();
public IList<int> ClassIds { get { return m_class_ids; } }
public IDictionary<int, byte[]> Hashes { get { return m_hashes; } }
public IDictionary<int, TypeTree> TypeTrees { get { return m_type_trees; } }
public void Load (AssetReader reader)
{
int format = reader.Format;
var version = reader.ReadCString();
var platform = reader.ReadInt32 ();
if (format >= 13)
{
bool has_type_trees = reader.ReadBool ();
int count = reader.ReadInt32 ();
for (int i = 0; i < count; ++i)
{
int class_id = reader.ReadInt32 ();
if (format >= 17)
{
reader.ReadByte ();
int script_id = reader.ReadInt16 ();
if (114 == class_id)
{
if (script_id >= 0)
class_id = -2 - script_id;
else
class_id = -1;
}
}
m_class_ids.Add (class_id);
byte[] hash = reader.ReadBytes (class_id < 0 ? 0x20 : 0x10);
m_hashes[class_id] = hash;
if (has_type_trees)
{
var tree = new TypeTree (format);
tree.Load (reader);
m_type_trees[class_id] = tree;
}
}
}
else
{
int count = reader.ReadInt32 ();
for (int i = 0; i < count; ++i)
{
int class_id = reader.ReadInt32 ();
var tree = new TypeTree (format);
tree.Load (reader);
m_type_trees[class_id] = tree;
}
}
}
}
internal class AudioClip
{
public int m_LoadType;
public int m_Channels;
public int m_Frequency;
public int m_BitsPerSample;
public float m_Length;
public bool m_IsTrackerFormat;
public int m_SubsoundIndex;
public bool m_PreloadAudioData;
public bool m_LoadInBackground;
public bool m_Legacy3D;
public string m_Source;
public long m_Offset;
public long m_Size;
public int m_CompressionFormat;
public void Load (AssetReader reader)
{
var name = reader.ReadString();
reader.Align();
m_LoadType = reader.ReadInt32();
m_Channels = reader.ReadInt32();
m_Frequency = reader.ReadInt32();
m_BitsPerSample = reader.ReadInt32();
m_Length = reader.ReadFloat();
m_IsTrackerFormat = reader.ReadBool();
reader.Align();
m_SubsoundIndex = reader.ReadInt32();
m_PreloadAudioData = reader.ReadBool();
m_LoadInBackground = reader.ReadBool();
m_Legacy3D = reader.ReadBool();
reader.Align();
m_Source = reader.ReadString();
reader.Align();
m_Offset = reader.ReadInt64();
m_Size = reader.ReadInt64();
m_CompressionFormat = reader.ReadInt32();
}
}
}

View File

@ -0,0 +1,194 @@
//! \file AssetReader.cs
//! \date Wed Apr 05 13:28:33 2017
//! \brief Unity asset reader class.
//
// 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.Runtime.InteropServices;
using System.Text;
using GameRes.Utility;
namespace GameRes.Formats.Unity
{
/// <summary>
/// AssetReader provides access to a serialized stream of Unity assets.
/// </summary>
internal sealed class AssetReader : IDisposable
{
IBinaryStream m_input;
int m_format;
public int Format { get { return m_format; } }
public long Position {
get { return m_input.Position; }
set { m_input.Position = value; }
}
public AssetReader (Stream input, string name)
{
m_input = BinaryStream.FromStream (input, name);
SetupReaders (0, false);
}
public Action Align;
public Func<ushort> ReadUInt16;
public Func<short> ReadInt16;
public Func<uint> ReadUInt32;
public Func<int> ReadInt32;
public Func<long> ReadInt64;
public Func<long> ReadId;
/// <summary>
/// Setup reader endianness accordingly.
/// </summary>
public void SetupReaders (int format, bool is_little_endian)
{
m_format = format;
if (is_little_endian)
{
ReadUInt16 = () => m_input.ReadUInt16();
ReadUInt32 = () => m_input.ReadUInt32();
ReadInt16 = () => m_input.ReadInt16();
ReadInt32 = () => m_input.ReadInt32();
ReadInt64 = () => m_input.ReadInt64();
}
else
{
ReadUInt16 = () => Binary.BigEndian (m_input.ReadUInt16());
ReadUInt32 = () => Binary.BigEndian (m_input.ReadUInt32());
ReadInt16 = () => Binary.BigEndian (m_input.ReadInt16());
ReadInt32 = () => Binary.BigEndian (m_input.ReadInt32());
ReadInt64 = () => Binary.BigEndian (m_input.ReadInt64());
}
if (m_format >= 14)
{
Align = () => {
long pos = m_input.Position;
if (0 != (pos & 3))
m_input.Position = (pos + 3) & ~3L;
};
ReadId = ReadInt64;
}
else
{
Align = () => {};
ReadId = () => ReadInt32();
}
}
/// <summary>
/// Set asset ID length. If <paramref name="long_id"/> is <c>true</c> IDs are 64-bit, otherwise 32-bit.
/// </summary>
public void SetupReadId (bool long_ids)
{
if (long_ids)
ReadId = ReadInt64;
else
ReadId = () => ReadInt32();
}
/// <summary>
/// Read bytes into specified buffer.
/// </summary>
public int Read (byte[] buffer, int offset, int count)
{
return m_input.Read (buffer, offset, count);
}
/// <summary>
/// Read null-terminated UTF8 string.
/// </summary>
public string ReadCString ()
{
return m_input.ReadCString (Encoding.UTF8);
}
/// <summary>
/// Read UTF8 string prefixed with length.
/// </summary>
public string ReadString ()
{
int length = ReadInt32();
if (0 == length)
return string.Empty;
var bytes = ReadBytes (length);
return Encoding.UTF8.GetString (bytes);
}
/// <summary>
/// Read <paramref name="length"/> bytes from stream and return them in a byte array.
/// May return less than <paramref name="length"/> bytes if end of file was encountered.
/// </summary>
public byte[] ReadBytes (int length)
{
return m_input.ReadBytes (length);
}
/// <summary>
/// Read unsigned 8-bits byte from a stream.
/// </summary>
public byte ReadByte ()
{
return m_input.ReadUInt8();
}
/// <summary>
/// Read byte and interpret is as a bool value, non-zero resulting in <c>true</c>.
/// </summary>
public bool ReadBool ()
{
return ReadByte() != 0;
}
[StructLayout(LayoutKind.Explicit)]
struct Union
{
[FieldOffset (0)]
public uint u;
[FieldOffset(0)]
public float f;
}
/// <summary>
/// Read float value from a stream.
/// </summary>
public float ReadFloat ()
{
var buf = new Union();
buf.u = ReadUInt32();
return buf.f;
}
bool _disposed = false;
public void Dispose ()
{
if (!_disposed)
{
m_input.Dispose();
_disposed = true;
}
GC.SuppressFinalize (this);
}
}
}

View File

@ -0,0 +1,402 @@
//! \file AudioFSB5.cs
//! \date Thu Apr 06 01:12:27 2017
//! \brief FMOD Sample Bank audio file.
//
// Based on [python-fsb5](https://github.com/HearthSim/python-fsb5)
//
// Copyright (c) 2016 Simon Pinfold
//
// C# 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.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using GameRes.Formats.Vorbis;
namespace GameRes.Formats.Fmod
{
[Export(typeof(AudioFormat))]
public class Fsb5Audio : AudioFormat
{
public override string Tag { get { return "FSB5"; } }
public override string Description { get { return "FMOD Sample Bank audio format"; } }
public override uint Signature { get { return 0x35425346; } } // 'FSB5'
public override bool CanWrite { get { return false; } }
public override SoundInput TryOpen (IBinaryStream file)
{
var fsb = new Fsb5Decoder (file);
var sound = fsb.Convert();
file.Dispose();
return sound;
}
public override ResourceScheme Scheme
{
get { return new FmodScheme { VorbisHeaders = Fsb5Decoder.VorbisHeaders }; }
set { Fsb5Decoder.VorbisHeaders = ((FmodScheme)value).VorbisHeaders; }
}
}
enum SoundFormat
{
// Order is crucial.
None,
Pcm8,
Pcm16,
Pcm24,
Pcm32,
PcmFloat,
GcAdpcm,
ImaAdpcm,
Vag,
Hevag,
Xma,
Mpeg,
Celt,
At9,
Xwma,
Vorbis,
}
enum ChunkType
{
Channels = 1,
SampleRate = 2,
Loop = 3,
VorbisData = 11,
}
internal class Sample
{
public int SampleRate;
public ushort Channels;
public long DataOffset;
public int SampleCount;
public byte[] Data;
public Dictionary<ChunkType, object> MetaData;
}
internal class Fsb5Decoder
{
IBinaryStream m_input;
int m_sample_header_size;
int m_name_table_size;
int m_header_size;
int m_data_size;
SoundFormat m_format;
static readonly HashSet<SoundFormat> Supported = new HashSet<SoundFormat> {
SoundFormat.Pcm8,
SoundFormat.Pcm16,
SoundFormat.Pcm32,
SoundFormat.PcmFloat,
SoundFormat.Vorbis,
};
public Fsb5Decoder (IBinaryStream input)
{
m_input = input;
}
List<Sample> ReadSamples ()
{
var header = m_input.ReadHeader (0x3C);
int version = header.ToInt32 (4);
int sample_count = header.ToInt32 (8);
m_sample_header_size = header.ToInt32 (0xC);
m_name_table_size = header.ToInt32 (0x10);
m_data_size = header.ToInt32 (0x14);
m_format = (SoundFormat)header.ToInt32 (0x18);
if (!Supported.Contains (m_format))
throw new NotSupportedException();
if (0 == version)
m_input.ReadInt32();
var samples = new List<Sample> (sample_count);
m_header_size = (int)m_input.Position;
for (int i = 0; i < sample_count; ++i)
{
long raw = m_input.ReadInt64();
bool next_chunk = 0 != (raw & 1);
int sample_rate = (int)((raw >> 1) & 0xF);
ushort channels = (ushort)(((raw >> 5) & 1) + 1);
long data_offset = ((raw >> 6) & 0xFFFFFFF) * 0x10;
int count = (int)((raw >> 34) & 0x3FFFFFFF);
var chunks = new Dictionary<ChunkType, object>();
while (next_chunk)
{
int d = m_input.ReadInt32();
next_chunk = 0 != (d & 1);
int chunk_size = (d >> 1) & 0xFFFFFF;
var chunk_type = (ChunkType)((d >> 25) & 0x7F);
object chunk;
switch (chunk_type)
{
case ChunkType.Channels:
chunk = m_input.ReadUInt8();
break;
case ChunkType.SampleRate:
chunk = m_input.ReadInt32();
break;
case ChunkType.Loop:
int v1 = m_input.ReadInt32();
int v2 = m_input.ReadInt32();
chunk = Tuple.Create (v1, v2);
break;
case ChunkType.VorbisData:
chunk = new VorbisData {
Crc32 = m_input.ReadUInt32(),
Data = m_input.ReadBytes (chunk_size-4) // XXX unused
};
break;
default:
chunk = m_input.ReadBytes (chunk_size);
break;
}
chunks[chunk_type] = chunk;
}
if (chunks.ContainsKey (ChunkType.SampleRate))
sample_rate = (int)chunks[ChunkType.SampleRate];
else if (SampleRates.ContainsKey (sample_rate))
sample_rate = SampleRates[sample_rate];
else
throw new InvalidFormatException ("Invalid FSB5 sample rate.");
var sample = new Sample {
SampleRate = sample_rate,
Channels = channels,
DataOffset = data_offset,
SampleCount = count,
MetaData = chunks,
Data = null
};
samples.Add (sample);
}
return samples;
}
public SoundInput Convert ()
{
var samples = ReadSamples();
var sample = samples[0];
int data_length;
if (samples.Count > 1)
data_length = (int)(samples[1].DataOffset - sample.DataOffset);
else
data_length = m_data_size;
m_input.Position = m_header_size + m_sample_header_size + m_name_table_size;
sample.Data = m_input.ReadBytes (data_length);
if (SoundFormat.Vorbis == m_format)
return RebuildVorbis (sample);
else
return RebuildPcm (sample);
}
SoundInput RebuildPcm (Sample sample)
{
var format = new WaveFormat
{
FormatTag = (ushort)(SoundFormat.PcmFloat == m_format ? 3 : 1),
Channels = sample.Channels,
SamplesPerSecond = (uint)sample.SampleRate,
};
switch (m_format)
{
case SoundFormat.Pcm8: format.BitsPerSample = 8; break;
case SoundFormat.Pcm16: format.BitsPerSample = 16; break;
case SoundFormat.PcmFloat:
case SoundFormat.Pcm32: format.BitsPerSample = 32; break;
default: throw new InvalidFormatException();
}
format.BlockAlign = (ushort)(format.Channels * format.BitsPerSample / 8);
format.SetBPS();
var pcm = new MemoryStream (sample.Data);
return new RawPcmInput (pcm, format);
}
SoundInput RebuildVorbis (Sample sample)
{
if (!sample.MetaData.ContainsKey (ChunkType.VorbisData))
throw new InvalidFormatException ("No VORBISDATA chunk in FSB5 Vorbis stream.");
var vorbis_data = sample.MetaData[ChunkType.VorbisData] as VorbisData;
var setup_data = GetVorbisHeader (vorbis_data.Crc32);
var state = new OggStreamState (1);
var id_packet = RebuildIdPacket (sample, 0x100, 0x800);
var comment_packet = RebuildCommentPacket();
var setup_packet = RebuildSetupPacket (setup_data);
var info = CreateVorbisInfo (sample, setup_packet);
var output = new MemoryStream();
state.PacketIn (id_packet);
state.Write (output);
state.PacketIn (comment_packet);
state.Write (output);
state.PacketIn (setup_packet);
state.Write (output);
state.Flush (output);
long packet_no = setup_packet.PacketNo + 1;
long granule_pos = 0;
int prev_block_size = 0;
using (var input = new BinMemoryStream (sample.Data))
{
var packet = new OggPacket();
int packet_size = ReadPacketSize (input);
while (packet_size > 0)
{
packet.SetPacket (packet_no++, input.ReadBytes (packet_size));
packet_size = ReadPacketSize (input);
packet.EoS = 0 == packet_size;
int block_size = info.PacketBlockSize (packet);
if (prev_block_size != 0)
granule_pos += (block_size + prev_block_size) / 4;
else
granule_pos = 0;
packet.GranulePos = granule_pos;
prev_block_size = block_size;
state.PacketIn (packet);
state.Write (output);
}
}
output.Position = 0;
return new OggInput (output);
}
VorbisInfo CreateVorbisInfo (Sample sample, OggPacket setup_packet)
{
var info = new VorbisInfo {
Channels = sample.Channels,
Rate = sample.SampleRate,
};
info.CodecSetup.BlockSizes[0] = 0x100;
info.CodecSetup.BlockSizes[1] = 0x800;
var comment = new VorbisComment { Vendor = VorbisComment.EncodeVendorString };
info.SynthesisHeaderin (comment, setup_packet);
return info;
}
int ReadPacketSize (IBinaryStream input)
{
int lo = input.ReadByte();
if (-1 == lo)
return 0;
int hi = input.ReadByte();
if (-1 == hi)
return 0;
return hi << 8 | lo;
}
OggPacket RebuildIdPacket (Sample sample, uint blocksize_short, uint blocksize_long)
{
using (var buf = new MemoryStream())
using (var output = new BinaryWriter (buf))
{
output.Write ((byte)1);
output.Write ("vorbis".ToCharArray());
output.Write (0);
output.Write ((byte)sample.Channels);
output.Write (sample.SampleRate);
output.Write (0);
output.Write (0);
output.Write (0);
int lo = VorbisInfo.CountBits (blocksize_short);
int hi = VorbisInfo.CountBits (blocksize_long);
int bits = hi << 4 | lo;
output.Write ((byte)bits);
output.Write ((byte)1);
output.Flush();
var packet = new OggPacket();
packet.SetPacket (0, buf.ToArray());
packet.BoS = true;
return packet;
}
}
OggPacket RebuildCommentPacket ()
{
var comment = new VorbisComment();
var packet = new OggPacket();
comment.HeaderOut (packet);
return packet;
}
OggPacket RebuildSetupPacket (byte[] setup_packet)
{
var packet = new OggPacket();
packet.SetPacket (2, setup_packet);
return packet;
}
static readonly Dictionary<int, int> SampleRates = new Dictionary<int,int> {
{ 1, 8000 },
{ 2, 11000 },
{ 3, 11025 },
{ 4, 16000 },
{ 5, 22050 },
{ 6, 24000 },
{ 7, 32000 },
{ 8, 44100 },
{ 9, 48000 },
};
public static byte[] GetVorbisHeader (uint id)
{
FmodVorbisSetup setup;
if (!VorbisHeaders.TryGetValue (id, out setup))
throw new InvalidFormatException (string.Format ("Unknown FSB5 Vorbis encoding 0x{0:X8}.", id));
if (null == setup.PatchData || 0 == setup.PatchData.Length)
return setup.VorbisData;
var data = setup.VorbisData.Clone() as byte[];
Buffer.BlockCopy (setup.PatchData, 0, data, setup.PatchOffset, setup.PatchData.Length);
return data;
}
internal static Dictionary<uint, FmodVorbisSetup> VorbisHeaders = new Dictionary<uint, FmodVorbisSetup>();
}
internal class VorbisData
{
public uint Crc32;
public byte[] Data; // ignored
}
[Serializable]
public class FmodVorbisSetup
{
public byte[] VorbisData;
public int PatchOffset;
public byte[] PatchData;
}
[Serializable]
public class FmodScheme : ResourceScheme
{
public Dictionary<uint, FmodVorbisSetup> VorbisHeaders;
}
}

View File

@ -0,0 +1,201 @@
//! \file BundleStream.cs
//! \date Wed Apr 05 13:30:19 2017
//! \brief Stream representing Unity bundle.
//
// 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using GameRes.Compression;
namespace GameRes.Formats.Unity
{
/// <summary>
/// Stream representing Unity asset bundle.
/// </summary>
internal class BundleStream : Stream
{
readonly ArcViewStream m_input;
readonly long m_length;
IList<BundleSegment> m_segments;
long m_position;
int m_current_segment;
byte[] m_buffer;
int m_buffer_pos;
int m_buffer_len;
byte[] m_packed;
public BundleStream (ArcView file, IList<BundleSegment> segments)
{
if (null == segments || 0 == segments.Count)
throw new ArgumentException ("Segments list is empty.", "segments");
m_input = file.CreateStream();
m_segments = segments;
var last_segment = m_segments[m_segments.Count-1];
m_length = last_segment.UnpackedOffset + last_segment.UnpackedSize;
m_position = 0;
m_current_segment = 0;
m_input.Position = m_segments[0].Offset;
}
public override bool CanRead { get { return !m_disposed; } }
public override bool CanSeek { get { return !m_disposed; } }
public override bool CanWrite { get { return false; } }
public override long Length { get { return m_length; } }
public override long Position
{
get { return m_position; }
set {
if (value == m_position)
return;
if (value < 0)
throw new ArgumentOutOfRangeException ("value", "Stream position is out of range.");
m_position = value;
int segment_index = 0;
for (int i = 1; i < m_segments.Count; ++i)
{
if (m_segments[i].UnpackedOffset > value)
break;
++segment_index;
}
var segment = m_segments[segment_index];
if (segment_index != m_current_segment)
{
m_current_segment = segment_index;
m_buffer_len = 0;
}
if (segment.IsCompressed)
{
m_buffer_pos = (int)(m_position - segment.UnpackedOffset);
}
else
{
m_buffer_pos = 0;
m_input.Position = segment.Offset + (m_position - segment.UnpackedOffset);
}
}
}
void ReadCompressedSegment (BundleSegment segment)
{
m_input.Position = segment.Offset;
if (null == m_packed || segment.PackedSize > m_packed.Length)
m_packed = new byte[segment.PackedSize];
int packed_size = m_input.Read (m_packed, 0, (int)segment.PackedSize);
if (null == m_buffer || segment.UnpackedSize > m_buffer.Length)
m_buffer = new byte[segment.UnpackedSize];
if (3 == segment.Compression)
m_buffer_len = Lz4Compressor.DecompressBlock (m_packed, packed_size, m_buffer, (int)segment.UnpackedSize);
else
throw new NotImplementedException ("Not supported Unity asset bundle compression.");
}
int ReadFromSegment (BundleSegment segment, byte[] buffer, int offset, int count)
{
Debug.Assert (m_position >= segment.UnpackedOffset && m_position <= segment.UnpackedOffset + segment.UnpackedSize);
if (!segment.IsCompressed)
{
int available = (int)Math.Min (count, (segment.UnpackedOffset + segment.UnpackedSize) - m_position);
return m_input.Read (buffer, offset, available);
}
else
{
if (0 == m_buffer_len)
ReadCompressedSegment (segment);
int available = Math.Min (count, m_buffer_len - m_buffer_pos);
Buffer.BlockCopy (m_buffer, m_buffer_pos, buffer, offset, available);
m_buffer_pos += available;
return available;
}
}
public override int Read (byte[] buffer, int offset, int count)
{
if (m_position >= m_length)
return 0;
int total_read = 0;
while (count > 0)
{
var segment = m_segments[m_current_segment];
int read = ReadFromSegment (segment, buffer, offset, count);
m_position += read;
total_read += read;
offset += read;
count -= read;
if (count > 0)
{
if (m_current_segment+1 == m_segments.Count)
break;
++m_current_segment;
m_buffer_len = m_buffer_pos = 0;
m_input.Position = m_segments[m_current_segment].Offset;
Debug.Assert (m_position == m_segments[m_current_segment].UnpackedOffset);
}
}
return total_read;
}
public override void Flush()
{
}
public override long Seek (long offset, SeekOrigin origin)
{
switch (origin)
{
case SeekOrigin.Current: offset += m_position; break;
case SeekOrigin.End: offset += m_length; break;
}
Position = offset;
return offset;
}
public override void SetLength (long length)
{
throw new NotSupportedException ("Stream.SetLength method is not supported.");
}
public override void Write (byte[] buffer, int offset, int count)
{
throw new NotSupportedException ("Stream.Write method is not supported.");
}
public override void WriteByte (byte value)
{
throw new NotSupportedException ("Stream.WriteByte method is not supported.");
}
bool m_disposed = false;
protected override void Dispose (bool disposing)
{
if (!m_disposed)
{
if (disposing)
m_input.Dispose();
m_disposed = true;
base.Dispose (disposing);
}
}
}
}

View File

@ -0,0 +1,416 @@
//! \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);
}
}
}

View File

@ -0,0 +1,830 @@
//! \file Vorbis.cs
//! \date Fri Apr 07 21:07:30 2017
//! \brief partial libvorbis port.
//
// Only parts crucial for FSB5 decoding got implemented.
// Parts that are not used in FSB5 decoder are left out completely or commented out.
//
// 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.
// -----------------------------------------------------------------------------
//
// libvorbis Copyright (c) 2002-2008 Xiph.org Foundation
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// - Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// - Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// - Neither the name of the Xiph.org Foundation nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
// OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace GameRes.Formats.Vorbis
{
// struct vorbis_info
// https://xiph.org/vorbis/doc/libvorbis/vorbis_info.html
class VorbisInfo
{
public int Version;
public int Channels;
public int Rate;
public int BitrateUpper;
public int BitrateNominal;
public int BitrateLower;
public CodecSetupInfo CodecSetup = new CodecSetupInfo();
const int TransormB = 1;
const int WindowB = 1;
const int TimeB = 1;
const int FloorB = 2;
const int ResB = 3;
const int MapB = 1;
Func<OggBitStream, VorbisInfoFloor>[] FloorMethods;
public VorbisInfo ()
{
FloorMethods = new Func<OggBitStream, VorbisInfoFloor>[] {
UnpackFloor0,
UnpackFloor1
};
}
// https://xiph.org/vorbis/doc/libvorbis/vorbis_synthesis_headerin.html
public void SynthesisHeaderin (VorbisComment vc, OggPacket op)
{
using (var input = new OggBitStream (op))
{
int packtype = input.ReadUInt8();
var buf = input.ReadBytes (6);
if (!buf.AsciiEqual ("vorbis"))
throw new InvalidDataException ("Not an Ogg/Vorbis stream.");
switch (packtype)
{
case 1:
if (!op.BoS)
throw InvalidHeader();
if (Rate != 0)
throw InvalidHeader();
UnpackInfo (input);
break;
case 3:
if (0 == Rate)
throw InvalidHeader();
vc.UnpackComment (input);
break;
case 5:
if (0 == Rate || null == vc.Vendor)
throw InvalidHeader();
UnpackBooks (input);
break;
default:
throw InvalidHeader();
}
}
}
internal static int iLog (uint num)
{
int bits = 0;
while (num != 0)
{
++bits;
num >>= 1;
}
return bits;
}
internal static int CountBits (uint num)
{
int bits = 0;
if (num != 0)
--num;
while (num != 0)
{
++bits;
num >>= 1;
}
return bits;
}
/// <summary>
/// Count number of bits set in <paramref name="num"/>.
/// </summary>
internal static int CountSetBits (uint num)
{
int bits = 0;
while (num != 0)
{
bits += (int)num & 1;
num >>= 1;
}
return bits;
}
// https://www.xiph.org/vorbis/doc/libvorbis/vorbis_packet_blocksize.html
public int PacketBlockSize (OggPacket op)
{
using (var input = new OggBitStream (op))
{
// Check the packet type
if (input.ReadBits (1) != 0)
throw new InvalidDataException ("Not an audio data packet.");
int modebits = 0;
for (int v = CodecSetup.Modes; v > 1; v >>= 1)
{
modebits++;
}
// read our mode and pre/post windowsize
int mode = input.ReadBits (modebits);
if (-1 == mode)
throw new InvalidDataException ("Invalid Ogg/Vorbis packet.");
return CodecSetup.BlockSizes[CodecSetup.ModeParam[mode].BlockFlag];
}
}
void UnpackInfo (OggBitStream input)
{
Version = input.ReadInt32();
if (Version != 0)
throw new InvalidDataException ("Invalid Vorbis encoder version.");
Channels = input.ReadUInt8();
Rate = input.ReadInt32();
BitrateUpper = input.ReadInt32();
BitrateNominal = input.ReadInt32();
BitrateLower = input.ReadInt32();
CodecSetup.BlockSizes[0] = 1 << input.ReadBits (4);
CodecSetup.BlockSizes[1] = 1 << input.ReadBits (4);
if (input.ReadBits (1) != 1)
throw InvalidHeader();
}
void UnpackBooks (OggBitStream input)
{
// codebooks
CodecSetup.Books = input.ReadUInt8() + 1;
if (CodecSetup.Books <= 0)
throw InvalidHeader();
for (int i = 0; i < CodecSetup.Books; ++i)
{
var param = StaticBookUnpack (input);
if (null == param)
throw InvalidHeader();
CodecSetup.BookParam[i] = param;
}
// time backend settings; hooks are unused
int times = input.ReadBits (6) + 1;
if (times <= 0)
throw InvalidHeader();
for (int i = 0; i < times; ++i)
{
int test = input.ReadBits (16);
if (test < 0 || test >= TimeB)
throw InvalidHeader();
}
// floor backend settings
CodecSetup.Floors = input.ReadBits (6) + 1;
if (CodecSetup.Floors <= 0)
throw InvalidHeader();
for (int i = 0; i < CodecSetup.Floors; i++)
{
int floor_type = input.ReadBits (16);
if (floor_type < 0 || floor_type >= FloorB)
throw InvalidHeader();
CodecSetup.FloorType[i] = floor_type;
var param = FloorMethods[floor_type] (input);
if (null == param)
throw InvalidHeader();
CodecSetup.FloorParam[i] = param;
}
// residue backend settings
CodecSetup.Residues = input.ReadBits (6) + 1;
if (CodecSetup.Residues <= 0)
throw InvalidHeader();
for (int i = 0; i < CodecSetup.Residues; ++i)
{
int residue_type = input.ReadBits (16);
if (residue_type < 0 || residue_type >= ResB)
throw InvalidHeader();
CodecSetup.ResidueType[i] = residue_type;
var param = UnpackResidue (input);
if (null == param)
throw InvalidHeader();
CodecSetup.ResidueParam[i] = param;
}
// map backend settings
CodecSetup.Maps = input.ReadBits (6) + 1;
if (CodecSetup.Maps <= 0)
throw InvalidHeader();
for (int i = 0; i < CodecSetup.Maps; ++i)
{
int map_type = input.ReadBits (16);
if (map_type < 0 || map_type >= MapB)
throw InvalidHeader();
CodecSetup.MapType[i] = map_type;
var param = UnpackMapping (input);
if (null == param)
throw InvalidHeader();
CodecSetup.MapParam[i] = param;
}
// mode settings
CodecSetup.Modes = input.ReadBits (6) + 1;
if (CodecSetup.Modes <= 0)
throw InvalidHeader();
for (int i = 0; i < CodecSetup.Modes; ++i)
{
CodecSetup.ModeParam[i].BlockFlag = input.ReadBits (1);
CodecSetup.ModeParam[i].WindowType = input.ReadBits (16);
CodecSetup.ModeParam[i].TransformType = input.ReadBits (16);
CodecSetup.ModeParam[i].Mapping = input.ReadBits (8);
if (CodecSetup.ModeParam[i].WindowType >= WindowB ||
CodecSetup.ModeParam[i].TransformType >= WindowB ||
CodecSetup.ModeParam[i].Mapping >= CodecSetup.Maps ||
CodecSetup.ModeParam[i].Mapping < 0)
throw InvalidHeader();
}
if (input.ReadBits (1) != 1)
throw InvalidHeader();
}
StaticCodebook StaticBookUnpack (OggBitStream input)
{
// make sure alignment is correct
if (input.ReadBits (24) != 0x564342)
return null;
var s = new StaticCodebook();
// first the basic parameters
s.dim = input.ReadBits (16);
s.entries = input.ReadBits (24);
if (-1 == s.entries)
return null;
if (iLog ((uint)s.dim) + iLog ((uint)s.entries) > 24)
return null;
// codeword ordering.... length ordered or unordered?
switch (input.ReadBits (1))
{
case 0:
// allocated but unused entries?
int unused = input.ReadBits (1);
// unordered
s.lengthlist = new byte[s.entries];
// allocated but unused entries?
if (unused > 0)
{
// yes, unused entries
for(int i = 0; i < s.entries; ++i)
{
if (input.ReadBits (1) > 0)
{
int num = input.ReadBits (5);
if (-1 == num)
return null;
s.lengthlist[i] = (byte)(num+1);
}
else
s.lengthlist[i] = 0;
}
}
else
{
// all entries used; no tagging
for (int i = 0; i < s.entries; ++i)
{
int num = input.ReadBits (5);
if (-1 == num)
return null;
s.lengthlist[i] = (byte)(num+1);
}
}
break;
case 1: // ordered
int length = input.ReadBits (5) + 1;
if (0 == length)
return null;
s.lengthlist = new byte[s.entries];
for (int i = 0; i < s.entries; )
{
int num = input.ReadBits (iLog ((uint)(s.entries-i)));
if (-1 == num || length > 32 || num > s.entries-i
|| (num > 0 && ((num-1) >> (length-1)) > 1))
return null;
for (int j = 0; j < num; ++j, ++i)
s.lengthlist[i] = (byte)length;
length++;
}
break;
default:
return null;
}
// Do we have a mapping to unpack?
switch((s.maptype = input.ReadBits (4)))
{
case 0: // no mapping
break;
case 1: case 2:
// implicitly populated value mapping
// explicitly populated value mapping
s.q_min = input.ReadInt32();
s.q_delta = input.ReadInt32();
s.q_quant = input.ReadBits (4) + 1;
s.q_sequencep = input.ReadBits (1);
if (-1 == s.q_sequencep)
return null;
int quantvals = 0;
switch (s.maptype)
{
case 1:
quantvals = s.dim == 0 ? 0 : s.Maptype1Quantvals();
break;
case 2:
quantvals = s.entries * s.dim;
break;
}
// quantized values
s.quantlist = new int[quantvals];
for (int i = 0; i < quantvals; ++i)
s.quantlist[i] = input.ReadBits (s.q_quant);
if (quantvals > 0 && s.quantlist[quantvals-1] == -1)
return null;
break;
default: // EOF
return null;
}
// all set
return s;
}
VorbisInfoFloor UnpackFloor0 (OggBitStream input)
{
var info = new VorbisInfoFloor();
int order = input.ReadBits (8);
int rate = input.ReadBits (16);
int barkmap = input.ReadBits (16);
int ampbits = input.ReadBits (6);
int ampdB = input.ReadBits (8);
int numbooks = input.ReadBits (4) + 1;
if (order < 1 || rate < 1 || barkmap < 1 || numbooks < 1)
return null;
for (int j = 0; j < numbooks; ++j)
{
int books = input.ReadByte();
if (books < 0 || books >= CodecSetup.Books)
return null;
}
return info;
}
VorbisInfoFloor UnpackFloor1 (OggBitStream input)
{
int max_class = -1;
var info = new VorbisInfoFloor();
// read partitions
int partitions = input.ReadBits (5); // only 0 to 31 legal
var partition_class = new int[partitions];
for (int j = 0; j < partitions; ++j)
{
partition_class[j] = input.ReadBits (4); // only 0 to 15 legal
if (partition_class[j] < 0)
return null;
if (max_class < partition_class[j])
max_class = partition_class[j];
}
// read partition classes
var class_dim = new int[max_class+1];
for (int j = 0; j < max_class+1; ++j)
{
class_dim[j] = input.ReadBits (3) + 1; // 1 to 8
int class_subs = input.ReadBits (2); // 0,1,2,3 bits
if (class_subs < 0)
return null;
if (class_subs > 0)
input.ReadBits (8); // class_book
for (int k = 0; k < (1 << class_subs); ++k)
{
int class_subbook = input.ReadBits (8) - 1; // info.class_subbook[j][k]
}
}
// read the post list
int mult = input.ReadBits (2) + 1; // only 1,2,3,4 legal now
int rangebits = input.ReadBits (4);
if (rangebits < 0)
return null;
int count = 0;
// var postlist = new int[VorbisInfoFloor.Posit + 2];
for (int j = 0, k = 0; j < partitions; ++j)
{
count += class_dim[partition_class[j]];
if (count > VorbisInfoFloor.Posit)
return null;
for (; k < count; ++k)
{
int t = input.ReadBits (rangebits);
if (t < 0 || t >= (1 << rangebits))
return null;
// postlist[k+2] = t;
}
}
// postlist[0] = 0;
// postlist[1] = 1<<rangebits;
// don't allow repeated values in post list as they'd result in
// zero-length segments
/*
var indices = Enumerable.Range (0, count+2).OrderBy (i => postlist[i]).ToArray();
for (int j = 1; j < count+2; j++)
if(postlist[indices[j-1]] == postlist[indices[j]])
return null;
*/
return info;
}
object UnpackResidue (OggBitStream input)
{
var info = new VorbisInfoResidue();
info.begin = input.ReadBits (24);
info.end = input.ReadBits (24);
info.grouping = input.ReadBits (24) + 1;
info.partitions = input.ReadBits (6) + 1;
info.groupbook = input.ReadBits (8);
// check for premature EOP
if (info.groupbook < 0)
return null;
int acc = 0;
for (int j = 0; j < info.partitions; ++j)
{
int cascade = input.ReadBits (3);
int cflag = input.ReadBits (1);
if (cflag < 0)
return null;
if (cflag > 0)
{
int c = input.ReadBits (5);
if (c < 0)
return null;
cascade |= c << 3;
}
// info.secondstages[j] = cascade;
acc += CountSetBits ((uint)cascade);
}
for (int j = 0; j < acc; ++j)
{
int book = input.ReadBits (8);
if (book < 0)
return null;
if (book >= CodecSetup.Books)
return null;
// if (CodecSetup.book_param[book].maptype == 0) return null;
// info.booklist[j] = book;
}
if (info.groupbook >= CodecSetup.Books)
return null;
/*
int entries = CodecSetup.book_param[info.groupbook].entries;
int dim = CodecSetup.book_param[info.groupbook].dim;
int partvals = 1;
if (dim < 1)
return null;
while (dim > 0)
{
partvals *= info.partitions;
if (partvals > entries)
return null;
dim--;
}
info.partvals = partvals;
*/
return info;
}
VorbisInfoMapping UnpackMapping (OggBitStream input)
{
var info = new VorbisInfoMapping();
int b = input.ReadBits (1);
if (b < 0)
return null;
if (b > 0)
{
info.submaps = input.ReadBits (4) + 1;
if (info.submaps <= 0)
return null;
}
else
info.submaps = 1;
b = input.ReadBits (1);
if (b < 0)
return null;
if (b > 0)
{
info.coupling_steps = input.ReadBits (8) + 1;
if (info.coupling_steps <= 0)
return null;
for (int i = 0; i < info.coupling_steps; ++i)
{
int bits = CountBits ((uint)Channels);
int testM = input.ReadBits (bits);
int testA = input.ReadBits (bits);
if (testM < 0 || testA < 0 || testM == testA
|| testM >= Channels || testA >= Channels)
return null;
}
}
if (input.ReadBits (2) != 0)
return null;
if (info.submaps > 1)
{
for (int i = 0; i < Channels; ++i)
{
int chmuxlist = input.ReadBits (4);
if (chmuxlist >= info.submaps || chmuxlist < 0)
return null;
}
}
for (int i = 0; i < info.submaps; ++i)
{
input.ReadByte(); // time submap unused
int floorsubmap = input.ReadByte();
if (floorsubmap >= CodecSetup.Floors || floorsubmap < 0)
return null;
int residuesubmap = input.ReadByte();
if (residuesubmap >= CodecSetup.Residues || residuesubmap < 0)
return null;
}
return info;
}
internal static InvalidDataException InvalidHeader ()
{
return new InvalidDataException ("Invalid header in Ogg/Vorbis stream.");
}
}
// struct vorbis_comment
// https://xiph.org/vorbis/doc/libvorbis/vorbis_comment.html
class VorbisComment
{
public List<byte[]> Comments;
public byte[] Vendor;
internal static readonly byte[] EncodeVendorString = Encoding.UTF8.GetBytes ("mørkt GARbro 20170407");
public VorbisComment ()
{
Comments = new List<byte[]>();
}
// https://xiph.org/vorbis/doc/libvorbis/vorbis_commentheader_out.html
public void HeaderOut (OggPacket packet)
{
using (var buf = new MemoryStream())
using (var output = new BinaryWriter (buf))
{
// preamble
output.Write ((byte)3);
output.Write ("vorbis".ToCharArray());
// vendor
output.Write (EncodeVendorString.Length);
output.Write (EncodeVendorString);
// comments
output.Write (Comments.Count);
foreach (var comment in Comments)
{
if (comment != null && comment.Length > 0)
{
output.Write (comment.Length);
output.Write (comment);
}
else
{
output.Write (0);
}
}
output.Write ((byte)1);
output.Flush();
packet.SetPacket (1, buf.ToArray());
packet.BoS = false;
packet.EoS = false;
packet.GranulePos = 0;
}
}
internal void UnpackComment (OggBitStream input)
{
int vendor_len = input.ReadInt32();
if (vendor_len < 0)
throw VorbisInfo.InvalidHeader();
var vendor = input.ReadBytes (vendor_len);
int count = input.ReadInt32();
if (count < 0)
throw VorbisInfo.InvalidHeader();
var comments = new List<byte[]> (count);
for (int i = 0; i < count; ++i)
{
int len = input.ReadInt32();
if (len < 0)
throw VorbisInfo.InvalidHeader();
var bytes = input.ReadBytes (len);
comments.Add (bytes);
}
if (input.ReadBits (1) != 1)
throw VorbisInfo.InvalidHeader();
this.Vendor = vendor;
this.Comments = comments;
}
}
// codec_setup_info
class CodecSetupInfo
{
public int[] BlockSizes = new int[2];
public int Modes;
public int Maps;
public int Floors;
public int Residues;
public int Books;
internal VorbisInfoMode[] ModeParam = new VorbisInfoMode[64];
internal int[] ResidueType = new int[64];
internal object[] ResidueParam = new object[64];
internal int[] FloorType = new int[64];
internal object[] FloorParam = new object[64];
internal int[] MapType = new int[64];
internal VorbisInfoMapping[] MapParam = new VorbisInfoMapping[64];
internal StaticCodebook[] BookParam = new StaticCodebook[256];
}
// struct vorbis_info_mode
struct VorbisInfoMode
{
public int BlockFlag;
public int WindowType;
public int TransformType;
public int Mapping;
}
// struct static_codebook
class StaticCodebook
{
public int dim; // codebook dimensions (elements per vector)
public int entries; // codebook entries
public byte[] lengthlist; // codeword lengths in bits
// mapping ***************************************************************
public int maptype; // 0=none
// 1=implicitly populated values from map column
// 2=listed arbitrary values
// The below does a linear, single monotonic sequence mapping.
public int q_min; // packed 32 bit float; quant value 0 maps to minval
public int q_delta; // packed 32 bit float; val 1 - val 0 == delta
public int q_quant; // bits: 0 < quant <= 16
public int q_sequencep; // bitflag
public int[] quantlist; // map == 1: (int)(entries^(1/dim)) element column map
// map == 2: list of dim*entries quantized entry vals
internal int Maptype1Quantvals ()
{
int vals = (int)Math.Floor (Math.Pow ((float)entries, 1.0f / dim));
// the above *should* be reliable, but we'll not assume that FP is
// ever reliable when bitstream sync is at stake; verify via integer
// means that vals really is the greatest value of dim for which
// vals^b->bim <= b->entries.
// treat the above as an initial guess
for (;;)
{
int acc = 1;
int acc1 = 1;
for (int i = 0; i < dim; ++i)
{
acc *= vals;
acc1 *= vals+1;
}
if (acc <= entries && acc1 > entries)
break;
if (acc > entries)
vals--;
else
vals++;
}
return vals;
}
}
class VorbisInfoMapping
{
public int submaps;
public int coupling_steps;
}
class VorbisInfoFloor
{
public const int Posit = 63;
}
class VorbisInfoResidue
{
public int begin;
public int end;
public int grouping;
public int partitions;
public int groupbook;
}
}

Binary file not shown.