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