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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_ArchiveFile.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
14static bool isWorthConvertingToOgg (AudioFile& source, int quality)
15{
16 if (dynamic_cast<juce::OggVorbisAudioFormat*> (source.getFormat()) != nullptr)
17 {
18 auto estimatedQuality = juce::OggVorbisAudioFormat().estimateOggFileQuality (source.getFile());
19
20 return estimatedQuality == 0
21 || quality < estimatedQuality; // if they're asking for a higher quality than the current one, no point in converting
22 }
23
24 return true;
25}
26
27//==============================================================================
28TracktionArchiveFile::IndexEntry::IndexEntry (juce::InputStream& in)
29{
30 offset = in.readInt();
31 length = in.readInt();
32 originalName = in.readString();
33 storedName = in.readString();
34
35 int numExtras = in.readShort();
36
37 while (--numExtras >= 0 && ! in.isExhausted())
38 {
39 extraNames.add (in.readString());
40 extraValues.add (in.readString());
41 }
42
43 DBG (originalName + " / " + storedName + " length: " << length);
44}
45
46TracktionArchiveFile::IndexEntry::~IndexEntry() {}
47
48void TracktionArchiveFile::IndexEntry::write (juce::OutputStream& out)
49{
50 out.writeInt (int (offset));
51 out.writeInt (int (length));
52 out.writeString (originalName);
53 out.writeString (storedName);
54
55 out.writeShort ((short) extraNames.size());
56
57 for (int i = 0; i < extraNames.size(); ++i)
58 {
59 out.writeString (extraNames[i]);
60 out.writeString (extraValues[i]);
61 }
62}
63
64
65//==============================================================================
66TracktionArchiveFile::TracktionArchiveFile (Engine& e, const juce::File& f)
67 : engine (e), file (f)
68{
69 readIndex();
70}
71
72TracktionArchiveFile::~TracktionArchiveFile()
73{
74 flush();
75}
76
77int TracktionArchiveFile::getMagicNumber()
78{
79 return (int) juce::ByteOrder::littleEndianInt ("TKNA");
80}
81
82juce::File TracktionArchiveFile::getFile() const
83{
84 return file;
85}
86
87void TracktionArchiveFile::readIndex()
88{
89 valid = false;
90 needToWriteIndex = false;
91 entries.clear();
92
93 juce::FileInputStream in (file);
94
95 if (in.openedOk())
96 {
97 const int magic = in.readInt();
98
99 if (magic == getMagicNumber())
100 {
101 indexOffset = in.readInt();
102 jassert (indexOffset >= 8 && indexOffset < file.getSize());
103
104 if (indexOffset >= 8 && indexOffset < file.getSize())
105 {
106 in.setPosition (indexOffset);
107 int numEntries = in.readInt();
108
109 while (--numEntries >= 0 && ! in.isExhausted())
110 entries.add (new IndexEntry (in));
111
112 valid = true;
113 }
114 }
115 }
116}
117
118void TracktionArchiveFile::flush()
119{
120 if (needToWriteIndex && valid)
121 {
122 juce::FileOutputStream out (file);
123
124 if (out.openedOk())
125 {
126 out.setPosition (indexOffset);
127 out.writeInt (entries.size());
128
129 for (auto e : entries)
130 e->write (out);
131
132 out.setPosition (4);
133 out.writeInt (int (indexOffset));
134 }
135
136 needToWriteIndex = false;
137 }
138}
139
140bool TracktionArchiveFile::isValidArchive() const
141{
142 return valid;
143}
144
145int TracktionArchiveFile::getNumFiles() const
146{
147 return entries.size();
148}
149
150juce::String TracktionArchiveFile::getOriginalFileName (int index) const
151{
152 if (auto i = entries[index])
153 return i->originalName.fromLastOccurrenceOf ("/", false, false);
154
155 return {};
156}
157
158int TracktionArchiveFile::indexOfFile (const juce::String& name) const
159{
160 for (int i = entries.size(); --i >= 0;)
161 if (getOriginalFileName(i).equalsIgnoreCase (name))
162 return i;
163
164 return -1;
165}
166
167std::unique_ptr<juce::InputStream> TracktionArchiveFile::createStoredInputStream (int index) const
168{
169 if (entries[index] != nullptr)
170 if (auto f = file.createInputStream())
171 return std::make_unique<juce::SubregionStream> (f.release(),
172 entries[index]->offset,
173 entries[index]->length,
174 true);
175
176 return {};
177}
178
179bool TracktionArchiveFile::extractFile (int index, const juce::File& destDirectory,
180 juce::File& fileCreated, bool askBeforeOverwriting)
181{
182 if (! destDirectory.createDirectory())
183 return false;
184
185 auto destFile = destDirectory.getChildFile (getOriginalFileName (index));
186 fileCreated = destFile;
187
188 if (askBeforeOverwriting && destFile.existsAsFile())
189 {
190 auto r = engine.getUIBehaviour()
191 .showYesNoCancelAlertBox (TRANS("Unpacking archive"),
192 TRANS("The file XZZX already exists - do you want to overwrite it?")
193 .replace ("XZZX", destFile.getFullPathName()),
194 TRANS("Overwrite"),
195 TRANS("Leave existing"));
196
197 if (r == 1) return true;
198 if (r == 0) return false;
199 }
200
201 if (destFile.isDirectory()
202 || ! destFile.hasWriteAccess()
203 || ! destFile.deleteFile()
204 || entries[index] == nullptr)
205 return false;
206
207 auto source = createStoredInputStream (index);
208
209 if (source == nullptr)
210 return false;
211
212 auto storedName = entries[index]->storedName;
213
214 if (storedName != entries[index]->originalName)
215 {
216 if (storedName.endsWithIgnoreCase (".flac"))
217 return AudioFileUtils::readFromFormat<juce::FlacAudioFormat> (engine, *source, destFile);
218
219 if (storedName.endsWithIgnoreCase (".ogg"))
220 return AudioFileUtils::readFromFormat<juce::OggVorbisAudioFormat> (engine, *source, destFile);
221
222 if (storedName.endsWithIgnoreCase (".gz"))
223 source = std::make_unique<juce::GZIPDecompressorInputStream> (source.release(), true);
224 else
226 }
227
228 {
229 juce::FileOutputStream out (destFile);
230
231 if (! out.openedOk())
232 return false;
233
234 out.writeFromInputStream (*source, -1);
235 }
236
237 return true;
238}
239
240bool TracktionArchiveFile::extractAll (const juce::File& destDirectory,
241 juce::Array<juce::File>& filesCreated)
242{
243 if (! destDirectory.createDirectory())
244 return false;
245
246 for (int i = 0; i < entries.size(); ++i)
247 {
248 juce::File fileCreated;
249
250 if (! extractFile (i, destDirectory, fileCreated, false))
251 return false;
252
253 filesCreated.add (fileCreated);
254 }
255
256 return true;
257}
258
259//==============================================================================
261{
262public:
263 ExtractionTask (TracktionArchiveFile& archive_, const juce::File& destDir_,
264 bool warnAboutOverwrite_,
265 juce::Array<juce::File>& filesCreated_,
266 bool& wasAborted_)
267 : ThreadPoolJobWithProgress (TRANS("Unpacking archive") + "..."),
268 archive (archive_), destDir (destDir_),
269 wasAborted (wasAborted_),
270 warnAboutOverwrite (warnAboutOverwrite_),
271 filesCreated (filesCreated_)
272 {
273 wasAborted = false;
274 }
275
276 JobStatus runJob()
277 {
280
281 if (! destDir.createDirectory())
282 return jobHasFinished;
283
284 for (int i = 0; i < archive.getNumFiles(); ++i)
285 {
286 if (shouldExit())
287 {
288 wasAborted = true;
289
290 for (auto& f : filesCreated)
291 f.deleteFile();
292
293 break;
294 }
295
296 progress = i / (float) archive.getNumFiles();
297
298 juce::File fileCreated;
299
300 if (! archive.extractFile (i, destDir, fileCreated, warnAboutOverwrite))
301 return jobHasFinished;
302
303 if (fileCreated.exists())
304 filesCreated.add (fileCreated);
305 }
306
307 ok = true;
308 return jobHasFinished;
309 }
310
311 float getCurrentTaskProgress()
312 {
313 return progress;
314 }
315
316 TracktionArchiveFile& archive;
317 juce::File destDir;
318 bool ok = false;
319 bool& wasAborted;
320 float progress = 0;
321 bool warnAboutOverwrite = false;
322 juce::Array<juce::File>& filesCreated;
323};
324
325//==============================================================================
326bool TracktionArchiveFile::extractAllAsTask (const juce::File& destDirectory, bool warnAboutOverwrite,
327 juce::Array<juce::File>& filesCreated, bool& wasAborted)
328{
329 ExtractionTask task (*this, destDirectory, warnAboutOverwrite, filesCreated, wasAborted);
330
331 engine.getUIBehaviour().runTaskWithProgressBar (task);
332
333 return task.ok;
334}
335
336bool TracktionArchiveFile::addFile (const juce::File& f, const juce::File& rootDirectory,
337 CompressionType compression)
338{
339 juce::String name;
340
341 if (f.isAChildOf (rootDirectory))
342 name = f.getRelativePathFrom (rootDirectory)
343 .replaceCharacter ('\\', '/');
344 else
345 name = f.getFileName();
346
347 return addFile (f, name, compression);
348}
349
350bool TracktionArchiveFile::addFile (const juce::File& f, const juce::String& filenameToUse,
351 CompressionType compression)
352{
353 // don't risk using ogg or flac on small audio files
354 if (compression != CompressionType::none && f.getSize() <= 16 * 1024)
355 compression = CompressionType::zip;
356
358 bool ok = false;
359
360 if (in.openedOk())
361 {
362 juce::FileOutputStream out (file);
363
364 if (out.openedOk())
365 {
366 if (! valid)
367 {
368 out.setPosition (0);
369 out.writeInt (getMagicNumber());
370 out.writeInt (int (indexOffset));
371 valid = true;
372 }
373
374 auto initialPosition = out.getPosition();
375
376 out.setPosition (indexOffset);
377 jassert (indexOffset < 2147483648);
378
379 if (indexOffset >= 2147483648)
380 {
381 TRACKTION_LOG_ERROR ("Archive too large when archiving file: " + f.getFileName());
382 return false;
383 }
384
385 auto filenameRoot = filenameToUse.substring (0, filenameToUse.lastIndexOfChar ('.'));
386
387 std::unique_ptr<IndexEntry> entry (new IndexEntry());
388 entry->offset = indexOffset;
389 entry->length = 0;
390 entry->originalName = filenameToUse;
391 entry->storedName = filenameToUse;
392
393 switch (compression)
394 {
395 case CompressionType::none:
396 {
397 out.writeFromInputStream (in, -1);
398 break;
399 }
400
401 case CompressionType::zip:
402 {
403 entry->storedName = filenameRoot + ".gz";
404
405 juce::GZIPCompressorOutputStream deflater (&out, 9, false);
406 deflater.writeFromInputStream (in, -1);
407 break;
408 }
409
410 case CompressionType::lossless:
411 {
412 AudioFile af (engine, f);
413
414 if (af.isOggFile() || af.isMp3File() || af.isFlacFile())
415 {
416 out.writeFromInputStream (in, -1); // no point re-compressing these
417 }
418 else
419 {
420 if (af.getBitsPerSample() > 24)
421 {
422 // FLAC can't do higher than 24 bits so just have to zip it instead..
423 entry->storedName = filenameRoot + ".gz";
424
425 juce::GZIPCompressorOutputStream deflater (&out, 9, false);
426 deflater.writeFromInputStream (in, -1);
427 }
428 else
429 {
430 entry->storedName = filenameRoot + ".flac";
431
432 if (! AudioFileUtils::convertToFormat<juce::FlacAudioFormat> (engine, f, out, 0,
434 {
435 needToWriteIndex = true;
436 TRACKTION_LOG_ERROR ("Failed to add file to archive flac: " + f.getFileName());
437 return false;
438 }
439 }
440 }
441
442 break;
443 }
444
445 case CompressionType::lossyGoodQuality:
446 case CompressionType::lossyMediumQuality:
447 case CompressionType::lossyLowQuality:
448 {
449 entry->storedName = filenameRoot + ".ogg";
450 entry->originalName = entry->storedName; // oggs get extracted as oggs, not named back to how they were
451
452 auto quality = getOggQuality (compression);
453 AudioFile af (engine, f);
454
455 if (! isWorthConvertingToOgg (af, quality))
456 {
457 juce::FileInputStream fin (af.getFile());
458
459 if (! fin.openedOk())
460 {
461 needToWriteIndex = true;
462 TRACKTION_LOG_ERROR ("Failed to add file to archive: " + f.getFileName());
463 return false;
464 }
465
466 out.writeFromInputStream (fin, -1);
467 }
468 else if (! AudioFileUtils::convertToFormat<juce::OggVorbisAudioFormat> (engine, f, out, quality,
470 {
471 needToWriteIndex = true;
472 TRACKTION_LOG_ERROR ("Failed to add file to archive ogg: " + f.getFileName());
473 return false;
474 }
475
476 break;
477 }
478
479 default:
480 {
481 TRACKTION_LOG_ERROR ("Unknown compression type when archiving file: " + f.getFileName());
483 break;
484 }
485 }
486
487 out.flush();
488
489 jassert (out.getPosition() > indexOffset);
490
491 entry->length = std::max (juce::int64(), out.getPosition() - indexOffset);
492
493 jassert (indexOffset + entry->length < 2147483648);
494
495 if (indexOffset + entry->length >= 2147483648)
496 {
497 out.setPosition (initialPosition);
498 out.truncate();
499 TRACKTION_LOG_ERROR ("Archive too large when archiving file: " + f.getFileName());
500 return false;
501 }
502
503 indexOffset += entry->length;
504 needToWriteIndex = true;
505 ok = true;
506
507 entries.add (entry.release());
508 }
509 }
510
511 return ok;
512}
513
514void TracktionArchiveFile::addFileInfo (const juce::String& filename,
515 const juce::String& itemName,
516 const juce::String& itemValue)
517{
518 auto i = indexOfFile (filename);
519
520 if (auto entry = entries[i])
521 {
522 entry->extraNames.add (itemName);
523 entry->extraValues.add (itemValue);
524 needToWriteIndex = true;
525 }
526}
527
528int TracktionArchiveFile::getOggQuality (CompressionType c)
529{
530 auto numOptions = juce::OggVorbisAudioFormat().getQualityOptions().size();
531
532 if (c == CompressionType::lossyGoodQuality) return numOptions - 1;
533 if (c == CompressionType::lossyMediumQuality) return numOptions / 2;
534
535 return numOptions / 5;
536}
537
538}} // namespace tracktion { inline namespace engine
void add(const ElementType &newElement)
static constexpr uint32 littleEndianInt(const void *bytes) noexcept
int64 getSize() const
String getFileName() const
File getChildFile(StringRef relativeOrAbsolutePath) const
String getRelativePathFrom(const File &directoryToBeRelativeTo) const
bool isAChildOf(const File &potentialParentDirectory) const
bool exists() const
Result createDirectory() const
static void JUCE_CALLTYPE disableDenormalisedNumberSupport(bool shouldDisable=true) noexcept
virtual bool setPosition(int64 newPosition)=0
virtual bool isExhausted()=0
virtual short readShort()
virtual int readInt()
virtual String readString()
int estimateOggFileQuality(const File &source)
StringArray getQualityOptions() override
virtual int64 getPosition()=0
virtual int64 writeFromInputStream(InputStream &source, int64 maxNumBytesToWrite)
virtual bool writeShort(short value)
virtual bool setPosition(int64 newPosition)=0
virtual void flush()=0
virtual bool writeInt(int value)
virtual bool writeString(const String &text)
int size() const noexcept
void add(String stringToAdd)
int lastIndexOfChar(juce_wchar character) const noexcept
String replaceCharacter(juce_wchar characterToReplace, juce_wchar characterToInsertInstead) const
String substring(int startIndex, int endIndex) const
String fromLastOccurrenceOf(StringRef substringToFind, bool includeSubStringInResult, bool ignoreCase) const
T flush(T... args)
T is_pointer_v
#define TRANS(stringLiteral)
#define jassert(expression)
#define DBG(textToWrite)
#define jassertfalse
typedef float
T max(T... args)
long long int64
write
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.