Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
VideoPresentation.cpp
Go to the documentation of this file.
2#include "AudioRuntime.hpp"
3#include <SDL3/SDL.h>
4#include <algorithm>
5#include <atomic>
6#include <cmath>
7#include <limits>
8
9namespace monopoly::video
10{
11 namespace
12 {
13 constexpr std::uint64_t BytesPerSecond = 48000U * 2U * 2U;
14 constexpr int MaximumQueuedAudio = static_cast<int>(BytesPerSecond / 2U);
15 std::string sdlError(const char* operation)
16 { return std::string(operation) + ": " + SDL_GetError(); }
17
18 struct StereoPanState
19 {
20 std::atomic<float> left{1.0F};
21 std::atomic<float> right{1.0F};
22 void set(std::int32_t percentage) noexcept
23 {
25 left.store(gains.left, std::memory_order_relaxed);
26 right.store(gains.right, std::memory_order_relaxed);
27 }
28 };
29
31 void* userdata, const SDL_AudioSpec* spec,
32 float* buffer, int buflen) noexcept
33 {
34 if (!userdata || !spec || !buffer || buflen <= 0 || spec->channels < 2)
35 return;
36 const auto& pan = *static_cast<const StereoPanState*>(userdata);
37 const float left = pan.left.load(std::memory_order_relaxed);
38 const float right = pan.right.load(std::memory_order_relaxed);
39 const int samples = buflen / static_cast<int>(sizeof(float));
40 const int channels = static_cast<int>(spec->channels);
41 for (int sample = 0; sample + 1 < samples; sample += channels)
42 {
43 buffer[sample] *= left;
44 buffer[sample + 1] *= right;
45 }
46 }
47
49 SDL_AudioStream* stream, StereoPanState& pan) noexcept
50 {
51 const auto device = SDL_GetAudioStreamDevice(stream);
52 return device != 0 &&
54 }
55
56 void uninstallStereoPan(SDL_AudioStream* stream) noexcept
57 {
58 const auto device = SDL_GetAudioStreamDevice(stream);
59 if (device != 0)
60 (void)SDL_SetAudioPostmixCallback(device, nullptr, nullptr);
61 }
62 }
63 std::expected<data::LegacyBitmapRGBA8, std::string> prepareVideoFrame(
65 {
66 if (!frame.width || !frame.height || frame.width > 8192 || frame.height > 8192 ||
67 frame.rgba.size() != static_cast<std::uint64_t>(frame.width) * frame.height * 4U)
68 return std::unexpected("decoded video frame dimensions do not match RGBA pixels");
69 if (options.saturation || options.brightness || options.contrast)
70 return std::unexpected("non-default legacy Indeo colour controls are unsupported");
71 if (options.flipVertically)
72 {
73 const auto stride = static_cast<std::size_t>(frame.width) * 4U;
74 for (std::size_t y = 0; y < frame.height / 2U; ++y)
75 std::swap_ranges(frame.rgba.begin() + y * stride,
76 frame.rgba.begin() + (y + 1U) * stride,
77 frame.rgba.begin() + (frame.height - 1U - y) * stride);
78 }
79 // L_Rend2D uses drawSolid to bypass alpha; alphaLevel is already 0..255.
80 for (std::size_t i = 3; i < frame.rgba.size(); i += 4)
81 {
82 const bool colourKey = !options.drawSolid && options.alphaLevel == 255 &&
83 frame.rgba[i - 3] == 0 && frame.rgba[i - 2] == 255 && frame.rgba[i - 1] == 0;
84 frame.rgba[i] = options.drawSolid ? 255 : colourKey ? 0 : options.alphaLevel;
85 }
86 return data::LegacyBitmapRGBA8{frame.width, frame.height, std::move(frame.rgba)};
87 }
88 std::expected<sequence::Matrix2D, std::string> videoFrameTransform(
89 std::uint32_t width, std::uint32_t height,
91 const sequence::Matrix2D& world)
92 {
93 const auto rectangleWidth = static_cast<std::int64_t>(bounds.right) - bounds.left;
94 const auto rectangleHeight = static_cast<std::int64_t>(bounds.bottom) - bounds.top;
95 if (!width || !height || rectangleWidth <= 0 || rectangleHeight <= 0 ||
96 !std::ranges::all_of(world.values, [](float value) { return std::isfinite(value); }))
97 return std::unexpected("video rectangle or world transform is invalid");
98 auto transform = sequence::identity2D();
99 transform.values[0] = static_cast<float>(rectangleWidth) / width;
100 transform.values[4] = static_cast<float>(rectangleHeight) / height;
101 transform.values[6] = static_cast<float>(bounds.left);
102 transform.values[7] = static_cast<float>(bounds.top);
103 return sequence::multiply(transform, world);
104 }
105
107 std::uint32_t width, std::uint32_t height) noexcept
108 {
109 // L_Seqncr.cpp:12793: double native dimensions, center, then clamp.
110 const auto doubledWidth = static_cast<std::int64_t>(width) * 2;
111 const auto doubledHeight = static_cast<std::int64_t>(height) * 2;
112 const auto left = std::max<std::int64_t>(0, (800 - doubledWidth) / 2);
113 const auto top = std::max<std::int64_t>(0, (600 - doubledHeight) / 2);
114 return {{}, static_cast<std::int32_t>(left), static_cast<std::int32_t>(top),
115 static_cast<std::int32_t>(std::min<std::int64_t>(800, left + doubledWidth)),
116 static_cast<std::int32_t>(std::min<std::int64_t>(600, top + doubledHeight))};
117 }
119 {
122 std::optional<DecodedVideoFrame> nextFrame;
124 bool resumed{};
125 bool flushed{};
126 bool anchored{};
128 std::uint64_t sequenceOrigin{};
129 std::uint64_t lastSequenceTime{};
130 std::uint64_t mediaOrigin{};
131 std::uint64_t submitted{};
132 std::uint64_t consumed{};
133 std::uint64_t audioOrigin{};
134 std::uint64_t tailSequence{};
135 std::uint64_t tailMedia{};
136 std::uint64_t lastElapsed{};
137 float gain{1.0F};
138 float frequencyRatio{1.0F};
139 StereoPanState pan;
141 {
142 if (stream)
143 {
146 }
147 }
149 {
150 if (stream)
151 {
154 }
155 stream = nullptr;
156 resumed = flushed = tailAnchored = false;
157 submitted = consumed = 0;
159 }
160 };
162 Presentation::~Presentation() = default;
163 std::expected<void, std::string> Presentation::open(
164 const std::filesystem::path& file, bool enableAudio, const DecoderOptions& options)
165 {
166 stop();
167 impl_->audioEnabled = enableAudio;
168 auto selected = options;
169 selected.decodeAudio = enableAudio;
170 return impl_->decoder.open(file, selected);
171 }
172 DecoderSnapshot Presentation::snapshot() const { return impl_->decoder.snapshot(); }
174 {
175 impl_->decoder.stop();
176 impl_->clearAudio();
177 impl_->nextFrame.reset();
178 impl_->anchored = false;
179 impl_->sequenceOrigin = impl_->lastSequenceTime = impl_->mediaOrigin = impl_->lastElapsed = 0;
180 }
181 std::expected<void, std::string> Presentation::seek(
182 std::uint64_t timestamp, std::uint64_t sequenceTime)
183 {
184 const auto seeked = impl_->decoder.seek(timestamp);
185 if (!seeked) return seeked;
186 impl_->mediaOrigin = impl_->lastElapsed = timestamp;
187 impl_->sequenceOrigin = impl_->lastSequenceTime = sequenceTime;
188 impl_->anchored = true;
189 impl_->clearAudio();
190 impl_->nextFrame.reset();
191 return {};
192 }
193 std::expected<void, std::string> Presentation::setGain(float gain)
194 {
195 if (!std::isfinite(gain))
196 return std::unexpected("video audio gain is not finite");
197 impl_->gain = std::clamp(gain, 0.0F, 1.0F);
198 if (impl_->stream &&
199 !SDL_SetAudioStreamGain(impl_->stream, impl_->gain))
200 return std::unexpected(sdlError("set video audio gain"));
201 return {};
202 }
203
204 std::expected<void, std::string> Presentation::setPitch(
205 std::uint32_t hertz, std::uint32_t originalHertz)
206 {
207 impl_->frequencyRatio = audio::legacyPitchFrequencyRatio(
209 if (impl_->stream &&
211 impl_->stream, impl_->frequencyRatio))
212 return std::unexpected(sdlError("set video audio pitch"));
213 return {};
214 }
215
216 void Presentation::setPanning(std::int32_t percentage) noexcept
217 {
218 impl_->pan.set(percentage);
219 }
220
221 std::expected<PresentationClock, std::string> Presentation::pump(
222 std::uint64_t sequenceTime, bool paused)
223 {
224 auto& state = *impl_;
225 state.lastSequenceTime = sequenceTime;
226 auto snapshot = state.decoder.snapshot();
228 return std::unexpected(snapshot.error);
229 if (!snapshot.metadata) return PresentationClock{};
230 if (!state.anchored && !snapshot.queuedVideoFrames && !snapshot.videoEnded)
231 return PresentationClock{state.mediaOrigin, 0, state.audioEnabled && snapshot.metadata->hasAudio, false};
232 if (!state.anchored)
233 {
234 state.sequenceOrigin = sequenceTime;
235 state.anchored = true;
236 }
237 const auto silentTime = state.mediaOrigin +
238 (sequenceTime >= state.sequenceOrigin ? sequenceTime - state.sequenceOrigin : 0);
239 if (!state.audioEnabled || !snapshot.metadata->hasAudio)
240 {
241 state.lastElapsed = paused ? state.lastElapsed : silentTime;
242 return PresentationClock{state.lastElapsed, 0, false, true};
243 }
244 if (!state.stream)
245 {
246 SDL_AudioSpec spec{SDL_AUDIO_S16LE, 2, 48000};
248 SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr);
249 if (!state.stream) return std::unexpected(sdlError("open video audio stream"));
250 if (!SDL_SetAudioStreamGain(state.stream, state.gain))
251 return std::unexpected(sdlError("set initial video audio gain"));
253 state.stream, state.frequencyRatio))
254 return std::unexpected(sdlError("set initial video audio pitch"));
255 if (!installStereoPan(state.stream, state.pan))
256 return std::unexpected(sdlError("set video audio panning callback"));
257 }
258 int queued = SDL_GetAudioStreamQueued(state.stream);
259 if (queued < 0) return std::unexpected(sdlError("query video audio queue"));
260 // Device callbacks pull PCM; total submitted minus still queued is the
261 // consumed source clock. Wall time never advances through an underrun.
262 state.consumed = std::max(state.consumed, state.submitted -
263 std::min(state.submitted, static_cast<std::uint64_t>(queued)));
264 for (int chunks = 0; chunks < 16 && queued < MaximumQueuedAudio; ++chunks)
265 {
266 auto chunk = state.decoder.popAudio();
267 if (!chunk) break;
268 if (chunk->pcm.empty() || chunk->pcm.size() % 4U ||
269 chunk->pcm.size() > static_cast<std::size_t>(MaximumQueuedAudio))
270 return std::unexpected("decoded video PCM block is outside stream limits");
271 if (!state.submitted) state.audioOrigin = chunk->timestampMicroseconds;
272 if (!SDL_PutAudioStreamData(state.stream, chunk->pcm.data(),
273 static_cast<int>(chunk->pcm.size())))
274 return std::unexpected(sdlError("queue video PCM"));
275 state.submitted += chunk->pcm.size();
276 queued += static_cast<int>(chunk->pcm.size());
277 }
278 snapshot = state.decoder.snapshot();
280 {
281 if (!SDL_FlushAudioStream(state.stream))
282 return std::unexpected(sdlError("flush final video PCM"));
283 state.flushed = true;
284 }
285 const bool shouldResume = !paused && state.submitted != 0;
286 if (shouldResume != state.resumed)
287 {
288 const bool changed = shouldResume ? SDL_ResumeAudioStreamDevice(state.stream) :
290 if (!changed) return std::unexpected(sdlError("pause/resume video audio"));
291 state.resumed = shouldResume;
292 }
293 const bool drained = snapshot.audioEnded && !snapshot.queuedAudioChunks && queued == 0;
294 auto elapsed = state.audioOrigin + (state.consumed / BytesPerSecond) * 1000000U +
295 (state.consumed % BytesPerSecond) * 1000000U / BytesPerSecond;
296 if (drained)
297 {
298 if (!state.tailAnchored)
299 {
300 state.tailAnchored = true;
301 state.tailSequence = sequenceTime;
302 state.tailMedia = elapsed;
303 }
304 elapsed = state.tailMedia +
305 (sequenceTime >= state.tailSequence ? sequenceTime - state.tailSequence : 0);
306 }
307 if (!paused) state.lastElapsed = std::max(state.lastElapsed, elapsed);
308 return PresentationClock{state.lastElapsed, state.consumed,
309 !state.submitted && !drained, drained};
310 }
311 std::expected<std::optional<DecodedVideoFrame>, std::string> Presentation::frameAt(
312 std::uint64_t elapsed)
313 {
314 auto& state = *impl_;
315 const auto snapshot = state.decoder.snapshot();
316 if (snapshot.phase == DecoderPhase::Failed) return std::unexpected(snapshot.error);
317 std::optional<DecodedVideoFrame> latest;
318 // Bound UI work even if the worker replenishes its bounded queue rapidly.
319 for (int frames = 0; frames < 8; ++frames)
320 {
321 if (!state.nextFrame) state.nextFrame = state.decoder.popVideo();
322 if (!state.nextFrame || state.nextFrame->timestampMicroseconds > elapsed) break;
323 latest = std::move(state.nextFrame);
324 state.nextFrame.reset();
325 }
326 if (latest && !state.anchored)
327 {
328 // The decoder may publish its first frame after pump's empty-queue
329 // snapshot. Anchor when that frame is actually presented, otherwise
330 // the next pump would incorrectly discard all elapsed parent time.
331 state.sequenceOrigin = state.lastSequenceTime;
332 state.anchored = true;
333 }
334 return latest;
335 }
337 {
338 const auto snapshot = impl_->decoder.snapshot();
339 return snapshot.videoEnded && !snapshot.queuedVideoFrames && !impl_->nextFrame;
340 }
341}
std::atomic< float > left
std::atomic< float > right
int top
std::uint32_t height
std::uint32_t width
unsigned state
unsigned index
std::size_t frame
std::uint16_t value
Definition Timers.cpp:16
std::expected< std::optional< DecodedVideoFrame >, std::string > frameAt(std::uint64_t elapsedMicroseconds)
std::expected< void, std::string > setPitch(std::uint32_t hertz, std::uint32_t originalHertz)
std::expected< PresentationClock, std::string > pump(std::uint64_t sequenceMicroseconds, bool paused=false)
std::expected< void, std::string > setGain(float gain)
void setPanning(std::int32_t percentage) noexcept
std::expected< void, std::string > open(const std::filesystem::path &file, bool enableAudio, const DecoderOptions &options={})
std::expected< void, std::string > seek(std::uint64_t timestampMicroseconds, std::uint64_t sequenceMicroseconds)
DecoderSnapshot snapshot() const
constexpr StereoPanGains legacyPanGains(std::int32_t percentage) noexcept
constexpr float legacyPitchFrequencyRatio(std::uint32_t requestedHertz, std::uint32_t originalHertz) noexcept
Matrix2D identity2D() noexcept
Matrix2D multiply(const Matrix2D &left, const Matrix2D &right) noexcept
std::expected< sequence::Matrix2D, std::string > videoFrameTransform(std::uint32_t width, std::uint32_t height, const data::Sequence2DBoundingBoxAttribute &bounds, const sequence::Matrix2D &world)
std::expected< data::LegacyBitmapRGBA8, std::string > prepareVideoFrame(DecodedVideoFrame frame, const data::SequenceVideoData &options)
data::Sequence2DBoundingBoxAttribute binkDoubleSizeBounds(std::uint32_t width, std::uint32_t height) noexcept
std::optional< DecoderMetadata > metadata
std::optional< DecodedVideoFrame > nextFrame