MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
SoundFileWriter.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <future>
7
8extern "C" {
9#include <libavcodec/avcodec.h>
10}
11
12namespace MayaFlux::Buffers {
13class AudioBuffer;
14}
15
16namespace MayaFlux::Kakshya {
17class SoundStreamContainer;
18}
19
20namespace MayaFlux::IO {
21
22/**
23 * @class SoundFileWriter
24 * @brief Asynchronous audio file encoder with a lock-free work queue.
25 *
26 * Accepts audio from multiple source types and encodes it to a file on a
27 * dedicated worker thread. The public API is fully non-blocking: every write
28 * call posts a work item to an SPSC queue and returns immediately. The worker
29 * thread owns all FFmpeg state and is the only thread that touches it.
30 *
31 * Supported input paths:
32 * - Raw interleaved double frames (span or vector, for RT/coroutine callers)
33 * - AudioBuffer — drains get_data() as interleaved doubles
34 * - SoundStreamContainer — drains get_data_as_double() (interleaved span)
35 *
36 * Threading model:
37 * Caller thread → push work items to m_queue (lock-free)
38 * Worker thread → drain queue, encode via AudioEncodeContext + FFmpegMuxContext
39 *
40 * Lifetime:
41 * open() spawns the worker. close() posts a CloseCmd and returns a
42 * future<bool> the caller may wait on. The destructor issues close() if
43 * still open and joins with a 5-second timeout; on timeout it detaches and
44 * logs CRITICAL.
45 *
46 * Error handling:
47 * Encode errors set last_error() and cause the worker to post false to the
48 * close promise. Subsequent write calls after an encode error are silently
49 * dropped (is_open() returns false).
50 */
51class MAYAFLUX_API SoundFileWriter {
52public:
55
60
61 // =========================================================================
62 // Lifecycle
63 // =========================================================================
64
65 /**
66 * @brief Open the output file and spawn the worker thread.
67 *
68 * Format is inferred from the filepath extension (wav, flac, ogg, mp4, mkv…).
69 * Pass explicit_codec to override the container's default audio codec.
70 *
71 * @param filepath Destination file path. Extension determines container.
72 * @param channels Number of interleaved channels in all submitted data.
73 * @param sample_rate PCM sample rate in Hz.
74 * @param explicit_codec Encoder override; AV_CODEC_ID_NONE = container default.
75 * @return True if the worker started successfully.
76 */
77 bool open(const std::string& filepath,
78 uint32_t channels,
79 uint32_t sample_rate,
80 AVCodecID explicit_codec);
81
82 /**
83 * @brief Post a close command to the worker.
84 *
85 * The worker drains the FIFO, flushes the encoder, writes the container
86 * trailer, and fulfils the returned future. The caller may co_await or
87 * .get() the future, or discard it if completion is not needed.
88 *
89 * Safe to call multiple times; subsequent calls return a future that is
90 * immediately ready with false.
91 *
92 * @return future<bool>: true = file complete; false = encode error.
93 */
94 std::future<bool> close();
95
96 /**
97 * @brief True if the worker is running and the encoder is healthy.
98 */
99 [[nodiscard]] bool is_open() const { return m_open.load(std::memory_order_acquire); }
100
101 // =========================================================================
102 // Write
103 // =========================================================================
104
105 /**
106 * @brief Post interleaved double-precision frames to the work queue.
107 *
108 * @param interleaved Interleaved PCM frames in double precision.
109 * @param num_frames Frame count (samples / channels). If 0, inferred as
110 * interleaved.size() / channels.
111 */
112 void write(std::span<const double> interleaved, uint32_t num_frames = 0);
113
114 /**
115 * @brief Post planar per-channel data to the work queue.
116 *
117 * Accepts the DataVariant vector directly from AudioOutputContainer::get_processed_data().
118 * Interleaving is deferred to the worker thread.
119 *
120 * @param planar Per-channel vectors; each element must be vector<double>.
121 */
122 void write(const std::vector<Kakshya::DataVariant>& planar);
123
124 /**
125 * @brief Post one AudioBuffer's mono sample data to the work queue.
126 */
127 void write(const std::shared_ptr<Buffers::AudioBuffer>& buffer);
128
129 /**
130 * @brief Post a SoundStreamContainer's planar channel data to the work queue.
131 *
132 * Reads get_data() on the caller thread, copies, and posts as PlanarChunk.
133 */
134 void write(const std::shared_ptr<Kakshya::SoundStreamContainer>& container);
135
136 // =========================================================================
137 // Error
138 // =========================================================================
139
140 [[nodiscard]] std::string last_error() const;
141
142private:
143 // -------------------------------------------------------------------------
144 // Work item types
145 // -------------------------------------------------------------------------
146
147 struct FrameChunk {
148 std::vector<double> samples;
149 uint32_t num_frames;
150 };
151
152 struct PlanarChunk {
153 std::vector<std::vector<double>> channels;
154 uint32_t num_frames;
155 };
156
157 struct CloseCmd { };
158
159 using WorkItem = std::variant<FrameChunk, PlanarChunk, CloseCmd>;
160
161 // -------------------------------------------------------------------------
162 // Queue
163 // -------------------------------------------------------------------------
164
165 static constexpr size_t k_queue_capacity = 4096;
166 std::unique_ptr<Memory::LockFreeQueue<WorkItem, k_queue_capacity>> m_queue;
167
168 // -------------------------------------------------------------------------
169 // Worker
170 // -------------------------------------------------------------------------
171
172 std::thread m_worker;
173 std::atomic<bool> m_open { false };
174 std::atomic<bool> m_closing { false };
175 std::promise<bool> m_close_promise;
176 std::shared_future<bool> m_close_future;
177
178 mutable std::mutex m_error_mutex;
179 std::string m_last_error;
180
181 uint32_t m_channels {};
182
183 void worker_loop(const std::string& filepath, uint32_t channels, uint32_t sample_rate,
184 AVCodecID codec_id);
185
186 void set_error(std::string msg);
187 bool post(const WorkItem& item);
188};
189
190} // namespace MayaFlux::IO
std::variant< FrameChunk, PlanarChunk, CloseCmd > WorkItem
SoundFileWriter & operator=(const SoundFileWriter &)=delete
std::promise< bool > m_close_promise
SoundFileWriter(const SoundFileWriter &)=delete
std::unique_ptr< Memory::LockFreeQueue< WorkItem, k_queue_capacity > > m_queue
SoundFileWriter(SoundFileWriter &&)=delete
SoundFileWriter & operator=(SoundFileWriter &&)=delete
std::shared_future< bool > m_close_future
bool is_open() const
True if the worker is running and the encoder is healthy.
Asynchronous audio file encoder with a lock-free work queue.
std::vector< std::vector< double > > channels