• 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Release] [C#] [LIBRARY] SABSFile library (open sound files)
#1
Music 
Hi everybody, Today I bring you a small project I've been working on (and struggling with).
The idea was to create a opensource dotZip-similar type of extracting and packaging library for the black ops soundfiles.
Sadly I was unable to finish the packaging (replacing) part.
I hope I can get people excited about this and you are free to improve it (please do)
I do not have that much experience involving algorithms and hex'n'bytes'n'stuff so excuse me if I didn't use the right methods (criticism is welcome)
I do believe that the information and the offsets of each flac file is defined in the header, because there's a lot of stuff in the header (that I don't understand, sadly)

Pro's:
  • Scan for FLAC files inside of sabs files
  • Get raw bytes from each flac file in there
  • Extract file directly from class
  • Replace (doesn't work properly, works when extracting again but crashes game.)
  • Very low memory usage

Cons:
  • Keep in mind that when calling the constructor (new SABSFile("d.sabs")) the class starts looking for offsets which takes a few seconds. Do NOT call it from the main thread in a form application.
  • Replacing fails ingame (soundbank failed to initialize)


master131-flac-extractor-style code example:
Code:
CSHARP Code
  1. Console.WriteLine("SABSFile library example extractor");
  2. string a = GetSteamPath()+"\\steamapps\\common\\Call of Duty Black Ops II";
  3. foreach(string file in Directory.GetFiles(GetSteamPath()+"\\steamapps\\common\\Call of Duty Black Ops II\\sound", "*.sabs"))
  4. {
  5. Console.WriteLine("Extracting " + Path.GetFileName(file));
  6. SABSFile sabs = new SABSFile(file);
  7. string path = Path.GetDirectoryName(Application.ExecutablePath);
  8. int i = 0;
  9. string dir = path+"\\extracted\\"+Path.GetFileNameWithoutExtension(file);
  10. if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
  11. else foreach (string filef in Directory.GetFiles(dir)) File.Delete(filef);
  12. foreach (SABSEntry entry in sabs.Entries)
  13. {
  14. i++;
  15. Console.Write("\r Progress: "+Convert.ToInt32((double)i / (double)sabs.Entries.Length * 100.0) + "%");
  16. entry.Extract(dir + "\\" + entry.Offset.ToString("X").ToLower() + ".flac");
  17. }
  18. Console.WriteLine();
  19. }

Result:
[Image: EwvWw.png]
[Image: GC4Pd.png]

All that with 20 lines of code?
Yes, that's the power of SABSFile

Download (library, no source, just example source)

.zip   SABSFile.zip (Size: 4.4 KB / Downloads: 123)
Source
CSHARP Code
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.IO;
  6. using System.Collections;
  7.  
  8. namespace JariZ
  9. {
  10. public class SABSFile
  11. {
  12. public SABSFile(string filename) {
  13. if (string.IsNullOrEmpty(filename)) throw new ArgumentException("Invalid argument");
  14. if (Path.GetExtension(filename) != ".sabs") throw new FileLoadException("Only SABS files supported");
  15. if (!File.Exists(filename)) throw new FileNotFoundException("SAB File not found");
  16. /*
  17.   try
  18.   {*/
  19. _filename = filename;
  20. byte[] header = new byte[8] { 0x66, 0x4C, 0x61, 0x43, 0x00, 0x00, 0x00, 0x22};
  21. HeaderSearch hs = new HeaderSearch(header);
  22. using (FileStream reader = new FileStream(filename, FileMode.Open))
  23. {
  24. int read;
  25. int index = 0x800;
  26. reader.Position = 0x800;
  27.  
  28. List<int> offsets = new List<int>();
  29. while ((read = reader.ReadByte()) >= 0)
  30. {
  31. index++;
  32. if (hs.Search((byte)read))
  33. {
  34. int offset = index - header.Length;
  35. offsets.Add(offset);
  36. }
  37. }
  38. offsets.Add(Convert.ToInt32(reader.Position-1));
  39.  
  40. int indexx = 0;
  41. foreach(int offset in offsets)
  42. {
  43. indexx++;
  44. if (indexx != 1)
  45. _entries.Add(new SABSEntry(this, offsets[indexx-2], offset));
  46. }
  47.  
  48. }
  49. /*
  50.   }
  51.   catch (Exception z)
  52.   {
  53.   throw new Exception("Unable to read file", z);
  54.   }*/
  55. }
  56.  
  57. string _filename = null;
  58. public string Filename
  59. {
  60. get
  61. {
  62. return _filename;
  63. }
  64. }
  65.  
  66.  
  67. List<SABSEntry> _entries = new List<SABSEntry>();
  68. public SABSEntry[] Entries
  69. {
  70. get
  71. {
  72. return _entries.ToArray();
  73. }
  74. }
  75. }
  76.  
  77. public class SABSEntry
  78. {
  79. public SABSEntry(SABSFile parent, int offsetstart, int offsetend)
  80. {
  81. _offsetend = offsetend;
  82. _offsetstart = offsetstart;
  83. _parent = parent;
  84. }
  85.  
  86. public int Offset
  87. {
  88. get
  89. {
  90. return _offsetstart;
  91. }
  92. }
  93.  
  94. public void Extract(out byte[] buffer)
  95. {
  96. List<byte> lbuffer = new List<byte>();
  97. using (FileStream fs = new FileStream(_parent.Filename, FileMode.Open))
  98. {
  99. fs.Position = _offsetstart;
  100. while (fs.Position <= _offsetend) lbuffer.Add((byte)fs.ReadByte());
  101. }
  102. buffer = lbuffer.ToArray();
  103. }
  104.  
  105. SABSFile _parent;
  106. public void Extract(string filename)
  107. {
  108. using (FileStream fs = new FileStream(_parent.Filename, FileMode.Open, FileAccess.Read, FileShare.None))
  109. {
  110. if (!Directory.Exists(Path.GetDirectoryName(filename))) Directory.CreateDirectory(Path.GetDirectoryName(filename));
  111. if (!File.Exists(filename)) File.Create(filename).Dispose();
  112. using(FileStream gs = new FileStream(filename, FileMode.Truncate, FileAccess.Write, FileShare.None))
  113. {
  114. fs.Position = _offsetstart;
  115. while (fs.Position <= _offsetend) gs.WriteByte((byte)fs.ReadByte());
  116. gs.Flush();
  117. gs.Close();
  118. fs.Close();
  119. gs.Dispose();
  120. fs.Dispose();
  121. }
  122. }
  123. }
  124.  
  125. public void Replace(byte[] content)
  126. {
  127. byte[] b = File.ReadAllBytes(_parent.Filename);
  128. List<byte> lb = new List<byte>();
  129. lb.AddRange(b);
  130. lb.RemoveRange(_offsetstart, _offsetend - _offsetstart);
  131. lb.InsertRange(_offsetstart, content);
  132. File.WriteAllBytes(_parent.Filename, lb.ToArray());
  133.  
  134. /*using (FileStream fs = new FileStream(_parent.Filename, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
  135.   {
  136.   fs.Position = _offsetstart;
  137.   byte[] otherdata = null;
  138.   bool crossed_offset = false;
  139.   string tempfile = string.Empty;
  140.   foreach(byte bite in content)
  141.   {
  142.   if (fs.Position <= _offsetend)
  143.   {
  144.   fs.WriteByte(bite);
  145.   }
  146.   else
  147.   {
  148.   //we've crossed our offset, no worries, but we need to start inserting data from now on.
  149.   //this is simple. we read the rest of the data, write it to a temporary file (to prevent memleaks)
  150.   if (!crossed_offset)
  151.   {
  152.   int b4 = Convert.ToInt32(fs.Position);
  153.   FileStream gs = new FileStream((tempfile = Path.GetRandomFileName()), FileMode.OpenOrCreate, FileAccess.Write, FileShare.None);
  154.   while (fs.Position < Convert.ToInt32(fs.Length)) gs.WriteByte((byte)fs.ReadByte());
  155.   gs.Flush();
  156.   gs.Close();
  157.   gs.Dispose();
  158.   crossed_offset = true;
  159.   fs.Position = b4;
  160.   }
  161.   fs.WriteByte(bite);
  162.   }
  163.   }
  164.   //now that we have written our new file, update the offset
  165.   _offsetend = Convert.ToInt32(fs.Position);
  166.   if (crossed_offset)
  167.   {
  168.   fs.Position = _offsetend;
  169.   FileStream gs = new FileStream(tempfile, FileMode.Open, FileAccess.Read, FileShare.None);
  170.   while (gs.Position < Convert.ToInt32(gs.Length)) fs.WriteByte((byte)gs.ReadByte());
  171.   gs.Flush();
  172.   gs.Close();
  173.   gs.Dispose();
  174.   }
  175.   fs.Flush();
  176.   fs.Close();
  177.   fs.Dispose();
  178.   }*/
  179. }
  180.  
  181. public void Replace(string filename)
  182. {
  183. Replace(File.ReadAllBytes(filename));
  184. }
  185.  
  186. int _offsetstart = 0;
  187. int _offsetend = 0;
  188. }
  189.  
  190. public class HeaderSearch
  191. {
  192. public HeaderSearch(byte[] header)
  193. {
  194. _bufferlength = header.Length;
  195. _header = header;
  196. buffer = new List<byte>(_bufferlength);
  197. _headerend = _header[_header.Length-1];
  198.  
  199. //fill buffer
  200. while (buffer.Count != _bufferlength) buffer.Add(0);
  201. }
  202.  
  203. byte[] _header;
  204. int _bufferlength = 0;
  205. List<byte> buffer;
  206. int _headerend = 0;
  207. public bool Search(byte newbyte)
  208. {
  209. buffer.RemoveAt(0);
  210. buffer.Add(newbyte);
  211. if (newbyte == _headerend)
  212. {
  213. byte[] bbuffer = buffer.ToArray();
  214. if (bbuffer.SequenceEqual(_header))
  215. return true;
  216. else return false;
  217. }
  218. else return false;
  219. }
  220. }
  221. }
  Reply
#2
Nice job however....
1. The way you are using to search for the FLAC header seems extremely slow. You are using a list and removing the 0th item, forcing a resize and array shift and therefore slowing down performance.
2. You have not taken the hash data at the end of the SABS file which will be appended to the end of the FLAC file into account (most players will probably ignore this but it can cause some problems).
3. You have hardcoded the position 0x800, but this could easily change if necessary. You should be relying on the header's start offset value or starting at 0. It's probably just a coincidence that all the data start at the same position in all the files.
4. Similar to point #1, using Lists for byte intensive operations like this can seriously slow down everything.
5.
Code:
gs.Flush();
gs.Close();
fs.Close();
gs.Dispose();
fs.Dispose();
None of that is necessary, the using statement automatically does that because FileStream implements IDisposable which will automatically call Dispose -> Close and Dispose -> Flush.
6. Try to avoid using Convert.ToInt32 and use the cast operator, aka (int). It just creates extra overhead for something simple.
7. Obviously the replace won't work if the original and replacement data size are different. The SABS file stores the positions of the data, it doesn't just check them in the order they appear in, otherwise it'd be super slow for the game to find a sound. The best way to solve this would be to pad the data with zeros if it is too small or throw an exception if the replacement data is too large. The problem with this though is if you are replacing music that is supposed to loop, it may create a major silence and I haven't found a solution around that yet.

Perhaps I might be too critical, but you wanted criticism so I gave it.
[Image: 30xhrep.png]

A casual conversation between barata and I about Nukem.
  Reply
#3
1. I know, I could load the whole thing into the memory with readallbytes or something but that would max memory usage trough the roof so this is what I was trying to avoid. I noticed it's slow but I simply could not think of a better way.
2. I didn't knew there was any data that wasn't part of the FLAC
3. I don't think that's a coincidence although I agree that it's a bad way
4. See 1.
5. Didn't knew that, I'll remember that, I just wanted to make sure the file was closed Tongue
6. I thought that was more likely to throw errors, but okay.
7. As I said in the introduction of the post, I already thought of that. There's a lot of data in the header (everything until 0x800) and I'm sure the positions are defined there as well. It would be pretty cool if we could change them so we could use larger files than the originals.
I will try to fuck around a bit more with replacing.
Also, Do you have any idea what the sabl files are and what they are used for?

PS: This was my method of approaching it, and I know, it's not perfect (at all). I'm still learning while I'm working on this and that's why I asked for criticism.

It should also be noted that the reason why I decided to release it anyway was because I got stuck and just didn't want to give up because it's a pretty cool idea.
  Reply
#4
No, I have no idea what the SABL files do or what they're used for.

> I noticed it's slow but I simply could not think of a better way.

Oh, there is. Wink Also there is a better method than pattern scanning for the FLAC header/signature. The SABS file actually stores an array of structs containing file size and file position.
[Image: 30xhrep.png]

A casual conversation between barata and I about Nukem.
  Reply
#5
Yes. that's what I was asking.
Where and how do I read it?
  Reply
#6
Code:
struct sabs_header_t
{
    char _0x0[20];
    int entry_count; // 0x14
    int start_offset; // 0x18
    char _0x1C[4];
    hash_block_t** hashes; // 0x20, relative. (not sure about this one)
    char _0x24[4];
    audio_entry_t** entries; // 0x28, relative.
};

struct hash_block_t
{
    char md5[16];
    char 0x10[16];
};

struct audio_entry_t
{
    char _0x00[4];
    int size; // 0x4
    int offset; // 0x8
    char _0x0C[8];
}; // size = 0x14

You can figure it out yourself.
[Image: 30xhrep.png]

A casual conversation between barata and I about Nukem.
  Reply
#7
Thanks, this is awesome!
  Reply
#8
Well, I failed, again. http://easycaptu.re/bEbW4
I'm not sure what I'm doing wrong but the header doesn't seem to match up with what you said either.
The only things that seem to be succeeding is the entry count and the offset (even though it says 0x8 instead of 0x800)
Idk, I've been trying for 4 hours now.
  Reply
#9
Don't use the MD5 hashes thing, it's incorrect, I'm still figuring that one out. As for it not working, it is a pointer to an array of hash_block_t and audio_entry_t. I forgot to add an extra * to show that, my bad.

Code:
fs.ReadBytes(20);
entrycount = fs.ReadInt32();
fs.ReadBytes(4); // start offset
fs.ReadBytes(4); // unknown
fs.ReadBytes(4); // hashes?
fs.ReadBytes(4); // unknown
int entryoffset = fs.ReadInt32();
fs.BaseStream.Seek(entryoffset, SeekOrigin.Begin);
for (int i = 0; i < entrycount; i++)
{
    fs.ReadBytes(4);
    SABSEntry sabs = new SABSEntry { Offset = fs.ReadInt32(), Size = fs.ReadInt32() };
    fs.ReadBytes(8);
    entries.Add(sabs);
}
[Image: 30xhrep.png]

A casual conversation between barata and I about Nukem.
  Reply
#10
Just came with a major breakthrough, and realised that the SABL files contain headerless WAV files, however they do not use the header format seen in Black Ops, I don't think they even have headers....

Anyway, I used mpl_nightclub.all.sabl as an example and found a water fountain sound stored as raw data encoded using 16-bit PCM signed, little endian, stereo and a sample rate of 44100KHz or 48000KHz (not sure which one, both sound the same).
[Image: 30xhrep.png]

A casual conversation between barata and I about Nukem.
  Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Help mod not reading .iwd files? hmann 4 4,165 10-13-2013, 20:14
Last Post: hmann
  i need help with the sabs files duckywson 2 3,037 08-31-2013, 19:46
Last Post: duckywson
  Help How do I open the admin menu for GeKKos QS Mod? conorr 1 2,845 08-15-2013, 13:52
Last Post: Yamato
  .FF Files tugay12 1 4,107 08-14-2013, 17:11
Last Post: DidUknowiPwn
Question Help Upgrade my point system from files to Database, help? EnVi Sweden Rocks 11 8,661 08-03-2013, 23:31
Last Post: EnVi Sweden Rocks
  Help GSC Reading Files?! Howl3r 11 7,532 07-30-2013, 04:00
Last Post: DidUknowiPwn
Question Help what program, open and edit plugins ??? lexa__33 1 2,642 06-24-2013, 09:36
Last Post: EnVi Sweden Rocks
Wink [Tutorial] Playing mp3,mid,etc... audio files on VB 2010! barata 2 17,450 05-07-2013, 08:03
Last Post: kmne68
Music unlimited breath scope+sound when walking aim LE3* 0 1,970 04-21-2013, 08:42
Last Post: LE3*
  ranked server files for mw3 with DLC odok 5 4,273 04-19-2013, 12:09
Last Post: Nekochan

Forum Jump:


Users browsing this thread: 1 Guest(s)