Anklang 0.3.0-460-gc4ef46ba
ASE — Anklang Sound Engine (C++)

« « « Anklang Documentation
Loading...
Searching...
No Matches
clip.cc
Go to the documentation of this file.
1 // This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0
2#include "clip.hh"
3#include "track.hh"
4#include "jsonipc/jsonipc.hh"
5#include "project.hh"
6#include "serialize.hh"
7#include "platform.hh"
8#include "compress.hh"
9#include "path.hh"
10#include "internal.hh"
11#include <atomic>
12
13#define CDEBUG(...) Ase::debug ("ClipNote", __VA_ARGS__)
14#define UDEBUG(...) Ase::debug ("undo", __VA_ARGS__)
15
16namespace Ase {
17
18// == Clip ==
19Clip::Clip () :
20 all_notes (this, "all_notes"),
21 end_tick (this, "end_tick")
22{}
23
24// == ClipNote ==
25bool
26ClipNote::operator== (const ClipNote &o) const
27{
28 return (tick == o.tick &&
29 id == o.id &&
30 channel == o.channel &&
31 key == o.key &&
32 selected == o.selected &&
33 duration == o.duration &&
34 velocity == o.velocity &&
35 fine_tune == o.fine_tune);
36}
37
38// == ClipImpl ==
39ClipImpl::ClipImpl (TrackImpl &parent)
40{
41 track_ = &parent;
42 notifytrack_ = on_event ("notify", [this] (const Event &event) {
43 if (track_)
44 track_->update_clips();
45 });
46}
47
48ClipImpl::~ClipImpl()
49{}
50
51ProjectImpl*
52ClipImpl::project () const
53{
54 return track_ ? track_->project() : nullptr;
55}
56
57bool
58ClipImpl::needs_serialize() const
59{
60 return notes_.size() > 0;
61}
62
63static std::atomic<uint> next_noteid { MIDI_NOTE_ID_FIRST };
64
65void
66ClipImpl::serialize (WritNode &xs)
67{
68 GadgetImpl::serialize (xs);
69
70 // save notes, along with their quantization
71 if (xs.in_save())
72 {
73 xs["ppq"] << TRANSPORT_PPQN;
74 OrderedEventsP event_vector = notes_.ordered_events<OrderedEventsV>();
75 for (ClipNote cnote : *event_vector)
76 {
77 WritNode xn = xs["notes"].push();
78 xn & cnote;
79 xn.value().filter ([] (const ValueField &field) {
80 if (field.name == "id" || field.name == "selected")
81 return true;
82 return false;
83 });
84 }
85 }
86 // load notes, re-quantize, re-assign ids
87 if (xs.in_load())
88 {
90 xs["ppq"] >> ppq;
92 xs["notes"] & cnotes;
93 long double ppqfactor = TRANSPORT_PPQN / (long double) ppq;
94 for (const auto &cnote : cnotes)
95 {
96 ClipNote note = cnote;
97 note.id = next_noteid++; // automatic id allocation for new notes
98 note.tick = llrintl (note.tick * ppqfactor);
99 note.duration = llrintl (note.duration * ppqfactor);
100 note.selected = false;
101 notes_.insert (note);
102 }
103 emit_notify ("notes");
104 all_notes.notify();
105 // TODO: serialize range
106 }
107}
108
110ClipImpl::clip_index () const
111{
112 return track_ ? track_->clip_index (*this) : -1;
113}
114
115void
116ClipImpl::assign_range (int64 starttick, int64 stoptick)
117{
118 assert_return (starttick >= 0);
119 assert_return (stoptick >= starttick);
120 const auto last_starttick_ = starttick_;
121 const auto last_stoptick_ = stoptick_;
122 const auto last_endtick_ = endtick_;
123 starttick_ = starttick;
124 stoptick_ = stoptick;
125 endtick_ = std::max (starttick_, stoptick_);
126 if (last_endtick_ != endtick_)
127 emit_notify ("end_tick");
128 if (last_stoptick_ != stoptick_)
129 emit_notify ("stop_tick");
130 if (last_starttick_ != starttick_)
131 emit_notify ("start_tick");
132}
133
134ClipNoteS
135ClipImpl::list_all_notes ()
136{
137 ClipNoteS cnotes;
138 auto events = tick_events();
139 cnotes.assign (events->begin(), events->end());
140 return cnotes;
141}
142
143void
144ClipImpl::set_all_notes (const ClipNoteS &notes)
145{
146 // TODO: implement setter
147 all_notes.notify();
148}
149
150ClipNoteS
151ClipImpl::get_all_notes () const
152{
153 auto events = tick_events();
154 ClipNoteS notes;
155 notes.assign (events->begin(), events->end());
156 return notes;
157}
158
159int64
160ClipImpl::get_end_tick () const
161{
162 return endtick_;
163}
164
165void
166ClipImpl::set_end_tick (int64 etick)
167{
168 endtick_ = etick;
169 end_tick.notify();
170}
171
173ClipImpl::OrderedEventsP
174ClipImpl::tick_events () const
175{
176 return const_cast<ClipImpl*> (this)->notes_.ordered_events<OrderedEventsV> ();
177}
178
179ClipImpl::EventImage::EventImage (const ClipNoteS &clipnotes)
180{
181 const size_t clipnotes_bytes = clipnotes.size() * sizeof (clipnotes[0]);
182 cbuffer = zstd_compress (clipnotes.data(), clipnotes_bytes, 4);
183 assert_return (cbuffer.size() > 0);
184 ProjectImpl::undo_mem_counter += sizeof (*this) + cbuffer.size();
185 UDEBUG ("ClipImpl: store undo (notes=%d): %d->%d (%f%%)", clipnotes.size(),
186 clipnotes_bytes, cbuffer.size(), cbuffer.size() * 100.0 / clipnotes_bytes);
187}
188
189ClipImpl::EventImage::~EventImage()
190{
191 ProjectImpl::undo_mem_counter -= sizeof (*this) + cbuffer.size();
192 UDEBUG ("ClipImpl: free undo mem: %d\n", sizeof (*this) + cbuffer.size());
193}
194
195void
196ClipImpl::push_undo (const ClipNoteS &clipnotes, const String &undogroup)
197{
198 auto thisp = shared_ptr_from (this);
199 EventImageP imagep = std::make_shared<EventImage> (clipnotes);
200 undo_scope (undogroup) += [thisp, imagep, undogroup] () { thisp->apply_undo (*imagep, undogroup); };
201}
202
203void
204ClipImpl::apply_undo (const EventImage &image, const String &undogroup)
205{
206 push_undo (notes_.copy(), undogroup);
207 ClipNoteS onotes;
208 const ssize_t osize = zstd_target_size (image.cbuffer);
209 assert_return (osize >= 0 && osize == sizeof (onotes[0]) * (osize / sizeof (onotes[0])));
210 onotes.resize (osize / sizeof (onotes[0]));
211 const ssize_t rsize = zstd_uncompress (image.cbuffer, onotes.data(), osize);
212 assert_return (rsize == osize);
213 notes_.clear_silently();
214 for (const ClipNote &note : onotes)
215 notes_.insert (note);
216 emit_notify ("notes");
217 all_notes.notify();
218}
219
220size_t
221ClipImpl::collapse_notes (EventsById &inotes, const bool preserve_selected)
222{
223 ClipNoteS copies = inotes.copy();
224 size_t collapsed = 0;
225 // sort notes by tick, keep order; delete from lhs, preserve newer entries on rhs
226 std::stable_sort (copies.begin(), copies.end(), [] (const ClipNote &a, const ClipNote &b) {
227 return a.tick < b.tick;
228 });
229 // remove duplicates at the same tick
230 for (size_t i = 0; i < copies.size(); i++) {
231 const ClipNote &note = copies[i];
232 for (size_t j = i + 1; j < copies.size(); j++) {
233 if (note.tick != copies[j].tick)
234 break;
235 if (note.key == copies[j].key && note.channel == copies[j].channel) {
236 if (note.selected != copies[j].selected && preserve_selected)
237 continue;
238 // note has a successor at same tick, with same key, channel
239 collapsed += inotes.remove (note);
240 }
241 }
242 }
243 return collapsed;
244}
245
246int32
247ClipImpl::change_batch (const ClipNoteS &batch, const String &undogroup)
248{
249 bool changes = false, selections = false;
250 // save undo image
251 const ClipNoteS orig_notes = notes_.copy();
252 // delete existing notes
253 for (const auto &note : batch)
254 if (note.id > 0 && (note.duration == 0 || note.channel < 0)) {
255 changes |= notes_.remove (note);
256 CDEBUG ("%s: delete notes: %d\n", __func__, note.id);
257 }
258 // modify *existing* notes
259 for (const auto &note : batch)
260 if (note.id > 0 && note.duration > 0 && note.channel >= 0) {
261 ClipNote replaced;
262 if (notes_.replace (note, &replaced) && !(note == replaced)) {
263 replaced.selected = !replaced.selected;
264 if (note == replaced)
265 selections = true; // only selection changed
266 else
267 changes = true;
268 CDEBUG ("%s: %s %d: new=%s old=%s\n", __func__, note == replaced ? "toggle" : "replace", note.id,
269 stringify_clip_note (note), stringify_clip_note (replaced));
270 }
271 }
272 // insert new notes
273 for (const auto &note : batch)
274 if (note.id <= 0 && note.duration > 0 && note.channel >= 0) {
275 ClipNote ev = note;
276 ev.id = next_noteid++; // automatic id allocation for new notes
277 assert_warn (ev.id >= MIDI_NOTE_ID_FIRST && ev.id <= MIDI_NOTE_ID_LAST);
278 const bool replaced = notes_.insert (ev);
279 changes |= !replaced;
280 CDEBUG ("%s: insert: %s%s\n", __func__, stringify_clip_note (ev), replaced ? " (REPLACED?)" : "");
281 }
282 // collapse overlapping notes
283 if (changes || selections) {
284 const size_t collapsed = collapse_notes (notes_, true);
285 changes = changes || collapsed;
286 if (collapsed) CDEBUG ("%s: collapsed=%d\n", __func__, collapsed);
287 }
288 // queue undo
289 if (!notes_.equals (orig_notes)) {
290 if (changes)
291 push_undo (orig_notes, undogroup.empty() ? "Change Notes" : undogroup);
292 if (changes) CDEBUG ("%s: notes=%d undo_size: %fMB\n", __func__, notes_.size(), project()->undo_size_guess() / (1024. * 1024));
293 emit_notify ("notes");
294 all_notes.notify();
295 }
296 return 0;
297}
298
299// == ClipImpl::Generator ==
301void
302ClipImpl::Generator::setup (const ClipImpl &clip)
303{
304 ProjectImpl *p = clip.project();
305 TickSignature tsig;
306 if (p)
307 tsig = p->signature();
308 events_ = clip.tick_events();
309 muted_ = false;
310 start_offset_ = 0;
311 loop_start_ = 0;
312 loop_end_ = tsig.bar_ticks() * 2;
313 const int LOOPS = 2;
314 last_ = loop_end_ - start_offset_ + LOOPS * (loop_end_ - loop_start_);
315 if (true) // keep looping
316 last_ = M52MAX;
317}
318
320void
321ClipImpl::Generator::jumpto (int64 target_tick)
322{
323 // negative ticks indicate delay
324 if (target_tick < 0)
325 {
326 xtick_ = target_tick;
327 itick_ = xtick_;
328 return;
329 }
330 // external position
331 xtick_ = std::min (target_tick, play_length());
332 // advance internal position by externally observable ticks
333 itick_ = start_offset_;
334 return_unless (xtick_ > 0);
335 // beyond loop end
336 if (itick_ >= loop_end_)
337 {
338 itick_ = xtick_;
339 return;
340 }
341 // until loop end
342 int64 delta = xtick_;
343 const int64 frag = std::min (delta, loop_end_ - itick_);
344 delta -= frag;
345 itick_ += frag;
346 if (itick_ == loop_end_)
347 {
348 itick_ = loop_start_;
349 // within loop (loop count is discarded)
350 if (delta)
351 {
352 const int64 frac = delta % (loop_end_ - loop_start_);
353 itick_ += frac;
354 }
355 }
356}
357
359int64
360ClipImpl::Generator::generate (int64 target_tick, const Receiver &receiver)
361{
362 if (0)
363 printerr ("generate: %d < %d (%+d) && %d > %d (%+d) (loop: %d %d) i=%d\n", xtick_, last_, xtick_ < last_,
364 target_tick, xtick_, target_tick > xtick_,
365 loop_start_, loop_end_, itick_);
366 const int64 old_xtick = xtick_;
367 return_unless (xtick_ < last_ && target_tick > xtick_, xtick_ - old_xtick);
368 int64 ticks = std::min (target_tick, last_) - xtick_;
369 // consume delay
370 if (xtick_ < 0)
371 {
372 const int64 delta = std::min (ticks, -xtick_);
373 ticks -= delta;
374 xtick_ += delta;
375 itick_ += delta;
376 if (itick_ == 0)
377 itick_ = start_offset_;
378 }
379 // here: ticks == 0 || xtick_ >= 0
380 while (ticks > 0)
381 {
382 // advance
383 const int64 delta = itick_ < loop_end_ ? std::min (ticks, loop_end_ - itick_) : ticks;
384 ticks -= delta;
385 const int64 x = xtick_;
386 xtick_ += delta;
387 const int64 a = itick_;
388 itick_ += delta;
389 const int64 b = itick_;
390 if (itick_ == loop_end_)
391 itick_ = loop_start_;
392 // generate notes within [a,b)
393 if (receiver && !muted_)
394 {
395 ClipNote index = { .tick = a };
396 const ClipNote *event = events_->lookup_after (index);
397 while (event && event->tick < b)
398 {
399 MidiEvent midievent = make_note_on (event->channel, event->key, event->velocity, event->fine_tune, event->id);
400 const int64 noteon_tick = x + event->tick - a;
401 receiver (noteon_tick, midievent);
402 midievent.type = MidiEvent::NOTE_OFF;
403 receiver (noteon_tick + event->duration, midievent);
404 event++;
405 if (event == &*events_->end())
406 break;
407 }
408 }
409 }
410 return xtick_ - old_xtick;
411}
412
413String
414stringify_clip_note (const ClipNote &n)
415{
416 return string_format ("{%d,%d,%d,%s,%d,%d,%f,%f}",
417 n.id, n.channel, n.key,
418 n.selected ? "true" : "false",
419 n.tick, n.duration, n.velocity, n.fine_tune);
420}
421
422} // Ase
OrderedEventsP tick_events() const
Retrieve const vector with all notes ordered by tick.
Definition clip.cc:174
Ase::Track implementation.
Definition track.hh:10
One entry in a Writ serialization document.
Definition serialize.hh:24
WritNode push()
Append new WritNode for serializing arrays during in_save().
Definition serialize.cc:285
Value & value()
Access the Value of this node.
Definition serialize.hh:199
bool in_load() const
Return true during deserialization.
Definition serialize.hh:175
bool in_save() const
Return true during serialization.
Definition serialize.hh:181
T empty(T... args)
#define assert_return(expr,...)
Return from the current function if expr is unmet and issue an assertion warning.
Definition internal.hh:29
#define return_unless(cond,...)
Return silently if cond does not evaluate to true with return value ...
Definition internal.hh:71
#define assert_warn(expr)
Issue an assertion warning if expr evaluates to false.
Definition internal.hh:33
llrintl
typedef double
T max(T... args)
T min(T... args)
The Anklang C++ API namespace.
Definition api.hh:9
std::string string_format(const char *format, const Args &...args) __attribute__((__format__(__printf__
Format a string similar to sprintf(3) with support for std::string and std::ostringstream convertible...
int32_t int32
A 32-bit signed integer.
Definition cxxaux.hh:28
int64_t int64
A 64-bit unsigned integer, use PRI*64 in format strings.
Definition cxxaux.hh:29
constexpr const uint MIDI_NOTE_ID_LAST
Last valid (internal) MIDI note event ID.
Definition clip.hh:14
constexpr const uint MIDI_NOTE_ID_FIRST
First (internal) MIDI note event ID (lower IDs are reserved for external notes).
Definition clip.hh:12
std::shared_ptr< typename std::remove_pointer< Source >::type > shared_ptr_from(Source *object)
Use shared_ptr_cast<>() to convert an object pointer into a shared_ptr<>.
Definition cxxaux.hh:345
constexpr const int64 TRANSPORT_PPQN
Maximum number of sample frames to calculate in Processor::render().
Definition transport.hh:53
T size(T... args)
T stable_sort(T... args)
Part specific note event representation.
Definition api.hh:251
int64 tick
UI selection flag.
Definition api.hh:256
bool selected
Musical note as MIDI key, 0 .. 127.
Definition api.hh:255
float velocity
Duration in number of ticks.
Definition api.hh:258
int64 duration
Position in ticks.
Definition api.hh:257
int8 channel
ID, > 0.
Definition api.hh:253
int8 key
MIDI Channel.
Definition api.hh:254
float fine_tune
Velocity, 0 .. +1.
Definition api.hh:259
Structure for callback based notifications.
Definition value.hh:113
MidiEvent data structure.
Definition midievent.hh:50
MidiEventType type
MidiEvent type, one of the MidiEventType members.
Definition midievent.hh:55
Container for a sorted array of opaque Event structures with binary lookup.
Definition eventlist.hh:13
Musical time signature and tick conversions.
Definition transport.hh:61
void filter(const std::function< bool(const ValueField &)> &pred)
Recursively purge/remove RECORD elements iff to pred (recordfield) == true.
Definition value.cc:262
typedef ssize_t