[bink] synced host movie playback to the guest audio clock

This commit is contained in:
ParantezTech
2026-07-28 01:05:43 +03:00
parent 94b1fb3b5a
commit da5a4b14df
3 changed files with 582 additions and 73 deletions
+3 -1
View File
@@ -133,10 +133,12 @@ internal static class Bink2MovieBridge
if (_playback.IsFinished) if (_playback.IsFinished)
{ {
var completedPath = _activePath; var completedPath = _activePath;
var progress = _playback.PlaybackProgress;
CloseActiveLocked(); CloseActiveLocked();
Console.Error.WriteLine( Console.Error.WriteLine(
"[LOADER][INFO] Bink2 bridge completed: " + "[LOADER][INFO] Bink2 bridge completed: " +
Path.GetFileName(completedPath)); $"{Path.GetFileName(completedPath)} after " +
$"{progress.Seconds:F2}s at frame {progress.FrameIndex}");
AttachNextQueuedMovieLocked(); AttachNextQueuedMovieLocked();
} }
return false; return false;
+129 -7
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics; using System.Diagnostics;
using SharpEmu.HLE.Host;
namespace SharpEmu.Libs.Bink; namespace SharpEmu.Libs.Bink;
@@ -36,6 +37,8 @@ internal sealed class BinkFramePlayback : IDisposable
private long _currentFrameIndex = -1; private long _currentFrameIndex = -1;
private long _nextDecodedFrameIndex; private long _nextDecodedFrameIndex;
private long _playbackStartTimestamp; private long _playbackStartTimestamp;
private double _audioStartSeconds;
private long _lastSkewTraceTimestamp;
private bool _playbackClockStarted; private bool _playbackClockStarted;
private bool _decoderCompleted; private bool _decoderCompleted;
private bool _stopRequested; private bool _stopRequested;
@@ -83,6 +86,26 @@ internal sealed class BinkFramePlayback : IDisposable
} }
} }
/// <summary>
/// Wall-clock seconds since the first frame was presented, and the index of
/// the last frame shown. Playback is on its own time base when these agree
/// with the movie's frame rate.
/// </summary>
internal (double Seconds, long FrameIndex) PlaybackProgress
{
get
{
lock (_gate)
{
return (
_playbackClockStarted
? Stopwatch.GetElapsedTime(_playbackStartTimestamp).TotalSeconds
: 0,
_currentFrameIndex);
}
}
}
internal bool TryGetFrame( internal bool TryGetFrame(
bool advanceClock, bool advanceClock,
out byte[] pixels, out byte[] pixels,
@@ -118,14 +141,13 @@ internal sealed class BinkFramePlayback : IDisposable
if (advanceClock && !_playbackClockStarted) if (advanceClock && !_playbackClockStarted)
{ {
_playbackStartTimestamp = Stopwatch.GetTimestamp(); _playbackStartTimestamp = Stopwatch.GetTimestamp();
_audioStartSeconds = GuestAudioClock.PlayedSeconds;
_playbackClockStarted = true; _playbackClockStarted = true;
} }
var elapsedSeconds = _playbackClockStarted var elapsedSeconds = CurrentPlaybackSecondsLocked();
? Stopwatch.GetElapsedTime(_playbackStartTimestamp).TotalSeconds TraceClockSkewLocked();
: 0; var targetFrameIndex = CurrentTargetFrameIndexLocked();
var targetFrameIndex = (long)Math.Floor(
elapsedSeconds * FramesPerSecondNumerator / FramesPerSecondDenominator);
DecodedFrame? replacement = null; DecodedFrame? replacement = null;
while (_decodedFrames.Count > 0 && while (_decodedFrames.Count > 0 &&
_decodedFrames.Peek().Index <= targetFrameIndex) _decodedFrames.Peek().Index <= targetFrameIndex)
@@ -166,6 +188,86 @@ internal sealed class BinkFramePlayback : IDisposable
} }
} }
/// <summary>
/// Time base for playback. A host-decoded movie runs on whatever clock it is
/// given, but the audio that belongs to it comes from the guest, which does
/// not advance at wall-clock rate on a slow frame. Following the audio keeps
/// the two together; SHARPEMU_MOVIE_CLOCK=wall restores the old behaviour.
/// </summary>
private static readonly bool _followGuestAudioClock = !string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_MOVIE_CLOCK"),
"wall",
StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Seconds of playback elapsed on the movie's time base. Falls back to wall
/// clock whenever guest audio is not flowing: a movie whose audio never
/// starts — or stops early — must still finish rather than hang on a clock
/// that will never advance again.
/// </summary>
private double CurrentPlaybackSecondsLocked()
{
if (!_playbackClockStarted)
{
return 0;
}
var wallSeconds = Stopwatch.GetElapsedTime(_playbackStartTimestamp).TotalSeconds;
if (!_followGuestAudioClock || !GuestAudioClock.IsRunning)
{
return wallSeconds;
}
return Math.Clamp(GuestAudioClock.PlayedSeconds - _audioStartSeconds, 0, wallSeconds);
}
private static readonly bool _traceClockSkew = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_MOVIE_SYNC"),
"1",
StringComparison.Ordinal);
/// <summary>
/// Logs how far the movie's wall clock has drifted from the guest audio the
/// movie is supposed to be in step with. A skew that is flat across playback
/// is a late audio start; one that grows is a rate mismatch, and the two need
/// different fixes. Caller holds <see cref="_gate"/>.
/// </summary>
private void TraceClockSkewLocked()
{
if (!_traceClockSkew || !_playbackClockStarted)
{
return;
}
var now = Stopwatch.GetTimestamp();
if (_lastSkewTraceTimestamp != 0 &&
Stopwatch.GetElapsedTime(_lastSkewTraceTimestamp) < TimeSpan.FromSeconds(1))
{
return;
}
_lastSkewTraceTimestamp = now;
var wallSeconds = Stopwatch.GetElapsedTime(_playbackStartTimestamp).TotalSeconds;
var audioSeconds = GuestAudioClock.PlayedSeconds - _audioStartSeconds;
Console.Error.WriteLine(
$"[PERF][MOVIE] wall_s={wallSeconds:F2} audio_s={audioSeconds:F2} " +
$"playback_s={CurrentPlaybackSecondsLocked():F2} " +
$"skew_s={wallSeconds - audioSeconds:F2} frame={_currentFrameIndex} " +
$"audio_running={GuestAudioClock.IsRunning}");
}
/// <summary>
/// The frame the movie's own time base says should be on screen right now.
/// Returns -1 until the first frame is presented, so the queue prefills
/// instead of instantly declaring everything late.
/// </summary>
private long CurrentTargetFrameIndexLocked() =>
_playbackClockStarted
? (long)Math.Floor(
CurrentPlaybackSecondsLocked() *
FramesPerSecondNumerator / FramesPerSecondDenominator)
: -1;
private void DecodeLoop() private void DecodeLoop()
{ {
try try
@@ -199,8 +301,28 @@ internal sealed class BinkFramePlayback : IDisposable
lock (_gate) lock (_gate)
{ {
_decodedFrames.Enqueue(new DecodedFrame( var frameIndex = _nextDecodedFrameIndex++;
_nextDecodedFrameIndex++, destination));
// Frames are pulled once per guest flip, so a title running
// well under the movie's frame rate cannot drain a queue
// this shallow fast enough and the movie stretches past its
// real duration — audio finishes while the last picture sits
// on screen and the next movie starts late. Once the clock
// has passed a queued frame it can never be shown, so retire
// it in favour of this newer one. Only superseded frames are
// dropped, never the newest, so a decoder that cannot keep
// up still advances the picture instead of freezing it.
var targetFrameIndex = CurrentTargetFrameIndexLocked();
if (frameIndex <= targetFrameIndex)
{
while (_decodedFrames.Count > 0 &&
_decodedFrames.Peek().Index <= targetFrameIndex)
{
_freeBuffers.Enqueue(_decodedFrames.Dequeue().Pixels);
}
}
_decodedFrames.Enqueue(new DecodedFrame(frameIndex, destination));
Monitor.PulseAll(_gate); Monitor.PulseAll(_gate);
} }
} }
@@ -1,7 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using FFmpeg.AutoGen; using FFmpeg.AutoGen;
using SharpEmu.HLE.Host;
namespace SharpEmu.Libs.Bink; namespace SharpEmu.Libs.Bink;
@@ -13,13 +15,29 @@ namespace SharpEmu.Libs.Bink;
/// </summary> /// </summary>
internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder
{ {
private const int OutputAudioChannels = 2;
private const int OutputAudioBytesPerSample = sizeof(short);
private readonly object _decodeGate = new();
private AVFormatContext* _formatContext; private AVFormatContext* _formatContext;
private AVCodecContext* _codecContext; private AVCodecContext* _codecContext;
private AVCodecContext* _audioCodecContext;
private SwsContext* _swsContext; private SwsContext* _swsContext;
private SwrContext* _swrContext;
private AVFrame* _frame; private AVFrame* _frame;
private AVFrame* _audioFrame;
private AVPacket* _packet; private AVPacket* _packet;
private IHostAudioStream? _audioStream;
private readonly int _videoStreamIndex; private readonly int _videoStreamIndex;
private readonly int _audioStreamIndex;
private readonly int _audioOutputSampleRate;
private AVChannelLayout _swrInputLayout;
private AVSampleFormat _swrInputFormat = AVSampleFormat.AV_SAMPLE_FMT_NONE;
private int _swrInputSampleRate;
private bool _swrInputLayoutValid;
private bool _draining; private bool _draining;
private bool _audioDraining;
private bool _audioFailed;
private int _disposed; private int _disposed;
public uint Width { get; } public uint Width { get; }
@@ -34,6 +52,10 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder
AVFormatContext* formatContext, AVFormatContext* formatContext,
AVCodecContext* codecContext, AVCodecContext* codecContext,
int videoStreamIndex, int videoStreamIndex,
AVCodecContext* audioCodecContext,
int audioStreamIndex,
IHostAudioStream? audioStream,
int audioOutputSampleRate,
uint width, uint width,
uint height, uint height,
uint framesPerSecondNumerator, uint framesPerSecondNumerator,
@@ -42,11 +64,16 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder
_formatContext = formatContext; _formatContext = formatContext;
_codecContext = codecContext; _codecContext = codecContext;
_videoStreamIndex = videoStreamIndex; _videoStreamIndex = videoStreamIndex;
_audioCodecContext = audioCodecContext;
_audioStreamIndex = audioStreamIndex;
_audioStream = audioStream;
_audioOutputSampleRate = audioOutputSampleRate;
Width = width; Width = width;
Height = height; Height = height;
FramesPerSecondNumerator = framesPerSecondNumerator; FramesPerSecondNumerator = framesPerSecondNumerator;
FramesPerSecondDenominator = framesPerSecondDenominator; FramesPerSecondDenominator = framesPerSecondDenominator;
_frame = ffmpeg.av_frame_alloc(); _frame = ffmpeg.av_frame_alloc();
_audioFrame = audioCodecContext is null ? null : ffmpeg.av_frame_alloc();
_packet = ffmpeg.av_packet_alloc(); _packet = ffmpeg.av_packet_alloc();
} }
@@ -93,6 +120,8 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder
AVFormatContext* formatContext = null; AVFormatContext* formatContext = null;
AVCodecContext* codecContext = null; AVCodecContext* codecContext = null;
AVCodecContext* audioCodecContext = null;
IHostAudioStream? audioStream = null;
try try
{ {
if (ffmpeg.avformat_open_input(&formatContext, path, null, null) < 0) if (ffmpeg.avformat_open_input(&formatContext, path, null, null) < 0)
@@ -151,6 +180,28 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder
frameRate = new AVRational { num = 30, den = 1 }; frameRate = new AVRational { num = 30, den = 1 };
} }
var audioStreamIndex = TryOpenAudioDecoder(
formatContext,
out audioCodecContext,
out var audioOutputSampleRate);
if (audioStreamIndex >= 0 && audioCodecContext is not null)
{
try
{
audioStream = HostPlatform.Current.Audio.OpenStereoPcm16Stream(
checked((uint)audioOutputSampleRate));
}
catch (Exception exception) when (exception is InvalidOperationException or
ArgumentOutOfRangeException)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Bink audio output unavailable: {exception.Message}");
ffmpeg.avcodec_free_context(&audioCodecContext);
audioStreamIndex = -1;
audioOutputSampleRate = 0;
}
}
var outputWidth = (uint)codecContext->width; var outputWidth = (uint)codecContext->width;
var outputHeight = (uint)codecContext->height; var outputHeight = (uint)codecContext->height;
if (maximumWidth > 0 && maximumHeight > 0 && if (maximumWidth > 0 && maximumHeight > 0 &&
@@ -175,12 +226,18 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder
formatContext, formatContext,
codecContext, codecContext,
videoStreamIndex, videoStreamIndex,
audioCodecContext,
audioStreamIndex,
audioStream,
audioOutputSampleRate,
outputWidth, outputWidth,
outputHeight, outputHeight,
(uint)frameRate.num, (uint)frameRate.num,
(uint)frameRate.den); (uint)frameRate.den);
formatContext = null; formatContext = null;
codecContext = null; codecContext = null;
audioCodecContext = null;
audioStream = null;
return true; return true;
} }
catch (DllNotFoundException) catch (DllNotFoundException)
@@ -194,6 +251,13 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder
ffmpeg.avcodec_free_context(&codecContext); ffmpeg.avcodec_free_context(&codecContext);
} }
if (audioCodecContext is not null)
{
ffmpeg.avcodec_free_context(&audioCodecContext);
}
audioStream?.Dispose();
if (formatContext is not null) if (formatContext is not null)
{ {
ffmpeg.avformat_close_input(&formatContext); ffmpeg.avformat_close_input(&formatContext);
@@ -201,52 +265,102 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder
} }
} }
private static int TryOpenAudioDecoder(
AVFormatContext* formatContext,
out AVCodecContext* codecContext,
out int outputSampleRate)
{
codecContext = null;
outputSampleRate = 0;
AVCodec* decoder = null;
var streamIndex = ffmpeg.av_find_best_stream(
formatContext, AVMediaType.AVMEDIA_TYPE_AUDIO, -1, -1, &decoder, 0);
if (streamIndex < 0 || decoder is null)
{
return -1;
}
var candidate = ffmpeg.avcodec_alloc_context3(decoder);
if (candidate is null)
{
return -1;
}
var stream = formatContext->streams[streamIndex];
if (ffmpeg.avcodec_parameters_to_context(candidate, stream->codecpar) < 0)
{
ffmpeg.avcodec_free_context(&candidate);
return -1;
}
candidate->thread_count = 0;
candidate->thread_type = ffmpeg.FF_THREAD_FRAME | ffmpeg.FF_THREAD_SLICE;
if (ffmpeg.avcodec_open2(candidate, decoder, null) < 0)
{
ffmpeg.avcodec_free_context(&candidate);
return -1;
}
outputSampleRate = candidate->sample_rate > 0 ? candidate->sample_rate : 48_000;
codecContext = candidate;
return streamIndex;
}
public bool TryDecodeNextFrame(Span<byte> destination) public bool TryDecodeNextFrame(Span<byte> destination)
{ {
var stride = checked((int)(Width * 4)); lock (_decodeGate)
var required = (long)stride * Height;
if (destination.Length < required)
{ {
return false; if (Volatile.Read(ref _disposed) != 0)
} {
return false;
}
if (!TryReceiveFrame()) var stride = checked((int)(Width * 4));
{ var required = (long)stride * Height;
return false; if (destination.Length < required)
} {
return false;
}
_swsContext = ffmpeg.sws_getCachedContext( if (!TryReceiveFrame())
_swsContext, {
_frame->width, return false;
_frame->height, }
(AVPixelFormat)_frame->format,
(int)Width,
(int)Height,
AVPixelFormat.AV_PIX_FMT_BGRA,
ffmpeg.SWS_FAST_BILINEAR,
null,
null,
null);
if (_swsContext is null)
{
ffmpeg.av_frame_unref(_frame);
return false;
}
fixed (byte* destinationPointer = destination) _swsContext = ffmpeg.sws_getCachedContext(
{
var destinationPlanes = new byte*[4] { destinationPointer, null, null, null };
var destinationStrides = new int[4] { stride, 0, 0, 0 };
var convertedRows = ffmpeg.sws_scale(
_swsContext, _swsContext,
_frame->data, _frame->width,
_frame->linesize,
0,
_frame->height, _frame->height,
destinationPlanes, (AVPixelFormat)_frame->format,
destinationStrides); (int)Width,
ffmpeg.av_frame_unref(_frame); (int)Height,
return convertedRows == (int)Height; AVPixelFormat.AV_PIX_FMT_BGRA,
ffmpeg.SWS_FAST_BILINEAR,
null,
null,
null);
if (_swsContext is null)
{
ffmpeg.av_frame_unref(_frame);
return false;
}
fixed (byte* destinationPointer = destination)
{
var destinationPlanes = new byte*[4] { destinationPointer, null, null, null };
var destinationStrides = new int[4] { stride, 0, 0, 0 };
var convertedRows = ffmpeg.sws_scale(
_swsContext,
_frame->data,
_frame->linesize,
0,
_frame->height,
destinationPlanes,
destinationStrides);
ffmpeg.av_frame_unref(_frame);
return convertedRows == (int)Height;
}
} }
} }
@@ -291,9 +405,17 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder
{ {
_draining = true; _draining = true;
ffmpeg.avcodec_send_packet(_codecContext, null); ffmpeg.avcodec_send_packet(_codecContext, null);
DrainAudioDecoder();
return true; return true;
} }
if (_packet->stream_index == _audioStreamIndex)
{
DecodeAudioPacket(_packet);
ffmpeg.av_packet_unref(_packet);
continue;
}
if (_packet->stream_index != _videoStreamIndex) if (_packet->stream_index != _videoStreamIndex)
{ {
ffmpeg.av_packet_unref(_packet); ffmpeg.av_packet_unref(_packet);
@@ -311,6 +433,248 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder
} }
} }
private void DecodeAudioPacket(AVPacket* packet)
{
if (_audioCodecContext is null || _audioFrame is null || _audioFailed)
{
return;
}
var sendResult = ffmpeg.avcodec_send_packet(_audioCodecContext, packet);
if (sendResult == ffmpeg.AVERROR(ffmpeg.EAGAIN))
{
DrainAvailableAudioFrames();
sendResult = ffmpeg.avcodec_send_packet(_audioCodecContext, packet);
}
if (sendResult < 0)
{
DisableAudio("packet decode failed");
return;
}
DrainAvailableAudioFrames();
}
private void DrainAudioDecoder()
{
if (_audioCodecContext is null || _audioFrame is null ||
_audioDraining || _audioFailed)
{
return;
}
_audioDraining = true;
var sendResult = ffmpeg.avcodec_send_packet(_audioCodecContext, null);
if (sendResult >= 0 || sendResult == ffmpeg.AVERROR(ffmpeg.EAGAIN))
{
DrainAvailableAudioFrames();
}
}
private void DrainAvailableAudioFrames()
{
while (_audioCodecContext is not null && _audioFrame is not null)
{
var receiveResult = ffmpeg.avcodec_receive_frame(_audioCodecContext, _audioFrame);
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) ||
receiveResult == ffmpeg.AVERROR_EOF)
{
return;
}
if (receiveResult < 0)
{
DisableAudio("frame decode failed");
return;
}
if (!SubmitAudioFrame())
{
ffmpeg.av_frame_unref(_audioFrame);
DisableAudio("host submission failed");
return;
}
ffmpeg.av_frame_unref(_audioFrame);
}
}
private bool SubmitAudioFrame()
{
if (_audioStream is null || _audioFrame is null ||
_audioFrame->nb_samples <= 0 || _audioFrame->extended_data is null)
{
return true;
}
var sampleRate = _audioFrame->sample_rate > 0
? _audioFrame->sample_rate
: _audioCodecContext->sample_rate;
if (sampleRate <= 0)
{
return false;
}
var inputLayout = _audioFrame->ch_layout;
var ownsInputLayout = false;
if (ffmpeg.av_channel_layout_check(&inputLayout) == 0)
{
inputLayout = _audioCodecContext->ch_layout;
}
if (ffmpeg.av_channel_layout_check(&inputLayout) == 0)
{
ffmpeg.av_channel_layout_default(
&inputLayout,
Math.Max(1, _audioFrame->ch_layout.nb_channels));
ownsInputLayout = true;
}
try
{
if (!EnsureAudioResampler(
&inputLayout,
(AVSampleFormat)_audioFrame->format,
sampleRate))
{
return false;
}
var maximumSamples = ffmpeg.swr_get_out_samples(
_swrContext, _audioFrame->nb_samples);
if (maximumSamples <= 0)
{
return true;
}
var outputBytes = checked(
maximumSamples * OutputAudioChannels * OutputAudioBytesPerSample);
var buffer = ArrayPool<byte>.Shared.Rent(outputBytes);
try
{
fixed (byte* output = buffer)
{
var outputPlanes = stackalloc byte*[1];
outputPlanes[0] = output;
var convertedSamples = ffmpeg.swr_convert(
_swrContext,
outputPlanes,
maximumSamples,
_audioFrame->extended_data,
_audioFrame->nb_samples);
if (convertedSamples < 0)
{
return false;
}
var convertedBytes = checked(
convertedSamples * OutputAudioChannels * OutputAudioBytesPerSample);
return _audioStream.Submit(buffer.AsSpan(0, convertedBytes));
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
finally
{
if (ownsInputLayout)
{
ffmpeg.av_channel_layout_uninit(&inputLayout);
}
}
}
private bool EnsureAudioResampler(
AVChannelLayout* inputLayout,
AVSampleFormat inputFormat,
int inputSampleRate)
{
var storedInputLayout = _swrInputLayout;
if (_swrContext is not null &&
_swrInputFormat == inputFormat &&
_swrInputSampleRate == inputSampleRate &&
ffmpeg.av_channel_layout_compare(&storedInputLayout, inputLayout) == 0)
{
return true;
}
FreeAudioResampler();
AVChannelLayout copiedInputLayout = default;
if (ffmpeg.av_channel_layout_copy(&copiedInputLayout, inputLayout) < 0)
{
return false;
}
AVChannelLayout outputLayout = default;
ffmpeg.av_channel_layout_default(&outputLayout, OutputAudioChannels);
SwrContext* context = null;
var allocateResult = ffmpeg.swr_alloc_set_opts2(
&context,
&outputLayout,
AVSampleFormat.AV_SAMPLE_FMT_S16,
_audioOutputSampleRate,
&copiedInputLayout,
inputFormat,
inputSampleRate,
0,
null);
ffmpeg.av_channel_layout_uninit(&outputLayout);
if (allocateResult < 0 || context is null || ffmpeg.swr_init(context) < 0)
{
if (context is not null)
{
ffmpeg.swr_free(&context);
}
ffmpeg.av_channel_layout_uninit(&copiedInputLayout);
return false;
}
_swrContext = context;
_swrInputLayout = copiedInputLayout;
_swrInputLayoutValid = true;
_swrInputFormat = inputFormat;
_swrInputSampleRate = inputSampleRate;
return true;
}
private void DisableAudio(string reason)
{
if (_audioFailed)
{
return;
}
_audioFailed = true;
Console.Error.WriteLine($"[LOADER][WARN] Bink audio disabled: {reason}.");
FreeAudioResampler();
_audioStream?.Dispose();
_audioStream = null;
}
private void FreeAudioResampler()
{
if (_swrContext is not null)
{
var context = _swrContext;
ffmpeg.swr_free(&context);
_swrContext = null;
}
if (_swrInputLayoutValid)
{
var inputLayout = _swrInputLayout;
ffmpeg.av_channel_layout_uninit(&inputLayout);
_swrInputLayout = default;
_swrInputLayoutValid = false;
}
_swrInputFormat = AVSampleFormat.AV_SAMPLE_FMT_NONE;
_swrInputSampleRate = 0;
}
public void Dispose() public void Dispose()
{ {
if (Interlocked.Exchange(ref _disposed, 1) != 0) if (Interlocked.Exchange(ref _disposed, 1) != 0)
@@ -318,38 +682,59 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder
return; return;
} }
if (_swsContext is not null) lock (_decodeGate)
{ {
ffmpeg.sws_freeContext(_swsContext); FreeAudioResampler();
_swsContext = null; _audioStream?.Dispose();
} _audioStream = null;
if (_packet is not null) if (_swsContext is not null)
{ {
var packet = _packet; ffmpeg.sws_freeContext(_swsContext);
ffmpeg.av_packet_free(&packet); _swsContext = null;
_packet = null; }
}
if (_frame is not null) if (_packet is not null)
{ {
var frame = _frame; var packet = _packet;
ffmpeg.av_frame_free(&frame); ffmpeg.av_packet_free(&packet);
_frame = null; _packet = null;
} }
if (_codecContext is not null) if (_frame is not null)
{ {
var codecContext = _codecContext; var frame = _frame;
ffmpeg.avcodec_free_context(&codecContext); ffmpeg.av_frame_free(&frame);
_codecContext = null; _frame = null;
} }
if (_formatContext is not null) if (_audioFrame is not null)
{ {
var formatContext = _formatContext; var frame = _audioFrame;
ffmpeg.avformat_close_input(&formatContext); ffmpeg.av_frame_free(&frame);
_formatContext = null; _audioFrame = null;
}
if (_codecContext is not null)
{
var codecContext = _codecContext;
ffmpeg.avcodec_free_context(&codecContext);
_codecContext = null;
}
if (_audioCodecContext is not null)
{
var codecContext = _audioCodecContext;
ffmpeg.avcodec_free_context(&codecContext);
_audioCodecContext = null;
}
if (_formatContext is not null)
{
var formatContext = _formatContext;
ffmpeg.avformat_close_input(&formatContext);
_formatContext = null;
}
} }
} }
} }