ACMX 2.139.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
ACMX2/file_audio.cpp
Go to the documentation of this file.
1/**
2 * @file file_audio.cpp
3 * @brief FFmpeg-based audio file decoder for audio-reactive shaders.
4 *
5 * Decodes an entire audio file upfront into a mono float buffer at
6 * 44 100 Hz using FFmpeg's libavformat / libavcodec / libswresample.
7 * Each video frame, file_audio_process_frame() advances through the buffer
8 * and feeds the shared AudioAnalyzer used by the shader uniform pipeline.
9 */
10
11#include "file_audio.hpp"
12#include "audio.hpp"
13
14#include <RtAudio.h>
15
16#include <algorithm>
17#include <atomic>
18#include <cctype>
19#include <cmath>
20#include <cstring>
21#include <filesystem>
22#include <fstream>
23#include <iostream>
24#include <memory>
25#include <string>
26#include <vector>
27
28extern "C" {
29#include <libavcodec/avcodec.h>
30#include <libavformat/avformat.h>
31#include <libavutil/opt.h>
32#include <libswresample/swresample.h>
33}
34
35static AVFormatContext *fmtCtx = nullptr;
36static AVCodecContext *codecCtx = nullptr;
37static SwrContext *swrCtx = nullptr;
38static int audioStreamIndex = -1;
39static std::vector<float> decodedSamples; // all decoded mono float samples at 44100 Hz
40static size_t playbackPos = 0;
41static double framePlaybackPos = 0.0;
42static std::atomic<bool> fileAudioActive{false};
43static std::atomic<bool> fileAudioRepeat{false};
44static std::vector<std::string> fileAudioSourcePaths;
45static std::vector<std::size_t> fileAudioSourceEndPositions;
46
47namespace {
48
49 constexpr unsigned int FILE_AUDIO_SAMPLE_RATE = 44100;
50
52 public:
53#ifdef __linux__
54 FileAudioOutput() : audio(RtAudio::LINUX_PULSE) {}
55#else
56 FileAudioOutput() = default;
57#endif
58
60
61 bool open(const float *samples, std::size_t sample_count, int output_device) {
62 close();
63 if (samples == nullptr || sample_count == 0)
64 return false;
65
66 try {
67 const std::vector<unsigned int> device_ids = audio.getDeviceIds();
68 if (device_ids.empty()) {
69 std::cerr << "acmx2: file_audio: No audio output devices found\n";
70 return false;
71 }
72
73 const unsigned int device = output_device >= 0 ? static_cast<unsigned int>(output_device) : audio.getDefaultOutputDevice();
74 const RtAudio::DeviceInfo info = audio.getDeviceInfo(device);
75 if (info.outputChannels == 0) {
76 std::cerr << "acmx2: file_audio: Selected device has no output channels\n";
77 return false;
78 }
79
80 output_channels = std::min(2U, info.outputChannels);
81 output_sample_rate = choose_sample_rate(info.sampleRates);
82 source_samples = samples;
83 source_sample_count = sample_count;
84 source_position = 0.0;
85 playback_position.store(0, std::memory_order_relaxed);
86 total_playback_position.store(0, std::memory_order_relaxed);
87
88 RtAudio::StreamParameters output_parameters;
89 output_parameters.deviceId = device;
90 output_parameters.nChannels = output_channels;
91 output_parameters.firstChannel = 0;
92
93 unsigned int buffer_frames = 512;
94 audio.openStream(&output_parameters, nullptr, RTAUDIO_FLOAT32, output_sample_rate, &buffer_frames, &FileAudioOutput::audio_callback, this);
95 configured = true;
96 std::cout << "acmx2: file_audio: Playback configured on device " << device << ": " << info.name << " (" << output_channels << " ch, " << output_sample_rate << " Hz)\n";
97 return true;
98 } catch (const std::exception &error) {
99 std::cerr << "acmx2: file_audio: Could not open output stream: " << error.what() << "\n";
100 close();
101 return false;
102 }
103 }
104
105 bool start() {
106 if (!configured || started.load(std::memory_order_acquire))
107 return configured;
108 try {
109 active.store(true, std::memory_order_release);
110 started.store(true, std::memory_order_release);
111 audio.startStream();
112 std::cout << "acmx2: file_audio: Playback started\n";
113 return true;
114 } catch (const std::exception &error) {
115 active.store(false, std::memory_order_release);
116 started.store(false, std::memory_order_release);
117 std::cerr << "acmx2: file_audio: Could not start output stream: " << error.what() << "\n";
118 return false;
119 }
120 }
121
122 void close() {
123 active.store(false, std::memory_order_release);
124 if (audio.isStreamOpen()) {
125 try {
126 if (audio.isStreamRunning())
127 audio.stopStream();
128 audio.closeStream();
129 } catch (const std::exception &error) {
130 std::cerr << "acmx2: file_audio: Error closing output stream: " << error.what() << "\n";
131 }
132 }
133 configured = false;
134 started.store(false, std::memory_order_release);
135 source_samples = nullptr;
137 source_position = 0.0;
139 playback_position.store(0, std::memory_order_relaxed);
140 total_playback_position.store(0, std::memory_order_relaxed);
141 }
142
143 bool is_configured() const { return configured; }
144
145 bool is_started() const { return started.load(std::memory_order_acquire); }
146
147 std::size_t position() const { return playback_position.load(std::memory_order_acquire); }
148
149 std::size_t total_position() const { return total_playback_position.load(std::memory_order_acquire); }
150
151 private:
152 static unsigned int choose_sample_rate(const std::vector<unsigned int> &rates) {
153 if (rates.empty() || std::find(rates.begin(), rates.end(), FILE_AUDIO_SAMPLE_RATE) != rates.end())
155 if (std::find(rates.begin(), rates.end(), 48000U) != rates.end())
156 return 48000;
157 return rates.front();
158 }
159
160 static int audio_callback(void *output_buffer, void *, unsigned int frame_count, double, RtAudioStreamStatus, void *user_data) { return static_cast<FileAudioOutput *>(user_data)->write_samples(static_cast<float *>(output_buffer), frame_count); }
161
162 int write_samples(float *output, unsigned int frame_count) {
163 if (output == nullptr)
164 return 0;
165
166 const double source_step = static_cast<double>(FILE_AUDIO_SAMPLE_RATE) / static_cast<double>(output_sample_rate);
167 for (unsigned int frame = 0; frame < frame_count; ++frame) {
168 float sample = 0.0f;
169 if (active.load(std::memory_order_relaxed) && source_position >= static_cast<double>(source_sample_count)) {
170 if (fileAudioRepeat.load(std::memory_order_relaxed)) {
171 source_position = std::fmod(source_position, static_cast<double>(source_sample_count));
172 } else {
173 active.store(false, std::memory_order_release);
174 source_position = static_cast<double>(source_sample_count);
175 }
176 }
177
178 const std::size_t index = static_cast<std::size_t>(source_position);
179 if (active.load(std::memory_order_relaxed) && index < source_sample_count) {
180 const std::size_t next_index = fileAudioRepeat.load(std::memory_order_relaxed) ? (index + 1) % source_sample_count : std::min(index + 1, source_sample_count - 1);
181 const float fraction = static_cast<float>(source_position - static_cast<double>(index));
182 sample = source_samples[index] + (source_samples[next_index] - source_samples[index]) * fraction;
183 source_position += source_step;
184 total_source_position += source_step;
185 } else if (!fileAudioRepeat.load(std::memory_order_relaxed)) {
186 active.store(false, std::memory_order_release);
187 source_position = static_cast<double>(source_sample_count);
188 }
189
190 for (unsigned int channel = 0; channel < output_channels; ++channel)
191 output[frame * output_channels + channel] = sample;
192 }
193
194 playback_position.store(std::min(static_cast<std::size_t>(source_position), source_sample_count), std::memory_order_release);
195 total_playback_position.store(static_cast<std::size_t>(total_source_position), std::memory_order_release);
196 return 0;
197 }
198
199 RtAudio audio;
200 const float *source_samples = nullptr;
201 std::size_t source_sample_count = 0;
202 double source_position = 0.0;
204 unsigned int output_channels = 0;
206 std::atomic<std::size_t> playback_position{0};
207 std::atomic<std::size_t> total_playback_position{0};
208 std::atomic<bool> started{false};
209 std::atomic<bool> active{false};
210 bool configured = false;
211 };
212
213 std::unique_ptr<FileAudioOutput> fileAudioOutput;
214
215} // namespace
216
218 if (swrCtx)
219 swr_free(&swrCtx);
220 if (codecCtx)
221 avcodec_free_context(&codecCtx);
222 if (fmtCtx)
223 avformat_close_input(&fmtCtx);
224 audioStreamIndex = -1;
225}
226
227static std::string trimPlaylistLine(std::string line) {
228 const std::string whitespace = " \t\r\n";
229 const std::size_t first = line.find_first_not_of(whitespace);
230 if (first == std::string::npos)
231 return {};
232 const std::size_t last = line.find_last_not_of(whitespace);
233 return line.substr(first, last - first + 1);
234}
235
236static bool isM3uPath(const std::string &filepath) {
237 std::string extension = std::filesystem::path(filepath).extension().string();
238 std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value) { return static_cast<char>(std::tolower(value)); });
239 return extension == ".m3u" || extension == ".m3u8";
240}
241
242static std::vector<std::string> readM3uPlaylist(const std::string &filepath) {
243 std::ifstream input(filepath);
244 if (!input) {
245 std::cerr << "acmx2: file_audio: Cannot open M3U playlist: " << filepath << "\n";
246 return {};
247 }
248
249 const std::filesystem::path playlistDirectory = std::filesystem::absolute(std::filesystem::path(filepath)).parent_path();
250 std::vector<std::string> paths;
251 std::string line;
252 bool firstLine = true;
253 while (std::getline(input, line)) {
254 if (firstLine && line.size() >= 3 && static_cast<unsigned char>(line[0]) == 0xef && static_cast<unsigned char>(line[1]) == 0xbb && static_cast<unsigned char>(line[2]) == 0xbf) {
255 line.erase(0, 3);
256 }
257 firstLine = false;
258 line = trimPlaylistLine(std::move(line));
259 if (line.empty() || line.front() == '#')
260 continue;
261
262 if (line.find("://") != std::string::npos) {
263 paths.push_back(line);
264 continue;
265 }
266
267 std::filesystem::path trackPath(line);
268 if (!trackPath.is_absolute())
269 trackPath = playlistDirectory / trackPath;
270 paths.push_back(trackPath.lexically_normal().string());
271 }
272 return paths;
273}
274
275/**
276 * @brief Decode every audio packet from the open format context.
277 *
278 * Reads packets from @c fmtCtx, sends them through the codec, and
279 * resamples the output to mono float 44 100 Hz via @c swrCtx.
280 * Decoded samples are appended to the module-level @c decodedSamples
281 * vector. The resampler is flushed at the end to capture trailing
282 * samples.
283 *
284 * @return @c true if decoding completed without fatal errors.
285 */
286static bool decodeAllSamples() {
287 AVPacket *pkt = av_packet_alloc();
288 AVFrame *frame = av_frame_alloc();
289 if (!pkt || !frame) {
290 if (pkt)
291 av_packet_free(&pkt);
292 if (frame)
293 av_frame_free(&frame);
294 return false;
295 }
296
297 while (av_read_frame(fmtCtx, pkt) >= 0) {
298 if (pkt->stream_index == audioStreamIndex) {
299 int ret = avcodec_send_packet(codecCtx, pkt);
300 while (ret >= 0) {
301 ret = avcodec_receive_frame(codecCtx, frame);
302 if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
303 break;
304 if (ret < 0) {
305 av_packet_unref(pkt);
306 av_frame_free(&frame);
307 av_packet_free(&pkt);
308 return false;
309 }
310
311 // Resample to mono float 44100 Hz
312 int outSamples = swr_get_out_samples(swrCtx, frame->nb_samples);
313 std::vector<float> buf(outSamples);
314 uint8_t *outBuf = reinterpret_cast<uint8_t *>(buf.data());
315 int converted = swr_convert(swrCtx, &outBuf, outSamples, const_cast<const uint8_t **>(frame->extended_data), frame->nb_samples);
316 if (converted > 0) {
317 decodedSamples.insert(decodedSamples.end(), buf.begin(), buf.begin() + converted);
318 }
319 }
320 }
321 av_packet_unref(pkt);
322 }
323
324 // Flush the resampler
325 int flushed = swr_convert(swrCtx, nullptr, 0, nullptr, 0);
326 if (flushed > 0) {
327 std::vector<float> buf(flushed);
328 uint8_t *outBuf = reinterpret_cast<uint8_t *>(buf.data());
329 flushed = swr_convert(swrCtx, &outBuf, flushed, nullptr, 0);
330 if (flushed > 0)
331 decodedSamples.insert(decodedSamples.end(), buf.begin(), buf.begin() + flushed);
332 }
333
334 av_frame_free(&frame);
335 av_packet_free(&pkt);
336 return true;
337}
338
339static bool decodeAudioFile(const std::string &filepath) {
341 const std::size_t initialSampleCount = decodedSamples.size();
342 if (avformat_open_input(&fmtCtx, filepath.c_str(), nullptr, nullptr) < 0) {
343 std::cerr << "acmx2: file_audio: Cannot open: " << filepath << "\n";
344 return false;
345 }
346 if (avformat_find_stream_info(fmtCtx, nullptr) < 0) {
347 std::cerr << "acmx2: file_audio: Cannot find stream info\n";
349 return false;
350 }
351
352 audioStreamIndex = -1;
353 for (unsigned i = 0; i < fmtCtx->nb_streams; ++i) {
354 if (fmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
355 audioStreamIndex = static_cast<int>(i);
356 break;
357 }
358 }
359 if (audioStreamIndex < 0) {
360 std::cerr << "acmx2: file_audio: No audio stream found in: " << filepath << "\n";
362 return false;
363 }
364
365 const AVCodec *codec = avcodec_find_decoder(fmtCtx->streams[audioStreamIndex]->codecpar->codec_id);
366 if (!codec) {
367 std::cerr << "acmx2: file_audio: Unsupported audio codec\n";
369 return false;
370 }
371 codecCtx = avcodec_alloc_context3(codec);
372 avcodec_parameters_to_context(codecCtx, fmtCtx->streams[audioStreamIndex]->codecpar);
373 if (avcodec_open2(codecCtx, codec, nullptr) < 0) {
374 std::cerr << "acmx2: file_audio: Cannot open codec\n";
376 return false;
377 }
378
379 // Set up resampler: input format → mono float 44100 Hz
380 AVChannelLayout outLayout = AV_CHANNEL_LAYOUT_MONO;
381 int ret = swr_alloc_set_opts2(&swrCtx, &outLayout, AV_SAMPLE_FMT_FLT, 44100, &codecCtx->ch_layout, codecCtx->sample_fmt, codecCtx->sample_rate, 0, nullptr);
382 if (ret < 0 || swr_init(swrCtx) < 0) {
383 std::cerr << "acmx2: file_audio: Cannot init resampler\n";
385 return false;
386 }
387
388 if (!decodeAllSamples()) {
389 std::cerr << "acmx2: file_audio: Decode failed\n";
390 decodedSamples.resize(initialSampleCount);
392 return false;
393 }
394
396 const std::size_t decodedSampleCount = decodedSamples.size() - initialSampleCount;
397 if (decodedSampleCount == 0) {
398 std::cerr << "acmx2: file_audio: No decoded samples in: " << filepath << "\n";
399 return false;
400 }
401
402 std::cout << "acmx2: file_audio: Loaded " << decodedSampleCount << " samples (" << (decodedSampleCount / 44100.0) << "s) from: " << filepath << "\n";
403 return true;
404}
405
406/// @brief Open and fully decode an audio file or M3U playlist to mono float PCM at 44.1 kHz.
407bool file_audio_open(const std::string &filepath) {
409
410 av_log_set_level(AV_LOG_ERROR);
411 decodedSamples.reserve(44100 * 300); // reserve ~5 minutes
412
413 const bool playlist = isM3uPath(filepath);
414 const std::vector<std::string> requestedPaths = playlist ? readM3uPlaylist(filepath) : std::vector<std::string>{filepath};
415 if (requestedPaths.empty()) {
416 std::cerr << "acmx2: file_audio: M3U playlist contains no tracks: " << filepath << "\n";
417 return false;
418 }
419
420 for (const std::string &trackPath : requestedPaths) {
421 if (decodeAudioFile(trackPath)) {
422 fileAudioSourcePaths.push_back(trackPath);
424 } else if (playlist) {
425 std::cerr << "acmx2: file_audio: Skipping unusable playlist track: " << trackPath << "\n";
426 }
427 }
428 if (fileAudioSourcePaths.empty()) {
430 return false;
431 }
432
433 playbackPos = 0;
434 framePlaybackPos = 0.0;
435 fileAudioActive = true;
436
437 if (playlist) {
438 std::cout << "acmx2: file_audio: Loaded M3U playlist with " << fileAudioSourcePaths.size() << " track(s), " << (decodedSamples.size() / 44100.0) << "s total: " << filepath << "\n";
439 }
440
441 return true;
442}
443
444std::vector<std::string> file_audio_source_paths() { return fileAudioSourcePaths; }
445
447 if (!fileAudioActive.load(std::memory_order_acquire) || fileAudioSourcePaths.empty() || fileAudioSourceEndPositions.empty())
448 return {};
449
450 const auto source = std::upper_bound(fileAudioSourceEndPositions.begin(), fileAudioSourceEndPositions.end(), playbackPos);
451 const std::size_t sourceIndex = std::min(static_cast<std::size_t>(source - fileAudioSourceEndPositions.begin()), fileAudioSourcePaths.size() - 1);
452 return fileAudioSourcePaths[sourceIndex];
453}
454
455bool file_audio_enable_output(int output_device) {
456 if (decodedSamples.empty())
457 return false;
458
459 fileAudioOutput = std::make_unique<FileAudioOutput>();
460 if (!fileAudioOutput->open(decodedSamples.data(), decodedSamples.size(), output_device)) {
461 fileAudioOutput.reset();
462 return false;
463 }
464 return true;
465}
466
467void file_audio_set_repeat(bool enabled) { fileAudioRepeat.store(enabled, std::memory_order_release); }
468
469bool file_audio_has_output_clock() { return fileAudioActive.load(std::memory_order_acquire) && fileAudioOutput != nullptr && fileAudioOutput->is_configured(); }
470
473 return 0.0;
474 return static_cast<double>(fileAudioOutput->total_position()) / static_cast<double>(FILE_AUDIO_SAMPLE_RATE);
475}
476
477/// @brief Advance one video-frame worth of samples and update audio analysis.
478void file_audio_process_frame(double video_fps, acmx2::audio::AudioAnalyzer &analyzer) {
479 if (!fileAudioActive || decodedSamples.empty())
480 return;
481
482 const bool output_playback = fileAudioOutput != nullptr && fileAudioOutput->is_configured();
483 if (output_playback && fileAudioOutput->is_started())
484 playbackPos = fileAudioOutput->position();
485
486 if (playbackPos >= decodedSamples.size()) {
487 if (fileAudioRepeat.load(std::memory_order_acquire)) {
488 playbackPos = 0;
489 framePlaybackPos = 0.0;
490 } else {
491 fileAudioActive = false;
492 return;
493 }
494 }
495
496 const double samples_per_frame = video_fps > 0.0 ? static_cast<double>(FILE_AUDIO_SAMPLE_RATE) / video_fps : 512.0;
497 size_t next_playback_pos = playbackPos;
498 if (output_playback) {
499 next_playback_pos += std::max<size_t>(1, static_cast<size_t>(std::floor(samples_per_frame)));
500 } else {
501 framePlaybackPos += samples_per_frame;
502 next_playback_pos = std::max(playbackPos + 1, static_cast<size_t>(std::floor(framePlaybackPos)));
503 }
504 next_playback_pos = std::min(next_playback_pos, decodedSamples.size());
505
506 unsigned int available = static_cast<unsigned int>(next_playback_pos - playbackPos);
507 const float *samples = decodedSamples.data() + playbackPos;
508
509 analyzer.process_samples(samples, available, 1);
510
511 if (output_playback) {
512 if (!fileAudioOutput->is_started() && !fileAudioOutput->start()) {
513 fileAudioOutput.reset();
514 playbackPos = next_playback_pos;
515 framePlaybackPos = static_cast<double>(playbackPos);
516 }
517 } else {
518 playbackPos = next_playback_pos;
519 }
520}
521
522/// @brief Return true while decoded file-audio samples remain.
523bool file_audio_is_active() { return fileAudioActive.load(std::memory_order_relaxed); }
524
525/// @brief Stop file-audio playback and release decoder/sample resources.
527 fileAudioActive = false;
528 fileAudioOutput.reset();
530 decodedSamples.clear();
531 decodedSamples.shrink_to_fit();
532 playbackPos = 0;
533 framePlaybackPos = 0.0;
534 fileAudioSourcePaths.clear();
536 fileAudioRepeat.store(false, std::memory_order_release);
537}
double file_audio_playback_time()
Return the current output-device playback timestamp in seconds.
static std::vector< float > decodedSamples
static SwrContext * swrCtx
static std::vector< std::size_t > fileAudioSourceEndPositions
bool file_audio_has_output_clock()
Check whether real-time output playback can provide the master clock.
static bool decodeAllSamples()
Decode every audio packet from the open format context.
static std::atomic< bool > fileAudioRepeat
static void closeDecoderResources()
static std::vector< std::string > readM3uPlaylist(const std::string &filepath)
static size_t playbackPos
void file_audio_close()
Stop file-audio playback and release decoder/sample resources.
static std::atomic< bool > fileAudioActive
std::vector< std::string > file_audio_source_paths()
Return the successfully decoded source tracks in playback order.
bool file_audio_open(const std::string &filepath)
Open and fully decode an audio file or M3U playlist to mono float PCM at 44.1 kHz.
void file_audio_set_repeat(bool enabled)
Enable or disable looping of the currently decoded audio file.
static double framePlaybackPos
bool file_audio_enable_output(int output_device)
Configure real-time playback of the decoded file through an output device.
static AVFormatContext * fmtCtx
static AVCodecContext * codecCtx
static std::vector< std::string > fileAudioSourcePaths
static std::string trimPlaylistLine(std::string line)
static int audioStreamIndex
static bool isM3uPath(const std::string &filepath)
bool file_audio_is_active()
Return true while decoded file-audio samples remain.
void file_audio_process_frame(double video_fps, acmx2::audio::AudioAnalyzer &analyzer)
Advance one video-frame worth of samples and update audio analysis.
std::string file_audio_current_source_path()
Return the source track at the current playback position.
static bool decodeAudioFile(const std::string &filepath)
File-based audio input for audio-reactive shaders.
Owns the audio-reactive analysis state shared by live and file audio.
void process_samples(const float *samples, unsigned int frame_count, unsigned int channels)
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)
int write_samples(float *output, unsigned int frame_count)
bool open(const float *samples, std::size_t sample_count, int output_device)
std::unique_ptr< FileAudioOutput > fileAudioOutput
constexpr unsigned int FILE_AUDIO_SAMPLE_RATE