ACMX 2.136.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 =
74 output_device >= 0
75 ? static_cast<unsigned int>(output_device)
76 : audio.getDefaultOutputDevice();
77 const RtAudio::DeviceInfo info = audio.getDeviceInfo(device);
78 if (info.outputChannels == 0) {
79 std::cerr << "acmx2: file_audio: Selected device has no output channels\n";
80 return false;
81 }
82
83 output_channels = std::min(2U, info.outputChannels);
84 output_sample_rate = choose_sample_rate(info.sampleRates);
85 source_samples = samples;
86 source_sample_count = sample_count;
87 source_position = 0.0;
88 playback_position.store(0, std::memory_order_relaxed);
89 total_playback_position.store(0, std::memory_order_relaxed);
90
91 RtAudio::StreamParameters output_parameters;
92 output_parameters.deviceId = device;
93 output_parameters.nChannels = output_channels;
94 output_parameters.firstChannel = 0;
95
96 unsigned int buffer_frames = 512;
97 audio.openStream(&output_parameters, nullptr, RTAUDIO_FLOAT32,
98 output_sample_rate, &buffer_frames,
100 configured = true;
101 std::cout << "acmx2: file_audio: Playback configured on device "
102 << device << ": " << info.name << " (" << output_channels
103 << " ch, " << output_sample_rate << " Hz)\n";
104 return true;
105 } catch (const std::exception &error) {
106 std::cerr << "acmx2: file_audio: Could not open output stream: "
107 << error.what() << "\n";
108 close();
109 return false;
110 }
111 }
112
113 bool start() {
114 if (!configured || started.load(std::memory_order_acquire))
115 return configured;
116 try {
117 active.store(true, std::memory_order_release);
118 started.store(true, std::memory_order_release);
119 audio.startStream();
120 std::cout << "acmx2: file_audio: Playback started\n";
121 return true;
122 } catch (const std::exception &error) {
123 active.store(false, std::memory_order_release);
124 started.store(false, std::memory_order_release);
125 std::cerr << "acmx2: file_audio: Could not start output stream: "
126 << error.what() << "\n";
127 return false;
128 }
129 }
130
131 void close() {
132 active.store(false, std::memory_order_release);
133 if (audio.isStreamOpen()) {
134 try {
135 if (audio.isStreamRunning())
136 audio.stopStream();
137 audio.closeStream();
138 } catch (const std::exception &error) {
139 std::cerr << "acmx2: file_audio: Error closing output stream: "
140 << error.what() << "\n";
141 }
142 }
143 configured = false;
144 started.store(false, std::memory_order_release);
145 source_samples = nullptr;
147 source_position = 0.0;
149 playback_position.store(0, std::memory_order_relaxed);
150 total_playback_position.store(0, std::memory_order_relaxed);
151 }
152
153 bool is_configured() const { return configured; }
154
155 bool is_started() const {
156 return started.load(std::memory_order_acquire);
157 }
158
159 std::size_t position() const {
160 return playback_position.load(std::memory_order_acquire);
161 }
162
163 std::size_t total_position() const {
164 return total_playback_position.load(std::memory_order_acquire);
165 }
166
167 private:
168 static unsigned int choose_sample_rate(const std::vector<unsigned int> &rates) {
169 if (rates.empty() ||
170 std::find(rates.begin(), rates.end(), FILE_AUDIO_SAMPLE_RATE) != rates.end())
172 if (std::find(rates.begin(), rates.end(), 48000U) != rates.end())
173 return 48000;
174 return rates.front();
175 }
176
177 static int audio_callback(void *output_buffer, void *, unsigned int frame_count,
178 double, RtAudioStreamStatus, void *user_data) {
179 return static_cast<FileAudioOutput *>(user_data)
180 ->write_samples(static_cast<float *>(output_buffer), frame_count);
181 }
182
183 int write_samples(float *output, unsigned int frame_count) {
184 if (output == nullptr)
185 return 0;
186
187 const double source_step =
188 static_cast<double>(FILE_AUDIO_SAMPLE_RATE) /
189 static_cast<double>(output_sample_rate);
190 for (unsigned int frame = 0; frame < frame_count; ++frame) {
191 float sample = 0.0f;
192 if (active.load(std::memory_order_relaxed) &&
193 source_position >= static_cast<double>(source_sample_count)) {
194 if (fileAudioRepeat.load(std::memory_order_relaxed)) {
195 source_position = std::fmod(
197 static_cast<double>(source_sample_count));
198 } else {
199 active.store(false, std::memory_order_release);
200 source_position = static_cast<double>(source_sample_count);
201 }
202 }
203
204 const std::size_t index = static_cast<std::size_t>(source_position);
205 if (active.load(std::memory_order_relaxed) && index < source_sample_count) {
206 const std::size_t next_index =
207 fileAudioRepeat.load(std::memory_order_relaxed)
208 ? (index + 1) % source_sample_count
209 : std::min(index + 1, source_sample_count - 1);
210 const float fraction =
211 static_cast<float>(source_position - static_cast<double>(index));
212 sample = source_samples[index] +
213 (source_samples[next_index] - source_samples[index]) * fraction;
214 source_position += source_step;
215 total_source_position += source_step;
216 } else if (!fileAudioRepeat.load(std::memory_order_relaxed)) {
217 active.store(false, std::memory_order_release);
218 source_position = static_cast<double>(source_sample_count);
219 }
220
221 for (unsigned int channel = 0; channel < output_channels; ++channel)
222 output[frame * output_channels + channel] = sample;
223 }
224
225 playback_position.store(
226 std::min(static_cast<std::size_t>(source_position), source_sample_count),
227 std::memory_order_release);
229 static_cast<std::size_t>(total_source_position),
230 std::memory_order_release);
231 return 0;
232 }
233
234 RtAudio audio;
235 const float *source_samples = nullptr;
236 std::size_t source_sample_count = 0;
237 double source_position = 0.0;
239 unsigned int output_channels = 0;
241 std::atomic<std::size_t> playback_position{0};
242 std::atomic<std::size_t> total_playback_position{0};
243 std::atomic<bool> started{false};
244 std::atomic<bool> active{false};
245 bool configured = false;
246 };
247
248 std::unique_ptr<FileAudioOutput> fileAudioOutput;
249
250} // namespace
251
253 if (swrCtx)
254 swr_free(&swrCtx);
255 if (codecCtx)
256 avcodec_free_context(&codecCtx);
257 if (fmtCtx)
258 avformat_close_input(&fmtCtx);
259 audioStreamIndex = -1;
260}
261
262static std::string trimPlaylistLine(std::string line) {
263 const std::string whitespace = " \t\r\n";
264 const std::size_t first = line.find_first_not_of(whitespace);
265 if (first == std::string::npos)
266 return {};
267 const std::size_t last = line.find_last_not_of(whitespace);
268 return line.substr(first, last - first + 1);
269}
270
271static bool isM3uPath(const std::string &filepath) {
272 std::string extension = std::filesystem::path(filepath).extension().string();
273 std::transform(extension.begin(), extension.end(), extension.begin(),
274 [](unsigned char value) {
275 return static_cast<char>(std::tolower(value));
276 });
277 return extension == ".m3u" || extension == ".m3u8";
278}
279
280static std::vector<std::string> readM3uPlaylist(const std::string &filepath) {
281 std::ifstream input(filepath);
282 if (!input) {
283 std::cerr << "acmx2: file_audio: Cannot open M3U playlist: "
284 << filepath << "\n";
285 return {};
286 }
287
288 const std::filesystem::path playlistDirectory =
289 std::filesystem::absolute(std::filesystem::path(filepath)).parent_path();
290 std::vector<std::string> paths;
291 std::string line;
292 bool firstLine = true;
293 while (std::getline(input, line)) {
294 if (firstLine && line.size() >= 3 &&
295 static_cast<unsigned char>(line[0]) == 0xef &&
296 static_cast<unsigned char>(line[1]) == 0xbb &&
297 static_cast<unsigned char>(line[2]) == 0xbf) {
298 line.erase(0, 3);
299 }
300 firstLine = false;
301 line = trimPlaylistLine(std::move(line));
302 if (line.empty() || line.front() == '#')
303 continue;
304
305 if (line.find("://") != std::string::npos) {
306 paths.push_back(line);
307 continue;
308 }
309
310 std::filesystem::path trackPath(line);
311 if (!trackPath.is_absolute())
312 trackPath = playlistDirectory / trackPath;
313 paths.push_back(trackPath.lexically_normal().string());
314 }
315 return paths;
316}
317
318/**
319 * @brief Decode every audio packet from the open format context.
320 *
321 * Reads packets from @c fmtCtx, sends them through the codec, and
322 * resamples the output to mono float 44 100 Hz via @c swrCtx.
323 * Decoded samples are appended to the module-level @c decodedSamples
324 * vector. The resampler is flushed at the end to capture trailing
325 * samples.
326 *
327 * @return @c true if decoding completed without fatal errors.
328 */
329static bool decodeAllSamples() {
330 AVPacket *pkt = av_packet_alloc();
331 AVFrame *frame = av_frame_alloc();
332 if (!pkt || !frame) {
333 if (pkt)
334 av_packet_free(&pkt);
335 if (frame)
336 av_frame_free(&frame);
337 return false;
338 }
339
340 while (av_read_frame(fmtCtx, pkt) >= 0) {
341 if (pkt->stream_index == audioStreamIndex) {
342 int ret = avcodec_send_packet(codecCtx, pkt);
343 while (ret >= 0) {
344 ret = avcodec_receive_frame(codecCtx, frame);
345 if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
346 break;
347 if (ret < 0) {
348 av_packet_unref(pkt);
349 av_frame_free(&frame);
350 av_packet_free(&pkt);
351 return false;
352 }
353
354 // Resample to mono float 44100 Hz
355 int outSamples = swr_get_out_samples(swrCtx, frame->nb_samples);
356 std::vector<float> buf(outSamples);
357 uint8_t *outBuf = reinterpret_cast<uint8_t *>(buf.data());
358 int converted = swr_convert(swrCtx, &outBuf, outSamples,
359 const_cast<const uint8_t **>(frame->extended_data),
360 frame->nb_samples);
361 if (converted > 0) {
362 decodedSamples.insert(decodedSamples.end(), buf.begin(), buf.begin() + converted);
363 }
364 }
365 }
366 av_packet_unref(pkt);
367 }
368
369 // Flush the resampler
370 int flushed = swr_convert(swrCtx, nullptr, 0, nullptr, 0);
371 if (flushed > 0) {
372 std::vector<float> buf(flushed);
373 uint8_t *outBuf = reinterpret_cast<uint8_t *>(buf.data());
374 flushed = swr_convert(swrCtx, &outBuf, flushed, nullptr, 0);
375 if (flushed > 0)
376 decodedSamples.insert(decodedSamples.end(), buf.begin(), buf.begin() + flushed);
377 }
378
379 av_frame_free(&frame);
380 av_packet_free(&pkt);
381 return true;
382}
383
384static bool decodeAudioFile(const std::string &filepath) {
386 const std::size_t initialSampleCount = decodedSamples.size();
387 if (avformat_open_input(&fmtCtx, filepath.c_str(), nullptr, nullptr) < 0) {
388 std::cerr << "acmx2: file_audio: Cannot open: " << filepath << "\n";
389 return false;
390 }
391 if (avformat_find_stream_info(fmtCtx, nullptr) < 0) {
392 std::cerr << "acmx2: file_audio: Cannot find stream info\n";
394 return false;
395 }
396
397 audioStreamIndex = -1;
398 for (unsigned i = 0; i < fmtCtx->nb_streams; ++i) {
399 if (fmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
400 audioStreamIndex = static_cast<int>(i);
401 break;
402 }
403 }
404 if (audioStreamIndex < 0) {
405 std::cerr << "acmx2: file_audio: No audio stream found in: " << filepath << "\n";
407 return false;
408 }
409
410 const AVCodec *codec = avcodec_find_decoder(fmtCtx->streams[audioStreamIndex]->codecpar->codec_id);
411 if (!codec) {
412 std::cerr << "acmx2: file_audio: Unsupported audio codec\n";
414 return false;
415 }
416 codecCtx = avcodec_alloc_context3(codec);
417 avcodec_parameters_to_context(codecCtx, fmtCtx->streams[audioStreamIndex]->codecpar);
418 if (avcodec_open2(codecCtx, codec, nullptr) < 0) {
419 std::cerr << "acmx2: file_audio: Cannot open codec\n";
421 return false;
422 }
423
424 // Set up resampler: input format → mono float 44100 Hz
425 AVChannelLayout outLayout = AV_CHANNEL_LAYOUT_MONO;
426 int ret = swr_alloc_set_opts2(&swrCtx,
427 &outLayout, AV_SAMPLE_FMT_FLT, 44100,
428 &codecCtx->ch_layout, codecCtx->sample_fmt, codecCtx->sample_rate,
429 0, nullptr);
430 if (ret < 0 || swr_init(swrCtx) < 0) {
431 std::cerr << "acmx2: file_audio: Cannot init resampler\n";
433 return false;
434 }
435
436 if (!decodeAllSamples()) {
437 std::cerr << "acmx2: file_audio: Decode failed\n";
438 decodedSamples.resize(initialSampleCount);
440 return false;
441 }
442
444 const std::size_t decodedSampleCount = decodedSamples.size() - initialSampleCount;
445 if (decodedSampleCount == 0) {
446 std::cerr << "acmx2: file_audio: No decoded samples in: " << filepath << "\n";
447 return false;
448 }
449
450 std::cout << "acmx2: file_audio: Loaded " << decodedSampleCount
451 << " samples (" << (decodedSampleCount / 44100.0)
452 << "s) from: " << filepath << "\n";
453 return true;
454}
455
456/// @brief Open and fully decode an audio file or M3U playlist to mono float PCM at 44.1 kHz.
457bool file_audio_open(const std::string &filepath) {
459
460 av_log_set_level(AV_LOG_ERROR);
461 decodedSamples.reserve(44100 * 300); // reserve ~5 minutes
462
463 const bool playlist = isM3uPath(filepath);
464 const std::vector<std::string> requestedPaths =
465 playlist ? readM3uPlaylist(filepath)
466 : std::vector<std::string>{filepath};
467 if (requestedPaths.empty()) {
468 std::cerr << "acmx2: file_audio: M3U playlist contains no tracks: "
469 << filepath << "\n";
470 return false;
471 }
472
473 for (const std::string &trackPath : requestedPaths) {
474 if (decodeAudioFile(trackPath)) {
475 fileAudioSourcePaths.push_back(trackPath);
477 } else if (playlist) {
478 std::cerr << "acmx2: file_audio: Skipping unusable playlist track: "
479 << trackPath << "\n";
480 }
481 }
482 if (fileAudioSourcePaths.empty()) {
484 return false;
485 }
486
487 playbackPos = 0;
488 framePlaybackPos = 0.0;
489 fileAudioActive = true;
490
491 if (playlist) {
492 std::cout << "acmx2: file_audio: Loaded M3U playlist with "
493 << fileAudioSourcePaths.size() << " track(s), "
494 << (decodedSamples.size() / 44100.0) << "s total: "
495 << filepath << "\n";
496 }
497
498 return true;
499}
500
501std::vector<std::string> file_audio_source_paths() {
503}
504
506 if (!fileAudioActive.load(std::memory_order_acquire) ||
508 return {};
509
510 const auto source = std::upper_bound(fileAudioSourceEndPositions.begin(),
513 const std::size_t sourceIndex = std::min(
514 static_cast<std::size_t>(source - fileAudioSourceEndPositions.begin()),
515 fileAudioSourcePaths.size() - 1);
516 return fileAudioSourcePaths[sourceIndex];
517}
518
519bool file_audio_enable_output(int output_device) {
520 if (decodedSamples.empty())
521 return false;
522
523 fileAudioOutput = std::make_unique<FileAudioOutput>();
524 if (!fileAudioOutput->open(decodedSamples.data(), decodedSamples.size(),
525 output_device)) {
526 fileAudioOutput.reset();
527 return false;
528 }
529 return true;
530}
531
532void file_audio_set_repeat(bool enabled) {
533 fileAudioRepeat.store(enabled, std::memory_order_release);
534}
535
537 return fileAudioActive.load(std::memory_order_acquire) &&
538 fileAudioOutput != nullptr && fileAudioOutput->is_configured();
539}
540
543 return 0.0;
544 return static_cast<double>(fileAudioOutput->total_position()) /
545 static_cast<double>(FILE_AUDIO_SAMPLE_RATE);
546}
547
548/// @brief Advance one video-frame worth of samples and update audio analysis.
549void file_audio_process_frame(double video_fps, acmx2::audio::AudioAnalyzer &analyzer) {
550 if (!fileAudioActive || decodedSamples.empty())
551 return;
552
553 const bool output_playback =
554 fileAudioOutput != nullptr && fileAudioOutput->is_configured();
555 if (output_playback && fileAudioOutput->is_started())
556 playbackPos = fileAudioOutput->position();
557
558 if (playbackPos >= decodedSamples.size()) {
559 if (fileAudioRepeat.load(std::memory_order_acquire)) {
560 playbackPos = 0;
561 framePlaybackPos = 0.0;
562 } else {
563 fileAudioActive = false;
564 return;
565 }
566 }
567
568 const double samples_per_frame =
569 video_fps > 0.0
570 ? static_cast<double>(FILE_AUDIO_SAMPLE_RATE) / video_fps
571 : 512.0;
572 size_t next_playback_pos = playbackPos;
573 if (output_playback) {
574 next_playback_pos += std::max<size_t>(
575 1, static_cast<size_t>(std::floor(samples_per_frame)));
576 } else {
577 framePlaybackPos += samples_per_frame;
578 next_playback_pos = std::max(
579 playbackPos + 1,
580 static_cast<size_t>(std::floor(framePlaybackPos)));
581 }
582 next_playback_pos = std::min(next_playback_pos, decodedSamples.size());
583
584 unsigned int available =
585 static_cast<unsigned int>(next_playback_pos - playbackPos);
586 const float *samples = decodedSamples.data() + playbackPos;
587
588 analyzer.process_samples(samples, available, 1);
589
590 if (output_playback) {
591 if (!fileAudioOutput->is_started() && !fileAudioOutput->start()) {
592 fileAudioOutput.reset();
593 playbackPos = next_playback_pos;
594 framePlaybackPos = static_cast<double>(playbackPos);
595 }
596 } else {
597 playbackPos = next_playback_pos;
598 }
599}
600
601/// @brief Return true while decoded file-audio samples remain.
603 return fileAudioActive.load(std::memory_order_relaxed);
604}
605
606/// @brief Stop file-audio playback and release decoder/sample resources.
608 fileAudioActive = false;
609 fileAudioOutput.reset();
611 decodedSamples.clear();
612 decodedSamples.shrink_to_fit();
613 playbackPos = 0;
614 framePlaybackPos = 0.0;
615 fileAudioSourcePaths.clear();
617 fileAudioRepeat.store(false, std::memory_order_release);
618}
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