/// <summary> /// Writes a buffer of PCM audio samples to the encoder and packetizer. Runs Opus encoding and potentially outputs one or more pages to the underlying Ogg stream. /// You can write any non-zero number of samples that you want here; there are no restrictions on length or packet boundaries /// </summary> /// <param name="data">The audio samples to write. If stereo, this will be interleaved</param> /// <param name="offset">The offset to use when reading data</param> /// <param name="count">The amount of PCM data to write</param> public void WriteSamples(short[] data, int offset, int count) { if (_finalized) { throw new InvalidOperationException("Cannot write new samples to Ogg file, the output stream is already closed!"); } if ((data.Length - offset) < count) { // Check that caller isn't lying about its buffer sizes throw new ArgumentOutOfRangeException("The given audio buffer claims to have " + count + " samples, but it actually only has " + (data.Length - offset)); } // Try and fill the opus frame // input cursor in RAW SAMPLES int inputCursor = 0; // output cursor in RAW SAMPLES int amountToWrite = Math.Min(_opusFrame.Length - _opusFrameIndex, count - inputCursor); while (amountToWrite > 0) { // Do we need to resample? if (_inputSampleRate != _encoderSampleRate) { // Resample the input // divide by channel count because these numbers are in samples-per-channel int in_len = (count - inputCursor) / _inputChannels; int out_len = amountToWrite / _inputChannels; _resampler.ProcessInterleaved(data, offset + inputCursor, ref in_len, _opusFrame, _opusFrameIndex, ref out_len); inputCursor += in_len * _inputChannels; _opusFrameIndex += out_len * _inputChannels; } else { // If no resampling, we can do a straight memcpy instead Array.Copy(data, offset + inputCursor, _opusFrame, _opusFrameIndex, amountToWrite); _opusFrameIndex += amountToWrite; inputCursor += amountToWrite; } if (_opusFrameIndex == _opusFrame.Length) { int maxBlockSize = 0x1000; if (_outputStream.Position < 0x1000) { maxBlockSize = 0x1000 - 0x200; } int pageFree = maxBlockSize - _payloadIndex - _headerIndex; int maxPayloadThisPage = (pageFree / 256) * 255 + (pageFree % 256) - 1; int reconstructed = (maxPayloadThisPage / 255) + 1 + maxPayloadThisPage; /* when due to segment sizes we would end up with a 1 byte gap, make sure that the next run will have at least 64 byte. * reason why this could happen is that "adding one byte" would require one segment more and thus occupies two byte more. * if this would happen, just reduce the calculated free space such that there is room for another segment. */ if (pageFree != reconstructed && maxPayloadThisPage > 64) { maxPayloadThisPage -= 64; } int lacingSpace = (255 - _lacingTableCount) * 255; int maxOpusData = Math.Min(lacingSpace, maxPayloadThisPage); int packetSize = _encoder.Encode(_opusFrame, 0, _opusFrameSamples, _currentPayload, _payloadIndex, maxOpusData); maxOpusData -= packetSize; /* pad to full remaining size if we would leave just a few bytes behind. picked a random number that seemed enough for opus */ if (maxOpusData > 0 && maxOpusData < 43) { if (OpusRepacketizer.PadPacket(_currentPayload, _payloadIndex, packetSize, packetSize + maxOpusData) != Enums.OpusError.OPUS_OK) { Console.WriteLine("[ERROR] Padding failed at offset 0x" + _outputStream.Position.ToString("X8")); throw new PaddingException("Failed to pad packet"); } else { packetSize += maxOpusData; } } _payloadIndex += packetSize; // Opus granules are measured in 48Khz samples. // Since the framesize is fixed (20ms) and the sample rate doesn't change, this is basically a constant value _granulePosition += FRAME_SIZE_MS * 48; // And update the lacing values in the header int segmentLength = packetSize; while (segmentLength >= 255) { segmentLength -= 255; _currentHeader[_headerIndex++] = 0xFF; _lacingTableCount++; } _currentHeader[_headerIndex++] = (byte)segmentLength; _lacingTableCount++; int remain = (maxBlockSize - _payloadIndex - _headerIndex); /* check if there would be enough space for lacing and payload each */ if (_lacingTableCount >= 248 || remain < 16) { if (remain > 0) { Console.WriteLine("[ERROR] Ogg Page with just a few bytes remaining. Weird bit rate chosen?"); Console.WriteLine("[ERROR] Offset 0x" + _outputStream.Position.ToString("X8")); throw new Exception("Failed to finalize page"); } FinalizePage(); } _opusFrameIndex = 0; } amountToWrite = Math.Min(_opusFrame.Length - _opusFrameIndex, count - inputCursor); } }
/// <summary> /// Writes a buffer of PCM audio samples to the encoder and packetizer. Runs Opus encoding and potentially outputs one or more pages to the underlying Ogg stream. /// You can write any non-zero number of samples that you want here; there are no restrictions on length or packet boundaries /// </summary> /// <param name="data">The audio samples to write. If stereo, this will be interleaved</param> /// <param name="offset">The offset to use when reading data</param> /// <param name="count">The amount of PCM data to write</param> public void WriteSamples(short[] data, int offset, int count) { if (_finalized) { throw new InvalidOperationException("Cannot write new samples to Ogg file, the output stream is already closed!"); } if ((data.Length - offset) < count) { // Check that caller isn't lying about its buffer sizes throw new ArgumentOutOfRangeException("The given audio buffer claims to have " + count + " samples, but it actually only has " + (data.Length - offset)); } // Try and fill the opus frame // input cursor in RAW SAMPLES int inputCursor = 0; // output cursor in RAW SAMPLES int amountToWrite = Math.Min(_opusFrame.Length - _opusFrameIndex, count - inputCursor); while (amountToWrite > 0) { // Do we need to resample? if (_inputSampleRate != _encoderSampleRate) { // Resample the input // divide by channel count because these numbers are in samples-per-channel int in_len = (count - inputCursor) / _inputChannels; int out_len = amountToWrite / _inputChannels; _resampler.ProcessInterleaved(data, offset + inputCursor, ref in_len, _opusFrame, _opusFrameIndex, ref out_len); inputCursor += in_len * _inputChannels; _opusFrameIndex += out_len * _inputChannels; } else { // If no resampling, we can do a straight memcpy instead Array.Copy(data, offset + inputCursor, _opusFrame, _opusFrameIndex, amountToWrite); _opusFrameIndex += amountToWrite; inputCursor += amountToWrite; } if (_opusFrameIndex == _opusFrame.Length) { // Frame is finished. Encode it int packetSize = _encoder.Encode(_opusFrame, 0, _opusFrameSamples, _currentPayload, _payloadIndex, _currentPayload.Length - _payloadIndex); _payloadIndex += packetSize; // Opus granules are measured in 48Khz samples. // Since the framesize is fixed (20ms) and the sample rate doesn't change, this is basically a constant value _granulePosition += FRAME_SIZE_MS * 48; // And update the lacing values in the header int segmentLength = packetSize; while (segmentLength >= 255) { segmentLength -= 255; _currentHeader[_headerIndex++] = 0xFF; _lacingTableCount++; } _currentHeader[_headerIndex++] = (byte)segmentLength; _lacingTableCount++; // And finalize the page if we need // 284 is a magic number meaning "our page is almost full so just finalize it" // A more proper implementation would have the packets span to the next page, but meh if (_lacingTableCount > 248) { FinalizePage(); } _opusFrameIndex = 0; } amountToWrite = Math.Min(_opusFrame.Length - _opusFrameIndex, count - inputCursor); } }