Loading...
Searching...
No Matches
TakeWriter.hpp
1#pragma once
2#include <Fx/Looper/AudioLayers.hpp>
3#include <Fx/Looper/LooperView.hpp>
4
5#include <score/document/DocumentContext.hpp>
6
7#include <core/document/Document.hpp>
8#include <core/document/DocumentModel.hpp>
9
10#include <ossia/audio/write_loop_wav.hpp>
11
12#include <boost/container/vector.hpp>
13
14#include <QDir>
15#include <QFile>
16#include <QFileInfo>
17#include <QStandardPaths>
18
19#include <algorithm>
20#include <array>
21#include <atomic>
22#include <cmath>
23#include <functional>
24#include <memory>
25#include <mutex>
26#include <string>
27#include <string_view>
28#include <vector>
29
30namespace Nodes::Looper
31{
44{
47 static constexpr int slots = 4;
48
49 QString folder;
50 QString prefix;
51 QString extension;
52 std::array<std::string, slots> paths;
53 std::array<std::string, slots> bases;
57 std::function<void(const view_message&)> tell_views;
58
59 static QString folder_for(const score::DocumentContext& ctx)
60 {
61 const QString file = ctx.document.metadata().fileName();
62 if(QFileInfo fi{file}; fi.isAbsolute() && fi.exists())
63 return fi.absolutePath() + "/Takes";
64
65 return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
66 + "/recordings/" + QString::number(ctx.document.id().val());
67 }
68
70 static std::shared_ptr<take_writer> make(
71 const score::DocumentContext& ctx, std::string_view prefix,
72 std::string_view extension)
73 {
74 auto w = std::make_shared<take_writer>();
75 w->folder = folder_for(ctx);
76 w->prefix = QString::fromUtf8(prefix.data(), qsizetype(prefix.size()));
77 w->extension = QString::fromUtf8(extension.data(), qsizetype(extension.size()));
78 for(auto& p : w->paths)
79 p.reserve(1024);
80 for(auto& p : w->bases)
81 p.reserve(1024);
82 return w;
83 }
84
85 QString base(int number) const
86 {
87 return QStringLiteral("%1/%2-take%3").arg(folder, prefix).arg(number);
88 }
89 QString file(int number, int layer) const
90 {
91 return layer < 0 ? QStringLiteral("%1.%2").arg(base(number), extension)
92 : QStringLiteral("%1-layer%2.%3").arg(base(number)).arg(layer + 1).arg(extension);
93 }
94
98 const std::string& next_path(int slot, int& number, int layer)
99 {
100 // Loopers of a document write from several workers: the choice of a
101 // number and the creation of its first file go together.
102 static std::mutex choosing;
103 std::lock_guard _{choosing};
104
105 QDir dir{folder};
106 QDir{}.mkpath(folder);
107 if(number <= 0)
108 {
109 // A number is free when no file of the folder has it, whichever of a
110 // take's files they are: the files of some passes alone have neither
111 // the mix nor the first layer.
112 const QString head = prefix + QStringLiteral("-take");
113 std::vector<int> used;
114 for(const QString& name :
115 dir.entryList({head + QStringLiteral("*")}, QDir::Files | QDir::Hidden))
116 {
117 int end = int(head.size());
118 while(end < name.size() && name[end].isDigit())
119 end++;
120 bool ok = false;
121 const int n = name.mid(head.size(), end - head.size()).toInt(&ok);
122 if(ok && end < name.size() && (name[end] == '.' || name[end] == '-'))
123 used.push_back(n);
124 }
125 std::sort(used.begin(), used.end());
126 number = 1;
127 for(int n : used)
128 if(n == number)
129 number++;
130 else if(n > number)
131 break;
132 }
133 const QString path = file(number, layer);
134 if(QFile f{path}; f.open(QIODevice::WriteOnly))
135 f.close();
136 paths[slot].assign(path.toStdString());
137 bases[slot].assign(base(number).toStdString());
138 return paths[slot];
139 }
140
142 const std::string& take_base(int slot) const noexcept { return bases[slot]; }
143 const std::string& path(int slot) const noexcept { return paths[slot]; }
144};
145
153{
158 int take{};
159 double date{};
160 halp::clip_stretch stretch{};
161 const char* extension{};
162
166 void written(take_writer& w, int slot, bool ok, int layers, bool placeable)
167 {
168 if(ok)
169 {
170 view.set_take(w.take_base(slot), extension, true, layers, stretch, placeable);
171 view.take = take;
172 view.take_date = date;
173 view.take_version = view.version;
174 }
175 if(w.tell_views)
176 w.tell_views(view);
177 }
178};
179
184{
185 boost::container::vector<float> data;
186 int channels{};
187 int64_t capacity{};
190 std::atomic_bool in_use{};
191
193 static std::shared_ptr<export_buffer> make(int channels, int64_t frames)
194 {
195 auto b = std::make_shared<export_buffer>();
196 b->channels = channels;
197 b->capacity = frames;
198 b->data.resize(std::size_t(channels) * std::size_t(frames), boost::container::default_init);
199 return b;
200 }
201
202 float* channel(int c) noexcept { return data.data() + std::size_t(c) * std::size_t(capacity); }
203};
204
209{
210 audio_snapshot passes;
211 stop_take_notice notice;
212 std::atomic_bool in_use{};
213};
214
219{
220 std::shared_ptr<take_writer> writer;
221 std::shared_ptr<export_buffer> audio;
224 std::shared_ptr<audio_stop_take> stop;
225 // Kept small: a job is posted to the workers in fixed storage.
226 int slot{};
228 int number{};
229 int layer{-1};
230 int channels{};
231 int rate{};
232 bool alone{};
233 int64_t frames{};
234 double tempo{};
235 int meter_numerator{4};
236 int meter_denominator{4};
237
238 bool write(const float* const* data)
239 {
240 const auto& path = writer->next_path(slot, number, layer);
241 const double quarters = tempo > 0 ? frames / double(rate) * tempo / 60. : 0.;
242 ossia::wav_loop_info info;
243 info.tempo = tempo;
244 info.beats = int(std::lround(quarters));
245 info.meter_numerator = meter_numerator;
246 info.meter_denominator = meter_denominator;
247 info.loop = true;
248 return ossia::write_loop_wav(path, channels, rate, frames, data, info);
249 }
250
251 bool run()
252 {
253 if(stop)
254 return run_stop();
255 if(!audio)
256 return false;
257 struct release
258 {
259 export_buffer& b;
260 ~release() { b.in_use = false; }
261 } done{*audio};
262 if(!writer || channels <= 0 || frames <= 0)
263 return false;
264
265 std::array<const float*, 16> data{};
266 channels = std::min<int>({channels, audio->channels, int(data.size())});
267 for(int c = 0; c < channels; c++)
268 data[c] = audio->channel(c);
269 return write(data.data());
270 }
271
275 bool run_stop()
276 {
277 auto& s = *stop;
278 struct release
279 {
281 ~release()
282 {
283 s.passes.pool.reset();
284 s.in_use = false;
285 }
286 } done{s};
287 std::array<const float*, 16> data{};
288 channels = std::min<int>({channels, s.passes.channels, int(data.size())});
289 bool ok = writer && s.passes.pool && channels > 0 && frames > 0;
290 if(ok)
291 {
292 std::vector<float> buffer(std::size_t(channels) * std::size_t(frames));
293 const auto channel = [&](int c) { return buffer.data() + std::size_t(c) * frames; };
294 for(int c = 0; c < channels; c++)
295 data[c] = channel(c);
296 number = 0;
297 for(layer = -1; ok && layer < s.passes.count; layer++)
298 {
299 s.passes.flatten(channels, channel, layer);
300 ok = write(data.data());
301 }
302 }
303 if(writer)
304 s.notice.written(*writer, slot, ok, s.passes.count, true);
305 return ok;
306 }
307};
308}
Definition AudioLayers.hpp:14
Definition AudioLayers.hpp:975
Definition TakeWriter.hpp:209
Definition TakeWriter.hpp:184
static std::shared_ptr< export_buffer > make(int channels, int64_t frames)
Main thread only. The memory is reserved without being written.
Definition TakeWriter.hpp:193
std::atomic_bool in_use
Definition TakeWriter.hpp:190
Definition TakeWriter.hpp:153
view_message view
What the looper showed when it stopped, with its last take.
Definition TakeWriter.hpp:155
void written(take_writer &w, int slot, bool ok, int layers, bool placeable)
Definition TakeWriter.hpp:166
int take
Definition TakeWriter.hpp:158
Definition TakeWriter.hpp:219
bool run_stop()
Definition TakeWriter.hpp:275
int number
Number of the take, 0 for a new one; -1 for the mix, else the layer.
Definition TakeWriter.hpp:228
std::shared_ptr< audio_stop_take > stop
Definition TakeWriter.hpp:224
Definition TakeWriter.hpp:44
std::function< void(const view_message &)> tell_views
Definition TakeWriter.hpp:57
static std::shared_ptr< take_writer > make(const score::DocumentContext &ctx, std::string_view prefix, std::string_view extension)
Main thread only.
Definition TakeWriter.hpp:70
const std::string & next_path(int slot, int &number, int layer)
Definition TakeWriter.hpp:98
const std::string & take_base(int slot) const noexcept
The path of a take's files without the layer and the extension.
Definition TakeWriter.hpp:142
static constexpr int slots
Definition TakeWriter.hpp:47
Definition LooperView.hpp:72
double take_date
Where the last take was recorded, in seconds from the process' start.
Definition LooperView.hpp:101
int take
Takes written so far; the files of the last one are take_files().
Definition LooperView.hpp:86
int version
Definition LooperView.hpp:142
Definition DocumentContext.hpp:18