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