using System; using FFmpeg.AutoGen; namespace ADOFAIRenderer.Util.Audio.Clip; /// /// Sample rate converter for interleaved 32-bit float PCM (libswresample). /// Input and output have the same channel count; only the sample rate changes. /// One instance keeps filter state between calls, so consecutive calls form one continuous stream. /// Reference: https://ffmpeg.org/doxygen/trunk/group__lswr.html /// internal sealed unsafe class PCMResampler : IDisposable { private SwrContext* _swr; private bool _flushed; public int Channels { get; } public int InputRate { get; } public int OutputRate { get; } /// Value of the SwrContext "filter_size" option (filter length per phase in taps at a 1:1 ratio). public int FilterSize { get; } public PCMResampler(int channels, int inputRate, int outputRate) { if (channels <= 0) throw new ArgumentOutOfRangeException(nameof(channels)); if (inputRate <= 0) throw new ArgumentOutOfRangeException(nameof(inputRate)); if (outputRate <= 0) throw new ArgumentOutOfRangeException(nameof(outputRate)); Channels = channels; InputRate = inputRate; OutputRate = outputRate; AVChannelLayout layout; ffmpeg.av_channel_layout_default(&layout, channels); SwrContext* swr = null; var ret = ffmpeg.swr_alloc_set_opts2( &swr, &layout, AVSampleFormat.AV_SAMPLE_FMT_FLT, outputRate, &layout, AVSampleFormat.AV_SAMPLE_FMT_FLT, inputRate, 0, null); ffmpeg.av_channel_layout_uninit(&layout); if (ret < 0 || swr == null) throw new InvalidOperationException($"swr_alloc_set_opts2 failed ({ret})."); _swr = swr; long filterSize; FilterSize = ffmpeg.av_opt_get_int(_swr, "filter_size", 0, &filterSize) >= 0 ? (int)filterSize : 32; Reset(); } /// Discards all buffered input and filter history. The next call starts a new stream. public void Reset() { // swr_init on an initialized context re-initializes it. // Reference: https://ffmpeg.org/doxygen/trunk/group__lswr.html (swr_init, swr_close) var ret = ffmpeg.swr_init(_swr); if (ret < 0) throw new InvalidOperationException($"swr_init failed ({ret})."); _flushed = false; } /// /// Feeds frames and writes up to frames. /// Input that does not fit in the output is buffered inside the context and returned by later calls. /// may be 0 to take only buffered output. /// /// Frames written to . public int Convert(float* input, int inputFrames, float* output, int outputCapacity) { if (_flushed) throw new InvalidOperationException("Resampler was flushed. Call Reset first."); // swr_convert treats a null input array as a flush request, so a non-null pointer is passed even for 0 frames. float dummy = 0; var inPlane = (byte*)(input != null ? input : &dummy); var outPlane = (byte*)output; var ret = ffmpeg.swr_convert(_swr, &outPlane, outputCapacity, &inPlane, inputFrames); if (ret < 0) throw new InvalidOperationException($"swr_convert failed ({ret})."); return ret; } /// /// Ends the input stream and writes up to remaining frames. /// Call repeatedly until it returns 0. is not allowed afterwards until . /// public int Flush(float* output, int outputCapacity) { _flushed = true; var outPlane = (byte*)output; var ret = ffmpeg.swr_convert(_swr, &outPlane, outputCapacity, null, 0); if (ret < 0) throw new InvalidOperationException($"swr_convert (flush) failed ({ret})."); return ret; } public void Dispose() { if (_swr == null) return; var swr = _swr; ffmpeg.swr_free(&swr); _swr = null; } }