context: // ****************************************************************************
  // 
  // FLV Extract
  // Copyright (C) 2006-2011  J.D. Purcell (moitah@yahoo.com)
  // 
  // This program is free software; you can redistribute it and/or modify
  // it under the terms of the GNU General Public License as published by
  // the Free Software Foundation; either version 2 of the License, or
  // (at your option) any later version.
  // 
  // This program is distributed in the hope that it will be useful,
  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  // GNU General Public License for more details.
  // 
  // You should have received a copy of the GNU General Public License
  // along with this program; if not, write to the Free Software
  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  // 
  // ****************************************************************************
  
  using System;
  using System.Collections.Generic;
  using System.IO;
  using System.Text;
  
  namespace JDP {
  	interface IAudioWriter {
  		void WriteChunk(byte[] chunk, uint timeStamp);
  		void Finish();
  		string Path { get; }
  	}
  
  	interface IVideoWriter {
  		void WriteChunk(byte[] chunk, uint timeStamp, int frameType);
  		void Finish(FractionUInt32 averageFrameRate);
  		string Path { get; }
  	}
  
  	public delegate bool OverwriteDelegate(string destPath);
  
  	public class FLVFile : IDisposable {
  		static readonly string[] _outputExtensions = new string[] { ".avi", ".mp3", ".264", ".aac", ".spx", ".txt" };
  
  		string _inputPath, _outputDirectory, _outputPathBase;
  		OverwriteDelegate _overwrite;
  		FileStream _fs;
  		long _fileOffset, _fileLength;
  		IAudioWriter _audioWriter;
  		IVideoWriter _videoWriter;
  		TimeCodeWriter _timeCodeWriter;
  		List<uint> _videoTimeStamps;
  		bool _extractAudio, _extractVideo, _extractTimeCodes;
  		bool _extractedAudio, _extractedVideo, _extractedTimeCodes;
  		FractionUInt32? _averageFrameRate, _trueFrameRate;
  		List<string> _warnings;
  
  		public FLVFile(string path) {
  			_inputPath = path;
  			_outputDirectory = Path.GetDirectoryName(path);
  			_warnings = new List<string>();
  			_fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 65536);
  			_fileOffset = 0;
  			_fileLength = _fs.Length;
  		}
  
  		public void Dispose() {
  			if (_fs != null) {
  				_fs.Close();
  				_fs = null;
  			}
  			CloseOutput(null, true);
  		}
  
  		public void Close() {
  			Dispose();
  		}
  
  		public string OutputDirectory {
  			get { return _outputDirectory; }
  			set { _outputDirectory = value; }
  		}
  
  		public FractionUInt32? AverageFrameRate {
  			get { return _averageFrameRate; }
  		}
  
  		public FractionUInt32? TrueFrameRate {
  			get { return _trueFrameRate; }
  		}
  
  		public string[] Warnings {
  			get { return _warnings.ToArray(); }
  		}
  
  		public bool ExtractedAudio {
  			get { return _extractedAudio; }
  		}
  
  		public bool ExtractedVideo {
  			get { return _extractedVideo; }
  		}
  
  		public bool ExtractedTimeCodes {
  			get { return _extractedTimeCodes; }
  		}
  
  		public void ExtractStreams(bool extractAudio, bool extractVideo, bool extractTimeCodes, OverwriteDelegate overwrite) {
  			uint dataOffset, flags, prevTagSize;
  
  			_outputPathBase = Path.Combine(_outputDirectory, Path.GetFileNameWithoutExtension(_inputPath));
  			_overwrite = overwrite;
  			_extractAudio = extractAudio;
  			_extractVideo = extractVideo;
  			_extractTimeCodes = extractTimeCodes;
  			_videoTimeStamps = new List<uint>();
  
  			Seek(0);
  			if (_fileLength < 4 || ReadUInt32() != 0x464C5601) {
  				if (_fileLength >= 8 && ReadUInt32() == 0x66747970) {
  					throw new Exception("This is a MP4 file. YAMB or MP4Box can be used to extract streams.");
  				}
  				else {
  					throw new Exception("This isn't a FLV file.");
  				}
  			}
  
  			if (Array.IndexOf(_outputExtensions, Path.GetExtension(_inputPath).ToLowerInvariant()) != -1) {
  				// Can't have the same extension as files we output
  				throw new Exception("Please change the extension of this FLV file.");
  			}
  
  			if (!Directory.Exists(_outputDirectory)) {
  				throw new Exception("Output directory doesn't exist.");
  			}
  
  			flags = ReadUInt8();
  			dataOffset = ReadUInt32();
  
  			Seek(dataOffset);
  
  			prevTagSize = ReadUInt32();
  			while (_fileOffset < _fileLength) {
  				if (!ReadTag()) break;
  				if ((_fileLength - _fileOffset) < 4) break;
  				prevTagSize = ReadUInt32();
  			}
  
  			_averageFrameRate = CalculateAverageFrameRate();
  			_trueFrameRate = CalculateTrueFrameRate();
  
  			CloseOutput(_averageFrameRate, false);
  		}
  
  		private void CloseOutput(FractionUInt32? averageFrameRate, bool disposing) {
  			if (_videoWriter != null) {
  				_videoWriter.Finish(averageFrameRate ?? new FractionUInt32(25, 1));
  				if (disposing && (_videoWriter.Path != null)) {
  					try { File.Delete(_videoWriter.Path); }
  					catch { }
  				}
  				_videoWriter = null;
  			}
  			if (_audioWriter != null) {
  				_audioWriter.Finish();
  				if (disposing && (_audioWriter.Path != null)) {
  					try { File.Delete(_audioWriter.Path); }
  					catch { }
  				}
  				_audioWriter = null;
  			}
  			if (_timeCodeWriter != null) {
  				_timeCodeWriter.Finish();
  				if (disposing && (_timeCodeWriter.Path != null)) {
  					try { File.Delete(_timeCodeWriter.Path); }
  					catch { }
  				}
  				_timeCodeWriter = null;
  			}
  		}
  
  		private bool ReadTag() {
  			uint tagType, dataSize, timeStamp, streamID, mediaInfo;
  			byte[] data;
  
  			if ((_fileLength - _fileOffset) < 11) {
  				return false;
  			}
  
  			// Read tag header
  			tagType = ReadUInt8();
  			dataSize = ReadUInt24();
  			timeStamp = ReadUInt24();
  			timeStamp |= ReadUInt8() << 24;
  			streamID = ReadUInt24();
  
  			// Read tag data
  			if (dataSize == 0) {
  				return true;
  			}
  			if ((_fileLength - _fileOffset) < dataSize) {
  				return false;
  			}
  			mediaInfo = ReadUInt8();
  			dataSize -= 1;
  			data = ReadBytes((int)dataSize);
  
  			if (tagType == 0x8) {  // Audio
  				if (_audioWriter == null) {
  					_audioWriter = _extractAudio ? GetAudioWriter(mediaInfo) : new DummyAudioWriter();
  					_extractedAudio = !(_audioWriter is DummyAudioWriter);
  				}
  				_audioWriter.WriteChunk(data, timeStamp);
  			}
  			else if ((tagType == 0x9) && ((mediaInfo >> 4) != 5)) { // Video
  				if (_videoWriter == null) {
  					_videoWriter = _extractVideo ? GetVideoWriter(mediaInfo) : new DummyVideoWriter();
  					_extractedVideo = !(_videoWriter is DummyVideoWriter);
  				}
  				if (_timeCodeWriter == null) {
  					string path = _outputPathBase + ".txt";
  					_timeCodeWriter = new TimeCodeWriter((_extractTimeCodes && CanWriteTo(path)) ? path : null);
  					_extractedTimeCodes = _extractTimeCodes;
  				}
  				_videoTimeStamps.Add(timeStamp);
  				_videoWriter.WriteChunk(data, timeStamp, (int)((mediaInfo & 0xF0) >> 4));
  				_timeCodeWriter.Write(timeStamp);
  			}
  
  			return true;
  		}
  
  		private IAudioWriter GetAudioWriter(uint mediaInfo) {
  			uint format = mediaInfo >> 4;
  			uint rate = (mediaInfo >> 2) & 0x3;
  			uint bits = (mediaInfo >> 1) & 0x1;
  			uint chans = mediaInfo & 0x1;
  			string path;
  
  			if ((format == 2) || (format == 14)) { // MP3
  				path = _outputPathBase + ".mp3";
  				if (!CanWriteTo(path)) return new DummyAudioWriter();
  				return new MP3Writer(path, _warnings);
  			}
  			else if ((format == 0) || (format == 3)) { // PCM
  				int sampleRate = 0;
  				switch (rate) {
  					case 0: sampleRate =  5512; break;
  					case 1: sampleRate = 11025; break;
  					case 2: sampleRate = 22050; break;
  					case 3: sampleRate = 44100; break;
  				}
  				path = _outputPathBase + ".wav";
  				if (!CanWriteTo(path)) return new DummyAudioWriter();
  				if (format == 0) {
  					_warnings.Add("PCM byte order unspecified, assuming little endian.");
  				}
  				return new WAVWriter(path, (bits == 1) ? 16 : 8,
  					(chans == 1) ? 2 : 1, sampleRate);
  			}
  			else if (format == 10) { // AAC
  				path = _outputPathBase + ".aac";
  				if (!CanWriteTo(path)) return new DummyAudioWriter();
  				return new AACWriter(path);
  			}
  			else if (format == 11) { // Speex
  				path = _outputPathBase + ".spx";
  				if (!CanWriteTo(path)) return new DummyAudioWriter();
  				return new SpeexWriter(path, (int)(_fileLength & 0xFFFFFFFF));
  			}
  			else {
  				string typeStr;
  
  				if (format == 1)
  					typeStr = "ADPCM";
  				else if ((format == 4) || (format == 5) || (format == 6))
  					typeStr = "Nellymoser";
  				else
  					typeStr = "format=" + format.ToString();
  
  				_warnings.Add("Unable to extract audio (" + typeStr + " is unsupported).");
  
  				return new DummyAudioWriter();
  			}
  		}
  
  		private IVideoWriter GetVideoWriter(uint mediaInfo) {
  			uint codecID = mediaInfo & 0x0F;
  			string path;
  
  			if ((codecID == 2) || (codecID == 4) || (codecID == 5)) {
  				path = _outputPathBase + ".avi";
  				if (!CanWriteTo(path)) return new DummyVideoWriter();
  				return new AVIWriter(path, (int)codecID, _warnings);
  			}
  			else if (codecID == 7) {
  				path = _outputPathBase + ".264";
  				if (!CanWriteTo(path)) return new DummyVideoWriter();
  				return new RawH264Writer(path);
  			}
  			else {
  				string typeStr;
  
  				if (codecID == 3)
  					typeStr = "Screen";
  				else if (codecID == 6)
  					typeStr = "Screen2";
  				else
  					typeStr = "codecID=" + codecID.ToString();
  
  				_warnings.Add("Unable to extract video (" + typeStr + " is unsupported).");
  
  				return new DummyVideoWriter();
  			}
  		}
  
  		private bool CanWriteTo(string path) {
  			if (File.Exists(path) && (_overwrite != null)) {
  				return _overwrite(path);
  			}
  			return true;
  		}
  
  		private FractionUInt32? CalculateAverageFrameRate() {
  			FractionUInt32 frameRate;
  			int frameCount = _videoTimeStamps.Count;
  
  			if (frameCount > 1) {
  				frameRate.N = (uint)(frameCount - 1) * 1000;
  				frameRate.D = _videoTimeStamps[frameCount - 1] - _videoTimeStamps[0];
  				frameRate.Reduce();
  				return frameRate;
  			}
  			else {
  				return null;
  			}
  		}
  
  		private FractionUInt32? CalculateTrueFrameRate() {
  			FractionUInt32 frameRate;
  			Dictionary<uint, uint> deltaCount = new Dictionary<uint, uint>();
  			int i, threshold;
  			uint delta, count, minDelta;
  
  			// Calculate the distance between the timestamps, count how many times each delta appears
  			for (i = 1; i < _videoTimeStamps.Count; i++) {
  				int deltaS = (int)((long)_videoTimeStamps[i] - (long)_videoTimeStamps[i - 1]);
  
  				if (deltaS <= 0) continue;
  				delta = (uint)deltaS;
  
  				if (deltaCount.ContainsKey(delta)) {
  					deltaCount[delta] += 1;
  				}
  				else {
  					deltaCount.Add(delta, 1);
  				}
  			}
  
  			threshold = _videoTimeStamps.Count / 10;
  			minDelta = UInt32.MaxValue;
  
  			// Find the smallest delta that made up at least 10% of the frames (grouping in delta+1
  			// because of rounding, e.g. a NTSC video will have deltas of 33 and 34 ms)
  			foreach (KeyValuePair<uint, uint> deltaItem in deltaCount) {
  				delta = deltaItem.Key;
  				count = deltaItem.Value;
  
  				if (deltaCount.ContainsKey(delta + 1)) {
  					count += deltaCount[delta + 1];
  				}
  
  				if ((count >= threshold) && (delta < minDelta)) {
  					minDelta = delta;
  				}
  			}
  
  			// Calculate the frame rate based on the smallest delta, and delta+1 if present
  			if (minDelta != UInt32.MaxValue) {
  				uint totalTime, totalFrames;
  
  				count = deltaCount[minDelta];
  				totalTime = minDelta * count;
  				totalFrames = count;
  
  				if (deltaCount.ContainsKey(minDelta + 1)) {
  					count = deltaCount[minDelta + 1];
  					totalTime += (minDelta + 1) * count;
  					totalFrames += count;
  				}
  
  				if (totalTime != 0) {
  					frameRate.N = totalFrames * 1000;
  					frameRate.D = totalTime;
  					frameRate.Reduce();
  					return frameRate;
  				}
  			}
  
  			// Unable to calculate frame rate
  			return null;
  		}
  
  		private void Seek(long offset) {
  			_fs.Seek(offset, SeekOrigin.Begin);
  			_fileOffset = offset;
  		}
  
  		private uint ReadUInt8() {
  			_fileOffset += 1;
  			return (uint)_fs.ReadByte();
  		}
  
  		private uint ReadUInt24() {
  			byte[] x = new byte[4];
  			_fs.Read(x, 1, 3);
  			_fileOffset += 3;
  			return BitConverterBE.ToUInt32(x, 0);
  		}
  
  		private uint ReadUInt32() {
  			byte[] x = new byte[4];
  			_fs.Read(x, 0, 4);
  			_fileOffset += 4;
  			return BitConverterBE.ToUInt32(x, 0);
  		}
  
  		private byte[] ReadBytes(int length) {
  			byte[] buff = new byte[length];
  			_fs.Read(buff, 0, length);
  			_fileOffset += length;
  			return buff;
  		}
  	}
  
  	class DummyAudioWriter : IAudioWriter {
  		public DummyAudioWriter() {
  		}
  
  		public void WriteChunk(byte[] chunk, uint timeStamp) {
  		}
  
  		public void Finish() {
  		}
  
  		public string Path {
  			get {
  				return null;
  			}
  		}
  	}
  
  	class DummyVideoWriter : IVideoWriter {
  		public DummyVideoWriter() {
  		}
  
  		public void WriteChunk(byte[] chunk, uint timeStamp, int frameType) {
  		}
  
  		public void Finish(FractionUInt32 averageFrameRate) {
  		}
  
  		public string Path {
  			get {
  				return null;
  			}
  		}
  	}
  
  	class MP3Writer : IAudioWriter {
  		string _path;
  		FileStream _fs;
  		List<string> _warnings;
  		List<byte[]> _chunkBuffer;
  		List<uint> _frameOffsets;
  		uint _totalFrameLength;
  		bool _isVBR;
  		bool _delayWrite;
  		bool _hasVBRHeader;
  		bool _writeVBRHeader;
  		int _firstBitRate;
  		int _mpegVersion;
  		int _sampleRate;
  		int _channelMode;
  		uint _firstFrameHeader;
  
  		public MP3Writer(string path, List<string> warnings) {
  			_path = path;
  			_fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, 65536);
  			_warnings = warnings;
  			_chunkBuffer = new List<byte[]>();
  			_frameOffsets = new List<uint>();
  			_delayWrite = true;
  		}
  
  		public void WriteChunk(byte[] chunk, uint timeStamp) {
  			_chunkBuffer.Add(chunk);
  			ParseMP3Frames(chunk);
  			if (_delayWrite && _totalFrameLength >= 65536) {
  				_delayWrite = false;
  			}
  			if (!_delayWrite) {
  				Flush();
  			}
  		}
  
  		public void Finish() {
  			Flush();
  			if (_writeVBRHeader) {
  				_fs.Seek(0, SeekOrigin.Begin);
  				WriteVBRHeader(false);
  			}
  			_fs.Close();
  		}
  
  		public string Path {
  			get {
  				return _path;
  			}
  		}
  
  		private void Flush() {
  			foreach (byte[] chunk in _chunkBuffer) {
  				_fs.Write(chunk, 0, chunk.Length);
  			}
  			_chunkBuffer.Clear();
  		}
  
  		private void ParseMP3Frames(byte[] buff) {
  			int[] MPEG1BitRate = new int[] { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 };
  			int[] MPEG2XBitRate = new int[] { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 };
  			int[] MPEG1SampleRate = new int[] { 44100, 48000, 32000, 0 };
  			int[] MPEG20SampleRate = new int[] { 22050, 24000, 16000, 0 };
  			int[] MPEG25SampleRate = new int[] { 11025, 12000, 8000, 0 };
  
  			int offset = 0;
  			int length = buff.Length;
  
  			while (length >= 4) {
  				ulong header;
  				int mpegVersion, layer, bitRate, sampleRate, padding, channelMode;
  				int frameLen;
  
  				header = (ulong)BitConverterBE.ToUInt32(buff, offset) << 32;
  				if (BitHelper.Read(ref header, 11) != 0x7FF) {
  					break;
  				}
  				mpegVersion = BitHelper.Read(ref header, 2);
  				layer = BitHelper.Read(ref header, 2);
  				BitHelper.Read(ref header, 1);
  				bitRate = BitHelper.Read(ref header, 4);
  				sampleRate = BitHelper.Read(ref header, 2);
  				padding = BitHelper.Read(ref header, 1);
  				BitHelper.Read(ref header, 1);
  				channelMode = BitHelper.Read(ref header, 2);
  
  				if ((mpegVersion == 1) || (layer != 1) || (bitRate == 0) || (bitRate == 15) || (sampleRate == 3)) {
  					break;
  				}
  
  				bitRate = ((mpegVersion == 3) ? MPEG1BitRate[bitRate] : MPEG2XBitRate[bitRate]) * 1000;
  
  				if (mpegVersion == 3)
  					sampleRate = MPEG1SampleRate[sampleRate];
  				else if (mpegVersion == 2)
  					sampleRate = MPEG20SampleRate[sampleRate];
  				else
  					sampleRate = MPEG25SampleRate[sampleRate];
  
  				frameLen = GetFrameLength(mpegVersion, bitRate, sampleRate, padding);
  				if (frameLen > length) {
  					break;
  				}
  
  				bool isVBRHeaderFrame = false;
  				if (_frameOffsets.Count == 0) {
  					// Check for an existing VBR header just to be safe (I haven't seen any in FLVs)
  					int o = offset + GetFrameDataOffset(mpegVersion, channelMode);
  					if (BitConverterBE.ToUInt32(buff, o) == 0x58696E67) { // "Xing"
  						isVBRHeaderFrame = true;
  						_delayWrite = false;
  						_hasVBRHeader = true;
  					}
  				}
  
  				if (isVBRHeaderFrame) { }
  				else if (_firstBitRate == 0) {
  					_firstBitRate = bitRate;
  					_mpegVersion = mpegVersion;
  					_sampleRate = sampleRate;
  					_channelMode = channelMode;
  					_firstFrameHeader = BitConverterBE.ToUInt32(buff, offset);
  				}
  				else if (!_isVBR && (bitRate != _firstBitRate)) {
  					_isVBR = true;
  					if (_hasVBRHeader) { }
  					else if (_delayWrite) {
  						WriteVBRHeader(true);
  						_writeVBRHeader = true;
  						_delayWrite = false;
  					}
  					else {
  						_warnings.Add("Detected VBR too late, cannot add VBR header.");
  					}
  				}
  
  				_frameOffsets.Add(_totalFrameLength + (uint)offset);
  
  				offset += frameLen;
  				length -= frameLen;
  			}
  
  			_totalFrameLength += (uint)buff.Length;
  		}
  
  		private void WriteVBRHeader(bool isPlaceholder) {
  			byte[] buff = new byte[GetFrameLength(_mpegVersion, 64000, _sampleRate, 0)];
  			if (!isPlaceholder) {
  				uint header = _firstFrameHeader;
  				int dataOffset = GetFrameDataOffset(_mpegVersion, _channelMode);
  				header &= 0xFFFF0DFF; // Clear bitrate and padding fields
  				header |= 0x00010000; // Set protection bit (indicates that CRC is NOT present)
  				header |= (uint)((_mpegVersion == 3) ? 5 : 8) << 12; // 64 kbit/sec
  				General.CopyBytes(buff, 0, BitConverterBE.GetBytes(header));
  				General.CopyBytes(buff, dataOffset, BitConverterBE.GetBytes((uint)0x58696E67)); // "Xing"
  				General.CopyBytes(buff, dataOffset + 4, BitConverterBE.GetBytes((uint)0x7)); // Flags
  				General.CopyBytes(buff, dataOffset + 8, BitConverterBE.GetBytes((uint)_frameOffsets.Count)); // Frame count
  				General.CopyBytes(buff, dataOffset + 12, BitConverterBE.GetBytes((uint)_totalFrameLength)); // File length
  				for (int i = 0; i < 100; i++) {
  					int frameIndex = (int)((i / 100.0) * _frameOffsets.Count);
  					buff[dataOffset + 16 + i] = (byte)((_frameOffsets[frameIndex] / (double)_totalFrameLength) * 256.0);
  				}
  			}
  			_fs.Write(buff, 0, buff.Length);
  		}
  
  		private int GetFrameLength(int mpegVersion, int bitRate, int sampleRate, int padding) {
  			return ((mpegVersion == 3) ? 144 : 72) * bitRate / sampleRate + padding;
  		}
  
  		private int GetFrameDataOffset(int mpegVersion, int channelMode) {
  			return 4 + ((mpegVersion == 3) ?
  				((channelMode == 3) ? 17 : 32) :
  				((channelMode == 3) ?  9 : 17));
  		}
  	}
  
  	class SpeexWriter : IAudioWriter {
  		const string _vendorString = "FLV Extract";
  		const uint _sampleRate = 16000;
  		const uint _msPerFrame = 20;
  		const uint _samplesPerFrame = _sampleRate / (1000 / _msPerFrame);
  		const int _targetPageDataSize = 4096;
  
  		string _path;
  		FileStream _fs;
  		int _serialNumber;
  		List<OggPacket> _packetList;
  		int _packetListDataSize;
  		byte[] _pageBuff;
  		int _pageBuffOffset;
  		uint _pageSequenceNumber;
  		ulong _granulePosition;
  
  		public SpeexWriter(string path, int serialNumber) {
  			_path = path;
  			_serialNumber = serialNumber;
  			_fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, 65536);
  			_fs.Seek((28 + 80) + (28 + 8 + _vendorString.Length), SeekOrigin.Begin); // Speex header + Vorbis comment
  			_packetList = new List<OggPacket>();
  			_packetListDataSize = 0;
  			_pageBuff = new byte[27 + 255 + _targetPageDataSize + 254]; // Header + max segment table + target data size + extra segment
  			_pageBuffOffset = 0;
  			_pageSequenceNumber = 2; // First audio packet
  			_granulePosition = 0;
  		}
  
  		public void WriteChunk(byte[] chunk, uint timeStamp) {
  			int[] subModeSizes = new int[] { 0, 43, 119, 160, 220, 300, 364, 492, 79 };
  			int[] wideBandSizes = new int[] { 0, 36, 112, 192, 352 };
  			int[] inBandSignalSizes = new int[] { 1, 1, 4, 4, 4, 4, 4, 4, 8, 8, 16, 16, 32, 32, 64, 64 };
  			int frameStart = -1;
  			int frameEnd = 0;
  			int offset = 0;
  			int length = chunk.Length * 8;
  			int x;
  
  			while (length - offset >= 5) {
  				x = BitHelper.Read(chunk, ref offset, 1);
  				if (x != 0) {
  					// wideband frame
  					x = BitHelper.Read(chunk, ref offset, 3);
  					if (x < 1 || x > 4) goto Error;
  					offset += wideBandSizes[x] - 4;
  				}
  				else {
  					x = BitHelper.Read(chunk, ref offset, 4);
  					if (x >= 1 && x <= 8) {
  						// narrowband frame
  						if (frameStart != -1) {
  							WriteFramePacket(chunk, frameStart, frameEnd);
  						}
  						frameStart = frameEnd;
  						offset += subModeSizes[x] - 5;
  					}
  					else if (x == 15) {
  						// terminator
  						break;
  					}
  					else if (x == 14) {
  						// in-band signal
  						if (length - offset < 4) goto Error;
  						x = BitHelper.Read(chunk, ref offset, 4);
  						offset += inBandSignalSizes[x];
  					}
  					else if (x == 13) {
  						// custom in-band signal
  						if (length - offset < 5) goto Error;
  						x = BitHelper.Read(chunk, ref offset, 5);
  						offset += x * 8;
  					}
  					else goto Error;
  				}
  				frameEnd = offset;
  			}
  			if (offset > length) goto Error;
  
  			if (frameStart != -1) {
  				WriteFramePacket(chunk, frameStart, frameEnd);
  			}
  
  			return;
  
  		Error:
  			throw new Exception("Invalid Speex data.");
  		}
  
  		public void Finish() {
  			WritePage();
  			FlushPage(true);
  			_fs.Seek(0, SeekOrigin.Begin);
  			_pageSequenceNumber = 0;
  			_granulePosition = 0;
  			WriteSpeexHeaderPacket();
  			WriteVorbisCommentPacket();
  			FlushPage(false);
  			_fs.Close();
  		}
  
  		public string Path {
  			get {
  				return _path;
  			}
  		}
  
  		private void WriteFramePacket(byte[] data, int startBit, int endBit) {
  			int lengthBits = endBit - startBit;
  			byte[] frame = BitHelper.CopyBlock(data, startBit, lengthBits);
  			if (lengthBits % 8 != 0) {
  				frame[frame.Length - 1] |= (byte)(0xFF >> ((lengthBits % 8) + 1)); // padding
  			}
  			AddPacket(frame, _samplesPerFrame, true);
  		}
  
  		private void WriteSpeexHeaderPacket() {
  			byte[] data = new byte[80];
  			General.CopyBytes(data, 0, Encoding.ASCII.GetBytes("Speex   ")); // speex_string
  			General.CopyBytes(data, 8, Encoding.ASCII.GetBytes("unknown")); // speex_version
  			data[28] = 1; // speex_version_id
  			data[32] = 80; // header_size
  			General.CopyBytes(data, 36, BitConverterLE.GetBytes((uint)_sampleRate)); // rate
  			data[40] = 1; // mode (e.g. narrowband, wideband)
  			data[44] = 4; // mode_bitstream_version
  			data[48] = 1; // nb_channels
  			General.CopyBytes(data, 52, BitConverterLE.GetBytes(unchecked((uint)-1))); // bitrate
  			General.CopyBytes(data, 56, BitConverterLE.GetBytes((uint)_samplesPerFrame)); // frame_size
  			data[60] = 0; // vbr
  			data[64] = 1; // frames_per_packet
  			AddPacket(data, 0, false);
  		}
  
  		private void WriteVorbisCommentPacket() {
  			byte[] vendorStringBytes = Encoding.ASCII.GetBytes(_vendorString);
  			byte[] data = new byte[8 + vendorStringBytes.Length];
  			data[0] = (byte)vendorStringBytes.Length;
  			General.CopyBytes(data, 4, vendorStringBytes);
  			AddPacket(data, 0, false);
  		}
  
  		private void AddPacket(byte[] data, uint sampleLength, bool delayWrite) {
  			OggPacket packet = new OggPacket();
  			if (data.Length >= 255) {
  				throw new Exception("Packet exceeds maximum size.");
  			}
  			_granulePosition += sampleLength;
  			packet.Data = data;
  			packet.GranulePosition = _granulePosition;
  			_packetList.Add(packet);
  			_packetListDataSize += data.Length;
  			if (!delayWrite || (_packetListDataSize >= _targetPageDataSize) || (_packetList.Count == 255)) {
  				WritePage();
  			}
  		}
  
  		private void WritePage() {
  			if (_packetList.Count == 0) return;
  			FlushPage(false);
  			WriteToPage(BitConverterBE.GetBytes(0x4F676753U), 0, 4); // "OggS"
  			WriteToPage((byte)0); // Stream structure version
  			WriteToPage((byte)((_pageSequenceNumber == 0) ? 0x02 : 0)); // Page flags
  			WriteToPage((ulong)_packetList[_packetList.Count - 1].GranulePosition); // Position in samples
  			WriteToPage((uint)_serialNumber); // Stream serial number
  			WriteToPage((uint)_pageSequenceNumber); // Page sequence number
  			WriteToPage((uint)0); // Checksum
  			WriteToPage((byte)_packetList.Count); // Page segment count
  			foreach (OggPacket packet in _packetList) {
  				WriteToPage((byte)packet.Data.Length);
  			}
  			foreach (OggPacket packet in _packetList) {
  				WriteToPage(packet.Data, 0, packet.Data.Length);
  			}
  			_packetList.Clear();
  			_packetListDataSize = 0;
  			_pageSequenceNumber++;
  		}
  
  		private void FlushPage(bool isLastPage) {
  			if (_pageBuffOffset == 0) return;
  
gt: 			if (isLastPage) _pageBuff[5] |= 0x04;