ACMX 2.136.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
ACMXVK/file_audio.cpp
Go to the documentation of this file.
1#include "file_audio.hpp"
2
3#include "audio.hpp"
5
6#include <rtaudio/RtAudio.h>
7
8#include <algorithm>
9#include <atomic>
10#include <cctype>
11#include <chrono>
12#include <cmath>
13#include <cstdint>
14#include <filesystem>
15#include <fstream>
16#include <iostream>
17#include <limits>
18#include <string>
19#include <string_view>
20#include <system_error>
21#include <vector>
22
23#ifdef _WIN32
24#ifndef NOMINMAX
25#define NOMINMAX
26#endif
27#include <windows.h>
28#endif
29
30extern "C" {
31#include <libavcodec/avcodec.h>
32#include <libavformat/avformat.h>
33#include <libavutil/channel_layout.h>
34#include <libavutil/error.h>
35#include <libavutil/mathematics.h>
36#include <libswresample/swresample.h>
37}
38
39namespace acmxvk::audio {
40 namespace {
41
42 constexpr unsigned int FILE_SAMPLE_RATE = 44100;
43
44 [[nodiscard]] std::string ffmpegError(int error) {
45 char message[AV_ERROR_MAX_STRING_SIZE]{};
46 av_strerror(error, message, sizeof(message));
47 return message;
48 }
49
50 [[nodiscard]] std::string
51 ffmpegPath(const std::filesystem::path &path) {
52#ifdef _WIN32
53 const std::u8string utf8 = path.u8string();
54 return {reinterpret_cast<const char *>(utf8.data()), utf8.size()};
55#else
56 return path.string();
57#endif
58 }
59
60 [[nodiscard]] bool
61 replaceFile(const std::filesystem::path &source,
62 const std::filesystem::path &destination,
63 std::error_code &error) {
64#ifdef _WIN32
65 if (::MoveFileExW(source.c_str(), destination.c_str(),
66 MOVEFILE_REPLACE_EXISTING |
67 MOVEFILE_WRITE_THROUGH) != 0) {
68 error.clear();
69 return true;
70 }
71 error = std::error_code(static_cast<int>(::GetLastError()),
72 std::system_category());
73 return false;
74#else
75 std::filesystem::rename(source, destination, error);
76 return !error;
77#endif
78 }
79
80 [[nodiscard]] bool resampleMonoRecording(std::vector<float> &samples,
81 unsigned int source_rate) {
82 if (source_rate == FILE_SAMPLE_RATE) {
83 return true;
84 }
85 if (source_rate == 0 || samples.empty() ||
86 samples.size() > static_cast<std::size_t>(
87 std::numeric_limits<int>::max())) {
88 std::cerr << "acmxvk: live audio recording has an invalid sample "
89 "rate or sample count\n";
90 return false;
91 }
92
93 const std::int64_t expected_count = av_rescale_rnd(
94 static_cast<std::int64_t>(samples.size()), FILE_SAMPLE_RATE,
95 source_rate, AV_ROUND_UP);
96 if (expected_count <= 0 ||
97 expected_count > std::numeric_limits<int>::max()) {
98 std::cerr << "acmxvk: resampled live audio is too large\n";
99 return false;
100 }
101
102 SwrContext *resampler = nullptr;
103 AVChannelLayout input_layout = AV_CHANNEL_LAYOUT_MONO;
104 AVChannelLayout output_layout = AV_CHANNEL_LAYOUT_MONO;
105 int result = swr_alloc_set_opts2(
106 &resampler, &output_layout, AV_SAMPLE_FMT_FLT, FILE_SAMPLE_RATE,
107 &input_layout, AV_SAMPLE_FMT_FLT, static_cast<int>(source_rate), 0,
108 nullptr);
109 av_channel_layout_uninit(&output_layout);
110 av_channel_layout_uninit(&input_layout);
111 if (result < 0 || resampler == nullptr ||
112 (result = swr_init(resampler)) < 0) {
113 std::cerr << "acmxvk: could not initialize live audio resampler";
114 if (result < 0) {
115 std::cerr << ": " << ffmpegError(result);
116 }
117 std::cerr << '\n';
118 swr_free(&resampler);
119 return false;
120 }
121
122 std::vector<float> converted_samples(
123 static_cast<std::size_t>(expected_count));
124 const std::uint8_t *input_data[] = {
125 reinterpret_cast<const std::uint8_t *>(samples.data())};
126 std::uint8_t *output_data[] = {
127 reinterpret_cast<std::uint8_t *>(converted_samples.data())};
128 int converted = swr_convert(
129 resampler, output_data, static_cast<int>(expected_count), input_data,
130 static_cast<int>(samples.size()));
131 if (converted >= 0 && converted < expected_count) {
132 std::uint8_t *flush_data[] = {reinterpret_cast<std::uint8_t *>(
133 converted_samples.data() + converted)};
134 const int flushed = swr_convert(
135 resampler, flush_data,
136 static_cast<int>(expected_count) - converted, nullptr, 0);
137 if (flushed < 0) {
138 converted = flushed;
139 } else {
140 converted += flushed;
141 }
142 }
143 swr_free(&resampler);
144 if (converted < 0) {
145 std::cerr << "acmxvk: could not resample live audio: "
146 << ffmpegError(converted) << '\n';
147 return false;
148 }
149
150 converted_samples.resize(static_cast<std::size_t>(converted));
151 samples = std::move(converted_samples);
152 std::cout << "acmxvk: resampled live audio from " << source_rate
153 << " Hz to " << FILE_SAMPLE_RATE << " Hz\n";
154 return !samples.empty();
155 }
156
157 [[nodiscard]] std::string trimPlaylistLine(std::string line) {
158 constexpr std::string_view WHITESPACE = " \t\r\n";
159 const std::size_t first = line.find_first_not_of(WHITESPACE);
160 if (first == std::string::npos) {
161 return {};
162 }
163 const std::size_t last = line.find_last_not_of(WHITESPACE);
164 return line.substr(first, last - first + 1);
165 }
166
167 [[nodiscard]] bool isM3uPath(const std::filesystem::path &path) {
168 std::string extension = path.extension().string();
169 std::transform(extension.begin(), extension.end(), extension.begin(),
170 [](unsigned char value) {
171 return static_cast<char>(std::tolower(value));
172 });
173 return extension == ".m3u" || extension == ".m3u8";
174 }
175
176 [[nodiscard]] bool isUrl(std::string_view path) {
177 return path.find("://") != std::string_view::npos;
178 }
179
180 [[nodiscard]] std::vector<std::string>
181 readM3uPlaylist(const std::filesystem::path &playlist) {
182 input::validate_file_size(playlist, "M3U playlist");
183 std::ifstream playlist_input(playlist);
184 if (!playlist_input) {
185 std::cerr << "acmxvk: could not open M3U playlist: "
186 << playlist.string() << '\n';
187 return {};
188 }
189
190 std::vector<std::string> paths;
191 std::string line;
192 std::size_t line_number = 1;
193 while (input::read_bounded_line(playlist_input, line,
194 "M3U playlist", line_number++)) {
195 line = trimPlaylistLine(std::move(line));
196 if (line.empty() || line.front() == '#') {
197 continue;
198 }
199 if (paths.size() >= input::MAX_AUDIO_PLAYLIST_ENTRIES) {
200 throw std::runtime_error(
201 "M3U playlist contains too many entries");
202 }
203 if (isUrl(line)) {
205 "M3U URL");
206 paths.push_back(std::move(line));
207 continue;
208 }
209
211 "M3U path");
212
213 std::filesystem::path track(line);
214 if (!track.is_absolute()) {
215 track = playlist.parent_path() / track;
216 }
217 paths.push_back(track.lexically_normal().string());
218 }
219 return paths;
220 }
221
223 public:
224#ifdef __linux__
225 FileAudioOutput() : stream(RtAudio::LINUX_PULSE) {}
226#else
227 FileAudioOutput() = default;
228#endif
229
231 close();
232 }
233
234 bool open(const float *source, std::size_t sample_count,
235 int requested_device, float requested_gain) {
236 close();
237 if (source == nullptr || sample_count == 0) {
238 return false;
239 }
240
241 try {
242 const std::vector<unsigned int> device_ids =
243 stream.getDeviceIds();
244 if (device_ids.empty()) {
245 std::cerr << "acmxvk: no audio output devices found\n";
246 return false;
247 }
248
249 const unsigned int device =
250 requested_device >= 0
251 ? static_cast<unsigned int>(requested_device)
252 : stream.getDefaultOutputDevice();
253 if (std::find(device_ids.begin(), device_ids.end(), device) ==
254 device_ids.end()) {
255 std::cerr << "acmxvk: audio output device " << device
256 << " was not found\n";
257 return false;
258 }
259 const RtAudio::DeviceInfo info = stream.getDeviceInfo(device);
260 input::validate_string(info.name,
262 "audio output device name");
263 if (info.outputChannels == 0) {
264 std::cerr << "acmxvk: audio device " << device
265 << " has no output channels\n";
266 return false;
267 }
268
269 output_channels = std::min(2U, info.outputChannels);
270 output_sample_rate = choose_sample_rate(info.sampleRates);
271 source_samples = source;
272 source_sample_count = sample_count;
273 source_position = 0.0;
275 gain = std::clamp(requested_gain, 0.0F, 4.0F);
276 playback_position.store(0, std::memory_order_relaxed);
277 total_playback_position.store(0, std::memory_order_relaxed);
278 completed_loops.store(0, std::memory_order_relaxed);
279 finished.store(false, std::memory_order_relaxed);
280
281 RtAudio::StreamParameters output_parameters;
282 output_parameters.deviceId = device;
283 output_parameters.nChannels = output_channels;
284 output_parameters.firstChannel = 0;
285
286 unsigned int buffer_frames = 512;
287 stream.openStream(&output_parameters, nullptr, RTAUDIO_FLOAT32,
288 output_sample_rate, &buffer_frames,
290 configured = true;
291 std::cout << "acmxvk: file audio output " << device << ": "
292 << info.name << " (" << output_sample_rate << " Hz, "
293 << output_channels << " channel"
294 << (output_channels == 1 ? "" : "s") << ", gain "
295 << gain << ")\n";
296 return true;
297 } catch (const std::exception &error) {
298 std::cerr << "acmxvk: audio output error: " << error.what()
299 << '\n';
300 close();
301 return false;
302 }
303 }
304
305 bool start() {
306 if (!configured || started.load(std::memory_order_acquire)) {
307 return configured;
308 }
309 try {
310 active.store(true, std::memory_order_release);
311 finished.store(false, std::memory_order_release);
312 stream.startStream();
313 started.store(true, std::memory_order_release);
314 std::cout << "acmxvk: file audio playback started\n";
315 return true;
316 } catch (const std::exception &error) {
317 active.store(false, std::memory_order_release);
318 std::cerr << "acmxvk: could not start audio output: "
319 << error.what() << '\n';
320 return false;
321 }
322 }
323
324 void close() {
325 active.store(false, std::memory_order_release);
326 if (stream.isStreamOpen()) {
327 try {
328 if (stream.isStreamRunning()) {
329 stream.stopStream();
330 }
331 stream.closeStream();
332 } catch (const std::exception &error) {
333 std::cerr << "acmxvk: error closing audio output: "
334 << error.what() << '\n';
335 }
336 }
337 configured = false;
338 started.store(false, std::memory_order_release);
339 finished.store(false, std::memory_order_release);
340 source_samples = nullptr;
342 source_position = 0.0;
344 playback_position.store(0, std::memory_order_relaxed);
345 total_playback_position.store(0, std::memory_order_relaxed);
346 completed_loops.store(0, std::memory_order_relaxed);
347 }
348
349 void set_repeat(bool enabled) {
350 repeat.store(enabled, std::memory_order_release);
351 }
352
353 [[nodiscard]] bool is_configured() const {
354 return configured;
355 }
356
357 [[nodiscard]] bool is_started() const {
358 return started.load(std::memory_order_acquire);
359 }
360
361 [[nodiscard]] bool is_finished() const {
362 return finished.load(std::memory_order_acquire);
363 }
364
365 [[nodiscard]] std::size_t position() const {
366 return playback_position.load(std::memory_order_acquire);
367 }
368
369 [[nodiscard]] std::uint64_t total_position() const {
370 return total_playback_position.load(std::memory_order_acquire);
371 }
372
373 [[nodiscard]] std::uint64_t loop_count() const {
374 return completed_loops.load(std::memory_order_acquire);
375 }
376
377 private:
378 [[nodiscard]] static unsigned int
379 choose_sample_rate(const std::vector<unsigned int> &rates) {
380 if (rates.empty() ||
381 std::find(rates.begin(), rates.end(), FILE_SAMPLE_RATE) !=
382 rates.end()) {
383 return FILE_SAMPLE_RATE;
384 }
385 constexpr unsigned int FALLBACK_SAMPLE_RATE = 48000;
386 if (std::find(rates.begin(), rates.end(), FALLBACK_SAMPLE_RATE) !=
387 rates.end()) {
388 return FALLBACK_SAMPLE_RATE;
389 }
390 return rates.front();
391 }
392
393 static int audio_callback(void *output_buffer, void *,
394 unsigned int frame_count, double,
395 RtAudioStreamStatus, void *user_data) {
396 return static_cast<FileAudioOutput *>(user_data)
397 ->write_samples(static_cast<float *>(output_buffer), frame_count);
398 }
399
400 int write_samples(float *output, unsigned int frame_count) {
401 if (output == nullptr) {
402 return 0;
403 }
404
405 const double source_step =
406 static_cast<double>(FILE_SAMPLE_RATE) /
407 static_cast<double>(output_sample_rate);
408 for (unsigned int frame = 0; frame < frame_count; ++frame) {
409 float sample = 0.0F;
410 if (active.load(std::memory_order_relaxed) &&
412 static_cast<double>(source_sample_count)) {
413 if (repeat.load(std::memory_order_relaxed)) {
414 source_position = std::fmod(
416 static_cast<double>(source_sample_count));
417 completed_loops.fetch_add(1,
418 std::memory_order_release);
419 } else {
420 active.store(false, std::memory_order_release);
421 finished.store(true, std::memory_order_release);
423 static_cast<double>(source_sample_count);
424 }
425 }
426
427 const std::size_t index =
428 static_cast<std::size_t>(source_position);
429 if (active.load(std::memory_order_relaxed) &&
430 index < source_sample_count) {
431 const std::size_t next_index =
432 repeat.load(std::memory_order_relaxed)
433 ? (index + 1) % source_sample_count
434 : std::min(index + 1, source_sample_count - 1);
435 const float fraction = static_cast<float>(
436 source_position - static_cast<double>(index));
437 sample = std::clamp(
438 (source_samples[index] +
439 (source_samples[next_index] - source_samples[index]) *
440 fraction) *
441 gain,
442 -1.0F, 1.0F);
443 source_position += source_step;
444 total_source_position += source_step;
445 }
446
447 for (unsigned int channel = 0; channel < output_channels;
448 ++channel) {
449 output[frame * output_channels + channel] = sample;
450 }
451 }
452
453 playback_position.store(
454 std::min(static_cast<std::size_t>(source_position),
456 std::memory_order_release);
458 static_cast<std::uint64_t>(total_source_position),
459 std::memory_order_release);
460 return 0;
461 }
462
463 RtAudio stream;
464 const float *source_samples = nullptr;
465 std::size_t source_sample_count = 0;
466 double source_position = 0.0;
468 unsigned int output_channels = 0;
470 float gain = 1.0F;
471 std::atomic<std::size_t> playback_position{0};
472 std::atomic<std::uint64_t> total_playback_position{0};
473 std::atomic<std::uint64_t> completed_loops{0};
474 std::atomic<bool> active{false};
475 std::atomic<bool> started{false};
476 std::atomic<bool> finished{false};
477 std::atomic<bool> repeat{false};
478 bool configured = false;
479 };
480
481 } // namespace
482
484 public:
486 close();
487 }
488
489 bool open(const std::string &requested_path) {
490 close();
492 "audio file path");
493 const std::filesystem::path source =
494 std::filesystem::absolute(requested_path).lexically_normal();
495 if (!std::filesystem::is_regular_file(source)) {
496 std::cerr << "acmxvk: audio file is not readable: "
497 << source.string() << '\n';
498 return false;
499 }
500
501 const bool playlist = isM3uPath(source);
502 const std::vector<std::string> requested_tracks =
503 playlist ? readM3uPlaylist(source)
504 : std::vector<std::string>{source.string()};
505 if (requested_tracks.empty()) {
506 std::cerr << "acmxvk: M3U playlist contains no tracks: "
507 << source.string() << '\n';
508 return false;
509 }
510
511 av_log_set_level(AV_LOG_ERROR);
512 for (const std::string &track : requested_tracks) {
513 if (decode_track(track)) {
514 track_paths.push_back(track);
515 track_end_positions.push_back(samples.size());
516 } else if (playlist) {
517 std::cerr << "acmxvk: skipping unusable playlist track: "
518 << track << '\n';
519 }
520 }
521 if (track_paths.empty()) {
522 close();
523 return false;
524 }
525
526 source_path = source.string();
527 playlist_source = playlist;
528 playback_position = 0.0;
530 active = true;
531 restart_pending = false;
532 if (playlist) {
533 std::cout << "acmxvk: loaded M3U playlist with "
534 << track_paths.size() << " track(s), "
535 << duration_seconds() << " seconds total: "
536 << source_path << '\n';
538 }
539 return true;
540 }
541
542 bool decode_track(const std::string &requested_path) {
544 requested_path,
545 isUrl(requested_path) ? input::StringKind::Url
547 "audio track");
548 const std::string source =
549 isUrl(requested_path)
550 ? requested_path
551 : std::filesystem::absolute(requested_path)
552 .lexically_normal()
553 .string();
554 if (!isUrl(source) &&
555 !std::filesystem::is_regular_file(std::filesystem::path(source))) {
556 std::cerr << "acmxvk: audio file is not readable: " << source
557 << '\n';
558 return false;
559 }
560 const std::size_t initial_sample_count = samples.size();
561
562 AVFormatContext *format = nullptr;
563 AVCodecContext *codec = nullptr;
564 SwrContext *resampler = nullptr;
565 AVPacket *packet = nullptr;
566 AVFrame *frame = nullptr;
567
568 auto release = [&]() {
569 av_frame_free(&frame);
570 av_packet_free(&packet);
571 swr_free(&resampler);
572 avcodec_free_context(&codec);
573 avformat_close_input(&format);
574 };
575
576 int result = avformat_open_input(&format, source.c_str(), nullptr, nullptr);
577 if (result < 0) {
578 std::cerr << "acmxvk: could not open audio file: "
579 << ffmpegError(result) << '\n';
580 release();
581 return false;
582 }
583 result = avformat_find_stream_info(format, nullptr);
584 if (result < 0) {
585 std::cerr << "acmxvk: could not read audio stream information: "
586 << ffmpegError(result) << '\n';
587 release();
588 return false;
589 }
590
591 const AVCodec *decoder = nullptr;
592 const int stream_index = av_find_best_stream(
593 format, AVMEDIA_TYPE_AUDIO, -1, -1, &decoder, 0);
594 if (stream_index < 0 || decoder == nullptr) {
595 std::cerr << "acmxvk: media file contains no decodable audio stream\n";
596 release();
597 return false;
598 }
599
600 codec = avcodec_alloc_context3(decoder);
601 if (codec == nullptr) {
602 std::cerr << "acmxvk: could not allocate the audio decoder\n";
603 release();
604 return false;
605 }
606 result = avcodec_parameters_to_context(
607 codec, format->streams[stream_index]->codecpar);
608 if (result < 0 || (result = avcodec_open2(codec, decoder, nullptr)) < 0) {
609 std::cerr << "acmxvk: could not initialize the audio decoder: "
610 << ffmpegError(result) << '\n';
611 release();
612 return false;
613 }
614 if (codec->sample_rate <= 0 || codec->ch_layout.nb_channels == 0) {
615 std::cerr << "acmxvk: audio stream has an invalid sample format\n";
616 release();
617 return false;
618 }
619
620 AVChannelLayout output_layout = AV_CHANNEL_LAYOUT_MONO;
621 result = swr_alloc_set_opts2(
622 &resampler, &output_layout, AV_SAMPLE_FMT_FLT,
623 static_cast<int>(FILE_SAMPLE_RATE), &codec->ch_layout,
624 codec->sample_fmt, codec->sample_rate, 0, nullptr);
625 av_channel_layout_uninit(&output_layout);
626 if (result < 0 || resampler == nullptr ||
627 (result = swr_init(resampler)) < 0) {
628 std::cerr << "acmxvk: could not initialize audio resampling: "
629 << ffmpegError(result) << '\n';
630 release();
631 return false;
632 }
633
634 packet = av_packet_alloc();
635 frame = av_frame_alloc();
636 if (packet == nullptr || frame == nullptr) {
637 std::cerr << "acmxvk: could not allocate FFmpeg audio frames\n";
638 release();
639 return false;
640 }
641
642 auto append_frame = [&]() -> bool {
643 const int capacity = static_cast<int>(av_rescale_rnd(
644 swr_get_delay(resampler, codec->sample_rate) + frame->nb_samples,
645 FILE_SAMPLE_RATE, codec->sample_rate, AV_ROUND_UP));
646 if (capacity <= 0) {
647 return true;
648 }
649 std::vector<float> converted(static_cast<std::size_t>(capacity));
650 std::uint8_t *output[] = {
651 reinterpret_cast<std::uint8_t *>(converted.data())};
652 const int count = swr_convert(
653 resampler, output, capacity,
654 const_cast<const std::uint8_t **>(frame->extended_data),
655 frame->nb_samples);
656 if (count < 0) {
657 std::cerr << "acmxvk: audio resampling failed: "
658 << ffmpegError(count) << '\n';
659 return false;
660 }
661 samples.insert(samples.end(), converted.begin(),
662 converted.begin() + count);
663 return true;
664 };
665
666 auto drain_decoder = [&]() -> bool {
667 while (true) {
668 const int receive = avcodec_receive_frame(codec, frame);
669 if (receive == AVERROR(EAGAIN) || receive == AVERROR_EOF) {
670 return true;
671 }
672 if (receive < 0) {
673 std::cerr << "acmxvk: audio decoding failed: "
674 << ffmpegError(receive) << '\n';
675 return false;
676 }
677 if (!append_frame()) {
678 return false;
679 }
680 av_frame_unref(frame);
681 }
682 };
683
684 bool decoded = true;
685 while ((result = av_read_frame(format, packet)) >= 0) {
686 if (packet->stream_index == stream_index) {
687 result = avcodec_send_packet(codec, packet);
688 if (result < 0 || !drain_decoder()) {
689 if (result < 0) {
690 std::cerr << "acmxvk: could not submit audio packet: "
691 << ffmpegError(result) << '\n';
692 }
693 decoded = false;
694 }
695 }
696 av_packet_unref(packet);
697 if (!decoded) {
698 break;
699 }
700 }
701 if (decoded) {
702 result = avcodec_send_packet(codec, nullptr);
703 decoded = (result >= 0 || result == AVERROR_EOF) && drain_decoder();
704 }
705
706 while (decoded) {
707 const int capacity = static_cast<int>(av_rescale_rnd(
708 swr_get_delay(resampler, codec->sample_rate), FILE_SAMPLE_RATE,
709 codec->sample_rate, AV_ROUND_UP));
710 if (capacity <= 0) {
711 break;
712 }
713 std::vector<float> converted(static_cast<std::size_t>(capacity));
714 std::uint8_t *output[] = {
715 reinterpret_cast<std::uint8_t *>(converted.data())};
716 const int count =
717 swr_convert(resampler, output, capacity, nullptr, 0);
718 if (count <= 0) {
719 decoded = count == 0;
720 break;
721 }
722 samples.insert(samples.end(), converted.begin(),
723 converted.begin() + count);
724 }
725
726 release();
727 const std::size_t decoded_sample_count =
728 samples.size() - initial_sample_count;
729 if (!decoded || decoded_sample_count == 0) {
730 samples.resize(initial_sample_count);
731 std::cerr << "acmxvk: audio file produced no usable samples: "
732 << source << '\n';
733 return false;
734 }
735
736 std::cout << "acmxvk: decoded audio track " << source << " ("
737 << static_cast<double>(decoded_sample_count) /
738 static_cast<double>(FILE_SAMPLE_RATE)
739 << " seconds, " << decoded_sample_count
740 << " mono samples at " << FILE_SAMPLE_RATE << " Hz)\n";
741 return true;
742 }
743
744 void close() {
745 output.reset();
746 samples.clear();
747 samples.shrink_to_fit();
748 source_path.clear();
749 track_paths.clear();
750 track_end_positions.clear();
751 playback_position = 0.0;
753 active = false;
754 repeat = false;
755 restart_pending = false;
756 playlist_source = false;
758 }
759
760 void set_repeat(bool enabled) {
761 repeat = enabled;
762 if (output != nullptr) {
763 output->set_repeat(enabled);
764 }
765 }
766
767 bool enable_output(int device, float gain) {
768 if (samples.empty()) {
769 return false;
770 }
771 auto requested_output = std::make_unique<FileAudioOutput>();
772 requested_output->set_repeat(repeat);
773 if (!requested_output->open(samples.data(), samples.size(), device,
774 gain)) {
775 return false;
776 }
777 output = std::move(requested_output);
779 return true;
780 }
781
782 void stop_output() {
783 output.reset();
784 }
785
786 [[nodiscard]] bool has_output_clock() const {
787 return active && output != nullptr && output->is_configured();
788 }
789
790 [[nodiscard]] double playback_time() const {
791 if (!has_output_clock()) {
792 return 0.0;
793 }
794 return static_cast<double>(output->total_position()) /
795 static_cast<double>(FILE_SAMPLE_RATE);
796 }
797
798 bool mux_into_video(const std::string &requested_video_path,
799 double video_duration) {
800 output.reset();
801 if (samples.empty() || !std::isfinite(video_duration) ||
802 video_duration <= 0.0) {
803 std::cerr << "acmxvk: cannot mux "
804 << (live_recording_source ? "live audio input"
805 : "file audio")
806 << " without samples and a positive video duration\n";
807 return false;
808 }
809
810 const std::filesystem::path video_path =
811 std::filesystem::absolute(requested_video_path).lexically_normal();
812 if (!std::filesystem::is_regular_file(video_path)) {
813 std::cerr << "acmxvk: encoded video is not readable for audio mux: "
814 << video_path.string() << '\n';
815 return false;
816 }
817
818 const double source_duration = duration_seconds();
819 const double mux_duration =
820 repeat ? video_duration : std::min(video_duration, source_duration);
821 const std::int64_t target_sample_count =
822 static_cast<std::int64_t>(std::floor(
823 mux_duration * static_cast<double>(FILE_SAMPLE_RATE)));
824 if (target_sample_count <= 0) {
825 std::cerr << "acmxvk: file audio mux duration is empty\n";
826 return false;
827 }
828
829 const auto unique_value = std::chrono::steady_clock::now()
830 .time_since_epoch()
831 .count();
832 const std::filesystem::path temporary_path =
833 video_path.parent_path() /
834 (video_path.stem().string() + ".acmxvk-mux-" +
835 std::to_string(unique_value) + video_path.extension().string());
836 const std::string video_url = ffmpegPath(video_path);
837 const std::string temporary_url = ffmpegPath(temporary_path);
838
839 AVFormatContext *input_context = nullptr;
840 AVFormatContext *output_context = nullptr;
841 AVCodecContext *audio_encoder = nullptr;
842 SwrContext *resampler = nullptr;
843 AVFrame *audio_frame = nullptr;
844 AVPacket *input_packet = nullptr;
845 AVPacket *audio_packet = nullptr;
846
847 auto cleanup = [&]() {
848 av_packet_free(&audio_packet);
849 av_packet_free(&input_packet);
850 av_frame_free(&audio_frame);
851 swr_free(&resampler);
852 avcodec_free_context(&audio_encoder);
853 avformat_close_input(&input_context);
854 if (output_context != nullptr) {
855 if ((output_context->oformat->flags & AVFMT_NOFILE) == 0) {
856 avio_closep(&output_context->pb);
857 }
858 avformat_free_context(output_context);
859 output_context = nullptr;
860 }
861 };
862
863 auto fail = [&](std::string_view message, int error) {
864 std::cerr << "acmxvk: " << message;
865 if (error < 0) {
866 std::cerr << ": " << ffmpegError(error);
867 }
868 std::cerr << '\n';
869 cleanup();
870 std::error_code remove_error;
871 std::filesystem::remove(temporary_path, remove_error);
872 return false;
873 };
874
875 int result = avformat_open_input(&input_context, video_url.c_str(),
876 nullptr, nullptr);
877 if (result < 0) {
878 return fail("could not open encoded video for audio mux", result);
879 }
880 result = avformat_find_stream_info(input_context, nullptr);
881 if (result < 0) {
882 return fail("could not read encoded video stream information",
883 result);
884 }
885 const int input_video_index = av_find_best_stream(
886 input_context, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
887 if (input_video_index < 0) {
888 return fail("encoded output contains no video stream",
889 input_video_index);
890 }
891
892 result = avformat_alloc_output_context2(
893 &output_context, nullptr, nullptr, temporary_url.c_str());
894 if (result < 0 || output_context == nullptr) {
895 return fail("could not create audio-mux output container", result);
896 }
897
898 AVStream *input_video = input_context->streams[input_video_index];
899 AVStream *output_video = avformat_new_stream(output_context, nullptr);
900 if (output_video == nullptr) {
901 return fail("could not create remuxed video stream", AVERROR(ENOMEM));
902 }
903 result = avcodec_parameters_copy(output_video->codecpar,
904 input_video->codecpar);
905 if (result < 0) {
906 return fail("could not copy encoded video parameters", result);
907 }
908 output_video->codecpar->codec_tag = 0;
909 output_video->time_base = input_video->time_base;
910 output_video->avg_frame_rate = input_video->avg_frame_rate;
911
912 const AVCodec *aac_encoder = avcodec_find_encoder(AV_CODEC_ID_AAC);
913 if (aac_encoder == nullptr) {
914 return fail("linked FFmpeg has no AAC encoder", AVERROR_ENCODER_NOT_FOUND);
915 }
916 AVStream *output_audio =
917 avformat_new_stream(output_context, aac_encoder);
918 if (output_audio == nullptr) {
919 return fail("could not create encoded audio stream", AVERROR(ENOMEM));
920 }
921 audio_encoder = avcodec_alloc_context3(aac_encoder);
922 if (audio_encoder == nullptr) {
923 return fail("could not allocate AAC encoder", AVERROR(ENOMEM));
924 }
925 audio_encoder->bit_rate = 192000;
926 audio_encoder->sample_fmt = AV_SAMPLE_FMT_FLTP;
927 audio_encoder->sample_rate = static_cast<int>(FILE_SAMPLE_RATE);
928 audio_encoder->time_base =
929 AVRational{1, static_cast<int>(FILE_SAMPLE_RATE)};
930 av_channel_layout_default(&audio_encoder->ch_layout, 1);
931 if ((output_context->oformat->flags & AVFMT_GLOBALHEADER) != 0) {
932 audio_encoder->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
933 }
934 result = avcodec_open2(audio_encoder, aac_encoder, nullptr);
935 if (result < 0) {
936 return fail("could not initialize AAC encoder", result);
937 }
938 result = avcodec_parameters_from_context(output_audio->codecpar,
939 audio_encoder);
940 if (result < 0) {
941 return fail("could not export AAC stream parameters", result);
942 }
943 output_audio->codecpar->codec_tag = 0;
944 output_audio->time_base = audio_encoder->time_base;
945
946 AVChannelLayout input_layout = AV_CHANNEL_LAYOUT_MONO;
947 result = swr_alloc_set_opts2(
948 &resampler, &audio_encoder->ch_layout, audio_encoder->sample_fmt,
949 audio_encoder->sample_rate, &input_layout, AV_SAMPLE_FMT_FLT,
950 static_cast<int>(FILE_SAMPLE_RATE), 0, nullptr);
951 av_channel_layout_uninit(&input_layout);
952 if (result < 0 || resampler == nullptr ||
953 (result = swr_init(resampler)) < 0) {
954 return fail("could not initialize audio mux resampler", result);
955 }
956
957 const int audio_frame_capacity =
958 audio_encoder->frame_size > 0 ? audio_encoder->frame_size : 1024;
959 audio_frame = av_frame_alloc();
960 input_packet = av_packet_alloc();
961 audio_packet = av_packet_alloc();
962 if (audio_frame == nullptr || input_packet == nullptr ||
963 audio_packet == nullptr) {
964 return fail("could not allocate audio mux frames", AVERROR(ENOMEM));
965 }
966 audio_frame->format = audio_encoder->sample_fmt;
967 audio_frame->sample_rate = audio_encoder->sample_rate;
968 audio_frame->nb_samples = audio_frame_capacity;
969 result = av_channel_layout_copy(&audio_frame->ch_layout,
970 &audio_encoder->ch_layout);
971 if (result < 0 || (result = av_frame_get_buffer(audio_frame, 0)) < 0) {
972 return fail("could not allocate AAC sample buffer", result);
973 }
974
975 if ((output_context->oformat->flags & AVFMT_NOFILE) == 0) {
976 result = avio_open(&output_context->pb, temporary_url.c_str(),
977 AVIO_FLAG_WRITE);
978 if (result < 0) {
979 return fail("could not open temporary mux output", result);
980 }
981 }
982 result = avformat_write_header(output_context, nullptr);
983 if (result < 0) {
984 return fail("could not write audio-mux container header", result);
985 }
986
987 std::int64_t source_position = 0;
988 std::int64_t encoded_position = 0;
989 std::vector<float> input_samples(
990 static_cast<std::size_t>(audio_frame_capacity));
991
992 auto drain_audio_packets = [&]() {
993 while (true) {
994 const int receive =
995 avcodec_receive_packet(audio_encoder, audio_packet);
996 if (receive == AVERROR(EAGAIN) || receive == AVERROR_EOF) {
997 return true;
998 }
999 if (receive < 0) {
1000 result = receive;
1001 return false;
1002 }
1003 audio_packet->stream_index = output_audio->index;
1004 av_packet_rescale_ts(audio_packet, audio_encoder->time_base,
1005 output_audio->time_base);
1006 const int write =
1007 av_interleaved_write_frame(output_context, audio_packet);
1008 av_packet_unref(audio_packet);
1009 if (write < 0) {
1010 result = write;
1011 return false;
1012 }
1013 }
1014 };
1015
1016 auto encode_audio_frame = [&]() {
1017 const std::int64_t remaining =
1018 target_sample_count - source_position;
1019 if (remaining <= 0) {
1020 return true;
1021 }
1022 const int source_count = static_cast<int>(
1023 std::min<std::int64_t>(remaining, audio_frame_capacity));
1024 int submitted_count = source_count;
1025 if (source_count < audio_frame_capacity &&
1026 (aac_encoder->capabilities &
1027 AV_CODEC_CAP_SMALL_LAST_FRAME) == 0) {
1028 submitted_count = audio_frame_capacity;
1029 }
1030 for (int index = 0; index < submitted_count; ++index) {
1031 if (index >= source_count) {
1032 input_samples[static_cast<std::size_t>(index)] = 0.0F;
1033 continue;
1034 }
1035 const std::size_t sample_index = repeat
1036 ? static_cast<std::size_t>(source_position + index) %
1037 samples.size()
1038 : static_cast<std::size_t>(source_position + index);
1039 input_samples[static_cast<std::size_t>(index)] =
1040 samples[sample_index];
1041 }
1042
1043 audio_frame->nb_samples = submitted_count;
1044 result = av_frame_make_writable(audio_frame);
1045 if (result < 0) {
1046 return false;
1047 }
1048 const std::uint8_t *input_data[] = {
1049 reinterpret_cast<const std::uint8_t *>(input_samples.data())};
1050 const int converted = swr_convert(
1051 resampler, audio_frame->data, submitted_count, input_data,
1052 submitted_count);
1053 if (converted < 0) {
1054 result = converted;
1055 return false;
1056 }
1057 audio_frame->nb_samples = converted;
1058 audio_frame->pts = encoded_position;
1059 result = avcodec_send_frame(audio_encoder, audio_frame);
1060 if (result < 0 || !drain_audio_packets()) {
1061 return false;
1062 }
1063 source_position += source_count;
1064 encoded_position += converted;
1065 return true;
1066 };
1067
1068 bool video_complete = false;
1069 while ((result = av_read_frame(input_context, input_packet)) >= 0) {
1070 if (input_packet->stream_index != input_video_index) {
1071 av_packet_unref(input_packet);
1072 continue;
1073 }
1074 const std::int64_t timestamp =
1075 input_packet->pts != AV_NOPTS_VALUE ? input_packet->pts
1076 : input_packet->dts;
1077 const double packet_time =
1078 timestamp == AV_NOPTS_VALUE
1079 ? 0.0
1080 : static_cast<double>(timestamp) *
1081 av_q2d(input_video->time_base);
1082 if (timestamp != AV_NOPTS_VALUE && packet_time > mux_duration) {
1083 av_packet_unref(input_packet);
1084 video_complete = true;
1085 break;
1086 }
1087
1088 const std::int64_t audio_target = std::min<std::int64_t>(
1089 target_sample_count,
1090 static_cast<std::int64_t>(std::ceil(
1091 (packet_time +
1092 static_cast<double>(audio_frame_capacity) /
1093 static_cast<double>(FILE_SAMPLE_RATE)) *
1094 static_cast<double>(FILE_SAMPLE_RATE))));
1095 while (source_position < audio_target) {
1096 if (!encode_audio_frame()) {
1097 return fail("could not encode AAC samples", result);
1098 }
1099 }
1100
1101 av_packet_rescale_ts(input_packet, input_video->time_base,
1102 output_video->time_base);
1103 input_packet->stream_index = output_video->index;
1104 input_packet->pos = -1;
1105 result = av_interleaved_write_frame(output_context, input_packet);
1106 av_packet_unref(input_packet);
1107 if (result < 0) {
1108 return fail("could not remux encoded video packet", result);
1109 }
1110 }
1111 if (!video_complete && result != AVERROR_EOF) {
1112 return fail("could not finish reading encoded video", result);
1113 }
1114
1115 while (source_position < target_sample_count) {
1116 if (!encode_audio_frame()) {
1117 return fail("could not finish AAC encoding", result);
1118 }
1119 }
1120 result = avcodec_send_frame(audio_encoder, nullptr);
1121 if (result < 0 || !drain_audio_packets()) {
1122 return fail("could not flush AAC encoder", result);
1123 }
1124 result = av_write_trailer(output_context);
1125 if (result < 0) {
1126 return fail("could not finalize audio-mux container", result);
1127 }
1128 if ((output_context->oformat->flags & AVFMT_NOFILE) == 0) {
1129 result = avio_closep(&output_context->pb);
1130 if (result < 0) {
1131 return fail("could not flush temporary mux output", result);
1132 }
1133 }
1134
1135 cleanup();
1136 std::error_code replace_error;
1137 if (!replaceFile(temporary_path, video_path, replace_error)) {
1138 std::cerr << "acmxvk: could not atomically replace encoded video "
1139 "with muxed output: "
1140 << replace_error.message() << '\n';
1141 std::error_code remove_error;
1142 std::filesystem::remove(temporary_path, remove_error);
1143 return false;
1144 }
1145
1146 std::cout << "acmxvk: muxed "
1148 ? "live audio input"
1149 : (track_paths.size() > 1 ? "audio playlist"
1150 : "audio file"))
1151 << " into " << video_path.string() << " (" << mux_duration
1152 << " seconds" << (repeat ? ", repeated" : "") << ")\n";
1153 return true;
1154 }
1155
1156 [[nodiscard]] double duration_seconds() const {
1157 return static_cast<double>(samples.size()) /
1158 static_cast<double>(FILE_SAMPLE_RATE);
1159 }
1160
1161 [[nodiscard]] const std::string &current_track_path() const {
1162 static const std::string EMPTY_PATH;
1163 if (!active || track_paths.empty() ||
1164 current_track_index >= track_paths.size()) {
1165 return EMPTY_PATH;
1166 }
1168 }
1169
1171 if (!playlist_source || track_paths.empty()) {
1172 return;
1173 }
1174 std::cout << "acmxvk: audio playlist track "
1175 << (current_track_index + 1) << '/' << track_paths.size()
1176 << ": " << current_track_path() << '\n';
1177 }
1178
1179 void update_current_track(double position) {
1180 while (current_track_index + 1 < track_end_positions.size() &&
1181 position >= static_cast<double>(
1185 }
1186 }
1187
1188 bool process_output_frame(double frames_per_second, AudioEngine &engine) {
1189 if (output->is_finished()) {
1190 active = false;
1191 engine.reset();
1192 std::cout << "acmxvk: audio "
1193 << (playlist_source ? "playlist" : "file")
1194 << " reached end of output stream\n";
1195 return false;
1196 }
1197
1198 const std::uint64_t output_loops = output->loop_count();
1199 if (output_loops != observed_output_loops) {
1200 observed_output_loops = output_loops;
1202 engine.reset();
1203 std::cout << "acmxvk: audio "
1204 << (playlist_source ? "playlist" : "file")
1205 << " reached end of output stream; "
1206 "restarting (--audio-repeat)\n";
1208 }
1209
1210 playback_position = static_cast<double>(output->position());
1212 const double samples_per_frame =
1213 static_cast<double>(FILE_SAMPLE_RATE) / frames_per_second;
1214 const std::size_t first = std::min(
1215 static_cast<std::size_t>(playback_position), samples.size());
1216 const std::size_t last = std::min(
1217 std::max(first + 1,
1218 static_cast<std::size_t>(playback_position +
1219 samples_per_frame)),
1220 samples.size());
1221 if (first < last) {
1222 engine.process_samples(samples.data() + first,
1223 static_cast<unsigned int>(last - first), 1,
1224 FILE_SAMPLE_RATE);
1225 }
1226
1227 if (!output->is_started() && !output->start()) {
1228 std::cerr << "acmxvk: continuing with silent file-audio "
1229 "analysis\n";
1230 output.reset();
1231 playback_position = static_cast<double>(last);
1232 }
1233 return true;
1234 }
1235
1236 bool process_frame(double frames_per_second, AudioEngine &engine) {
1237 if (samples.empty() || !active) {
1238 engine.reset();
1239 return false;
1240 }
1241 if (restart_pending) {
1242 playback_position = 0.0;
1244 restart_pending = false;
1245 engine.reset();
1247 }
1248 const double rate =
1249 std::isfinite(frames_per_second) && frames_per_second > 0.0
1250 ? frames_per_second
1251 : 60.0;
1252 if (has_output_clock()) {
1253 return process_output_frame(rate, engine);
1254 }
1255 const double next_position =
1256 std::min(playback_position +
1257 static_cast<double>(FILE_SAMPLE_RATE) / rate,
1258 static_cast<double>(samples.size()));
1259 const std::size_t first = static_cast<std::size_t>(playback_position);
1260 const std::size_t last = std::min(
1261 std::max(first + 1, static_cast<std::size_t>(next_position)),
1262 samples.size());
1263 engine.process_samples(samples.data() + first,
1264 static_cast<unsigned int>(last - first), 1,
1265 FILE_SAMPLE_RATE);
1266 playback_position = next_position;
1268 if (playback_position >= static_cast<double>(samples.size())) {
1269 if (repeat) {
1270 restart_pending = true;
1271 std::cout << "acmxvk: audio "
1272 << (playlist_source ? "playlist" : "file")
1273 << " reached end of stream; "
1274 "restarting (--audio-repeat)\n";
1275 } else {
1276 active = false;
1277 std::cout << "acmxvk: audio "
1278 << (playlist_source ? "playlist" : "file")
1279 << " reached end of stream\n";
1280 }
1281 }
1282 return true;
1283 }
1284
1285 bool process_at_time(double seconds, double frames_per_second,
1286 AudioEngine &engine) {
1287 if (samples.empty() || !std::isfinite(seconds) || seconds < 0.0) {
1288 engine.reset();
1289 return false;
1290 }
1291
1292 const double rate =
1293 std::isfinite(frames_per_second) && frames_per_second > 0.0
1294 ? frames_per_second
1295 : 60.0;
1296 double target_position =
1297 seconds * static_cast<double>(FILE_SAMPLE_RATE);
1298 if (repeat) {
1299 target_position = std::fmod(
1300 target_position, static_cast<double>(samples.size()));
1301 if (target_position < playback_position) {
1303 }
1304 active = true;
1305 } else if (target_position >= static_cast<double>(samples.size())) {
1306 active = false;
1307 playback_position = static_cast<double>(samples.size());
1308 engine.reset();
1309 return false;
1310 }
1311
1312 playback_position = std::max(0.0, target_position);
1314 const double samples_per_frame =
1315 static_cast<double>(FILE_SAMPLE_RATE) / rate;
1316 const std::size_t first = std::min(
1317 static_cast<std::size_t>(playback_position), samples.size());
1318 const std::size_t last = std::min(
1319 std::max(first + 1,
1320 static_cast<std::size_t>(playback_position +
1321 samples_per_frame)),
1322 samples.size());
1323 if (first >= last) {
1324 engine.reset();
1325 return false;
1326 }
1327 engine.process_samples(samples.data() + first,
1328 static_cast<unsigned int>(last - first), 1,
1329 FILE_SAMPLE_RATE);
1330 return true;
1331 }
1332
1333 std::vector<float> samples;
1334 std::string source_path;
1335 std::vector<std::string> track_paths;
1336 std::vector<std::size_t> track_end_positions;
1337 std::unique_ptr<FileAudioOutput> output;
1338 double playback_position = 0.0;
1339 std::size_t current_track_index = 0;
1340 std::uint64_t observed_output_loops = 0;
1341 bool active = false;
1342 bool repeat = false;
1343 bool restart_pending = false;
1344 bool playlist_source = false;
1346 };
1347
1348 FileAudioSource::FileAudioSource() : impl(std::make_unique<Impl>()) {}
1350
1351 bool FileAudioSource::open(const std::string &path) {
1352 return impl->open(path);
1353 }
1354
1356 impl->close();
1357 }
1358
1359 void FileAudioSource::set_repeat(bool enabled) {
1360 impl->set_repeat(enabled);
1361 }
1362
1363 bool FileAudioSource::enable_output(int device, float gain) {
1364 return impl->enable_output(device, gain);
1365 }
1366
1368 impl->stop_output();
1369 }
1370
1372 return impl->has_output_clock();
1373 }
1374
1376 return impl->playback_time();
1377 }
1378
1379 bool FileAudioSource::mux_into_video(const std::string &video_path,
1380 double video_duration) {
1381 return impl->mux_into_video(video_path, video_duration);
1382 }
1383
1385 std::vector<float> samples, unsigned int sample_rate,
1386 const std::string &video_path, double video_duration) {
1387 if (!resampleMonoRecording(samples, sample_rate)) {
1388 return false;
1389 }
1390
1391 FileAudioSource source;
1392 source.impl->samples = std::move(samples);
1393 source.impl->track_paths.emplace_back("live audio input");
1394 source.impl->live_recording_source = true;
1395 return source.impl->mux_into_video(video_path, video_duration);
1396 }
1397
1399 return !impl->samples.empty();
1400 }
1401
1403 return impl->active;
1404 }
1405
1407 return impl->duration_seconds();
1408 }
1409
1410 const std::string &FileAudioSource::path() const {
1411 return impl->source_path;
1412 }
1413
1414 std::size_t FileAudioSource::track_count() const {
1415 return impl->track_paths.size();
1416 }
1417
1418 const std::string &FileAudioSource::current_track_path() const {
1419 return impl->current_track_path();
1420 }
1421
1422 bool FileAudioSource::process_frame(double frames_per_second,
1423 AudioEngine &engine) {
1424 return impl->process_frame(frames_per_second, engine);
1425 }
1426
1428 double frames_per_second,
1429 AudioEngine &engine) {
1430 return impl->process_at_time(seconds, frames_per_second, engine);
1431 }
1432
1433} // namespace acmxvk::audio
void process_samples(const float *samples, unsigned int frame_count, unsigned int channels, unsigned int sample_rate)
bool process_at_time(double seconds, double frames_per_second, AudioEngine &engine)
std::unique_ptr< FileAudioOutput > output
std::vector< std::size_t > track_end_positions
bool enable_output(int device, float gain)
bool decode_track(const std::string &requested_path)
bool open(const std::string &requested_path)
bool mux_into_video(const std::string &requested_video_path, double video_duration)
bool process_frame(double frames_per_second, AudioEngine &engine)
bool process_output_frame(double frames_per_second, AudioEngine &engine)
const std::string & current_track_path() const
bool process_frame(double frames_per_second, AudioEngine &engine)
bool process_at_time(double seconds, double frames_per_second, AudioEngine &engine)
const std::string & current_track_path() const
bool enable_output(int device=-1, float gain=1.0F)
bool mux_into_video(const std::string &video_path, double video_duration)
bool open(const std::string &path)
static bool mux_recording_into_video(std::vector< float > samples, unsigned int sample_rate, const std::string &video_path, double video_duration)
const std::string & path() const
static unsigned int choose_sample_rate(const std::vector< unsigned int > &rates)
static int audio_callback(void *output_buffer, void *, unsigned int frame_count, double, RtAudioStreamStatus, void *user_data)
bool open(const float *source, std::size_t sample_count, int requested_device, float requested_gain)
std::vector< std::string > readM3uPlaylist(const std::filesystem::path &playlist)
bool resampleMonoRecording(std::vector< float > &samples, unsigned int source_rate)
bool replaceFile(const std::filesystem::path &source, const std::filesystem::path &destination, std::error_code &error)
bool isM3uPath(const std::filesystem::path &path)
std::string ffmpegPath(const std::filesystem::path &path)
bool read_bounded_line(std::istream &input, std::string &line, std::string_view context, std::size_t line_number, std::size_t maximum_bytes)
constexpr std::size_t MAX_AUDIO_PLAYLIST_ENTRIES
void validate_file_size(const std::filesystem::path &path, std::string_view context, std::uintmax_t maximum_bytes)
void validate_string(std::string_view value, StringKind kind, std::string_view context, bool allow_empty)