Add synchronized MPEG movie audio
This commit is contained in:
182
godot/MovieAudioOutput.cs
Normal file
182
godot/MovieAudioOutput.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using System;
|
||||
using Godot;
|
||||
|
||||
internal enum MovieAudioRoute
|
||||
{
|
||||
Muted = 0,
|
||||
Music = 1,
|
||||
SoundEffect = 2,
|
||||
Voice = 3,
|
||||
Movie = 4,
|
||||
}
|
||||
|
||||
/// <summary>Godot-main-thread PCM sink and sound-hardware clock for one movie playback.</summary>
|
||||
internal sealed class MovieAudioOutput : IDisposable
|
||||
{
|
||||
private const int MaximumPushFrames = 4096;
|
||||
|
||||
private readonly IMovieDecoder _decoder;
|
||||
private readonly AudioStreamPlayer _player;
|
||||
private readonly int _sampleRate;
|
||||
private readonly double _outputLatencySeconds;
|
||||
private AudioStreamGeneratorPlayback? _playback;
|
||||
private MovieAudioChunk? _pending;
|
||||
private int _pendingOffsetFrames;
|
||||
private long _pendingGapFrames;
|
||||
private long _submittedFrames;
|
||||
private long _clockMs;
|
||||
private bool _timelineAnchored;
|
||||
private bool _started;
|
||||
private bool _prerolling;
|
||||
private bool _submissionCompleted;
|
||||
private bool _disposed;
|
||||
|
||||
public MovieAudioRoute Route { get; }
|
||||
public long ClockMs => _clockMs;
|
||||
public long SubmittedThroughMs => _sampleRate <= 0 ? 0 : _submittedFrames * 1000 / _sampleRate;
|
||||
public int BufferUnderruns => _playback?.GetSkips() ?? 0;
|
||||
|
||||
public MovieAudioOutput(Node owner, IMovieDecoder decoder, MovieAudioRoute route,
|
||||
double outputLatencySeconds)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(owner);
|
||||
_decoder = decoder ?? throw new ArgumentNullException(nameof(decoder));
|
||||
MovieAudioInfo info = decoder.AudioInfo
|
||||
?? throw new ArgumentException("movie decoder has no audio stream", nameof(decoder));
|
||||
if (info.SampleRate <= 0 || info.Channels != 2)
|
||||
throw new ArgumentOutOfRangeException(nameof(decoder), "movie PCM must be stereo with a positive sample rate");
|
||||
|
||||
Route = route;
|
||||
_sampleRate = info.SampleRate;
|
||||
_outputLatencySeconds = Math.Max(0, outputLatencySeconds);
|
||||
var generator = new AudioStreamGenerator
|
||||
{
|
||||
MixRate = _sampleRate,
|
||||
BufferLength = 0.25f,
|
||||
};
|
||||
_player = new AudioStreamPlayer
|
||||
{
|
||||
Stream = generator,
|
||||
Bus = RouteBus(route),
|
||||
VolumeDb = route == MovieAudioRoute.Muted ? -80.0f : 0.0f,
|
||||
};
|
||||
owner.AddChild(_player);
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (_disposed) return;
|
||||
if (_started)
|
||||
{
|
||||
double audibleSeconds = _player.GetPlaybackPosition()
|
||||
+ AudioServer.GetTimeSinceLastMix()
|
||||
- _outputLatencySeconds;
|
||||
long candidate = (long)Math.Floor(Math.Max(0, audibleSeconds) * 1000.0);
|
||||
if (candidate > _clockMs) _clockMs = candidate;
|
||||
}
|
||||
FeedAvailablePcm();
|
||||
_decoder.AdvancePlaybackClock(_clockMs);
|
||||
}
|
||||
|
||||
private void FeedAvailablePcm()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (_pending == null && !_decoder.TryTakeAudioChunk(out _pending))
|
||||
{
|
||||
if (_decoder.AudioDecodingCompleted && !_submissionCompleted)
|
||||
{
|
||||
_submissionCompleted = true;
|
||||
_decoder.MarkAudioSubmitted();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_pending == null) return;
|
||||
if (!_started) Start();
|
||||
|
||||
if (_pendingOffsetFrames == 0 && _pendingGapFrames == 0)
|
||||
{
|
||||
long audibleFrame = _clockMs * _sampleRate / 1000;
|
||||
if (audibleFrame > _submittedFrames) _submittedFrames = audibleFrame;
|
||||
long targetFrame = MovieAudioTimeline.PresentationFrame(
|
||||
_pending.PresentationTimeMs, _sampleRate);
|
||||
MovieAudioAdjustment adjustment = MovieAudioTimeline.Align(
|
||||
_submittedFrames, targetFrame, _pending.FrameCount, _sampleRate, _timelineAnchored);
|
||||
_pendingGapFrames = adjustment.GapFrames;
|
||||
_pendingOffsetFrames = adjustment.SkipFrames;
|
||||
_timelineAnchored = true;
|
||||
}
|
||||
|
||||
int available = _playback!.GetFramesAvailable();
|
||||
if (available <= 0) return;
|
||||
if (_pendingGapFrames > 0)
|
||||
{
|
||||
int count = (int)Math.Min(Math.Min(_pendingGapFrames, available), MaximumPushFrames);
|
||||
if (!_playback.PushBuffer(new Vector2[count])) return;
|
||||
FinishPreroll();
|
||||
_pendingGapFrames -= count;
|
||||
_submittedFrames += count;
|
||||
continue;
|
||||
}
|
||||
|
||||
int remaining = _pending.FrameCount - _pendingOffsetFrames;
|
||||
if (remaining <= 0)
|
||||
{
|
||||
_pending = null;
|
||||
_pendingOffsetFrames = 0;
|
||||
continue;
|
||||
}
|
||||
int frames = Math.Min(Math.Min(remaining, available), MaximumPushFrames);
|
||||
var output = new Vector2[frames];
|
||||
for (int index = 0; index < frames; index++)
|
||||
{
|
||||
int source = checked((_pendingOffsetFrames + index) * 2);
|
||||
output[index] = new Vector2(
|
||||
_pending.InterleavedStereo[source],
|
||||
_pending.InterleavedStereo[source + 1]);
|
||||
}
|
||||
if (!_playback.PushBuffer(output)) return;
|
||||
FinishPreroll();
|
||||
_pendingOffsetFrames += frames;
|
||||
_submittedFrames += frames;
|
||||
if (_pendingOffsetFrames == _pending.FrameCount)
|
||||
{
|
||||
_pending = null;
|
||||
_pendingOffsetFrames = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_player.Play();
|
||||
_player.StreamPaused = true;
|
||||
_playback = _player.GetStreamPlayback() as AudioStreamGeneratorPlayback
|
||||
?? throw new InvalidOperationException("Godot did not create movie audio generator playback");
|
||||
_started = true;
|
||||
_prerolling = true;
|
||||
}
|
||||
|
||||
private void FinishPreroll()
|
||||
{
|
||||
if (!_prerolling) return;
|
||||
_prerolling = false;
|
||||
_player.StreamPaused = false;
|
||||
}
|
||||
|
||||
private static StringName RouteBus(MovieAudioRoute route) => route switch
|
||||
{
|
||||
MovieAudioRoute.Music => "Music",
|
||||
MovieAudioRoute.SoundEffect => "SFX",
|
||||
MovieAudioRoute.Voice => "Voice",
|
||||
_ => "Movie",
|
||||
};
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_player.Stop();
|
||||
_player.QueueFree();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user