tracktion-engine 3.0-10-g034fdde4aa5
Tracktion Engine — High level data model for audio applications

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_EditFileOperations.cpp
Go to the documentation of this file.
1 /*
2 ,--. ,--. ,--. ,--.
3 ,-' '-.,--.--.,--,--.,---.| |,-.,-' '-.`--' ,---. ,--,--, Copyright 2024
4 '-. .-'| .--' ,-. | .--'| /'-. .-',--.| .-. || \ Tracktion Software
5 | | | | \ '-' \ `--.| \ \ | | | |' '-' '| || | Corporation
6 `---' `--' `--`--'`---'`--'`--' `---' `--' `---' `--''--' www.tracktion.com
7
8 Tracktion Engine uses a GPL/commercial licence - see LICENCE.md for details.
9*/
10
11namespace tracktion { inline namespace engine
12{
13
15{
17 : Thread ("TemporyFileWriter") {}
18
20 {
21 flushAllFiles();
22 stopThread (10000);
23 jassert (pending.isEmpty());
24 }
25
26 void writeTreeToFile (juce::ValueTree&& v, const juce::File& f)
27 {
28 TRACKTION_ASSERT_MESSAGE_THREAD
30 waiter.signal();
32 }
33
34 void flushAllFiles()
35 {
36 TRACKTION_ASSERT_MESSAGE_THREAD
37 waiter.signal();
39
40 while (! pending.isEmpty())
41 Thread::sleep (50);
42 }
43
44private:
45 void run() override
46 {
47 while (! threadShouldExit())
48 {
49 while (! pending.isEmpty())
50 writeToFile (pending.removeAndReturn (0));
51
52 waiter.wait (1000);
53 }
54 }
55
56 void writeToFile (std::pair<juce::ValueTree, juce::File> item)
57 {
58 item.second.deleteFile();
60 item.first.writeToStream (os);
61 }
62
65};
66
67//==============================================================================
69{
70 struct Data
71 {
72 Data (Edit& e)
73 : edit (e)
74 {
76 }
77
78 ~Data()
79 {
81
82 // If we managed to shutdown cleanly (i.e. without crashing) then delete the temp file
83 if (auto item = getProjectItemForEdit (edit))
84 EditFileOperations::getTempVersionOfEditFile (item->getSourceFile()).deleteFile();
85 }
86
87 void refresh()
88 {
89 editSnapshot->refreshFromProjectManager();
90 }
91
92 Edit& edit;
93 juce::Time timeOfLastSave { juce::Time::getCurrentTime() };
94 EditSnapshot::Ptr editSnapshot { EditSnapshot::getEditSnapshot (edit.engine, edit.getProjectItemID()) };
95 };
96
97 SharedEditFileDataCache() = default;
98
99 std::shared_ptr<Data> get (Edit& edit)
100 {
101 for (auto& ptr : sharedData)
102 if (&ptr->edit == &edit)
103 return ptr;
104
105 auto newData = std::make_shared<Data> (edit);
106 sharedData.push_back (newData);
107 return newData;
108 }
109
110 void refresh()
111 {
112 for (auto& ptr : sharedData)
113 ptr->refresh();
114 }
115
116 void cleanUp()
117 {
118 sharedData.erase (std::remove_if (sharedData.begin(), sharedData.end(),
119 [] (auto& ptr) { return ptr.use_count() == 1; }),
120 sharedData.end());
121 }
122
123private:
125};
126
127
128//==============================================================================
130{
132 : data (cache->get (e))
133 {
134 jassert (data);
135 }
136
138 {
139 data.reset(); // Make sure this is reset before calling cleanUp
140 cache->cleanUp();
141 }
142
143 void writeValueTreeToDisk (juce::ValueTree&& v, const juce::File& f)
144 {
145 editFileWriter->writeTreeToFile (std::move (v), f);
146 }
147
151};
152
153
154//==============================================================================
155EditFileOperations::EditFileOperations (Edit& e)
156 : edit (e), state (edit.state),
157 sharedDataPimpl (new SharedDataPimpl (e)),
158 timeOfLastSave (sharedDataPimpl->data->timeOfLastSave),
159 editSnapshot (sharedDataPimpl->data->editSnapshot)
160{
161}
162
163EditFileOperations::~EditFileOperations()
164{
165}
166
167juce::File EditFileOperations::getEditFile() const
168{
169 return edit.editFileRetriever();
170}
171
172bool EditFileOperations::writeToFile (const juce::File& file, bool writeQuickBinaryVersion)
173{
175 bool ok = false;
177
178 if (! writeQuickBinaryVersion)
179 {
180 EditPlaybackContext::RealtimePriorityDisabler realtimeDisabler (edit.engine);
182 sharedDataPimpl->editFileWriter->flushAllFiles();
183 }
184
185 if (file.hasWriteAccess() && ! file.isDirectory())
186 {
187 if (writeQuickBinaryVersion)
188 {
189 sharedDataPimpl->writeValueTreeToDisk (edit.state.createCopy(), file);
190 }
191 else
192 {
193 edit.flushState();
194
195 if (editSnapshot != nullptr)
196 editSnapshot->setState (edit.state, edit.getLength());
197
198 if (auto xml = edit.state.createXml())
199 ok = xml->writeTo (file);
200
201 jassert (ok);
202 }
203 }
204
205 if (ok)
206 timeOfLastSave = juce::Time::getCurrentTime();
207
208 return ok;
209}
210
211static bool editSaveError (Edit& edit, const juce::File& file, bool warnOfFailure)
212{
213 // failed..
214 TRACKTION_LOG_ERROR ("Can't write to edit file: " + file.getFullPathName());
215
216 if (warnOfFailure)
217 {
218 juce::String s (TRANS("Unable to save edit \"XEDTX\" to file: XFNX")
219 .replace ("XEDTX", edit.getName())
220 .replace ("XFNX", file.getFullPathName()));
221
222 if (! file.hasWriteAccess())
223 s << "\n\n(" << TRANS("File or directory is read-only") << ")";
224
225 return edit.engine.getUIBehaviour().showOkCancelAlertBox (TRANS("Save edit"), s,
226 TRANS("Carry on anyway"));
227 }
228
229 return false;
230}
231
232bool EditFileOperations::save (bool warnOfFailure,
233 bool forceSaveEvenIfNotModified,
234 bool offerToDiscardChanges)
235{
237 auto editFile = getEditFile();
238
239 if (editFile == juce::File())
240 return false;
241
243 edit.getParameterControlMappings().saveToEdit();
244
245 auto tempFile = getTempVersionFile();
246
247 if (! saveTempVersion (true))
248 return editSaveError (edit, tempFile, warnOfFailure);
249
250 if (forceSaveEvenIfNotModified || edit.hasChangedSinceSaved())
251 {
252 // Updates the project list if showing
253 if (auto proj = getProjectForEdit (edit))
254 proj->Selectable::changed();
255
256 if (offerToDiscardChanges)
257 {
258 const int r = edit.engine.getUIBehaviour().showYesNoCancelAlertBox (TRANS("Closing Edit"),
259 TRANS("Do you want to save your changes to \"XNMX\" before closing it?")
260 .replace ("XNMX", edit.getName()),
261 TRANS("Save"),
262 TRANS("Discard changes"));
263
264 if (r != 1)
265 {
266 tempFile.deleteFile();
267 return r == 2;
268 }
269 }
270
271 if (editSnapshot != nullptr)
272 editSnapshot->refreshCacheAndNotifyListeners();
273
274 if (! tempFile.moveFileTo (editFile))
275 return editSaveError (edit, editFile, warnOfFailure);
276
277 edit.engine.getEngineBehaviour().editHasBeenSaved (edit, editFile);
278 }
279
280 tempFile.deleteFile();
281
282 if (auto item = getProjectItemForEdit (edit))
283 item->setLength (edit.getLength().inSeconds());
284
285 edit.resetChangedStatus();
286 return true;
287}
288
289bool EditFileOperations::saveAs()
290{
291 #if JUCE_MODAL_LOOPS_PERMITTED
293 auto newEditName = getNonExistentSiblingWithIncrementedNumberSuffix (getEditFile(), false);
294
295 juce::FileChooser chooser (TRANS("Save As") + "...",
296 newEditName,
297 juce::String ("*") + editFileSuffix);
298
299 if (chooser.browseForFileToSave (false))
300 return saveAs (chooser.getResult().withFileExtension (editFileSuffix));
301 #endif
302
303 return false;
304}
305
306bool EditFileOperations::saveAs (const juce::File& f, bool forceOverwriteExisting)
307{
308 if (f == getEditFile())
309 return save (true, false, false);
310
311 if (f.existsAsFile() && ! forceOverwriteExisting)
312 {
313 if (! edit.engine.getUIBehaviour().showOkCancelAlertBox (TRANS("Save Edit") + "...",
314 TRANS("The file XFNX already exists. Do you want to overwrite it?")
315 .replace ("XFNX", "\n\n" + f.getFullPathName() + "\n\n"),
316 TRANS("Overwrite")))
317 return false;
318 }
319
320 if (auto project = getProjectForEdit (edit))
321 {
322 if (auto item = getProjectItemForEdit (edit))
323 {
324 if (f.create())
325 {
326 if (auto newItem = project->createNewItem (f, item->getType(),
328 item->getDescription(),
329 item->getCategory(),
330 true))
331 {
332 auto oldTempFile = getTempVersionFile();
333
334 newItem->copyAllPropertiesFrom (*item);
335 newItem->setName (f.getFileNameWithoutExtension(), ProjectItem::SetNameMode::forceNoRename);
336
337 jassert (edit.getProjectItemID() != newItem->getID());
338 edit.setProjectItemID (newItem->getID());
339 editSnapshot = EditSnapshot::getEditSnapshot (edit.engine, edit.getProjectItemID());
340
341 const bool ok = save (true, true, false);
342
343 if (ok)
344 oldTempFile.deleteFile();
345
346 edit.sendSourceFileUpdate();
347 return ok;
348 }
349 }
350 }
351 }
352 else
353 {
355
357 edit.getParameterControlMappings().saveToEdit();
358
359 auto tempFile = getTempVersionFile();
360
361 if (! saveTempVersion (true))
362 return editSaveError (edit, tempFile, true);
363
364 if (editSnapshot != nullptr)
365 editSnapshot->refreshCacheAndNotifyListeners();
366
367 if (f.existsAsFile())
368 f.deleteFile();
369
370 if (! tempFile.moveFileTo (f))
371 return editSaveError (edit, f, true);
372
373 tempFile.deleteFile();
374
375 edit.resetChangedStatus();
376 edit.engine.getEngineBehaviour().editHasBeenSaved (edit, f);
377
378 return true;
379 }
380
382 return false;
383}
384
385bool EditFileOperations::saveTempVersion (bool forceSaveEvenIfUnchanged)
386{
388
389 if (! (forceSaveEvenIfUnchanged || edit.hasChangedSinceSaved()))
390 return true;
391
392 return writeToFile (getTempVersionFile(), ! forceSaveEvenIfUnchanged);
393}
394
395juce::File EditFileOperations::getTempVersionOfEditFile (const juce::File& f)
396{
397 return f != juce::File() ? f.getSiblingFile (".tmp_" + f.getFileNameWithoutExtension())
398 : juce::File();
399}
400
401juce::File EditFileOperations::getTempVersionFile() const
402{
403 return getTempVersionOfEditFile (getEditFile());
404}
405
406void EditFileOperations::deleteTempVersion()
407{
408 getTempVersionFile().deleteFile();
409}
410
411//==============================================================================
412void EditFileOperations::updateEditFiles()
413{
415}
416
417//==============================================================================
419{
420 if (auto item = pm.getProjectItem (itemID))
421 return loadEditFromFile (pm.engine, item->getSourceFile(), itemID);
422
423 return {};
424}
425
430
432{
434 juce::ValueTree state;
435
436 if (auto xml = juce::parseXML (f))
437 {
438 updateLegacyEdit (*xml);
439 state = juce::ValueTree::fromXml (*xml);
440 }
441
442 if (! state.isValid())
443 {
444 if (juce::FileInputStream is (f); is.openedOk())
445 {
446 if (state = juce::ValueTree::readFromStream (is); state.hasType (IDs::EDIT))
447 state = updateLegacyEdit (state);
448 else
449 state = {};
450 }
451 }
452
453 if (! state.isValid())
454 {
455 // If the file already exists and is not empty, don't write over it as it could have been corrupted and be recoverable
456 if (f.existsAsFile() && f.getSize() > 0)
457 return {};
458
459 state = juce::ValueTree (IDs::EDIT);
460 state.setProperty (IDs::appVersion, e.getPropertyStorage().getApplicationVersion(), nullptr);
461 }
462
463 state.setProperty (IDs::projectID, itemID.toString(), nullptr);
464
465 return state;
466}
467
472
474{
475 auto editState = loadEditFromFile (engine, editFile, ProjectItemID{});
476
477 if (! editState.isValid())
478 return {};
479
480 auto id = ProjectItemID::fromProperty (editState, IDs::projectID);
481
482 if (! id.isValid())
484
485 return Edit::createEdit (Edit::Options
486 {
487 engine,
488 editState,
489 id,
490 role,
491 nullptr,
493 [editFile] { return editFile; },
494 {}
495 });
496}
497
499{
500 auto id = ProjectItemID::createNewID (0);
501
502 return Edit::createEdit (Edit::Options
503 {
504 engine,
505 loadEditFromFile (engine, {}, id),
506 id,
508 nullptr,
510 [editFile] { return editFile; },
511 {}
512 });
513}
514
515}} // namespace tracktion { inline namespace engine
bool isEmpty() const noexcept
void add(const ElementType &newElement)
ElementType removeAndReturn(int indexToRemove)
bool openedOk() const noexcept
bool existsAsFile() const
int64 getSize() const
const String & getFullPathName() const noexcept
File getSiblingFile(StringRef siblingFileName) const
String getFileNameWithoutExtension() const
Result create() const
bool deleteFile() const
bool isValid() const noexcept
Thread(const String &threadName, size_t threadStackSize=osDefaultStackSize)
bool startThread()
bool threadShouldExit() const
bool stopThread(int timeOutMilliseconds)
static Time JUCE_CALLTYPE getCurrentTime() noexcept
std::unique_ptr< XmlElement > createXml() const
ValueTree createCopy() const
void signal() const
bool wait(double timeOutMilliseconds=-1.0) const
static void saveAllSettings(Engine &engine)
Saves the settings of all open custom surfaces.
The Tracktion Edit class!
static std::unique_ptr< Edit > createEdit(Options)
Creates an Edit for the given options.
juce::ValueTree state
The ValueTree of the Edit state.
std::function< juce::File()> editFileRetriever
This callback can be set to return the file for this Edit.
EditRole
Enum used to determine what an Edit is being used for.
@ forEditing
Creates an Edit for normal use.
TimeDuration getLength() const
Returns the end time of last clip.
void flushState()
Saves the plugin, automap and ARA states to the state ValueTree.
static std::unique_ptr< Edit > createEditForExamining(Engine &, juce::ValueTree, EditRole role=EditRole::forExamining)
Creates an Edit that loads a state, using the role Edit::forExamining.
ProjectItemID getProjectItemID() const noexcept
Returns the ProjectItemID of the Edit.
static int getDefaultNumUndoLevels() noexcept
Returns the default number of undo levels that should be used.
Engine & engine
A reference to the Engine.
The Engine is the central class for all tracktion sessions.
PropertyStorage & getPropertyStorage() const
Returns the PropertyStorage user settings customisable XML file.
An ID representing one of the items in a Project.
static ProjectItemID createNewID(int projectID) noexcept
Generates a new ID for a given project.
ProjectItem::Ptr getProjectItem(ProjectItemID)
tries to find the project that contains an id, and open it as a ProjectItem.
static bool isSelectableValid(const Selectable *) noexcept
checks whether this object has been deleted.
T is_pointer_v
#define TRANS(stringLiteral)
#define jassert(expression)
#define jassertfalse
std::unique_ptr< XmlElement > parseXML(const String &textToParse)
std::unique_ptr< Edit > loadEditForExamining(ProjectManager &pm, ProjectItemID itemID, Edit::EditRole role)
Uses the ProjectManager to find an Edit file and open it.
juce::ValueTree loadEditFromProjectManager(ProjectManager &pm, ProjectItemID itemID)
Uses the ProjectManager to find an Edit file and load it as a ValueTree.
juce::ValueTree loadEditFromFile(Engine &e, const juce::File &f, ProjectItemID itemID)
Legacy, will be deprecated soon.
ProjectItem::Ptr getProjectItemForEdit(const Edit &e)
Tries to find the project item that refers to this edit (but may return nullptr!)
Project::Ptr getProjectForEdit(const Edit &e)
Tries to find the project that contains this edit (but may return nullptr!)
juce::ValueTree createEmptyEdit(Engine &e)
Legacy, will be deprecated soon.
juce::ValueTree updateLegacyEdit(const juce::ValueTree &)
Converts old edit formats to the latest structure.
T remove_if(T... args)
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.