Anklang-0.3.0.dev886+g785567a1 anklang-0.3.0.dev886+g785567a1
ASE — Anklang Sound Engine (C++)

« « « Anklang Documentation
Loading...
Searching...
No Matches
project.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 "trkn/tracktion.hh" // PCH include must come first
3
4#include "project.hh"
5#include "jsonipc/jsonipc.hh"
6#include "main.hh"
7#include "compress.hh"
8#include "path.hh"
9#include "unicode.hh"
10#include "serialize.hh"
11#include "storage.hh"
12#include "server.hh"
13#include "internal.hh"
14#include <list>
15
16#define UDEBUG(...) Ase::debug ("undo", __VA_ARGS__)
17
18using namespace std::literals;
19namespace te = tracktion::engine;
20
21namespace Ase {
22
23static Preference synth_latency_pref =
24 Preference ({
25 "project.default_license", _("Default License"), "",
26 "CC-BY-SA-4.0 - https://creativecommons.org/licenses/by-sa/4.0/legalcode",
27 "",
28 {}, STANDARD, {
29 String ("descr=") + _("Default LICENSE to apply in the project properties."), } });
30
32
33// == Project ==
34Project::Project()
35{}
36
38Project::last_project()
39{
40 return g_projects.empty() ? nullptr : g_projects.back();
41}
42
43// == TransportListener ==
45{
46 tracktion::TransportControl &transport;
47 ProjectImpl &project_;
49 LoopID ppt = LoopID::INVALID;
50 FastMemory::Block transport_block_;
51 std::list<std::function<void()>> stopped_callbacks_;
52 te::Edit *edit_ = nullptr;
53public:
54 struct Position {
55 int fps = 0, frame = 0;
56 int bar = 0, beat = 0;
57 int sxth = 0, tick = 0;
58 int snum = 0, sden = 0;
59 double bpm = 0, sec = 0;
60 int min = 0;
61 } &pos;
62 TransportListener (tracktion::TransportControl &tc, ProjectImpl &project) :
63 transport (tc), project_ (project),
64 transport_block_ (SERVER->telemem_allocate (sizeof (Position))),
65 edit_ (project.edit_.get()),
66 pos (*new (transport_block_.block_start) Position{})
67 {
68 assert_return (this_thread_is_ase());
69 transport.addChangeListener (this); // for ChangeListener
70 transport.addListener (this); // for TransportControl::Listener
71 if (edit_)
72 edit_->state.addListener (this);
73 }
74 ~TransportListener() override
75 {
76 assert_return (this_thread_is_ase());
77 if (edit_)
78 edit_->state.removeListener (this);
79 transport.removeListener (this);
80 transport.removeChangeListener (this);
81 SERVER->telemem_release (transport_block_);
82 }
83 void
84 valueTreePropertyChanged (juce::ValueTree &vtree, const juce::Identifier &id) override
85 {
86 return_unless (project_.edit_);
87 if (id == tracktion_engine::IDs::name) // vtree == edit_->state
88 project_.emit_notify ("name");
89 if (id == tracktion_engine::IDs::bpm) // vtree == edit_->tempoSequence.getTempo (0)->state
90 project_.emit_notify ("bpm");
91 if (id == tracktion_engine::IDs::numerator)
92 project_.emit_notify ("numerator");
93 if (id == tracktion_engine::IDs::denominator)
94 project_.emit_notify ("denominator");
95 if (id == tracktion_engine::IDs::volume) {
96 auto mvp = project_.edit_->getMasterVolumePlugin();
97 if (mvp && vtree == mvp->state)
98 project_.emit_notify ("master_volume");
99 }
100 }
101 void
102 valueTreeChildAdded (juce::ValueTree &parent, juce::ValueTree &child) override
103 {
104 if (parent == edit_->state && te::TrackList::isTrack (child))
105 project_.emit_notify ("all_tracks");
106 }
107 void
108 valueTreeChildRemoved (juce::ValueTree &parent, juce::ValueTree &child, int) override
109 {
110 if (parent == edit_->state && te::TrackList::isTrack (child))
111 project_.emit_notify ("all_tracks");
112 }
113 void valueTreeChildOrderChanged (juce::ValueTree&, int, int) override {}
114 void valueTreeParentChanged (juce::ValueTree&) override {}
115 void
116 changeListenerCallback (juce::ChangeBroadcaster *source) override
117 {
118 if (source == &transport) {
119 transport_changed ("change");
120 }
121 }
122 void autoSaveNow () override {}
123 void setAllLevelMetersActive (bool become_inactive) override {}
124 void setVideoPosition (tracktion::TimePosition pos, bool force_jump) override {}
125 void recordingStarted (tracktion::SyncPoint start, std::optional<tracktion::TimeRange> punch_range) override {}
126 void recordingStopped (tracktion::SyncPoint sync_point, bool discard_recordings) override {}
127 void recordingAboutToStart (tracktion::InputDeviceInstance &device, tracktion::EditItemID target) override {}
128 void recordingAboutToStop (tracktion::InputDeviceInstance &device, tracktion::EditItemID target) override {}
129 void recordingFinished (tracktion::InputDeviceInstance &device, tracktion::EditItemID target,
131 void
132 playbackContextChanged () override
133 {
134 tracktion::EditPlaybackContext *context = transport.getCurrentPlaybackContext();
135 Ase::diag ("PlaybackContextChanged: context=%p graph=%d playing=%d position=%.3fsecs\n", context,
136 context ? context->isPlaybackGraphAllocated() : 0,
137 context ? context->isPlaying() : 0,
138 context ? context->getPosition().inSeconds() : 0);
139 }
140 void
141 startVideo () override
142 {
143 assert_return (this_thread_is_ase());
144 poll_position();
145 if (ppt == LoopID::INVALID) // TODO: can we optimize telemetry form trkn?
146 ppt = main_loop->add ([this] { this->poll_position(); return true; }, std::chrono::milliseconds (16));
147 project_.emit_notify ("is_playing");
148 transport_changed ("start-video");
149 }
150 void
151 stopVideo () override
152 {
153 assert_return (this_thread_is_ase());
154 main_loop->cancel (&ppt);
155 poll_position();
156 project_.emit_notify ("is_playing");
157 transport_changed ("stop-video");
158 while (!stopped_callbacks_.empty()) {
159 const auto f = stopped_callbacks_.front();
160 stopped_callbacks_.pop_front();
161 f();
162 }
163 }
164 void
165 run_when_stopped (const std::function<void()> &f)
166 {
167 if (transport.isPlaying())
168 stopped_callbacks_.push_back (f);
169 else
170 f();
171 }
172 void
173 transport_changed (const std::string &what)
174 {
175 auto position = transport.getPosition();
176 Ase::printerr ("Transport: playing=%d position=%.3fsecs (%s)\n",
177 transport.isPlaying(), position.inSeconds(), what.c_str());
178 }
179 void
180 poll_position()
181 {
182 auto context = transport.getCurrentPlaybackContext();
183 return_unless (!!context);
184
185 auto &transport = project_.edit_->getTransport();
186 auto &tempoSeq = project_.edit_->tempoSequence;
187
188 // Get Current Time - getPosition() is the cursor position.
189 // Use edit->getCurrentPlaybackContext()->getAudibleTimelineTime() for compensating for latency
190 const tracktion::TimePosition currentPos = transport.getPosition();
191 const double totalSeconds = currentPos.inSeconds();
192
193 // Calculate Minutes / Seconds / Millis
194 // We use std::abs to handle potential negative times (pre-roll) safely
195 const double absSeconds = std::abs (totalSeconds);
196 const int intSeconds = int (absSeconds);
197 pos.min = intSeconds / 60;
198 pos.sec = absSeconds - pos.min * 60;
199
200 // Calculate Musical Position (Bars & Beats)
201 tracktion::tempo::BarsAndBeats bab = tempoSeq.toBarsAndBeats (currentPos);
202
203 // Tracktion uses 0-based indexing for Bars and Beats internally.
204 pos.bar = bab.bars;
205 pos.beat = bab.getWholeBeats();
206
207 // Calculate Sub-beat divisions (Sixteenths and Ticks)
208 // bab.getFractionalBeats() returns the remainder of the beat (0.0 to 0.999...)
209 double fractionalBeat = bab.getFractionalBeats().inBeats();
210
211 // Sixteenths: There are 4 sixteenths in a beat
212 pos.sxth = int (fractionalBeat * 4.0);
213
214 // Ticks: Tracktion standard is 960 PPQ (Pulses Per Quarter note)
215 pos.tick = int (fractionalBeat * 960.0);
216
217 // Get Tempo and Time Signature at this specific moment
218 // (Tempo can change during the song, so we ask for the value *at* currentPos)
219 pos.bpm = tempoSeq.getBpmAt (currentPos);
220 auto &timesig = tempoSeq.getTimeSigAt (currentPos);
221 pos.snum = timesig.numerator;
222 pos.sden = timesig.denominator;
223
224 // Calculate Frames (SMPTE)
225 const te::TimecodeDisplayFormat tdf = project_.edit_->getTimecodeFormat();
226 pos.fps = tdf.getFPS();
227
228 // Simple frame calculation: seconds * fps
229 // (Note: This is a basic calculation. For Drop-frame SMPTE, use tdf.toFullTimecode(...))
230 pos.frame = int (absSeconds * pos.fps) % int (pos.fps);
231 }
232};
233
234
235// == ProjectImpl ==
236using StringPairS = std::vector<std::tuple<String,String>>;
237
239 String loading_file;
240 String writer_cachedir;
241 String anklang_dir;
242 StringPairS writer_files;
243 StringPairS asset_hashes;
245 ptrp_ (ptrp)
246 {
247 *ptrp_ = this;
248 }
249 ~PStorage()
250 {
251 *ptrp_ = nullptr;
252 }
253private:
254 PStorage **const ptrp_ = nullptr;
255};
256
257ProjectImpl::ProjectImpl()
258{
259 bpm (120);
260 numerator (4);
261 denominator (4);
262 edit_ = std::make_unique<te::Edit> (*trkn_engine(), te::Edit::forEditing);
263 if (edit_) {
264 register_ase_obj (this, *edit_);
265 transport_listener_ = std::make_unique<TransportListener> (edit_->getTransport(), *this);
266 }
267 if (!edit_ || !transport_listener_)
268 fatal_error ("failed to create tracktion::engine::edit");
269
271
272 /* TODO: MusicalTuning
273 * group = _("Tuning");
274 * Prop ("musical_tuning", _("Musical Tuning"), _("Tuning"), MusicalTuning::OD_12_TET, {
275 * "descr="s + _("The tuning system which specifies the tones or pitches to be used. "
276 * "Due to the psychoacoustic properties of tones, various pitch combinations can "
277 * "sound \"natural\" or \"pleasing\" when used in combination, the musical "
278 * "tuning system defines the number and spacing of frequency values applied."), "" },
279 * enum_lister<MusicalTuning>);
280 */
281}
282
283void
284ProjectImpl::deactivate_edit()
285{
286 return_unless (!!edit_);
287 auto &transport = edit_->getTransport();
288 if (transport.isPlaying() || transport.isRecording())
289 transport.stop (true, true);
290 transport.freePlaybackContext();
292 edit_ = nullptr;
293}
294
295ProjectImpl::~ProjectImpl()
296{
297 unregister_ase_obj (this, edit_.get());
298 deactivate_edit();
299 transport_listener_ = nullptr;
300 edit_ = nullptr;
301}
302
303
304void
305ProjectImpl::force_shutdown_all ()
306{
307 rescan:
308 for (size_t i = 0; i < g_projects.size(); i++)
309 if (g_projects[i]->edit_) {
310 g_projects[i]->deactivate_edit();
311 goto rescan; // callbacks can change anything
312 }
313}
314
315String
316ProjectImpl::name() const
317{
318 // Edit.getName() requires af ProjectItem, which we dont use
319 return edit_ ? edit_->state.getProperty (tracktion_engine::IDs::name).toString().toStdString() : "";
320}
321
322void
323ProjectImpl::name (const std::string &nm)
324{
325 return_unless (!!edit_);
326 // tracktion_engine::getProjectItemForEdit (*edit_)->setName (nm, tracktion_engine::ProjectItem::SetNameMode::doDefault);
327 edit_->state.setProperty (tracktion_engine::IDs::name, juce::String (nm), &edit_->getUndoManager());
328}
329
332{
334 v.push_back (telemetry_field ("current_tick", &transport_listener_->pos.tick));
335 v.push_back (telemetry_field ("current_bar", &transport_listener_->pos.bar));
336 v.push_back (telemetry_field ("current_beat", &transport_listener_->pos.beat));
337 v.push_back (telemetry_field ("current_sixteenth", &transport_listener_->pos.sxth));
338 v.push_back (telemetry_field ("current_bpm", &transport_listener_->pos.bpm));
339 v.push_back (telemetry_field ("current_numerator", &transport_listener_->pos.snum));
340 v.push_back (telemetry_field ("current_denominator", &transport_listener_->pos.sden));
341 v.push_back (telemetry_field ("current_minutes", &transport_listener_->pos.min));
342 v.push_back (telemetry_field ("current_seconds", &transport_listener_->pos.sec));
343 return v;
344}
345
346void
347ProjectImpl::foreach_track (const std::function<bool(Track&,int)> &cb)
348{
349 std::function<bool(te::Track&,int)> foreach_track = [&] (te::Track &t, int depth)
350 {
351 const TrackImplP trackp = TrackImpl::from_trkn (t);
352 if (!trackp || !cb (*trackp, depth))
353 return false;
354 if (trackp->is_folder())
355 for (auto subtrack : dynamic_cast<te::FolderTrack*> (&t)->getAllSubTracks (false /*recursive*/))
356 if (subtrack &&
357 false == foreach_track (*subtrack, depth + 1))
359 return true;
360 };
361 edit_->visitAllTopLevelTracks ([&] (te::Track &t) { return foreach_track (t, 0); });
362}
363
364ProjectImplP
365ProjectImpl::create (const String &projectname)
366{
367 ProjectImplP project = ProjectImpl::make_shared();
368 g_projects.push_back (project);
369 project->name (projectname);
370 project->edit_->getUndoManager().clearUndoHistory();
371 return project;
372}
373
374void
376{
377 return_unless (!discarded_);
379 const size_t nerased = Aux::erase_first (g_projects, [this] (auto ptr) { return ptr.get() == this; });
380 if (nerased)
381 {} // resource cleanups
382 discarded_ = true;
383}
384
385void
387{
388 // Project has no parent; just emit the `removed` event
390}
391
392void
394{
397 // TODO: still needed for trkn?
398}
399
400void
402{
404 // TODO: still needed for trkn?
406}
407
408static bool
409is_anklang_dir (const String &path)
410{
411 return Path::check (Path::join (path, ".anklang.project"), "r");
412}
413
414static String
415find_anklang_parent_dir (const String &path)
416{
417 for (String p = path; !p.empty() && !Path::isroot (p); p = Path::dirname (p))
418 if (is_anklang_dir (p))
419 return p;
420 return "";
421}
422
423static bool
424make_anklang_dir (const String &path)
425{
426 String mime = Path::join (path, ".anklang.project");
427 return Path::stringwrite (mime, "# ANKLANG(1) project directory\n");
428}
429
430Error
432{
434 assert_return (storage_ == nullptr, Error::OPERATION_BUSY);
435 PStorage storage (&storage_); // storage_ = &storage;
436 const String dotanklang = ".anklang";
438 // check path is a file
439 if (path.back() == '/' ||
440 Path::check (path, "d")) // need file not directory
441 return Error::FILE_IS_DIR;
442 // force .anklang extension
443 if (!string_endswith (path, dotanklang))
444 path += dotanklang;
445 // existing files need proper project directories
446 if (Path::check (path, "e")) // existing file
447 {
448 const String dir = Path::dirname (path);
449 if (!is_anklang_dir (dir))
450 return Error::NO_PROJECT_DIR;
452 path = dir; // file inside project dir
453 }
454 else // new file name
455 {
457 const String parentdir = Path::dirname (path);
458 if (is_anklang_dir (parentdir))
459 path = parentdir;
460 else { // use projectfile stem as dir
461 assert_return (string_endswith (path, dotanklang), Error::INTERNAL);
462 path.resize (path.size() - dotanklang.size());
463 }
464 }
465 // create parent directory
466 if (!Path::mkdirs (path))
467 return ase_error_from_errno (errno);
468 // ensure path is_anklang_dir
469 if (!make_anklang_dir (path))
470 return ase_error_from_errno (errno);
471 storage_->anklang_dir = path;
472 const String abs_projectfile = Path::join (path, projectfile);
473 // create backups
474 if (Path::check (abs_projectfile, "e"))
475 {
476 const String backupdir = Path::join (path, "backup");
477 if (!Path::mkdirs (backupdir))
478 return ase_error_from_errno (errno ? errno : EPERM);
479 const StringPair parts = Path::split_extension (projectfile, true);
480 const String backupname = Path::join (backupdir, parts.first + now_strftime (" (%y%m%dT%H%M%S)") + parts.second);
481 const String backupglob = Path::join (backupdir, parts.first + " ([0-9]*[0-9]T[0-9]*[0-9])" + parts.second);
482 if (!Path::rename (abs_projectfile, backupname))
483 ASE_SERVER.user_note (string_format ("## Backup failed\n%s: \\\nFailed to create backup: \\\n%s",
484 backupname, ase_error_blurb (ase_error_from_errno (errno))));
485 else // successful backup, now prune
486 {
489 strings_version_sort (&backups, true);
490 const int bmax = 24;
491 while (backups.size() > bmax)
492 {
493 const String bfile = backups.back();
494 backups.pop_back();
496 }
497 }
498 }
499 // start writing
501 storage_->writer_cachedir = anklang_cachedir_create();
502 storage_->asset_hashes.clear();
503 StorageWriter ws (Storage::AUTO_ZSTD);
504 Error error = ws.open_with_mimetype (abs_projectfile, "application/x-anklang");
505 if (!error)
506 {
507 // serialize Project
508 String jsd = json_stringify (*this, Writ::RELAXED);
509 jsd += '\n';
510 error = ws.store_file_data ("project.json", jsd, true);
511 }
512 if (!error)
513 for (const auto &[path, dest] : storage_->writer_files) {
514 error = ws.store_file (dest, path);
515 if (!!error) {
516 printerr ("%s: %s: %s: %s\n", program_alias(), __func__, path, ase_error_blurb (error));
517 break;
518 }
519 }
520 storage_->writer_files.clear();
521 if (!error)
522 error = ws.close();
523 if (!error)
524 saved_filename_ = abs_projectfile;
525 if (!!error)
526 ws.remove_opened();
527 anklang_cachedir_cleanup (storage_->writer_cachedir);
528 return error;
529}
530
531Error
532ProjectImpl::snapshot_project (String &json)
533{
534 assert_return (storage_ == nullptr, Error::OPERATION_BUSY);
535 // writer setup
536 PStorage storage (&storage_); // storage_ = &storage;
537 storage_->writer_cachedir = anklang_cachedir_create();
538 if (storage_->writer_cachedir.empty() || !Path::check (storage_->writer_cachedir, "d"))
539 return Error::NO_PROJECT_DIR;
540 storage_->anklang_dir = storage_->writer_cachedir;
541 storage_->asset_hashes.clear();
542 // serialize Project
543 json = json_stringify (*this, Writ::RELAXED) + '\n';
544 // cleanup
545 anklang_cachedir_cleanup (storage_->writer_cachedir);
546 return Error::NONE;
547}
548
549String
550ProjectImpl::writer_file_name (const String &fspath) const
551{
552 assert_return (storage_ != nullptr, "");
553 assert_return (!storage_->writer_cachedir.empty(), "");
554 return Path::join (storage_->writer_cachedir, fspath);
555}
556
557Error
558ProjectImpl::writer_add_file (const String &fspath)
559{
560 assert_return (storage_ != nullptr, Error::INTERNAL);
561 assert_return (!storage_->writer_cachedir.empty(), Error::INTERNAL);
562 if (!Path::check (fspath, "frw"))
563 return Error::FILE_NOT_FOUND;
564 if (!string_startswith (fspath, storage_->writer_cachedir))
565 return Error::FILE_OPEN_FAILED;
566 storage_->writer_files.push_back ({ fspath, Path::basename (fspath) });
567 return Error::NONE;
568}
569
570Error
571ProjectImpl::writer_collect (const String &fspath, String *hexhashp)
572{
573 assert_return (storage_ != nullptr, Error::INTERNAL);
574 assert_return (!storage_->anklang_dir.empty(), Error::INTERNAL);
575 if (!Path::check (fspath, "fr"))
576 return Error::FILE_NOT_FOUND;
577 // determine hash of file to collect
578 const String hexhash = string_to_hex (blake3_hash_file (fspath));
579 if (hexhash.empty())
580 return ase_error_from_errno (errno ? errno : EIO);
581 // resolve against existing hashes
582 for (const auto &hf : storage_->asset_hashes)
583 if (std::get<0> (hf) == hexhash)
584 {
585 *hexhashp = hexhash;
586 return Error::NONE;
587 }
588 // file may be within project directory
590 if (Path::dircontains (storage_->anklang_dir, fspath, &relpath))
591 {
592 storage_->asset_hashes.push_back ({ hexhash, relpath });
593 *hexhashp = hexhash;
594 return Error::NONE;
595 }
596 // determine unique path name
597 const size_t file_size = Path::file_size (fspath);
598 const String basedir = storage_->anklang_dir;
599 relpath = Path::join ("samples", Path::basename (fspath));
600 String dest = Path::join (basedir, relpath);
601 size_t i = 0;
602 while (Path::check (dest, "e"))
603 {
604 if (file_size == Path::file_size (dest))
605 {
606 const String althash = string_to_hex (blake3_hash_file (dest));
607 if (althash == hexhash)
608 {
609 // found file with same hash within project directory
610 storage_->asset_hashes.push_back ({ hexhash, relpath });
611 *hexhashp = hexhash;
612 return Error::NONE;
613 }
614 }
615 // add counter to create unique name
616 const StringPair parts = Path::split_extension (relpath, true);
617 dest = Path::join (basedir, string_format ("%s(%u)%s", parts.first, ++i, parts.second));
618 }
619 // create parent dir
621 return ase_error_from_errno (errno);
622 // copy into project dir
623 const bool copied = Path::copy_file (fspath, dest);
624 if (!copied)
625 return ase_error_from_errno (errno);
626 // success
627 storage_->asset_hashes.push_back ({ hexhash, relpath });
628 *hexhashp = hexhash;
629 return Error::NONE;
630}
631
632String
634{
635 return encodefs (saved_filename_);
636}
637
638Error
640{
641 const String filename = decodefs (utf8filename);
642 assert_return (storage_ == nullptr, Error::OPERATION_BUSY);
643 PStorage storage (&storage_); // storage_ = &storage;
644 String fname = filename;
645 // turn /dir/.anklang.project -> /dir/
646 if (Path::basename (fname) == ".anklang.project" && is_anklang_dir (Path::dirname (fname)))
648 // turn /dir/ -> /dir/dir.anklang
649 if (Path::check (fname, "d"))
650 fname = Path::join (fname, Path::basename (Path::strip_slashes (Path::normalize (fname)))) + ".anklang";
651 // add missing '.anklang' extension
652 if (!Path::check (fname, "e"))
653 fname += ".anklang";
654 // check for readable file
655 if (!Path::check (fname, "e"))
656 return ase_error_from_errno (errno);
657 // try reading .anklang container
658 StorageReader rs (Storage::AUTO_ZSTD);
659 Error error = rs.open_for_reading (fname);
660 if (!!error)
661 return error;
662 if (rs.stringread ("mimetype") != "application/x-anklang")
663 return Error::BAD_PROJECT;
664 // find project.json *inside* container
665 String jsd = rs.stringread ("project.json");
666 if (jsd.empty() && errno)
667 return Error::FORMAT_INVALID;
668 storage_->loading_file = fname;
669 storage_->anklang_dir = find_anklang_parent_dir (storage_->loading_file);
670#if 0 // unimplemented
672 // search in dirname or dirname/..
673 if (is_anklang_dir (dirname))
674 rs.search_dir (dirname);
675 else
676 {
678 if (is_anklang_dir (dirname))
679 rs.search_dir (dirname);
680 }
681#endif
682 // parse project
683 if (!json_parse (jsd, *this))
684 return Error::PARSE_ERROR;
685 saved_filename_ = storage_->loading_file;
686 return Error::NONE;
687}
688
689StreamReaderP
690ProjectImpl::load_blob (const String &fspath)
691{
692 assert_return (storage_ != nullptr, nullptr);
693 assert_return (!storage_->loading_file.empty(), nullptr);
694 return stream_reader_zip_member (storage_->loading_file, fspath);
695}
696
698String
700{
701 return_unless (storage_ && storage_->asset_hashes.size(), "");
702 return_unless (!storage_->anklang_dir.empty(), "");
703 for (const auto& [hash,relpath] : storage_->asset_hashes)
704 if (hexhash == hash)
705 return Path::join (storage_->anklang_dir, relpath);
706 return "";
707}
708
709void
711{
712 // TODO: use tracktion_engine saving & loading
713}
714
715String
717{
718 String json;
719 Error error = snapshot_project (json);
720 if (!!error) {
721 warning ("Project: failed to serialize project: %s\n", ase_error_blurb (error));
722 return "";
723 }
724 return Re::grep (regex, json, group);
725}
726
727UndoScope::UndoScope (ProjectImplP projectp, const String &scopename) :
728 projectp_ (projectp),
729 scopename_ (scopename)
730{
732 assert_return (projectp->edit_);
733 projectp->edit_->getUndoManager().beginNewTransaction (juce::String (scopename));
734}
735
736UndoScope::~UndoScope()
737{
738 assert_return (projectp_);
739 assert_return (projectp_->edit_);
740 projectp_->edit_->getUndoManager().beginNewTransaction();
741}
742
743UndoScope
744ProjectImpl::undo_scope (const String &scopename)
745{
746 assert_warn (scopename != "");
747 return UndoScope (shared_ptr_cast<ProjectImpl> (this), scopename);
748}
749
750UndoScope
751ProjectImpl::add_undo_scope (const String &scopename)
752{
753 return UndoScope (shared_ptr_cast<ProjectImpl> (this), scopename);
754}
755
756void
758{
759 return_unless (!!edit_);
760 const bool had_undo = edit_->getUndoManager().canUndo();
761 edit_->getUndoManager().undo();
762 if (had_undo)
763 emit_notify ("dirty");
764}
765
766bool
768{
769 return_unless (!!edit_, false);
770 return edit_->getUndoManager().canUndo();
771}
772
773void
775{
776 return_unless (!!edit_);
777 const bool had_redo = edit_->getUndoManager().canRedo();
778 edit_->getUndoManager().redo();
779 if (had_redo)
780 emit_notify ("dirty");
781}
782
783bool
785{
786 return_unless (!!edit_, false);
787 return edit_->getUndoManager().canRedo();
788}
789
790double
792{
793 return_unless (!!edit_, 0.0);
794 return edit_->getLength().inSeconds();
795}
796
797double
799{
800 return_unless (!!edit_, 0.0);
801 auto volPlugin = edit_->getMasterVolumePlugin();
802 return_unless (!!volPlugin, 0.0);
803 return te::volumeFaderPositionToDB (volPlugin->volume.get());
804}
805
806void
808{
809 return_unless (!!edit_);
810 auto volPlugin = edit_->getMasterVolumePlugin();
812 const float sliderPos = te::decibelsToVolumeFaderPosition (db);
813 volPlugin->volume = sliderPos;
814 volPlugin->volParam->updateFromAttachedValue();
815}
816
817void
823
824void
830
831void
832ProjectImpl::clear_undo ()
833{
834 return_unless (!!edit_);
836 emit_notify ("dirty");
837}
838
839void
840ProjectImpl::bpm (double newbpm)
841{
842 return_unless (!!edit_);
843 const double nbpm = CLAMP (newbpm, MIN_BPM, MAX_BPM);
844 auto &tempoSeq = edit_->tempoSequence;
845 auto *tempo = tempoSeq.getTempo (0);
846 if (tempo && tempo->getBpm() != nbpm)
847 tempo->setBpm (nbpm);
848}
849
850double
851ProjectImpl::bpm () const
852{
853 return_unless (!!edit_, 120.0);
854 auto *tempo = edit_->tempoSequence.getTempo (0);
855 return tempo ? tempo->getBpm() : 120.0;
856}
857
858void
859ProjectImpl::numerator (double num)
860{
861 return_unless (!!edit_);
862 auto &tempoSeq = edit_->tempoSequence;
863 auto *timeSig = tempoSeq.getTimeSig (0);
864 if (timeSig && timeSig->numerator != num)
865 timeSig->numerator = num;
866}
867
868double
869ProjectImpl::numerator () const
870{
871 return_unless (!!edit_, 4.0);
872 auto *timeSig = edit_->tempoSequence.getTimeSig (0);
873 return timeSig ? timeSig->numerator : 4.0;
874}
875
876void
877ProjectImpl::denominator (double den)
878{
879 return_unless (!!edit_);
880 auto &tempoSeq = edit_->tempoSequence;
881 auto *timeSig = tempoSeq.getTimeSig (0);
882 if (timeSig && timeSig->denominator != den)
883 timeSig->denominator = den;
884}
885
886double
887ProjectImpl::denominator () const
888{
889 return_unless (!!edit_, 4.0);
890 auto *timeSig = edit_->tempoSequence.getTimeSig (0);
891 return timeSig ? timeSig->denominator : 4.0;
892}
893
894void
896{
897 assert_return (!discarded_);
899 if (edit_->getTransport().isPlayContextActive())
900 edit_->getTransport().play (false);
901}
902
903void
905{
906 if (edit_->getTransport().isPlaying())
907 edit_->getTransport().stop (false, false);
908}
909
910void
912{
913 edit_->getTransport().stop (false, true);
914 transport_listener_->run_when_stopped ([this] {
915 // wait until stopped, so the new position persists
916 edit_->getTransport().setPosition (tracktion::TimePosition::fromSeconds (0.0));
917 transport_listener_->poll_position();
918 });
919}
920
921bool
923{
924 return edit_->getTransport().isPlaying();
925}
926
927void
929{
930 if (is_playing() == play)
931 return;
932 if (is_playing())
934 else
936}
937
938TrackP
940{
941 return_unless (edit_ && !discarded_, nullptr);
942 auto t = edit_->insertNewAudioTrack (tracktion::TrackInsertPoint (nullptr, nullptr), nullptr);
943 if (!t) return nullptr;
944 TrackImplP track = TrackImpl::from_trkn (*t);
945 emit_event ("track", "insert", { { "track", track }, });
946 emit_notify ("all_tracks");
947 return track;
948}
949
950TrackS
952{
954 auto tf = [&] (Track &track, int depth)
955 {
956 tracks.push_back (shared_ptr_cast<TrackImpl> (&track));
957 return true;
958 };
959 foreach_track (tf);
960 return tracks;
961}
962
964ProjectImpl::track_index (const Track &child) const
965{
966 ssize_t index = 0;
967 ssize_t found = -1;
968 auto tf = [&] (Track &track, int depth)
969 {
970 if (&track == &child)
971 {
972 found = index;
973 return false;
974 }
975 index++;
976 return true;
977 };
978 const_cast<ProjectImpl*> (this)->foreach_track (tf);
979 return found;
980}
981
983ProjectImpl::bar_ticks () const
984{
985 return_unless (!!edit_, 0);
986 auto &tempoSeq = edit_->tempoSequence;
987 auto *timeSig = tempoSeq.getTimeSig (0);
988 if (!timeSig)
989 return 0;
990
991 const int beats_per_bar = timeSig->numerator;
992 const int beat_unit = timeSig->denominator;
993
994 // Calculate beat ticks: SEMIQUAVER_TICKS * (16 / beat_unit)
995 // SEMIQUAVER_TICKS = TRANSPORT_PPQN / 4 = 1209600
996 const int64 SEMIQUAVER_TICKS = 1209600;
997 const int semiquavers_per_beat = 16 / beat_unit;
998 const int64 beat_ticks = SEMIQUAVER_TICKS * semiquavers_per_beat;
999
1000 return beat_ticks * beats_per_bar;
1001}
1002
1003TrackP
1005{
1006 return_unless (!!edit_, nullptr);
1007 auto *masterTrack = edit_->getMasterTrack();
1008 return_unless (masterTrack, nullptr);
1009 return TrackImpl::from_trkn (*masterTrack);
1010}
1011
1014{
1015 return {}; // TODO: DeviceInfo
1016}
1017
1018} // Ase
#define EPERM
T back(T... args)
T c_str(T... args)
bool is_active() override
Check whether this is the active synthesis engine project.
Definition device.hh:20
void _deactivate() override
Stop processing the corresponding AudioProcessor.
Definition device.cc:27
void _activate() override
Add AudioProcessor to the Engine and start processing.
Definition device.cc:20
void emit_notify(const String &detail) override
Emit notify:detail, multiple notifications maybe coalesced if a CoalesceNotifies instance exists.
Definition object.cc:164
void remove_self() override
Remove self from parent container.
Definition gadget.cc:167
void group_undo(const String &undoname) override
Merge upcoming undo steps.
Definition project.cc:818
void _deactivate() override
Stop processing the corresponding AudioProcessor.
Definition project.cc:401
void redo() override
Redo the last undo modification.
Definition project.cc:774
bool can_redo() override
Check if any redo steps have been recorded.
Definition project.cc:784
TrackP master_track() override
Retrieve the master track.
Definition project.cc:1004
void undo() override
Undo the last project modification.
Definition project.cc:757
String match_serialized(const String &regex, int group) override
Match regex against the serialized project state.
Definition project.cc:716
bool is_playing() const override
Check whether a project is currently playing (song sequencing).
Definition project.cc:922
void remove_self() override
Remove self from parent container.
Definition project.cc:386
bool can_undo() override
Check if any undo steps have been recorded.
Definition project.cc:767
void serialize(WritNode &xs) override
Serialize members and childern.
Definition project.cc:710
TelemetryFieldS telemetry() const override
Retrieve project telemetry locations.
Definition project.cc:331
TrackS all_tracks() override
List all tracks of the project.
Definition project.cc:951
void ungroup_undo() override
Stop merging undo steps.
Definition project.cc:825
String saved_filename() override
Retrieve UTF-8 filename for save or from load.
Definition project.cc:633
TrackP create_track() override
Create and append a new Track.
Definition project.cc:939
Error save_project(const String &utf8filename, bool collect) override
Store Project and collect external files.
Definition project.cc:431
void _activate() override
Add AudioProcessor to the Engine and start processing.
Definition project.cc:393
double master_volume() const override
Get master volume in dB.
Definition project.cc:798
String loader_resolve(const String &hexhash)
Find file from hash code, returns fspath.
Definition project.cc:699
void start_playback() override
Start playback of a project, requires active sound engine.
Definition project.hh:69
DeviceInfo device_info() override
Describe this Device type.
Definition project.cc:1013
void discard() override
Discard project and associated resources.
Definition project.cc:375
Error load_project(const String &utf8filename) override
Load project from file filename.
Definition project.cc:639
double length() const override
Get the end time of the last clip in seconds.
Definition project.cc:791
void pause_playback() override
Pause playback at the current position.
Definition project.cc:904
void stop_playback() override
Stop project playback.
Definition project.cc:911
static String grep(const String &regex, const String &input, int group=0, Flags=DEFAULT)
Find regex in input and return matching string.
Definition regex.cc:225
Container for Clip objects and sequencing information.
Definition api.hh:269
One entry in a Writ serialization document.
Definition serialize.hh:24
void beginNewTransaction()
bool canUndo() const
bool canRedo() const
ValueTree & setProperty(const Identifier &name, const var &newValue, UndoManager *undoManager)
void addListener(Listener *listener)
const var & getProperty(const Identifier &name) const noexcept
void removeListener(Listener *listener)
VolumeAndPanPlugin::Ptr getMasterVolumePlugin() const
juce::ValueTree state
TransportControl & getTransport() const noexcept
TimeDuration getLength() const
void visitAllTopLevelTracks(std::function< bool(Track &)>) const
MasterTrack * getMasterTrack() const
TempoSequence tempoSequence
juce::ReferenceCountedObjectPtr< AudioTrack > insertNewAudioTrack(TrackInsertPoint, SelectionManager *)
juce::UndoManager & getUndoManager() noexcept
TimeSigSetting * getTimeSig(int index) const
TempoSetting * getTempo(int index) const
void ensureContextAllocated(bool alwaysReallocate=false)
void play(bool justSendMMCIfEnabled)
void stop(bool discardRecordings, bool clearDevices, bool canSendMMCStop=true)
T clear(T... args)
#define ASE_CLASS_NON_COPYABLE(ClassName)
Delete copy ctor and assignment operator.
Definition cxxaux.hh:111
dirname
T empty(T... args)
errno
#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:73
#define CLAMP(v, mi, ma)
Yield v clamped to [mi … ma].
Definition internal.hh:60
#define assert_warn(expr)
Issue an assertion warning if expr evaluates to false.
Definition internal.hh:33
#define _(...)
Retrieve the translation of a C or C++ string.
Definition internal.hh:18
typedef int
size_t erase_first(C &container, const std::function< bool(typename C::value_type const &value)> &pred)
Erase first element for which pred() is true in vector or list.
Definition utils.hh:268
void glob(const String &pathpattern, StringS &dirs, StringS &files)
Create list with directories and filenames matching pathpattern with shell wildcards.
Definition path.cc:812
String basename(const String &path)
Strips all directory components from path and returns the resulting file name.
Definition path.cc:68
String dirname(const String &path)
Retrieve the directory part of the filename path.
Definition path.cc:60
bool mkdirs(const String &dirpath, uint mode)
Create the directories in dirpath with mode, check errno on false returns.
Definition path.cc:197
bool check(const String &file, const String &mode)
Definition path.cc:625
String strip_slashes(const String &path)
Strip trailing directory terminators.
Definition path.cc:118
String abspath(const String &path, const String &incwd)
Definition path.cc:134
void rmrf(const String &dir)
Recursively delete directory tree.
Definition path.cc:236
size_t file_size(const String &path)
Retrieve the on-disk size in bytes of path.
Definition path.cc:514
bool copy_file(const String &src, const String &dest)
Copy a file to a new non-existing location, sets errno and returns false on error.
Definition path.cc:244
bool isroot(const String &path, bool dos_drives)
Return wether path is an absolute pathname which identifies the root directory.
Definition path.cc:160
bool dircontains(const String &dirpath, const String &descendant, String *relpath)
Check if descendant belongs to the directory hierarchy under dirpath.
Definition path.cc:221
String normalize(const String &path)
Convert path to normal form.
Definition path.cc:86
The Anklang C++ API namespace.
Definition api.hh:8
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...
String anklang_cachedir_create()
Create exclusive cache directory for this process' runtime.
Definition storage.cc:106
String string_to_hex(const String &input)
Convert bytes in string input to hexadecimal numbers.
Definition strings.cc:1171
bool json_parse(const String &jsonstring, T &target)
Parse a well formed JSON string and assign contents to target.
Definition serialize.hh:538
int64_t int64
A 64-bit unsigned integer, use PRI*64 in format strings.
Definition cxxaux.hh:29
void register_ase_obj(VirtualBase *ase_impl, tracktion::Selectable &selectable)
Helper: register AseImpl with a tracktion Selectable via ase_obj_.
Definition trkn-utils.cc:62
Error
Enum representing Error states.
Definition api.hh:21
const char * ase_error_blurb(Error error)
Describe Error condition.
Definition server.cc:270
std::string decodefs(const std::string &utf8str)
Decode UTF-8 string back into file system path representation, extracting surrogate code points as by...
Definition unicode.cc:131
void anklang_cachedir_clean_stale()
Clean stale cache directories from past runtimes, may be called from any thread.
Definition storage.cc:161
String program_alias()
Retrieve the program name as used for logging or debug messages.
Definition platform.cc:817
std::string String
Convenience alias for std::string.
Definition cxxaux.hh:35
constexpr const char STANDARD[]
STORAGE GUI READABLE WRITABLE.
Definition api.hh:13
void unregister_ase_obj(VirtualBase *ase_impl, tracktion::Selectable *selectable)
Helper: unregister AseImpl from a tracktion Selectable (selectable may be nullptr)
Definition trkn-utils.cc:70
bool string_endswith(const String &string, const String &fragment)
Returns whether string ends with fragment.
Definition strings.cc:863
void anklang_cachedir_cleanup(const String &cachedir)
Cleanup a cachedir previously created with anklang_cachedir_create().
Definition storage.cc:142
std::string encodefs(const std::string &fschars)
Encode a file system path consisting of bytes into UTF-8, using surrogate code points to store non UT...
Definition unicode.cc:112
String json_stringify(const T &source, Writ::Flags flags=Writ::Flags(0))
Create JSON string from source.
Definition serialize.hh:530
bool string_startswith(const String &string, const String &fragment)
Returns whether string starts with fragment.
Definition strings.cc:846
Info for device types.
Definition api.hh:200
T push_back(T... args)
T resize(T... args)
T size(T... args)
Reference for an allocated memory block.
Definition memory.hh:90
typedef ssize_t