Loading...
Searching...
No Matches
AudioRecorder.hpp
1#pragma once
2#include <score/document/DocumentContext.hpp>
3#include <score/tools/File.hpp>
4
5#include <ossia/audio/drwav_write_handle.hpp>
6#include <ossia/network/value/detail/value_conversion_impl.hpp>
7
8#include <QDateTime>
9
10#include <AvndProcesses/AddressTools.hpp>
11#include <halp/audio.hpp>
12#include <halp/callback.hpp>
13
14namespace avnd_tools
15{
21{
22 halp_meta(name, "Audio recorder")
23 halp_meta(author, "ossia team")
24 halp_meta(category, "Audio/Recording")
25 halp_meta(description, "Records audio to a file")
26 halp_meta(c_name, "avnd_audio_recorder")
27 halp_meta(
28 manual_url,
29 "https://ossia.io/score-docs/processes/audio-utilities.html#audio-recorder")
30 halp_meta(uuid, "4463dddc-6acf-4106-a680-fed67d8030da")
31
32 using audio_buffer = boost::container::vector<double>;
33
34 // Threaded worker
36 {
37 explicit recorder_thread(const score::DocumentContext& context)
38 : context{context}
39 {
40 }
41 const score::DocumentContext& context;
42 ossia::drwav_write_handle f;
43 std::string filename;
44 std::string actual_filename;
45 int channels = 0;
46 int rate = 0;
47 bool must_record = false;
48
49 bool reopen()
50 {
51 if(must_record)
52 {
53 // Open the file with the correct substitutions
54 auto fname = QString::fromStdString(this->filename);
55
56 fname.replace("%t", QDateTime::currentDateTimeUtc().toString());
57 fname = score::locateFilePath(fname, context);
58 actual_filename = fname.toStdString();
59 f.open(actual_filename, channels, rate, 16);
60 return false;
61 }
62 else
63 {
64 bool must_reply = false;
65 if(f.is_open() && f.written_frames() > 0)
66 {
67 must_reply = true;
68 }
69 f.close();
70 return must_reply;
71 }
72 }
73
74 void write(int frames, audio_buffer& data)
75 {
76 int these_channels = data.size() / frames;
77 if(these_channels != channels)
78 {
79 channels = these_channels;
80 reopen();
81 }
82
83 if(!f.is_open())
84 return;
85
86 // Put addresses to the first sample of each channel in an array,
87 // e.g. arr[0] points to the first channel, arr[1] to the second, etc.
88 const double** arr = (const double**)alloca(channels * sizeof(double*));
89 for(int c = 0; c < channels; ++c)
90 arr[c] = data.data() + c * frames;
91
92 // Write the data
93 f.write_pcm_frames(frames, arr);
94 }
95 };
96 std::shared_ptr<recorder_thread> impl;
97
99 {
100 std::string path;
101 int rate{};
102 bool must_record{};
103 bool operator()(recorder_thread& self)
104 {
105 using namespace std;
106 swap(self.filename, path);
107 self.rate = rate;
108 self.must_record = must_record;
109 return self.reopen();
110 }
111 };
112
114 {
115 int frames{};
116 audio_buffer data;
117 bool operator()(recorder_thread& self)
118 {
119 self.write(frames, data);
120 return false;
121 }
122 };
123 using worker_message = ossia::variant<reset_message, process_message>;
124
125 struct
126 {
127 std::function<void(std::shared_ptr<recorder_thread>, worker_message)> request;
128 static std::function<void(AudioRecorder&)>
129 work(std::shared_ptr<recorder_thread> t, worker_message&& mess)
130 {
131 bool reply = ossia::visit(
132 [&]<typename M>(M&& msg) { return std::forward<M>(msg)(*t); },
133 std::move(mess));
134 if(reply)
135 {
136 return [filename = t->actual_filename](AudioRecorder& self) mutable {
137 using namespace std;
138 swap(self.filename_to_output, filename);
139 };
140 }
141 else
142 {
143 return {};
144 }
145 }
146 } worker;
147
148 // Object definition
149 struct
150 {
151 halp::dynamic_audio_bus<"Audio", double> audio;
152 struct : halp::lineedit<"File pattern", "">
153 {
154 void update(AudioRecorder& self) { self.update(); }
155 } filename;
156
157 struct : halp::toggle<"Record">
158 {
159 void update(AudioRecorder& self)
160 {
161 if(prev != value)
162 {
163 prev = value;
164 self.update();
165 }
166 }
167 bool prev{false};
168 } record;
169 } inputs;
170
171 struct
172 {
173 halp::callback<"Filename", std::string> finished;
174 } outputs;
175
176 const score::DocumentContext* ossia_document_context{};
177 int current_rate = 0;
178 void prepare(halp::setup s)
179 {
180 current_rate = s.rate;
181 SCORE_ASSERT(ossia_document_context);
182 impl = std::make_shared<recorder_thread>(*ossia_document_context);
183 update();
184 }
185
186 void update()
187 {
188 if(impl)
189 worker.request(impl, reset_message{inputs.filename, current_rate, inputs.record});
190 }
191
192 void operator()(int frames)
193 {
194 if(!std::exchange(started, true))
195 update();
196
197 data.resize(inputs.audio.channels * frames, boost::container::default_init);
198 for(int c = 0; c < inputs.audio.channels; c++)
199 {
200 double* src = inputs.audio.samples[c];
201 double* ptr = data.data() + c * frames;
202 for(int i = 0; i < frames; i++)
203 ptr[i] = src[i];
204 }
205
206 worker.request(impl, process_message{frames, std::move(data)});
207
208 if(!filename_to_output.empty())
209 {
210 outputs.finished(filename_to_output);
211 filename_to_output.clear();
212 }
213 }
214
215 bool started{};
216 std::optional<int64_t> first_message_sent_pos;
217 std::optional<int64_t> last_message_sent_pos;
218
219 audio_buffer data;
220 std::string filename_to_output;
221};
222
223}
QString locateFilePath(const QString &filename, const score::DocumentContext &ctx) noexcept
Definition File.cpp:57
STL namespace.
Definition AudioRecorder.hpp:114
Definition AudioRecorder.hpp:36
Definition AudioRecorder.hpp:99
Definition AudioRecorder.hpp:21
Definition DocumentContext.hpp:18