/// <summary> /// Constructs a stream that will accept PCM audio input, and automatically encode it to Opus and packetize it using Ogg, /// writing the output pages to an underlying stream (usually a file stream). /// You are allowed to change the encoding parameters mid-stream using the properties of the OpusEncoder; the only thing you /// cannot change is the sample rate and num# of channels. /// </summary> /// <param name="encoder">An opus encoder to use for output</param> /// <param name="outputStream">A base stream to accept the encoded ogg file output</param> /// <param name="fileTags">(optional) A set of tags to include in the encoded file</param> /// <param name="inputSampleRate">The actual real sample rate of your input data (NOT the encoder's sample rate). /// The opus encoder usually requires 48Khz input, but most MP3s and such will give you 44.1Khz. To get the /// sample rates to line up properly in this case, set the encoder to 48000 and pass inputSampleRate = 44100, /// and the write stream will perform resampling for you automatically (Note that resampling will slow down /// the encoding).</param> public OpusOggWriteStream(OpusEncoder encoder, Stream outputStream, OpusTags fileTags = null, int inputSampleRate = 0, int logicalStreamId = 0) { _encoder = encoder; if (_encoder.UseDTX) { throw new ArgumentException("DTX is not currently supported in Ogg streams"); } _inputSampleRate = inputSampleRate; if (_inputSampleRate == 0) { _inputSampleRate = _encoder.SampleRate; } if (logicalStreamId == 0) { logicalStreamId = new Random().Next(); } _logicalStreamId = logicalStreamId; _encoderSampleRate = encoder.SampleRate; _inputChannels = encoder.NumChannels; _outputStream = outputStream; _opusFrameIndex = 0; _granulePosition = 0; _opusFrameSamples = (int)((long)_encoderSampleRate * FRAME_SIZE_MS / 1000); _opusFrame = new short[_opusFrameSamples * _inputChannels]; _crc = new Crc(); _resampler = SpeexResampler.Create(_inputChannels, _inputSampleRate, _encoderSampleRate, 5); BeginNewPage(); WriteOpusHeadPage(); WriteOpusTagsPage(fileTags); }
/// <summary> /// Looks for the next opus data packet in the Ogg stream and queues it up. /// If the end of stream has been reached, this does nothing. /// </summary> private void QueueNextPacket() { if (_endOfStream) { return; } DataPacket packet = _packetProvider.GetNextPacket(); if (packet == null || packet.IsEndOfStream) { _endOfStream = true; _nextDataPacket = null; return; } byte[] buf = new byte[packet.Length]; packet.Read(buf, 0, packet.Length); packet.Done(); if (buf.Length > 8 && "OpusHead".Equals(Encoding.UTF8.GetString(buf, 0, 8))) { _header = OpusHeader.ParsePacket(buf, buf.Length); QueueNextPacket(); } else if (buf.Length > 8 && "OpusTags".Equals(Encoding.UTF8.GetString(buf, 0, 8))) { _tags = OpusTags.ParsePacket(buf, buf.Length); QueueNextPacket(); } else { _nextDataPacket = buf; } }
internal static OpusTags ParsePacket(byte[] packet, int packetLength) { if (packetLength < 8) { return(null); } if (!"OpusTags".Equals(Encoding.UTF8.GetString(packet, 0, 8))) { return(null); } OpusTags returnVal = new OpusTags(); int cursor = 8; int nextFieldLength = BitConverter.ToInt32(packet, cursor); cursor += 4; if (nextFieldLength > 0) { returnVal._comment = Encoding.UTF8.GetString(packet, cursor, nextFieldLength); cursor += nextFieldLength; } int numTags = BitConverter.ToInt32(packet, cursor); cursor += 4; for (int c = 0; c < numTags; c++) { nextFieldLength = BitConverter.ToInt32(packet, cursor); cursor += 4; if (nextFieldLength > 0) { string tag = Encoding.UTF8.GetString(packet, cursor, nextFieldLength); cursor += nextFieldLength; int eq = tag.IndexOf('='); if (eq > 0) { string key = tag.Substring(0, eq); string val = tag.Substring(eq + 1); returnVal._fields[key] = val; } } } return(returnVal); }
/// <summary> /// Looks for the next opus data packet in the Ogg stream and queues it up. /// If the end of stream has been reached, this does nothing. /// </summary> private void QueueNextPacket() { if (_endOfStream) { return; } DataPacket packet = _packetProvider.GetNextPacket(); if (packet == null) { _endOfStream = true; _nextDataPacket = null; return; } PageGranulePosition = packet.PageGranulePosition; PagePosition = packet.PageSequenceNumber; byte[] buf = new byte[packet.Length]; packet.Read(buf, 0, packet.Length); packet.Done(); if (buf.Length > 8 && "OpusHead".Equals(Encoding.UTF8.GetString(buf, 0, 8))) { QueueNextPacket(); } else if (buf.Length > 8 && "OpusTags".Equals(Encoding.UTF8.GetString(buf, 0, 8))) { Tags = OpusTags.ParsePacket(buf, buf.Length); QueueNextPacket(); } else { _nextDataPacket = buf; } }
/// <summary> /// Writes an Ogg page for the OpusTags, given an input tag set /// </summary> /// <param name="tags"></param> private void WriteOpusTagsPage(OpusTags tags = null) { if (tags == null) { tags = new OpusTags(); } if (string.IsNullOrEmpty(tags.Comment)) { tags.Comment = CodecHelpers.GetVersionString(); } if (_payloadIndex != 0) { throw new InvalidOperationException("Must begin writing OpusTags on a new page!"); } // BUGBUG: Very long tags can overflow the page and corrupt the stream _payloadIndex += WriteValueToByteBuffer("OpusTags", _currentPayload, _payloadIndex); // write comment int stringLength = WriteValueToByteBuffer(tags.Comment, _currentPayload, _payloadIndex + 4); _payloadIndex += WriteValueToByteBuffer(stringLength, _currentPayload, _payloadIndex); _payloadIndex += stringLength; // capture the location of the tag count field to fill in later int numTagsIndex = _payloadIndex; _payloadIndex += 4; // write each tag. skipping empty or invalid ones int tagsWritten = 0; foreach (var kvp in tags.Fields) { if (string.IsNullOrEmpty(kvp.Key) || string.IsNullOrEmpty(kvp.Value)) { continue; } string tag = kvp.Key + "=" + kvp.Value; stringLength = WriteValueToByteBuffer(tag, _currentPayload, _payloadIndex + 4); _payloadIndex += WriteValueToByteBuffer(stringLength, _currentPayload, _payloadIndex); _payloadIndex += stringLength; tagsWritten++; } // Write actual tag count WriteValueToByteBuffer(tagsWritten, _currentPayload, numTagsIndex); // Write segment data, ensuring we can handle tags longer than 255 bytes int tagsSegmentSize = _payloadIndex; while (tagsSegmentSize >= 255) { _currentHeader[_headerIndex++] = 255; _lacingTableCount++; tagsSegmentSize -= 255; } _currentHeader[_headerIndex++] = (byte)tagsSegmentSize; _lacingTableCount++; FinalizePage(); }
/// <summary> /// Constructs a stream that will accept PCM audio input, and automatically encode it to Opus and packetize it using Ogg, /// writing the output pages to an underlying stream (usually a file stream). /// You are allowed to change the encoding parameters mid-stream using the properties of the OpusEncoder; the only thing you /// cannot change is the sample rate and num# of channels. /// </summary> /// <param name="outputStream">A base stream to accept the encoded ogg file output</param> /// <param name="sampleRate"></param> /// <param name="channelCount"></param> /// <param name="preSkip"></param> /// <param name="sampleCount"></param> /// <param name="fileTags">(optional) A set of tags to include in the encoded file</param> /// <param name="inputSampleRate">The actual real sample rate of your input data (NOT the encoder's sample rate). /// The opus encoder usually requires 48Khz input, but most MP3s and such will give you 44.1Khz. To get the /// sample rates to line up properly in this case, set the encoder to 48000 and pass inputSampleRate = 44100, /// and the write stream will perform resampling for you automatically (Note that resampling will slow down /// the encoding).</param> public OpusOggWriteStream(Stream outputStream, int sampleRate, int channelCount, int preSkip, int sampleCount = -1, OpusTags fileTags = null, int inputSampleRate = 0) { _inputSampleRate = inputSampleRate; if (_inputSampleRate == 0) { _inputSampleRate = sampleRate; } _logicalStreamId = new Random().Next(); _encoderSampleRate = sampleRate; _inputChannels = channelCount; _outputStream = outputStream; //_opusFrameIndex = 0; _granulePosition = 0; _opusFrameSamples = (int)((long)_encoderSampleRate * FRAME_SIZE_MS / 1000); _opusFrame = new short[_opusFrameSamples * _inputChannels]; _crc = new Crc(); _resampler = SpeexResampler.Create(_inputChannels, _inputSampleRate, _encoderSampleRate, 5); _preSkipSamples = preSkip; _sampleCount = sampleCount == -1 ? int.MaxValue : preSkip + sampleCount; BeginNewPage(); WriteOpusHeadPage(); WriteOpusTagsPage(fileTags); }