Anklang-0.3.0-1416-gfa8ce8d2 anklang-0.3.0-1416-gfa8ce8d2
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 == project_.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 ((te::Clip::isClipState (vtree) && (id == te::IDs::start || id == te::IDs::length)) ||
94 vtree.hasType (te::IDs::TEMPO) || vtree.hasType (te::IDs::TIMESIG))
95 project_.emit_notify ("length");
96 }
97 void
98 valueTreeChildAdded (juce::ValueTree &parent, juce::ValueTree &child) override
99 {
100 if (parent == edit_->state && te::TrackList::isTrack (child))
101 project_.emit_notify ("all_tracks");
102 if (te::TrackList::isTrack (child) || te::Clip::isClipState (child) || parent.hasType (te::IDs::TEMPOSEQUENCE))
103 project_.emit_notify ("length");
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 if (te::TrackList::isTrack (child) || te::Clip::isClipState (child) || parent.hasType (te::IDs::TEMPOSEQUENCE))
111 project_.emit_notify ("length");
112 }
113 void
114 valueTreeChildOrderChanged (juce::ValueTree &parent, int, int newIndex) override
115 {
116 // Reordering tracks changes all_tracks() return order, so emit like add/remove
117 if (parent == edit_->state && te::TrackList::isTrack (parent.getChild (newIndex)))
118 project_.emit_notify ("all_tracks");
119 }
120 void valueTreeParentChanged (juce::ValueTree&) override {}
121 void
122 changeListenerCallback (juce::ChangeBroadcaster *source) override
123 {
124 if (source == &transport) {
125 transport_changed ("change");
126 }
127 }
128 void autoSaveNow () override {}
129 void setAllLevelMetersActive (bool become_inactive) override {}
130 void setVideoPosition (tracktion::TimePosition pos, bool force_jump) override {}
131 void recordingStarted (tracktion::SyncPoint start, std::optional<tracktion::TimeRange> punch_range) override {}
132 void recordingStopped (tracktion::SyncPoint sync_point, bool discard_recordings) override {}
133 void recordingAboutToStart (tracktion::InputDeviceInstance &device, tracktion::EditItemID target) override {}
134 void recordingAboutToStop (tracktion::InputDeviceInstance &device, tracktion::EditItemID target) override {}
135 void recordingFinished (tracktion::InputDeviceInstance &device, tracktion::EditItemID target,
137 void
138 playbackContextChanged () override
139 {
140 tracktion::EditPlaybackContext *context = transport.getCurrentPlaybackContext();
141 Ase::diag ("PlaybackContextChanged: context=%p graph=%d playing=%d position=%.3fsecs\n", context,
142 context ? context->isPlaybackGraphAllocated() : 0,
143 context ? context->isPlaying() : 0,
144 context ? context->getPosition().inSeconds() : 0);
145 }
146 void
147 startVideo () override
148 {
149 assert_return (this_thread_is_ase());
150 poll_position();
151 if (ppt == LoopID::INVALID) // TODO: can we optimize telemetry form trkn?
152 ppt = main_loop->add ([this] { this->poll_position(); return true; }, std::chrono::milliseconds (16));
153 project_.emit_notify ("is_playing");
154 transport_changed ("start-video");
155 }
156 void
157 stopVideo () override
158 {
159 assert_return (this_thread_is_ase());
160 main_loop->cancel (&ppt);
161 poll_position();
162 project_.emit_notify ("is_playing");
163 transport_changed ("stop-video");
164 while (!stopped_callbacks_.empty()) {
165 const auto f = stopped_callbacks_.front();
166 stopped_callbacks_.pop_front();
167 f();
168 }
169 }
170 void
171 run_when_stopped (const std::function<void()> &f)
172 {
173 if (transport.isPlaying())
174 stopped_callbacks_.push_back (f);
175 else
176 f();
177 }
178 void
179 transport_changed (const std::string &what)
180 {
181 auto position = transport.getPosition();
182 Ase::printerr ("Transport: playing=%d position=%.3fsecs (%s)\n",
183 transport.isPlaying(), position.inSeconds(), what.c_str());
184 }
185 void
186 poll_position()
187 {
188 auto context = transport.getCurrentPlaybackContext();
189 return_unless (!!context);
190
191 auto &transport = project_.edit_->getTransport();
192 auto &tempoSeq = project_.edit_->tempoSequence;
193
194 // Get Current Time - getPosition() is the cursor position.
195 // Use edit->getCurrentPlaybackContext()->getAudibleTimelineTime() for compensating for latency
196 const tracktion::TimePosition currentPos = transport.getPosition();
197 const double totalSeconds = currentPos.inSeconds();
198
199 // Calculate Minutes / Seconds / Millis
200 // We use std::abs to handle potential negative times (pre-roll) safely
201 const double absSeconds = std::abs (totalSeconds);
202 const int intSeconds = int (absSeconds);
203 pos.min = intSeconds / 60;
204 pos.sec = absSeconds - pos.min * 60;
205
206 // Calculate Musical Position (Bars & Beats)
207 tracktion::tempo::BarsAndBeats bab = tempoSeq.toBarsAndBeats (currentPos);
208
209 // Tracktion uses 0-based indexing for Bars and Beats internally.
210 pos.bar = bab.bars;
211 pos.beat = bab.getWholeBeats();
212
213 // Calculate Sub-beat divisions (Sixteenths and Ticks)
214 // bab.getFractionalBeats() returns the remainder of the beat (0.0 to 0.999...)
215 double fractionalBeat = bab.getFractionalBeats().inBeats();
216
217 // Sixteenths: There are 4 sixteenths in a beat
218 pos.sxth = int (fractionalBeat * 4.0);
219
220 // Ticks: Tracktion standard is 960 PPQ (Pulses Per Quarter note)
221 pos.tick = int (fractionalBeat * 960.0);
222
223 // Get Tempo and Time Signature at this specific moment
224 // (Tempo can change during the song, so we ask for the value *at* currentPos)
225 pos.bpm = tempoSeq.getBpmAt (currentPos);
226 auto &timesig = tempoSeq.getTimeSigAt (currentPos);
227 pos.snum = timesig.numerator;
228 pos.sden = timesig.denominator;
229
230 // Calculate Frames (SMPTE)
231 const te::TimecodeDisplayFormat tdf = project_.edit_->getTimecodeFormat();
232 pos.fps = tdf.getFPS();
233
234 // Simple frame calculation: seconds * fps
235 // (Note: This is a basic calculation. For Drop-frame SMPTE, use tdf.toFullTimecode(...))
236 pos.frame = int (absSeconds * pos.fps) % int (pos.fps);
237 }
238};
239
240
241// == ProjectImpl ==
242using StringPairS = std::vector<std::tuple<String,String>>;
243
245 String loading_file;
246 String writer_cachedir;
247 String anklang_dir;
248 StringPairS writer_files;
249 StringPairS asset_hashes;
251 ptrp_ (ptrp)
252 {
253 *ptrp_ = this;
254 }
255 ~PStorage()
256 {
257 *ptrp_ = nullptr;
258 }
259private:
260 PStorage **const ptrp_ = nullptr;
261};
262
263static void
264test_sfz (ProjectImpl *project, te::Edit *edit, const String &filename)
265{
266 TrackP track = project->create_track();
267 assert (track);
268 track->name ("LiquidSFZTest");
269
272
273 double start = 0;
274 double duration = 4;
275 ClipP clip = trackimpl->create_midi_clip ("NotesClip", start, duration);
276 assert (clip);
277
279 auto add_note = [&] (int start, int key, int duration)
280 {
282 note.id = -1;
283 note.note_number = key;
284 note.start_beat = start;
285 note.length_beats = duration;
286 note.velocity = 102;
287 batch.push_back (note);
288 };
289 add_note (0, 60, 1);
290 add_note (1, 64, 1);
291 add_note (2, 67, 1);
292 clip->change_batch (batch, "Add Note");
293
294 auto& engine = edit->engine;
295 engine.getPluginManager().createBuiltInType<LiquidSFZPlugin>();
296
297 auto plugin = trackimpl->create_plugin (LiquidSFZPlugin::xmlTypeName);
298 assert (plugin);
299
301 if (auto liquidsfz = dynamic_cast<LiquidSFZPlugin *> (pluginimpl->plugin()))
302 liquidsfz->load (filename);
303
304 auto &transport = edit->getTransport();
305 transport.setLoopRange({ tracktion::TimePosition::fromSeconds (start), tracktion::TimeDuration::fromSeconds (duration) });
306 transport.looping = true;
307 transport.setPosition (tracktion::TimePosition::fromSeconds (start));
308}
309
310static bool
311trkn_has_track_name (te::Edit &edit, const juce::String &name)
312{
313 bool found = false;
314 edit.visitAllTracksRecursive ([&] (te::Track &track) {
315 found = track.getName() == name;
316 return !found;
317 });
318 return found;
319}
320
321ProjectImpl::ProjectImpl()
322{
323 edit_ = std::make_unique<te::Edit> (*trkn_engine(), te::Edit::forEditing);
324 if (edit_) {
325 register_ase_obj (this, *edit_);
326 transport_listener_ = std::make_unique<TransportListener> (edit_->getTransport(), *this);
327 }
328 if (!edit_ || !transport_listener_)
329 fatal_error ("failed to create tracktion::engine::edit");
330
331 if (auto filename = getenv ("SFZ"))
332 test_sfz (this, edit_.get(), filename);
333
334 edit_->visitAllTracksRecursive ([] (te::Track &track) {
335 if (track.isAudioTrack() || track.isFolderTrack())
336 track.setName (track.getName());
337 return true;
338 });
340
341 /* TODO: MusicalTuning
342 * group = _("Tuning");
343 * Prop ("musical_tuning", _("Musical Tuning"), _("Tuning"), MusicalTuning::OD_12_TET, {
344 * "descr="s + _("The tuning system which specifies the tones or pitches to be used. "
345 * "Due to the psychoacoustic properties of tones, various pitch combinations can "
346 * "sound \"natural\" or \"pleasing\" when used in combination, the musical "
347 * "tuning system defines the number and spacing of frequency values applied."), "" },
348 * enum_lister<MusicalTuning>);
349 */
350}
351
352void
353ProjectImpl::deactivate_edit()
354{
355 return_unless (!!edit_);
356 auto &transport = edit_->getTransport();
357 if (transport.isPlaying() || transport.isRecording())
358 transport.stop (true, true);
359 transport.freePlaybackContext();
361 transport_listener_ = nullptr; // detach from edit_ and transport
362 edit_ = nullptr;
363}
364
365ProjectImpl::~ProjectImpl()
366{
367 unregister_ase_obj (this, edit_.get());
368 deactivate_edit();
369}
370
371
372void
373ProjectImpl::force_shutdown_all ()
374{
375 rescan:
376 for (size_t i = 0; i < g_projects.size(); i++)
377 if (g_projects[i]->edit_) {
378 g_projects[i]->deactivate_edit();
379 goto rescan; // callbacks can change anything
380 }
381}
382
383String
384ProjectImpl::name() const
385{
386 // Edit.getName() requires af ProjectItem, which we dont use
387 return edit_ ? edit_->state.getProperty (tracktion_engine::IDs::name).toString().toStdString() : "";
388}
389
390void
391ProjectImpl::name (const std::string &nm)
392{
393 return_unless (!!edit_);
394 // tracktion_engine::getProjectItemForEdit (*edit_)->setName (nm, tracktion_engine::ProjectItem::SetNameMode::doDefault);
395 edit_->getUndoManager().beginNewTransaction ("Set Project Name");
396 edit_->state.setProperty (tracktion_engine::IDs::name, juce::String (nm), &edit_->getUndoManager());
397}
398
401{
403 return_unless (transport_listener_, v);
404 v.push_back (telemetry_field ("current_tick", &transport_listener_->pos.tick));
405 v.push_back (telemetry_field ("current_bar", &transport_listener_->pos.bar));
406 v.push_back (telemetry_field ("current_beat", &transport_listener_->pos.beat));
407 v.push_back (telemetry_field ("current_sixteenth", &transport_listener_->pos.sxth));
408 v.push_back (telemetry_field ("current_bpm", &transport_listener_->pos.bpm));
409 v.push_back (telemetry_field ("current_numerator", &transport_listener_->pos.snum));
410 v.push_back (telemetry_field ("current_denominator", &transport_listener_->pos.sden));
411 v.push_back (telemetry_field ("current_minutes", &transport_listener_->pos.min));
412 v.push_back (telemetry_field ("current_seconds", &transport_listener_->pos.sec));
413 return v;
414}
415
416void
417ProjectImpl::foreach_track (const std::function<bool(Track&,int)> &cb) const
418{
419 std::function<bool(te::Track&,int)> foreach_track = [&] (te::Track &t, int depth)
420 {
421 const TrackImplP trackp = TrackImpl::from_trkn (t);
422 if (!trackp || !cb (*trackp, depth))
423 return false;
424 if (trackp->is_folder())
425 for (auto subtrack : dynamic_cast<te::FolderTrack*> (&t)->getAllSubTracks (false /*recursive*/))
426 if (subtrack &&
427 false == foreach_track (*subtrack, depth + 1))
429 return true;
430 };
431 edit_->visitAllTopLevelTracks ([&] (te::Track &t) { return foreach_track (t, 0); });
432}
433
434ProjectImplP
435ProjectImpl::create (const String &projectname)
436{
437 ProjectImplP project = ProjectImpl::make_shared();
438 g_projects.push_back (project);
439 project->name (projectname);
440 project->edit_->getUndoManager().clearUndoHistory();
441 return project;
442}
443
444void
446{
447 return_unless (!discarded_);
449 const size_t nerased = Aux::erase_first (g_projects, [this] (auto ptr) { return ptr.get() == this; });
450 if (nerased)
451 {} // resource cleanups
452 discarded_ = true;
453}
454
455void
457{
458 // Project has no parent; just emit the `removed` event
460}
461
462static bool
463is_anklang_dir (const String &path)
464{
465 return Path::check (Path::join (path, ".anklang.project"), "r");
466}
467
468static String
469find_anklang_parent_dir (const String &path)
470{
471 for (String p = path; !p.empty() && !Path::isroot (p); p = Path::dirname (p))
472 if (is_anklang_dir (p))
473 return p;
474 return "";
475}
476
477static bool
478make_anklang_dir (const String &path)
479{
480 String mime = Path::join (path, ".anklang.project");
481 return Path::stringwrite (mime, "# ANKLANG(1) project directory\n");
482}
483
484Error
486{
488 assert_return (storage_ == nullptr, Error::OPERATION_BUSY);
489 PStorage storage (&storage_); // storage_ = &storage;
490 const String dotanklang = ".anklang";
492 // check path is a file
493 if (path.back() == '/' ||
494 Path::check (path, "d")) // need file not directory
495 return Error::FILE_IS_DIR;
496 // force .anklang extension
497 if (!string_endswith (path, dotanklang))
498 path += dotanklang;
499 // existing files need proper project directories
500 if (Path::check (path, "e")) // existing file
501 {
502 const String dir = Path::dirname (path);
503 if (!is_anklang_dir (dir))
504 return Error::NO_PROJECT_DIR;
506 path = dir; // file inside project dir
507 }
508 else // new file name
509 {
511 const String parentdir = Path::dirname (path);
512 if (is_anklang_dir (parentdir))
513 path = parentdir;
514 else { // use projectfile stem as dir
515 assert_return (string_endswith (path, dotanklang), Error::INTERNAL);
516 path.resize (path.size() - dotanklang.size());
517 }
518 }
519 // create parent directory
520 if (!Path::mkdirs (path))
521 return ase_error_from_errno (errno);
522 // ensure path is_anklang_dir
523 if (!make_anklang_dir (path))
524 return ase_error_from_errno (errno);
525 storage_->anklang_dir = path;
526 const String abs_projectfile = Path::join (path, projectfile);
527 // create backups
528 if (Path::check (abs_projectfile, "e"))
529 {
530 const String backupdir = Path::join (path, "backup");
531 if (!Path::mkdirs (backupdir))
532 return ase_error_from_errno (errno ? errno : EPERM);
533 const StringPair parts = Path::split_extension (projectfile, true);
534 const String backupname = Path::join (backupdir, parts.first + now_strftime (" (%y%m%dT%H%M%S)") + parts.second);
535 const String backupglob = Path::join (backupdir, parts.first + " ([0-9]*[0-9]T[0-9]*[0-9])" + parts.second);
536 if (!Path::rename (abs_projectfile, backupname))
537 ASE_SERVER.user_note (string_format ("## Backup failed\n%s: \\\nFailed to create backup: \\\n%s",
538 backupname, ase_error_blurb (ase_error_from_errno (errno))));
539 else // successful backup, now prune
540 {
543 strings_version_sort (&backups, true);
544 const int bmax = 24;
545 while (backups.size() > bmax)
546 {
547 const String bfile = backups.back();
548 backups.pop_back();
550 }
551 }
552 }
553 // start writing
555 storage_->writer_cachedir = anklang_cachedir_create();
556 storage_->asset_hashes.clear();
557 StorageWriter ws (Storage::AUTO_ZSTD);
558 Error error = ws.open_with_mimetype (abs_projectfile, "application/x-anklang");
559 if (!error)
560 {
561 // serialize Project (TODO: use tracktion_engine saving & loading)
562 error = ws.store_file_data ("project.json", "{}\n", true);
563 }
564 if (!error)
565 for (const auto &[path, dest] : storage_->writer_files) {
566 error = ws.store_file (dest, path);
567 if (!!error) {
568 printerr ("%s: %s: %s: %s\n", program_alias(), __func__, path, ase_error_blurb (error));
569 break;
570 }
571 }
572 storage_->writer_files.clear();
573 if (!error)
574 error = ws.close();
575 if (!error)
576 saved_filename_ = abs_projectfile;
577 if (!!error)
578 ws.remove_opened();
579 anklang_cachedir_cleanup (storage_->writer_cachedir);
580 return error;
581}
582
583Error
584ProjectImpl::snapshot_project (String &json)
585{
586 assert_return (storage_ == nullptr, Error::OPERATION_BUSY);
587 // writer setup
588 PStorage storage (&storage_); // storage_ = &storage;
589 storage_->writer_cachedir = anklang_cachedir_create();
590 if (storage_->writer_cachedir.empty() || !Path::check (storage_->writer_cachedir, "d"))
591 return Error::NO_PROJECT_DIR;
592 storage_->anklang_dir = storage_->writer_cachedir;
593 storage_->asset_hashes.clear();
594 // serialize Project (TODO: use tracktion_engine saving & loading)
595 json = "{}";
596 // cleanup
597 anklang_cachedir_cleanup (storage_->writer_cachedir);
598 return Error::NONE;
599}
600
601String
602ProjectImpl::writer_file_name (const String &fspath) const
603{
604 assert_return (storage_ != nullptr, "");
605 assert_return (!storage_->writer_cachedir.empty(), "");
606 return Path::join (storage_->writer_cachedir, fspath);
607}
608
609Error
610ProjectImpl::writer_add_file (const String &fspath)
611{
612 assert_return (storage_ != nullptr, Error::INTERNAL);
613 assert_return (!storage_->writer_cachedir.empty(), Error::INTERNAL);
614 if (!Path::check (fspath, "frw"))
615 return Error::FILE_NOT_FOUND;
616 if (!string_startswith (fspath, storage_->writer_cachedir))
617 return Error::FILE_OPEN_FAILED;
618 storage_->writer_files.push_back ({ fspath, Path::basename (fspath) });
619 return Error::NONE;
620}
621
622Error
623ProjectImpl::writer_collect (const String &fspath, String *hexhashp)
624{
625 assert_return (storage_ != nullptr, Error::INTERNAL);
626 assert_return (!storage_->anklang_dir.empty(), Error::INTERNAL);
627 if (!Path::check (fspath, "fr"))
628 return Error::FILE_NOT_FOUND;
629 // determine hash of file to collect
630 const String hexhash = string_to_hex (blake3_hash_file (fspath));
631 if (hexhash.empty())
632 return ase_error_from_errno (errno ? errno : EIO);
633 // resolve against existing hashes
634 for (const auto &hf : storage_->asset_hashes)
635 if (std::get<0> (hf) == hexhash)
636 {
637 *hexhashp = hexhash;
638 return Error::NONE;
639 }
640 // file may be within project directory
642 if (Path::dircontains (storage_->anklang_dir, fspath, &relpath))
643 {
644 storage_->asset_hashes.push_back ({ hexhash, relpath });
645 *hexhashp = hexhash;
646 return Error::NONE;
647 }
648 // determine unique path name
649 const size_t file_size = Path::file_size (fspath);
650 const String basedir = storage_->anklang_dir;
651 relpath = Path::join ("samples", Path::basename (fspath));
652 String dest = Path::join (basedir, relpath);
653 size_t i = 0;
654 while (Path::check (dest, "e"))
655 {
656 if (file_size == Path::file_size (dest))
657 {
658 const String althash = string_to_hex (blake3_hash_file (dest));
659 if (althash == hexhash)
660 {
661 // found file with same hash within project directory
662 storage_->asset_hashes.push_back ({ hexhash, relpath });
663 *hexhashp = hexhash;
664 return Error::NONE;
665 }
666 }
667 // add counter to create unique name
668 const StringPair parts = Path::split_extension (relpath, true);
669 dest = Path::join (basedir, string_format ("%s(%u)%s", parts.first, ++i, parts.second));
670 }
671 // create parent dir
673 return ase_error_from_errno (errno);
674 // copy into project dir
675 const bool copied = Path::copy_file (fspath, dest);
676 if (!copied)
677 return ase_error_from_errno (errno);
678 // success
679 storage_->asset_hashes.push_back ({ hexhash, relpath });
680 *hexhashp = hexhash;
681 return Error::NONE;
682}
683
684String
686{
687 return encodefs (saved_filename_);
688}
689
690Error
692{
693 const String filename = decodefs (utf8filename);
694 assert_return (storage_ == nullptr, Error::OPERATION_BUSY);
695 PStorage storage (&storage_); // storage_ = &storage;
696 String fname = filename;
697 // turn /dir/.anklang.project -> /dir/
698 if (Path::basename (fname) == ".anklang.project" && is_anklang_dir (Path::dirname (fname)))
700 // turn /dir/ -> /dir/dir.anklang
701 if (Path::check (fname, "d"))
702 fname = Path::join (fname, Path::basename (Path::strip_slashes (Path::normalize (fname)))) + ".anklang";
703 // add missing '.anklang' extension
704 if (!Path::check (fname, "e"))
705 fname += ".anklang";
706 // check for readable file
707 if (!Path::check (fname, "e"))
708 return ase_error_from_errno (errno);
709 // try reading .anklang container
710 StorageReader rs (Storage::AUTO_ZSTD);
711 Error error = rs.open_for_reading (fname);
712 if (!!error)
713 return error;
714 if (rs.stringread ("mimetype") != "application/x-anklang")
715 return Error::BAD_PROJECT;
716 // find project.json *inside* container
717 String jsd = rs.stringread ("project.json");
718 if (jsd.empty() && errno)
719 return Error::FORMAT_INVALID;
720 storage_->loading_file = fname;
721 storage_->anklang_dir = find_anklang_parent_dir (storage_->loading_file);
722#if 0 // unimplemented
724 // search in dirname or dirname/..
725 if (is_anklang_dir (dirname))
726 rs.search_dir (dirname);
727 else
728 {
730 if (is_anklang_dir (dirname))
731 rs.search_dir (dirname);
732 }
733#endif
734 // parse project (TODO: use tracktion_engine loading)
735 // jsd contains project state from tracktion_engine (currently {})
736 saved_filename_ = storage_->loading_file;
737 return Error::NONE;
738}
739
740StreamReaderP
741ProjectImpl::load_blob (const String &fspath)
742{
743 assert_return (storage_ != nullptr, nullptr);
744 assert_return (!storage_->loading_file.empty(), nullptr);
745 return stream_reader_zip_member (storage_->loading_file, fspath);
746}
747
749String
751{
752 return_unless (storage_ && storage_->asset_hashes.size(), "");
753 return_unless (!storage_->anklang_dir.empty(), "");
754 for (const auto& [hash,relpath] : storage_->asset_hashes)
755 if (hexhash == hash)
756 return Path::join (storage_->anklang_dir, relpath);
757 return "";
758}
759
760String
762{
763 String json;
764 Error error = snapshot_project (json);
765 if (!!error) {
766 warning ("Project: failed to serialize project: %s\n", ase_error_blurb (error));
767 return "";
768 }
769 return Re::grep (regex, json, group);
770}
771
772UndoScope::UndoScope (ProjectImplP projectp, const String &scopename) :
773 projectp_ (projectp),
774 scopename_ (scopename)
775{
777 assert_return (projectp->edit_);
778 projectp->edit_->getUndoManager().beginNewTransaction (juce::String (scopename));
779}
780
781UndoScope::~UndoScope()
782{
783 assert_return (projectp_);
784 assert_return (projectp_->edit_);
785 projectp_->edit_->getUndoManager().beginNewTransaction();
786}
787
788UndoScope
789ProjectImpl::undo_scope (const String &scopename)
790{
791 assert_warn (scopename != "");
792 return UndoScope (shared_ptr_cast<ProjectImpl> (this), scopename);
793}
794
795UndoScope
796ProjectImpl::add_undo_scope (const String &scopename)
797{
798 return UndoScope (shared_ptr_cast<ProjectImpl> (this), scopename);
799}
800
801void
803{
804 return_unless (!!edit_);
806 if (edit_->getUndoManager().undo()) {
807 trkn_restore_parameter_values (*edit_);
808 emit_notify ("dirty");
809 }
810}
811
812bool
814{
815 return_unless (!!edit_, false);
816 return edit_->getUndoManager().canUndo();
817}
818
819void
821{
822 return_unless (!!edit_);
824 if (edit_->getUndoManager().redo()) {
825 trkn_restore_parameter_values (*edit_);
826 emit_notify ("dirty");
827 }
828}
829
830bool
832{
833 return_unless (!!edit_, false);
834 return edit_->getUndoManager().canRedo();
835}
836
837double
839{
840 return_unless (!!edit_, 0.0);
841 return edit_->getLength().inSeconds();
842}
843
844void
846{
847 warning ("Project.length is read-only");
848}
849
852{
853 return { "all_tracks", "length" };
854}
855
856void
862
863void
869
870void
871ProjectImpl::clear_undo ()
872{
873 return_unless (!!edit_);
875 emit_notify ("dirty");
876}
877
878void
879ProjectImpl::bpm (double newbpm)
880{
881 edit_->tempoSequence.getTempo (0)->setBpm (newbpm);
882}
883
884double
885ProjectImpl::bpm () const
886{
887 return edit_->tempoSequence.getTempo (0)->getBpm();
888}
889
890void
891ProjectImpl::numerator (int num)
892{
893 edit_->tempoSequence.getTimeSig (0)->numerator = num;
894}
895
896int
897ProjectImpl::numerator () const
898{
899 return edit_->tempoSequence.getTimeSig (0)->numerator;
900}
901
902void
903ProjectImpl::denominator (int den)
904{
905 edit_->tempoSequence.getTimeSig (0)->denominator = den;
906}
907
908int
909ProjectImpl::denominator () const
910{
911 return edit_->tempoSequence.getTimeSig (0)->denominator;
912}
913
914void
916{
917 assert_return (!discarded_);
919 if (edit_->getTransport().isPlayContextActive())
920 edit_->getTransport().play (false);
921}
922
923void
925{
926 if (edit_->getTransport().isPlaying())
927 edit_->getTransport().stop (false, false);
928}
929
930void
932{
933 edit_->getTransport().stop (false, true);
934 transport_listener_->run_when_stopped ([this] {
935 // wait until stopped, so the new position persists
936 edit_->getTransport().setPosition (tracktion::TimePosition::fromSeconds (0.0));
937 transport_listener_->poll_position();
938 });
939}
940
941bool
943{
944 return edit_->getTransport().isPlaying();
945}
946
947void
949{
950 if (is_playing() == play)
951 return;
952 if (is_playing())
954 else
956}
957
958TrackP
960{
961 return_unless (edit_ && !discarded_, nullptr);
962 auto t = edit_->insertNewAudioTrack (tracktion::TrackInsertPoint (nullptr, nullptr), nullptr);
963 if (!t) return nullptr;
964 int n = te::getAudioTracks (*edit_).size();
965 while (trkn_has_track_name (*edit_, "Track " + juce::String (n)))
966 n++;
967 t->setName ("Track " + juce::String (n));
968 TrackImplP track = TrackImpl::from_trkn (*t);
969 emit_event ("track", "insert", { { "track", track }, });
970 emit_notify ("all_tracks");
971 return track;
972}
973
974TrackS
976{
978 auto tf = [&] (Track &track, int depth)
979 {
980 tracks.push_back (shared_ptr_cast<TrackImpl> (&track));
981 return true;
982 };
983 foreach_track (tf);
984 return tracks;
985}
986
987void
989{
990 warning ("Project.all_tracks is read-only");
991}
992
993TrackP
995{
996 return_unless (!!edit_, nullptr);
997 auto *masterTrack = edit_->getMasterTrack();
998 return_unless (masterTrack, nullptr);
999 return TrackImpl::from_trkn (*masterTrack);
1000}
1001
1004{
1005 return {}; // TODO: DeviceInfo
1006}
1007
1008} // 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:86
void group_undo(const String &undoname) override
Merge upcoming undo steps.
Definition project.cc:857
void redo() override
Redo the last undo modification.
Definition project.cc:820
bool can_redo() override
Check if any redo steps have been recorded.
Definition project.cc:831
TrackP master_track() override
Retrieve the master track.
Definition project.cc:994
void undo() override
Undo the last project modification.
Definition project.cc:802
String match_serialized(const String &regex, int group) override
Match regex against the serialized project state.
Definition project.cc:761
bool is_playing() const override
Check whether a project is currently playing (song sequencing).
Definition project.cc:942
void remove_self() override
Remove self from parent container.
Definition project.cc:456
bool can_undo() override
Check if any undo steps have been recorded.
Definition project.cc:813
StringS read_only_properties() const override
List x()/x (v) properties that this object can't change, their setters ignore the value or warn.
Definition project.cc:851
TelemetryFieldS telemetry() const override
Retrieve project telemetry locations.
Definition project.cc:400
TrackS all_tracks() const override
List all tracks, changes on notify:all_tracks.
Definition project.cc:975
void ungroup_undo() override
Stop merging undo steps.
Definition project.cc:864
String saved_filename() override
Retrieve UTF-8 filename for save or from load.
Definition project.cc:685
TrackP create_track() override
Create and append a new Track.
Definition project.cc:959
Error save_project(const String &utf8filename, bool collect) override
Store Project and collect external files.
Definition project.cc:485
String loader_resolve(const String &hexhash)
Find file from hash code, returns fspath.
Definition project.cc:750
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:1003
void discard() override
Discard project and associated resources.
Definition project.cc:445
Error load_project(const String &utf8filename) override
Load project from file filename.
Definition project.cc:691
double length() const override
End time of the last clip in seconds, changes on notify:length.
Definition project.cc:838
void pause_playback() override
Pause playback at the current position.
Definition project.cc:924
void stop_playback() override
Stop project playback.
Definition project.cc:931
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:278
void beginNewTransaction()
bool canUndo() const
bool canRedo() const
bool hasType(const Identifier &typeName) const noexcept
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)
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 *)
void visitAllTracksRecursive(std::function< bool(Track &)>) const
juce::UndoManager & getUndoManager() noexcept
TimeSigSetting * getTimeSig(int index) const
TempoSetting * getTempo(int index) const
void setBpm(double newBpm)
void setName(const juce::String &)
virtual bool isFolderTrack() const
juce::String getName() const override
virtual bool isAudioTrack() 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 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
void register_ase_obj(VirtualBase *ase_impl, tracktion::Selectable &selectable)
Helper: register AseImpl with a tracktion Selectable via ase_obj_.
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
void unregister_ase_obj(VirtualBase *ase_impl, tracktion::Selectable *selectable)
Helper: unregister AseImpl from a tracktion Selectable (selectable may be nullptr)
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:202
juce::Array< AudioTrack * > getAudioTracks(const Edit &)
T push_back(T... args)
T resize(T... args)
T size(T... args)
A Tracktion MIDI note with a session handle for edits.
Definition api.hh:229
Reference for an allocated memory block.
Definition memory.hh:89