SHA256
121 lines
5.1 KiB
C#
121 lines
5.1 KiB
C#
using System;
|
|
using FFmpeg.AutoGen;
|
|
|
|
namespace ADOFAIRenderer.Util.Audio.Clip;
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="Convert"/> calls form one continuous stream.
|
|
/// Reference: https://ffmpeg.org/doxygen/trunk/group__lswr.html
|
|
/// </summary>
|
|
internal sealed unsafe class PCMResampler : IDisposable
|
|
{
|
|
/// <summary>Resampler context owned by this instance; null after <see cref="Dispose"/>.</summary>
|
|
private SwrContext* _swr;
|
|
|
|
/// <summary>True after <see cref="Flush"/> until <see cref="Reset"/>.</summary>
|
|
private bool _flushed;
|
|
|
|
/// <summary>Channel count of input and output (interleaved).</summary>
|
|
public int Channels { get; }
|
|
|
|
/// <summary>Input sample rate in Hz.</summary>
|
|
public int InputRate { get; }
|
|
|
|
/// <summary>Output sample rate in Hz.</summary>
|
|
public int OutputRate { get; }
|
|
|
|
/// <summary>Value of the SwrContext "filter_size" option (filter length per phase in taps at a 1:1 ratio).</summary>
|
|
public int FilterSize { get; }
|
|
|
|
/// <summary>Creates and initializes a float-to-float resampler with the default channel layout for <paramref name="channels"/>.</summary>
|
|
/// <exception cref="ArgumentOutOfRangeException">A count or rate is not positive.</exception>
|
|
/// <exception cref="InvalidOperationException">libswresample cannot allocate or initialize the context.</exception>
|
|
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;
|
|
|
|
// Falls back to 32, the libswresample default of "filter_size", when the option cannot be read.
|
|
// Reference: https://github.com/FFmpeg/FFmpeg/blob/master/libswresample/options.c (filter_size)
|
|
long filterSize;
|
|
FilterSize = ffmpeg.av_opt_get_int(_swr, "filter_size", 0, &filterSize) >= 0 ? (int)filterSize : 32;
|
|
|
|
Reset();
|
|
}
|
|
|
|
/// <summary>Discards all buffered input and filter history. The next call starts a new stream.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feeds <paramref name="inputFrames"/> frames and writes up to <paramref name="outputCapacity"/> frames.
|
|
/// Input that does not fit in the output is buffered inside the context and returned by later calls.
|
|
/// <paramref name="inputFrames"/> may be 0 to take only buffered output.
|
|
/// </summary>
|
|
/// <returns>Frames written to <paramref name="output"/>.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ends the input stream and writes up to <paramref name="outputCapacity"/> remaining frames.
|
|
/// Call repeatedly until it returns 0. <see cref="Convert"/> is not allowed afterwards until <see cref="Reset"/>.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Frees the context. Safe to call more than once.</summary>
|
|
public void Dispose()
|
|
{
|
|
if (_swr == null) return;
|
|
var swr = _swr;
|
|
ffmpeg.swr_free(&swr);
|
|
_swr = null;
|
|
}
|
|
}
|