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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_WaveInputDevice.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 private juce::Timer,
16 private juce::AsyncUpdater
17{
18public:
20 : juce::ThreadPoolJob ("SpaceCheck"), engine (e), file (f)
21 {
22 startTimer (1000);
23 }
24
25 ~DiskSpaceCheckTask() override
26 {
27 stopTimer();
28 engine.getBackgroundJobs().getPool().removeJob (this, true, 10000);
29 }
30
31 JobStatus runJob() override
32 {
33 auto bytesFree = file.getBytesFreeOnVolume();
34
35 if (bytesFree > 0 && bytesFree < 1024 * 1024 * 50)
37
38 return jobHasFinished;
39 }
40
41 void handleAsyncUpdate() override
42 {
43 TransportControl::stopAllTransports (engine, false, false);
44 engine.getUIBehaviour().showWarningMessage (TRANS("Recording error - disk space is getting dangerously low!"));
45 }
46
47 void timerCallback() override
48 {
49 startTimer (31111);
50 engine.getBackgroundJobs().getPool().addJob (this, false);
51 }
52
53 Engine& engine;
54 juce::File file;
55
57};
58
59//==============================================================================
60static const char* projDirPattern = "%projectdir%";
61static const char* editPattern = "%edit%";
62static const char* trackPattern = "%track%";
63static const char* datePattern = "%date%";
64static const char* timePattern = "%time%";
65static const char* takePattern = "%take%";
66
67static juce::String expandPatterns (Edit& ed, const juce::String& s, Track* track, int take)
68{
69 juce::String editName (TRANS("Unknown"));
70 juce::String trackName (TRANS("Unknown"));
72
74
75 if (track != nullptr)
76 trackName = juce::File::createLegalFileName (track->getName());
77
78 if (auto proj = getProjectForEdit (ed))
79 {
80 projDir = proj->getDirectoryForMedia (ProjectItem::Category::recorded).getFullPathName();
81 }
82 else if (ed.editFileRetriever)
83 {
84 auto editFile = ed.editFileRetriever();
85
86 if (editFile != juce::File() && editFile.getParentDirectory().isDirectory())
87 projDir = editFile.getParentDirectory().getFullPathName();
88 }
89
90 auto now = juce::Time::getCurrentTime();
91
92 juce::String date;
93
94 date << now.getDayOfMonth()
95 << juce::Time::getMonthName (now.getMonth(), true)
96 << now.getYear();
97
98 auto time = juce::String::formatted ("%d%02d%02d",
99 now.getHours(),
100 now.getMinutes(),
101 now.getSeconds());
102
103 juce::String s2;
104
105 if (! s.contains (takePattern))
106 s2 = s + "_" + juce::String (takePattern);
107 else
108 s2 = s;
109
110 return juce::File::createLegalPathName (s2.replace (projDirPattern, projDir, true)
111 .replace (editPattern, editName, true)
112 .replace (trackPattern, trackName, true)
113 .replace (datePattern, date, true)
114 .replace (timePattern, time, true)
115 .replace (takePattern, juce::String (take), true)
116 .trim());
117}
118
119
120//==============================================================================
122{
124 {
125 lengthInSeconds = e.getPropertyStorage().getProperty (SettingID::retrospectiveRecord, 30);
126 }
127
128 void updateSizeIfNeeded (int newNumChannels, double newSampleRate)
129 {
130 int newNumSamples = juce::roundToInt (lengthInSeconds * newSampleRate);
131
132 if (newNumChannels != numChannels || newNumSamples != numSamples || newSampleRate != sampleRate)
133 {
134 numChannels = newNumChannels;
135 numSamples = newNumSamples;
136 sampleRate = newSampleRate;
137
138 fifo.setSize (numChannels, std::max (1, numSamples));
139 fifo.reset();
140 }
141 }
142
143 void processBuffer (double streamTime, const juce::AudioBuffer<float>& inputBuffer, int numSamplesIn)
144 {
145 if (numSamplesIn < numSamples)
146 {
147 lastStreamTime = streamTime;
148
149 fifo.ensureFreeSpace (numSamplesIn);
150 fifo.write (inputBuffer);
151 }
152 }
153
154 void syncToEdit (Edit& edit, EditPlaybackContext& context, double streamTime, int numSamplesIn)
155 {
156 const juce::SpinLock::ScopedLockType sl (editInfoLock);
157 auto& pei = editInfo[edit.getProjectItemID()];
158
159 if (context.isPlaying())
160 {
161 pei.pausedTime = 0s;
162 pei.lastEditTime = context.globalStreamTimeToEditTime (streamTime);
163 }
164 else
165 {
166 pei.pausedTime = pei.pausedTime + TimeDuration::fromSamples (numSamplesIn, sampleRate);
167 }
168 }
169
170 bool wasRecentlyPlaying (Edit& edit)
171 {
172 const juce::SpinLock::ScopedLockType sl (editInfoLock);
173 auto& pei = editInfo[edit.getProjectItemID()];
174
175 return (pei.lastEditTime >= 0s && pei.pausedTime < 20s);
176 }
177
178 void removeEditSync (Edit& edit)
179 {
180 const juce::SpinLock::ScopedLockType sl (editInfoLock);
181 auto itr = editInfo.find (edit.getProjectItemID());
182
183 if (itr != editInfo.end())
184 editInfo.erase (itr);
185 }
186
187 double lengthInSeconds = 30.0;
188
189 AudioFifo fifo { 1, 1 };
190 double lastStreamTime = 0;
191
192 int numChannels = 0;
193 int numSamples = 0;
194 double sampleRate = 0;
195
197 {
198 TimePosition lastEditTime = -1s;
199 TimeDuration pausedTime;
200 };
201
203 juce::SpinLock editInfoLock;
204
206};
207
208
209//==============================================================================
210//==============================================================================
212{
213public:
215 : InputDeviceInstance (dev, c),
216 inputBuffer (1, 128)
217 {
218 getWaveInput().addInstance (this);
219 }
220
221 ~WaveInputDeviceInstance() override
222 {
224 params.unloopedTimeToEndRecording = context.getUnloopedPosition();
225 params.isLooping = context.transport.looping;
226 params.markedRange = context.transport.getLoopRange();
227 stopRecording (params);
228
229 auto& wi = getWaveInput();
230
231 if (wi.retrospectiveBuffer)
232 wi.retrospectiveBuffer->removeEditSync (edit);
233
234 getWaveInput().removeInstance (this);
235 }
236
237 bool isRecordingActive() const override
238 {
239 return getWaveInput().mergeMode != 2 && InputDeviceInstance::isRecordingActive();
240 }
241
242 bool isRecordingActive (EditItemID targetID) const override
243 {
244 return getWaveInput().mergeMode != 2 && InputDeviceInstance::isRecordingActive (targetID);
245 }
246
247 bool isRecordingQueuedToStop (EditItemID targetID) override
248 {
249 return getRecordStopper().isQueued (targetID);
250 }
251
252 bool shouldTrackContentsBeMuted (const Track& t) override
253 {
254 bool isTrackRecordingWithPunch = false, muteTrackNow = false,
255 muteTrackContentsWhilstRecording = false, isActivelyRecording = false;
256
257 {
258 const std::shared_lock sl (contextLock);
259
260 if (recordingContexts.empty())
261 return false;
262
263 if (auto recContext = getContextForID (t.itemID))
264 {
265 isTrackRecordingWithPunch = recContext->recordingWithPunch;
266 muteTrackNow = recContext->muteTargetNow;
267 muteTrackContentsWhilstRecording = recContext->muteTrackContentsWhilstRecording;
268 isActivelyRecording = recContext->hasHitThreshold;
269 }
270 }
271
272 if (muteTrackContentsWhilstRecording && isActivelyRecording)
273 return true;
274
275 if (isTrackRecordingWithPunch
276 && muteTrackNow
277 && getWaveInput().mergeMode == 1)
278 return true;
279
280 return false;
281 }
282
283 juce::AudioFormat* getFormatToUse() const
284 {
285 return edit.engine.getAudioFileFormatManager().getNamedFormat (getWaveInput().outputFormat);
286 }
287
288 static tl::expected<juce::File, juce::String> getDestinationRecordingFile (Edit& ed, EditItemID targetID,
289 const juce::AudioFormat& format, juce::String filenameMask)
290 {
291 juce::File recordedFile;
292 int take = 1;
293
294 auto track = findTrackForID (ed, targetID);
295
296 if (! track)
297 if (auto cs = findClipSlotForID (ed, targetID))
298 track = &cs->track;
299
300 do
301 {
302 recordedFile = juce::File (expandPatterns (ed, filenameMask, track, take++)
303 + format.getFileExtensions()[0]);
304 } while (recordedFile.exists());
305
306 if (! recordedFile.getParentDirectory().createDirectory())
307 {
308 TRACKTION_LOG_ERROR ("Record fail: can't create parent directory: " + recordedFile.getFullPathName());
309
310 return TRANS("The directory\nXZZX\ndoesn't exist")
311 .replace ("XZZX", recordedFile.getParentDirectory().getFullPathName());
312 }
313
314 if (! recordedFile.getParentDirectory().hasWriteAccess())
315 {
316 TRACKTION_LOG_ERROR ("Record fail: directory is read-only: " + recordedFile.getFullPathName());
317
318 return TRANS("The directory\nXZZX\n doesn't have write-access")
319 .replace ("XZZX", recordedFile.getParentDirectory().getFullPathName());
320 }
321
322 if (! recordedFile.deleteFile())
323 {
324 TRACKTION_LOG_ERROR ("Record fail: can't overwrite file: " + recordedFile.getFullPathName());
325
326 return TRANS("Can't overwrite the existing file:") + "\n" + recordedFile.getFullPathName();
327 }
328
329 return recordedFile;
330 }
331
332 tl::expected<std::unique_ptr<RecordingContext>, juce::String> prepareToRecordTarget (EditItemID targetID, TimeRange punchRange)
333 {
335 TRACKTION_ASSERT_MESSAGE_THREAD
336
338 {
339 if (getContextForID (targetID))
340 return tl::unexpected (TRANS("Recording already in progress"));
341
342 if (auto proj = getProjectForEdit (edit))
343 if (proj->isReadOnly())
344 return tl::unexpected (TRANS("The current project is read-only, so new clips can't be recorded into it!"));
345
346 auto format = getFormatToUse();
347 const auto res = getDestinationRecordingFile (edit, targetID, *format, getWaveInput().filenameMask);
348
349 if (! res)
350 return tl::unexpected (res.error());
351
352 auto recordedFile = res.value();
353 auto rc = std::make_unique<WaveRecordingContext> (context, targetID, recordedFile);
354 rc->sampleRate = edit.engine.getDeviceManager().getSampleRate();
355
356 juce::StringPairArray metadata;
357 AudioFileUtils::addBWAVStartToMetadata (metadata, (SampleCount) tracktion::toSamples (punchRange.getStart(), rc->sampleRate));
358 auto& wi = getWaveInput();
359
360 rc->fileWriter = std::make_unique<AudioFileWriter> (AudioFile (edit.engine, recordedFile), format,
361 wi.isStereoPair() ? 2 : 1,
362 rc->sampleRate, wi.bitDepth, metadata, 0);
363
364 if (rc->fileWriter->isOpen())
365 {
367 rc->firstRecCallback = true;
368
369 const auto adjustSeconds = wi.getAdjustmentSeconds();
370 rc->adjustSamples = (int) tracktion::toSamples (adjustSeconds, rc->sampleRate);
371
372 if (! owner.isTrackDevice())
373 rc->adjustSamples += context.getLatencySamples();
374
375 rc->adjustDurationAtStart = TimeDuration::fromSamples (rc->adjustSamples, rc->sampleRate);
376
378 {
379 rc->recordingWithPunch = true;
380
381 auto loopRange = context.transport.getLoopRange();
382 auto muteStart = std::max (punchRange.getStart(), loopRange.getStart());
383 auto muteEnd = punchRange.getEnd();
384
385 if (punchRange.getStart() < loopRange.getEnd() - 0.5s)
386 {
387 punchRange = punchRange.withEnd (punchRange.getEnd() + 0.8s);
388 muteEnd = loopRange.getEnd();
389 }
390
391 rc->muteTimes = { muteStart, muteEnd };
392 }
393
394 rc->punchTimes = punchRange;
395 rc->recordingBlockRange = rc->punchTimes.withEnd (rc->punchTimes.getEnd() + adjustSeconds);
396 rc->hasHitThreshold = (wi.recordTriggerDb <= -50.0f);
397
398 if (edit.engine.getUIBehaviour().shouldGenerateLiveWaveformsWhenRecording())
399 {
400 if ((rc->thumbnail = edit.engine.getRecordingThumbnailManager().getThumbnailFor (recordedFile)))
401 {
402 rc->thumbnail->reset (wi.isStereoPair() ? 2 : 1, rc->sampleRate);
403 rc->thumbnail->punchInTime = punchRange.getStart();
404 }
405 }
406
407 return rc;
408 }
409 else
410 {
411 TRACKTION_LOG_ERROR ("Record fail: couldn't write to file: " + recordedFile.getFullPathName());
412
413 return tl::unexpected (TRANS("Couldn't record!") + "\n\n"
414 + TRANS("Couldn't create the file: XZZX").replace ("XZZX", recordedFile.getFullPathName()));
415 }
416 }
418
419 return tl::unexpected (TRANS("Unable to start recording"));
420 }
421
423 {
425 TRACKTION_ASSERT_MESSAGE_THREAD
426 assert (params.punchRange.getEnd() < TimePosition::fromSeconds (std::numeric_limits<double>::max()));
427
429
430 if (params.targets.empty())
431 for (auto dest : destinations)
432 if (dest->recordEnabled)
433 params.targets.push_back (dest->targetID);
434
435 for (auto target : params.targets)
436 results.emplace_back (prepareToRecordTarget (target, params.punchRange));
437
438 return results;
439 }
440
442 {
443 TRACKTION_ASSERT_MESSAGE_THREAD
444 bool hasAddedContexts = false;
445
446 for (auto& recContext : newContexts)
447 {
448 if (auto midiContext = dynamic_cast<WaveRecordingContext*> (recContext.get()))
449 {
450 const auto targetID = midiContext->targetID;
451
452 {
453 const std::unique_lock sl (contextLock);
454 recordingContexts.push_back (std::unique_ptr<WaveRecordingContext> (midiContext));
455 }
456
457 hasAddedContexts = true;
458 recContext.release();
459 context.transport.callRecordingAboutToStartListeners (*this, targetID);
460 }
461 }
462
463 if (hasAddedContexts && ! edit.getTransport().isPlaying())
464 edit.getTransport().play (false);
465
466 // Remove now empty contents and return the rest
467 return std::move (erase_if_null (newContexts));
468 }
469
471 {
472 {
473 const std::shared_lock sl (contextLock);
474
475 for (auto& recContext : recordingContexts)
476 if (recContext->targetID == targetID)
477 return recContext->punchTimes.getStart();
478 }
479
481 }
482
483 bool isRecording (EditItemID targetID) override
484 {
485 return getContextForID (targetID) != nullptr;
486 }
487
488 bool isRecording() override
489 {
490 const std::shared_lock sl (contextLock);
491 return ! recordingContexts.empty();
492 }
493
494 tl::expected<Clip::Array, juce::String> stopRecording (StopRecordingParameters params) override
495 {
496 TRACKTION_ASSERT_MESSAGE_THREAD
497 // Reserve to avoid allocating whilst
498 const auto numContextsRecording = [this]
499 {
500 const std::shared_lock sl (contextLock);
501 return recordingContexts.size();
502 }();
503
505 contextsToStop.reserve (numContextsRecording);
506
507 // Stop the relevant contexts
508 {
509 const std::unique_lock sl (contextLock);
510
511 for (auto& recContext : recordingContexts)
512 {
513 if (! params.targetsToStop.empty())
514 if (! contains_v (params.targetsToStop, recContext->targetID))
515 continue;
516
517 recContext->unloopedStopTime = params.unloopedTimeToEndRecording;
518 contextsToStop.push_back (std::move (recContext));
519 }
520
521 // Erase any now-empty contexts
522 erase_if_null (recordingContexts);
523 assert ((recordingContexts.size() + contextsToStop.size()) == numContextsRecording);
524 }
525
526 // Now apply those stop contexts whilst not holding the lock
527 Clip::Array clips;
528 juce::String error;
529
530 for (auto& recContext : contextsToStop)
531 {
532 const auto targetID = recContext->targetID;
533 auto stopCallback = std::move (recContext->stopCallback);
534 context.transport.callRecordingAboutToStopListeners (*this, targetID);
535 auto res = applyRecording (std::move (recContext),
537 params.isLooping, params.markedRange,
538 params.discardRecordings);
539 context.transport.callRecordingFinishedListeners (*this, targetID,
540 res.value_or (Clip::Array()));
541
542 if (stopCallback)
543 stopCallback (res);
544
545 res.map ([&] (auto c) { clips.addArray (std::move (c)); })
546 .map_error ([&] (auto err) { error = err; });
547 }
548
549 if (! error.isEmpty())
550 return tl::unexpected (error);
551
552 return clips;
553 }
554
556 std::function<void (tl::expected<Clip::Array, juce::String>)> callback) override
557 {
558 TRACKTION_ASSERT_MESSAGE_THREAD
559 // Reserve to avoid allocating whilst
560 const auto getNumContextsRecording = [this]
561 {
562 const std::shared_lock sl (contextLock);
563 return recordingContexts.size();
564 };
565
566 if (params.targetsToStop.empty())
567 {
568 params.targetsToStop.reserve (getNumContextsRecording());
569
570 const std::shared_lock sl (contextLock);
571
572 for (auto& recContext : recordingContexts)
573 params.targetsToStop.push_back (recContext->targetID);
574 }
575
576 // Set the punch out time for the contexts
577 {
578 const std::unique_lock sl (contextLock);
579
580 for (auto& recContext : recordingContexts)
581 {
582 if (! contains_v (params.targetsToStop, recContext->targetID))
583 continue;
584
585 recContext->unloopedStopTime = params.unloopedTimeToEndRecording;
586
587 // Set the recordingBlockRange to the stop time plus the adjust time when recording started as this number
588 // of samples will have been dropped. To ensure the final file has the correct number of samples, we need
589 // to record this many samples past the "real" end
590 recContext->recordingBlockRange = recContext->recordingBlockRange.withEnd (recContext->unloopedStopTime
591 + recContext->adjustDurationAtStart);
592
593 // Unlock whilst doing potentially allocating ops to avoid priority inversion
594 {
595 contextLock.unlock();
596 recContext->stopCallback = callback;
597 recContext->stopParams = params;
598 recContext->stopParams.targetsToStop = { recContext->targetID };
599 contextLock.lock();
600 }
601 }
602 }
603
604 // Add the rec context to a timer list to poll if the recording can be stopped
605 for (auto targetID : params.targetsToStop)
606 getRecordStopper().addTargetToStop (targetID);
607 }
608
609 juce::File getRecordingFile (EditItemID targetID) const override
610 {
611 const std::shared_lock sl (contextLock);
612
613 if (auto rc = getContextForID (targetID))
614 return rc->file;
615
616 return {};
617 }
618
620 {
622 : RecordingContext (targetID_),
623 editPlaybackContext (epc), file (f)
624 {}
625
626 EditPlaybackContext& editPlaybackContext;
627 Engine& engine { editPlaybackContext.edit.engine };
628 juce::File file;
629 double sampleRate = 44100.0;
630 TimeRange punchTimes;
631 TimeRange muteTimes;
635 TimeDuration adjustDurationAtStart;
636 std::atomic<bool> hasHitThreshold { false };
637 bool firstRecCallback = false, recordingWithPunch = false;
638 int adjustSamples = 0;
639 std::atomic<bool> muteTargetNow { false };
640 const bool muteTrackContentsWhilstRecording = engine.getEngineBehaviour().muteTrackContentsWhilstRecording();
641
643 DiskSpaceCheckTask diskSpaceChecker { engine, file };
645 WaveInputRecordingThread::ScopedInitialiser threadInitialiser { engine.getWaveInputRecordingThread() };
646 const detail::ScopedActiveRecordingDevice scopedActiveRecordingDevice { editPlaybackContext };
647
648 std::function<void (tl::expected<Clip::Array, juce::String>)> stopCallback;
649 StopRecordingParameters stopParams;
650
651 void addBlockToRecord (const juce::AudioBuffer<float>& buffer, int start, int numSamples)
652 {
653 if (fileWriter != nullptr)
654 engine.getWaveInputRecordingThread().addBlockToRecord (*fileWriter, buffer,
655 start, numSamples, thumbnail);
656 }
657
662 {
664
665 if (auto localCopy = std::move (fileWriter))
666 engine.getWaveInputRecordingThread().waitForWriterToFinish (*localCopy);
667 }
668 };
669
670 tl::expected<Clip::Array, juce::String> applyRecording (std::unique_ptr<WaveRecordingContext> rc,
671 TimePosition unloopedEndTime,
672 bool isLooping, TimeRange loopRange,
673 bool discardRecordings)
674 {
675 TRACKTION_ASSERT_MESSAGE_THREAD
677
678 if (! rc || discardRecordings)
679 for (auto c : consumers)
680 c->discardRecordings (rc ? rc->targetID : EditItemID());
681
682 if (! rc)
683 return {};
684
685 rc->closeFileWriter();
686
687 // If we didn't get as far as adding any samples, delete the header of the file that will have been written
688 if (rc->punchTimes.getStart() >= context.getUnloopedPosition())
689 {
690 rc->file.deleteFile();
691 return {};
692 }
693
694 if (! rc->file.existsAsFile() || rc->file.getSize() == 0)
695 return {};
696
697 const AudioFile recordedFile (edit.engine, rc->file);
698 auto clipOwner = findClipOwnerForID (edit, rc->targetID);
699
700 if (discardRecordings || ! clipOwner)
701 {
702 recordedFile.deleteFile();
703 return {};
704 }
705
706 // Never loop or punch record to slots
707 const bool isClipSlot = dynamic_cast<ClipSlot*> (clipOwner) != nullptr;
708 const bool wasPunchRecording = isClipSlot ? false : edit.recordingPunchInOut;
709 const bool wasLoopRecording = isClipSlot ? false : isLooping;
710
711 return applyLastRecording (*rc, recordedFile, *clipOwner,
712 { rc->punchTimes.getStart(), unloopedEndTime },
713 wasLoopRecording, wasPunchRecording, loopRange.getEnd());
714 }
715
716 tl::expected<Clip::Array, juce::String> applyLastRecording (const WaveRecordingContext& rc,
717 const AudioFile& recordedFile, ClipOwner& destClipOwner,
718 TimeRange recordedRange,
719 bool isLooping, bool isPunching, TimePosition loopEnd)
720 {
722 auto& engine = edit.engine;
723 auto& afm = engine.getAudioFileManager();
724 afm.forceFileUpdate (recordedFile);
725
726 auto recordedFileLength = TimeDuration::fromSeconds (recordedFile.getLength());
727
728 if (recordedFileLength <= 1ms)
729 return {};
730
731 auto newClipLen = std::min (recordedFileLength,
732 recordedRange.getLength());
733
734 if (newClipLen <= 0.01s)
735 {
737 juce::String s;
738
739 if (! rc.hasHitThreshold)
740 s = TRANS("The device \"XZZX\" \nnever reached the trigger threshold set for recording (THRX).")
741 .replace ("XZZX", getWaveInput().getName())
742 .replace ("THRX", gainToDbString (dbToGain (getWaveInput().recordTriggerDb)));
743 else if (isPunching && rc.punchTimes.getStart() >= recordedRange.getEnd())
744 s = TRANS("The device \"XZZX\" \nnever got as far as the punch-in marker, so didn't start recording!")
745 .replace ("XZZX", getWaveInput().getName());
746 else
747 s = TRANS("The device \"XZZX\" \nrecorded a zero-length file which won't be added to the edit")
748 .replace ("XZZX", getWaveInput().getName());
749
750 recordedFile.deleteFile();
751
752 return tl::unexpected (s);
753 }
754
755 if (auto proj = getProjectForEdit (edit))
756 {
757 if (auto projectItem = proj->createNewItem (recordedFile.getFile(),
758 ProjectItem::waveItemType(),
759 recordedFile.getFile().getFileNameWithoutExtension(),
760 {}, ProjectItem::Category::recorded, true))
761 {
762 return applyLastRecording (rc, projectItem, recordedFile, destClipOwner,
763 recordedFileLength, newClipLen, isLooping, isPunching, loopEnd);
764 }
765
766 return tl::unexpected (proj->isReadOnly() ? TRANS("Couldn't add the new recording to the project, because the project is read-only")
767 : TRANS("Couldn't add the new recording to the project!"));
768 }
769 else
770 {
771 return applyLastRecording (rc, nullptr, recordedFile, destClipOwner,
772 recordedFileLength, newClipLen, isLooping, isPunching, loopEnd);
773 }
774
775 return {};
776 }
777
778 tl::expected<Clip::Array, juce::String> applyLastRecording (const WaveRecordingContext& rc, const ProjectItem::Ptr projectItem,
779 const AudioFile& recordedFile, ClipOwner& destClipOwner,
780 TimeDuration recordedFileLength, TimeDuration newClipLen,
781 bool isLooping, bool isPunching, TimePosition loopEnd)
782 {
784 jassert (projectItem == nullptr || projectItem->getID().isValid());
785 auto& engine = edit.engine;
786 auto& afm = engine.getAudioFileManager();
787
789 juce::Array<juce::File> filesCreated;
790 filesCreated.add (recordedFile.getFile());
791
792 if (isLooping)
793 if (! splitRecordingIntoMultipleTakes (context, recordedFile, projectItem, recordedFileLength, extraTakes, filesCreated))
794 return tl::unexpected (TRANS("Couldn't create audio files for multiple takes"));
795
796 auto endPos = rc.punchTimes.getStart() + newClipLen;
797
798 if (isPunching || context.transport.looping)
799 endPos = juce::jlimit (rc.punchTimes.getStart() + 0.5s, loopEnd, endPos);
800
801 Clip::Ptr newClip;
802 bool replaceOldClips = getWaveInput().mergeMode == 1;
803 const auto loopRange = edit.getTransport().getLoopRange();
804
805 if (replaceOldClips && isPunching)
806 {
807 if (projectItem != nullptr)
808 newClip = insertWaveClip (destClipOwner, getNameForNewClip (destClipOwner), projectItem->getID(),
809 { { loopRange.getStart(), endPos }, {} }, DeleteExistingClips::yes);
810 else
811 newClip = insertWaveClip (destClipOwner, getNameForNewClip (destClipOwner), recordedFile.getFile(),
812 { { loopRange.getStart(), endPos }, {} }, DeleteExistingClips::yes);
813
814 if (newClip != nullptr)
815 newClip->setStart (rc.punchTimes.getStart(), false, false);
816 }
817 else
818 {
819 if (projectItem != nullptr)
820 newClip = insertWaveClip (destClipOwner, getNameForNewClip (destClipOwner), projectItem->getID(),
821 { { rc.punchTimes.getStart(), endPos }, {} },
822 replaceOldClips ? DeleteExistingClips::yes : DeleteExistingClips::no);
823 else
824 newClip = insertWaveClip (destClipOwner, getNameForNewClip (destClipOwner), recordedFile.getFile(),
825 { { rc.punchTimes.getStart(), endPos }, {} },
826 replaceOldClips ? DeleteExistingClips::yes : DeleteExistingClips::no);
827 }
828
829 if (newClip == nullptr)
830 {
831 juce::String s ("Couldn't insert new clip after recording: ");
832 s << rc.punchTimes.getStart() << " " << rc.punchTimes.getStart()
833 << " " << endPos << " " << getWaveInput().getAdjustmentSeconds()
834 << " " << recordedFileLength;
835
836 TRACKTION_LOG_ERROR (s);
837
838 engine.getUIBehaviour().showWarningMessage (TRANS("Couldn't add the new recording to the project!"));
839 return {};
840 }
841
843
844 if (isPunching)
845 {
846 if (newClip->getPosition().getStart() < loopRange.getStart())
847 newClip->setStart (loopRange.getStart(), true, false);
848
849 if (newClip->getPosition().getEnd() > loopRange.getEnd()
850 && newClip->getPosition().getStart() < loopRange.getEnd() - 0.1s)
851 newClip->setEnd (loopRange.getEnd(), true);
852 }
853
854 for (auto& f : filesCreated)
855 {
856 AudioFileUtils::applyBWAVStartTime (f, (SampleCount) tracktion::toSamples (newClip->getPosition().getStartOfSource(), rc.sampleRate));
857 afm.forceFileUpdate (AudioFile (edit.engine, f));
858 }
859
860 if (auto wc = dynamic_cast<WaveAudioClip*> (newClip.get()))
861 {
862 if (extraTakes.size() > 0)
863 {
864 for (auto& take : extraTakes)
865 wc->addTake (take->getID());
866 }
867 else if (filesCreated.size() > 1)
868 {
869 for (auto& f : filesCreated)
870 wc->addTake (f);
871 }
872 }
873
874 Clip::Array clips;
875 clips.add (newClip.get());
876 return clips;
877 }
878
879 static bool splitRecordingIntoMultipleTakes (EditPlaybackContext& epc,
880 const AudioFile& recordedFile,
881 const ProjectItem::Ptr& projectItem,
882 TimeDuration recordedFileLength,
884 juce::Array<juce::File>& filesCreated)
885 {
886 auto& edit = epc.edit;
887 auto& afm = edit.engine.getAudioFileManager();
888
889 // break the wave into separate takes..
890 if (projectItem != nullptr)
891 extraTakes.add (projectItem);
892
893 int take = 1;
894 auto loopLength = epc.transport.getLoopRange().getLength();
895
896 for (;;)
897 {
898 const auto takeStart = toPosition (loopLength * take);
899 const auto takeEnd = toPosition (std::min (loopLength * (take + 1),
900 recordedFileLength));
901
902 if ((takeEnd - takeStart) < 0.1s)
903 break;
904
905 const auto takeRange = TimeRange (takeStart, takeEnd);
906 auto takeFile = recordedFile.getFile()
907 .getSiblingFile (recordedFile.getFile().getFileNameWithoutExtension()
908 + "_take_" + juce::String (take + 1))
909 .withFileExtension (recordedFile.getFile().getFileExtension())
910 .getNonexistentSibling (false);
911
912 afm.releaseFile (recordedFile);
913
914 if (AudioFileUtils::copySectionToNewFile (edit.engine, recordedFile.getFile(), takeFile, takeRange) < 0)
915 return false;
916
917 if (projectItem != nullptr)
918 {
919 if (auto takeObject = projectItem->getProject()->createNewItem (takeFile, ProjectItem::waveItemType(),
920 recordedFile.getFile().getFileNameWithoutExtension()
921 + " #" + juce::String (take + 1),
922 {},
923 ProjectItem::Category::recorded, true))
924 {
925 extraTakes.add (takeObject);
926 filesCreated.add (takeFile);
927 }
928 }
929 else
930 {
931 filesCreated.add (takeFile);
932 }
933
934 ++take;
935 }
936
937 // chop down the original wave file..
938 auto tempFile = recordedFile.getFile().getNonexistentSibling (false);
939
940 if (AudioFileUtils::copySectionToNewFile (edit.engine, recordedFile.getFile(), tempFile, TimeRange (0.0s, loopLength)) > 0)
941 {
942 afm.releaseFile (recordedFile);
943 tempFile.moveFileTo (recordedFile.getFile());
944 filesCreated.add (recordedFile.getFile());
945 afm.forceFileUpdate (recordedFile);
946
947 if (projectItem != nullptr)
948 projectItem->verifyLength();
949
950 return true;
951 }
952
953 return false;
954 }
955
957 {
958 juce::Array<Clip*> clips;
959
960 for (auto dstTrack : getTargetTracks (*this))
961 {
962 if (armedOnly && ! isRecordingActive (dstTrack->itemID))
963 continue;
964
965 auto& wi = getWaveInput();
966
967 auto recordBuffer = wi.getRetrospectiveRecordBuffer();
968
969 if (recordBuffer == nullptr)
970 return nullptr;
971
972 auto format = getFormatToUse();
973 const auto res = getDestinationRecordingFile (edit, dstTrack->itemID, *format, getWaveInput().filenameMask);
974
975 if (! res)
976 return {};
977
978 const auto recordedFile = res.value();
979 juce::StringPairArray metadata;
980
981 {
982 AudioFileWriter writer (AudioFile (dstTrack->edit.engine, recordedFile), format,
983 recordBuffer->numChannels,
984 recordBuffer->sampleRate,
985 wi.bitDepth, metadata, 0);
986
987 if (writer.isOpen())
988 {
989 int numReady;
990 juce::AudioBuffer<float> scratchBuffer (recordBuffer->numChannels, 1000);
991
992 while ((numReady = recordBuffer->fifo.getNumReady()) > 0)
993 {
994 auto toRead = std::min (numReady, scratchBuffer.getNumSamples());
995
996 if (! recordBuffer->fifo.read (scratchBuffer, 0, toRead)
997 || ! writer.appendBuffer (scratchBuffer, toRead))
998 return nullptr;
999 }
1000 }
1001 }
1002
1003 auto proj = getProjectForEdit (edit);
1004
1005 if (proj == nullptr)
1006 {
1007 jassertfalse; // TODO
1008 return nullptr;
1009 }
1010
1011 auto projectItem = proj->createNewItem (recordedFile, ProjectItem::waveItemType(),
1012 recordedFile.getFileNameWithoutExtension(),
1013 {}, ProjectItem::Category::recorded, true);
1014
1015 if (projectItem == nullptr)
1016 continue;
1017
1018 jassert (projectItem->getID().isValid());
1019
1020 auto clipName = getNameForNewClip (*dstTrack);
1021 TimePosition start;
1022 const auto recordedLength = TimeDuration::fromSeconds (AudioFile (dstTrack->edit.engine, recordedFile).getLength());
1023
1024 if (context.isPlaying() || recordBuffer->wasRecentlyPlaying (edit))
1025 {
1026 const auto blockSizeSeconds = edit.engine.getDeviceManager().getBlockLength();
1027 auto adjust = -wi.getAdjustmentSeconds() + blockSizeSeconds;
1028
1029 adjust = adjust - TimeDuration::fromSamples (context.getLatencySamples(), edit.engine.getDeviceManager().getSampleRate());
1030
1031 // TODO: Still not quite sure why the adjustment needs to be a block more with
1032 // the tracktion_graph engine, this may need correcting in the future
1033 if (context.getNodePlayHead() != nullptr)
1034 adjust = adjust + blockSizeSeconds;
1035
1036 if (context.isPlaying())
1037 {
1038 start = context.globalStreamTimeToEditTime (recordBuffer->lastStreamTime) - recordedLength + adjust;
1039 }
1040 else
1041 {
1042 auto& pei = recordBuffer->editInfo[edit.getProjectItemID()];
1043 start = pei.lastEditTime + pei.pausedTime - recordedLength + adjust;
1044 pei.lastEditTime = -1s;
1045 }
1046 }
1047 else
1048 {
1049 auto position = context.getPosition();
1050
1051 if (position >= 5s)
1052 start = position - recordedLength;
1053 else
1054 start = std::max<TimePosition> (0.0s, position);
1055 }
1056
1057 ClipPosition clipPos = { { start, start + recordedLength }, {} };
1058
1059 if (start < 0s)
1060 {
1061 clipPos.offset = toDuration (-start);
1062 clipPos.time = clipPos.time.withStart (0s);
1063 }
1064
1065 auto newClip = dstTrack->insertWaveClip (clipName, projectItem->getID(), clipPos, false);
1066
1067 if (newClip == nullptr)
1068 continue;
1069
1071
1072 AudioFileUtils::applyBWAVStartTime (recordedFile, (SampleCount) tracktion::toSamples (newClip->getPosition().getStartOfSource(), recordBuffer->sampleRate));
1073
1074 edit.engine.getAudioFileManager().forceFileUpdate (AudioFile (dstTrack->edit.engine, recordedFile));
1075
1076 if (dstTrack->playSlotClips.get())
1077 {
1078 if (auto slot = getFreeSlot (*dstTrack))
1079 {
1080 newClip->setUsesProxy (false);
1081 newClip->setStart (0_tp, false, true);
1082
1083 if (! newClip->isLooping())
1084 newClip->setLoopRangeBeats ({ 0_bp, newClip->getLengthInBeats() });
1085
1086 newClip->removeFromParent();
1087 slot->setClip (newClip.get());
1088 }
1089 }
1090
1091 clips.add (newClip.get());
1092 }
1093
1094 return clips;
1095 }
1096
1097 void copyIncomingDataIntoBuffer (const float* const* allChannels, int numChannels, int numSamples)
1098 {
1099 auto& wi = getWaveInput();
1100 auto& channelSet = wi.getChannelSet();
1101 inputBuffer.setSize (channelSet.size(), numSamples);
1102
1103 if (numChannels == 0)
1104 {
1105 inputBuffer.clear();
1106 return;
1107 }
1108
1109 for (const auto& ci : wi.getChannels())
1110 {
1111 if (juce::isPositiveAndBelow (ci.indexInDevice, numChannels))
1112 {
1113 auto inputIndex = channelSet.getChannelIndexForType (ci.channel);
1114 juce::FloatVectorOperations::copy (inputBuffer.getWritePointer (inputIndex),
1115 allChannels[ci.indexInDevice], numSamples);
1116 }
1117 else
1118 {
1119 jassertfalse; // Is an input device getting created with more channels than the total number of device channels?
1120 }
1121 }
1122 }
1123
1124 void acceptInputBuffer (const float* const* allChannels, int numChannels, int numSamples,
1125 double streamTime, LevelMeasurer* measurerToUpdate,
1126 RetrospectiveRecordBuffer* retrospectiveBuffer, bool addToRetrospective)
1127 {
1129 copyIncomingDataIntoBuffer (allChannels, numChannels, numSamples);
1130
1131 auto inputGainDb = getWaveInput().inputGainDb;
1132
1133 if (inputGainDb > 0.01f || inputGainDb < -0.01f)
1134 inputBuffer.applyGain (0, numSamples, dbToGain (inputGainDb));
1135
1136 if (measurerToUpdate != nullptr)
1137 measurerToUpdate->processBuffer (inputBuffer, 0, numSamples);
1138
1139 if (retrospectiveBuffer != nullptr)
1140 {
1141 if (addToRetrospective)
1142 {
1143 retrospectiveBuffer->updateSizeIfNeeded (inputBuffer.getNumChannels(),
1144 edit.engine.getDeviceManager().getSampleRate());
1145 retrospectiveBuffer->processBuffer (streamTime, inputBuffer, numSamples);
1146 }
1147
1148 retrospectiveBuffer->syncToEdit (edit, context, streamTime, numSamples);
1149 }
1150
1151 {
1152 const juce::ScopedLock sl (consumerLock);
1153
1154 for (auto n : consumers)
1155 n->acceptInputBuffer (choc::buffer::createChannelArrayView (inputBuffer.getArrayOfWritePointers(),
1156 (choc::buffer::ChannelCount) inputBuffer.getNumChannels(),
1157 (choc::buffer::FrameCount) numSamples));
1158 }
1159
1160 {
1161 const auto blockStart = context.globalStreamTimeToEditTimeUnlooped (streamTime);
1162 const std::shared_lock sl (contextLock);
1163
1164 for (auto& recordingContext : recordingContexts)
1165 {
1166 const TimeRange blockRange (blockStart, TimeDuration::fromSamples (numSamples, recordingContext->sampleRate));
1167
1168 recordingContext->muteTargetNow = recordingContext->muteTimes.overlaps (blockRange);
1169
1170 if (recordingContext->recordingBlockRange.overlaps (blockRange))
1171 {
1172 if (! recordingContext->hasHitThreshold)
1173 {
1174 auto bufferLevelDb = gainToDb (inputBuffer.getMagnitude (0, numSamples));
1175 recordingContext->hasHitThreshold = bufferLevelDb > getWaveInput().recordTriggerDb;
1176
1177 if (! recordingContext->hasHitThreshold)
1178 return;
1179
1180 recordingContext->punchTimes = recordingContext->punchTimes.withStart (blockRange.getStart());
1181
1182 if (recordingContext->thumbnail != nullptr)
1183 recordingContext->thumbnail->punchInTime = blockRange.getStart();
1184 }
1185
1186 if (recordingContext->firstRecCallback)
1187 {
1188 recordingContext->firstRecCallback = false;
1189
1190 auto timeDiff = blockRange.getStart() - recordingContext->recordingBlockRange.getStart();
1191 recordingContext->adjustSamples -= (int) tracktion::toSamples (timeDiff, recordingContext->sampleRate);
1192 }
1193
1194 const int adjustSamples = recordingContext->adjustSamples;
1195
1196 if (adjustSamples < 0)
1197 {
1198 // add silence
1199 AudioScratchBuffer silence (inputBuffer.getNumChannels(), -adjustSamples);
1200 silence.buffer.clear();
1201
1202 recordingContext->addBlockToRecord (silence.buffer, 0, silence.buffer.getNumSamples());
1203 recordingContext->adjustSamples = 0;
1204 }
1205 else if (adjustSamples > 0)
1206 {
1207 // drop samples
1208 if (adjustSamples >= numSamples)
1209 {
1210 recordingContext->adjustSamples -= numSamples;
1211 }
1212 else
1213 {
1214 recordingContext->addBlockToRecord (inputBuffer, adjustSamples, numSamples - adjustSamples);
1215 recordingContext->adjustSamples = 0;
1216 }
1217 }
1218 else
1219 {
1220 recordingContext->addBlockToRecord (inputBuffer, 0, numSamples);
1221 }
1222 }
1223 }
1224 }
1225 }
1226
1227protected:
1228 mutable std::shared_mutex contextLock;
1230 std::unique_ptr<RecordStopper> recordStopper;
1231
1232 juce::AudioBuffer<float> inputBuffer;
1233
1234 WaveRecordingContext* getContextForID (EditItemID targetID) const
1235 {
1236 const std::shared_lock sl (contextLock);
1237
1238 for (auto& recContext : recordingContexts)
1239 if (recContext->targetID == targetID)
1240 return recContext.get();
1241
1242 return nullptr;
1243 }
1244
1245 RecordStopper& getRecordStopper()
1246 {
1247 TRACKTION_ASSERT_MESSAGE_THREAD
1248 if (! recordStopper)
1249 recordStopper = std::make_unique<RecordStopper> ([this] (auto targetID)
1250 {
1251 const auto unloopedTimeNow = context.getUnloopedPosition();
1252
1253 const std::shared_lock sl (contextLock);
1254
1255 if (auto recContext = getContextForID (targetID))
1256 {
1257 if (unloopedTimeNow >= recContext->recordingBlockRange.getEnd())
1258 {
1259 auto stopParams = recContext->stopParams;
1260
1261 // Temp unlock as stopRecording takes a unique lock
1262 contextLock.unlock_shared();
1263 auto res = stopRecording (stopParams);
1264 contextLock.lock_shared();
1265
1266 return RecordStopper::HasFinished::yes;
1267 }
1268 }
1269
1270 return RecordStopper::HasFinished::no;
1271 });
1272
1273 return *recordStopper;
1274 }
1275
1276 WaveInputDevice& getWaveInput() const noexcept { return static_cast<WaveInputDevice&> (owner); }
1277
1278 //==============================================================================
1279 juce::Array<Consumer*> consumers;
1280 juce::CriticalSection consumerLock;
1281
1282 void addConsumer (Consumer* consumer) override
1283 {
1284 const juce::ScopedLock sl (consumerLock);
1285 consumers.addIfNotAlreadyThere (consumer);
1286 }
1287
1288 void removeConsumer (Consumer* consumer) override
1289 {
1290 const juce::ScopedLock sl (consumerLock);
1291 consumers.removeAllInstancesOf (consumer);
1292 }
1293
1295};
1296
1297//==============================================================================
1298WaveInputDevice::WaveInputDevice (Engine& e, const juce::String& devType,
1299 const WaveDeviceDescription& desc, DeviceType t)
1300 : InputDevice (e, devType, desc.name, "wavein_" + juce::String::toHexString (desc.name.hashCode())),
1301 deviceChannels (desc.channels),
1302 deviceType (t),
1303 channelSet (createChannelSet (desc.channels))
1304{
1305 enabled = desc.enabled;
1306 loadProps();
1307}
1308
1309WaveInputDevice::~WaveInputDevice()
1310{
1311 notifyListenersOfDeletion();
1312 closeDevice();
1313}
1314
1315juce::StringArray WaveInputDevice::getMergeModes()
1316{
1318 s.add (TRANS("Overlay newly recorded clips onto edit"));
1319 s.add (TRANS("Replace old clips in edit with new ones"));
1320 s.add (TRANS("Don't make recordings from this device"));
1321
1322 return s;
1323}
1324
1325juce::StringArray WaveInputDevice::getRecordFormatNames()
1326{
1328
1329 auto& afm = engine.getAudioFileFormatManager();
1330 s.add (afm.getWavFormat()->getFormatName());
1331 s.add (afm.getAiffFormat()->getFormatName());
1332 s.add (afm.getFlacFormat()->getFormatName());
1333
1334 return s;
1335}
1336
1338{
1339 if (! isTrackDevice() && retrospectiveBuffer == nullptr)
1340 retrospectiveBuffer = std::make_unique<RetrospectiveRecordBuffer> (ed.edit.engine);
1341
1342 return new WaveInputDeviceInstance (*this, ed);
1343}
1344
1345void WaveInputDevice::resetToDefault()
1346{
1347 juce::String propName = isTrackDevice() ? "TRACKTION_TRACK_DEVICE" : getName();
1348 engine.getPropertyStorage().removePropertyItem (SettingID::wavein, propName);
1349 loadProps();
1350}
1351
1352void WaveInputDevice::setEnabled (bool b)
1353{
1354 if (enabled != b)
1355 {
1356 enabled = b;
1357 changed();
1358
1359 if (! isTrackDevice())
1360 {
1361 engine.getDeviceManager().setWaveInChannelsEnabled (deviceChannels, b);
1362 }
1363 else
1364 {
1365 // reload our properties in case another track device has changed them
1366 loadProps();
1367 }
1368 // do nothing now! this object is probably deleted..
1369 }
1370}
1371
1372juce::String WaveInputDevice::openDevice()
1373{
1374 return {};
1375}
1376
1377void WaveInputDevice::closeDevice()
1378{
1379 saveProps();
1380}
1381
1382void WaveInputDevice::loadProps()
1383{
1384 filenameMask = getDefaultMask();
1385 inputGainDb = 0.0f;
1386 monitorMode = MonitorMode::automatic;
1387 outputFormat = engine.getAudioFileFormatManager().getDefaultFormat()->getFormatName();
1388
1389 recordTriggerDb = -50.0f;
1390 mergeMode = 0;
1391 bitDepth = 24;
1392 recordAdjustMs = 0;
1393
1394 juce::String propName = isTrackDevice() ? "TRACKTION_TRACK_DEVICE" : getName();
1395
1396 if (auto n = engine.getPropertyStorage().getXmlPropertyItem (SettingID::wavein, propName))
1397 {
1398 filenameMask = n->getStringAttribute ("filename", filenameMask);
1399 inputGainDb = (float) n->getDoubleAttribute ("gainDb", inputGainDb);
1400 monitorMode = magic_enum::enum_cast<MonitorMode> (n->getStringAttribute ("monitorMode").toStdString()).value_or (MonitorMode::automatic);
1401
1402 outputFormat = n->getStringAttribute ("format", outputFormat);
1403 bitDepth = n->getIntAttribute ("bits", bitDepth);
1404
1405 if (! getRecordFormatNames().contains (outputFormat))
1406 outputFormat = engine.getAudioFileFormatManager().getDefaultFormat()->getFormatName();
1407
1408 recordTriggerDb = (float) n->getDoubleAttribute ("triggerDb", recordTriggerDb);
1409 mergeMode = n->getIntAttribute ("mode", mergeMode);
1410 recordAdjustMs = n->getDoubleAttribute ("adjustMs", recordAdjustMs);
1411
1412 if (recordAdjustMs != 0)
1413 TRACKTION_LOG ("Manual record adjustment: " + juce::String (recordAdjustMs) + "ms");
1414 }
1415
1416 checkBitDepth();
1417}
1418
1419void WaveInputDevice::saveProps()
1420{
1421 juce::XmlElement n ("SETTINGS");
1422
1423 n.setAttribute ("filename", filenameMask);
1424 n.setAttribute ("gainDb", inputGainDb);
1425 n.setAttribute ("monitorMode", std::string (magic_enum::enum_name (monitorMode)));
1426 n.setAttribute ("format", outputFormat);
1427 n.setAttribute ("bits", bitDepth);
1428 n.setAttribute ("triggerDb", recordTriggerDb);
1429 n.setAttribute ("mode", mergeMode);
1430 n.setAttribute ("adjustMs", recordAdjustMs);
1431
1432 juce::String propName = isTrackDevice() ? "TRACKTION_TRACK_DEVICE" : getName();
1433
1434 engine.getPropertyStorage().setXmlPropertyItem (SettingID::wavein, propName, n);
1435}
1436
1437//==============================================================================
1439{
1440 if (getDeviceType() == trackWaveDevice)
1441 return getAlias() + " (" + getType() + ")";
1442
1444}
1445
1446bool WaveInputDevice::isStereoPair() const
1447{
1448 return deviceChannels.size() == 2;
1449}
1450
1451void WaveInputDevice::setStereoPair (bool stereo)
1452{
1453 if (isTrackDevice())
1454 {
1456 return;
1457 }
1458
1459 auto& dm = engine.getDeviceManager();
1460
1461 if (deviceChannels.size() == 2)
1462 dm.setDeviceInChannelStereo (std::max (deviceChannels[0].indexInDevice, deviceChannels[1].indexInDevice), stereo);
1463 else if (deviceChannels.size() == 1)
1464 dm.setDeviceInChannelStereo (deviceChannels[0].indexInDevice, stereo);
1465}
1466
1467void WaveInputDevice::setRecordAdjustmentMs (double ms)
1468{
1469 recordAdjustMs = juce::jlimit (-500.0, 500.0, ms);
1470 changed();
1471 saveProps();
1472}
1473
1474void WaveInputDevice::setInputGainDb (float newGain)
1475{
1476 if (newGain != inputGainDb)
1477 {
1478 inputGainDb = newGain;
1479 changed();
1480 saveProps();
1481 }
1482}
1483
1484void WaveInputDevice::setRecordTriggerDb (float newDB)
1485{
1486 if (recordTriggerDb != newDB)
1487 {
1488 recordTriggerDb = newDB;
1489 changed();
1490 saveProps();
1491 }
1492}
1493
1494juce::String WaveInputDevice::getDefaultMask()
1495{
1496 juce::String defaultFile;
1497 defaultFile << projDirPattern << juce::File::getSeparatorChar() << editPattern << '_'
1498 << trackPattern<< '_' << TRANS("Take") << '_' << takePattern;
1499
1500 return defaultFile;
1501}
1502
1503void WaveInputDevice::setFilenameMask (const juce::String& newMask)
1504{
1505 if (filenameMask != newMask)
1506 {
1507 filenameMask = newMask.isNotEmpty() ? newMask
1508 : getDefaultMask();
1509 changed();
1510 saveProps();
1511 }
1512}
1513
1514void WaveInputDevice::setFilenameMaskToDefault()
1515{
1516 if (getDefaultMask() != filenameMask)
1517 setFilenameMask ({});
1518}
1519
1520void WaveInputDevice::setBitDepth (int newDepth)
1521{
1522 if (bitDepth != newDepth)
1523 {
1524 bitDepth = newDepth;
1525 changed();
1526 saveProps();
1527 }
1528}
1529
1530void WaveInputDevice::checkBitDepth()
1531{
1532 if (! getAvailableBitDepths().contains (bitDepth))
1533 bitDepth = getAvailableBitDepths().getLast();
1534}
1535
1536juce::Array<int> WaveInputDevice::getAvailableBitDepths() const
1537{
1538 return getFormatToUse()->getPossibleBitDepths();
1539}
1540
1541juce::AudioFormat* WaveInputDevice::getFormatToUse() const
1542{
1543 return engine.getAudioFileFormatManager().getNamedFormat (outputFormat);
1544}
1545
1546void WaveInputDevice::setOutputFormat (const juce::String& newFormat)
1547{
1548 if (outputFormat != newFormat)
1549 {
1550 outputFormat = newFormat;
1551 checkBitDepth();
1552 changed();
1553 saveProps();
1554 }
1555}
1556
1557juce::String WaveInputDevice::getMergeMode() const
1558{
1559 return getMergeModes() [mergeMode];
1560}
1561
1562void WaveInputDevice::setMergeMode (const juce::String& newMode)
1563{
1564 int newIndex = getMergeModes().indexOf (newMode);
1565
1566 if (mergeMode != newIndex)
1567 {
1568 mergeMode = newIndex;
1569 changed();
1570 saveProps();
1571 }
1572}
1573
1574TimeDuration WaveInputDevice::getAdjustmentSeconds()
1575{
1576 auto& dm = engine.getDeviceManager();
1577 const double autoAdjustMs = isTrackDevice() ? 0.0 : dm.getRecordAdjustmentMs();
1578
1579 return TimeDuration::fromSeconds (juce::jlimit (-3.0, 3.0, 0.001 * (recordAdjustMs + autoAdjustMs)));
1580}
1581
1582//==============================================================================
1583void WaveInputDevice::addInstance (WaveInputDeviceInstance* i)
1584{
1585 const juce::ScopedLock sl (instanceLock);
1586 instances.addIfNotAlreadyThere (i);
1587}
1588
1589void WaveInputDevice::removeInstance (WaveInputDeviceInstance* i)
1590{
1591 const juce::ScopedLock sl (instanceLock);
1592 instances.removeAllInstancesOf (i);
1593}
1594
1595//==============================================================================
1596void WaveInputDevice::consumeNextAudioBlock (const float* const* allChannels, int numChannels, int numSamples, double streamTime)
1597{
1598 if (enabled)
1599 {
1600 bool isFirst = true;
1601 const juce::ScopedLock sl (instanceLock);
1602
1603 // do this first, as writing to file trashes the buffer
1604 for (auto i : instances)
1605 {
1606 i->acceptInputBuffer (allChannels, numChannels, numSamples, streamTime,
1607 isFirst ? &levelMeasurer : nullptr,
1608 (! retrospectiveRecordLock) ? retrospectiveBuffer.get() : nullptr, isFirst);
1609 isFirst = false;
1610 }
1611 }
1612}
1613
1614//==============================================================================
1615void WaveInputDevice::updateRetrospectiveBufferLength (double length)
1616{
1617 if (retrospectiveBuffer != nullptr)
1618 retrospectiveBuffer->lengthInSeconds = length;
1619}
1620
1621//==============================================================================
1623{
1624 BlockQueue()
1625 {
1626 for (int i = 32; --i >= 0;)
1627 addToFreeQueue (new QueuedBlock());
1628 }
1629
1631 {
1632 QueuedBlock() = default;
1633
1634 void load (AudioFileWriter& w, const juce::AudioBuffer<float>& newBuffer,
1635 int start, int numSamples, const RecordingThumbnailManager::Thumbnail::Ptr& thumb)
1636 {
1637 buffer.setSize (newBuffer.getNumChannels(), numSamples);
1638
1639 for (int i = buffer.getNumChannels(); --i >= 0;)
1640 buffer.copyFrom (i, 0, newBuffer, i, start, numSamples);
1641
1642 writer = &w;
1643 thumbnail = thumb;
1644 }
1645
1646 std::atomic<AudioFileWriter*> writer { nullptr };
1647 QueuedBlock* next = nullptr;
1648 juce::AudioBuffer<float> buffer { 2, 512 };
1650
1652 };
1653
1654 juce::CriticalSection freeQueueLock, pendingQueueLock;
1655 QueuedBlock* firstPending = nullptr;
1656 QueuedBlock* lastPending = nullptr;
1657 QueuedBlock* firstFree = nullptr;
1658 std::atomic<int> numPending { 0 };
1659
1660 QueuedBlock* findFreeBlock()
1661 {
1662 const juce::ScopedLock sl (freeQueueLock);
1663
1664 if (firstFree == nullptr)
1665 return new QueuedBlock();
1666
1667 auto* b = firstFree;
1668 firstFree = b->next;
1669 b->next = nullptr;
1670 return b;
1671 }
1672
1673 void addToFreeQueue (QueuedBlock* b) noexcept
1674 {
1675 jassert (b != nullptr);
1676 const juce::ScopedLock sl (freeQueueLock);
1677 b->writer = nullptr;
1678 b->next = firstFree;
1679 firstFree = b;
1680 }
1681
1682 void addToPendingQueue (QueuedBlock* b) noexcept
1683 {
1684 jassert (b != nullptr);
1685 b->next = nullptr;
1686 const juce::ScopedLock sl (pendingQueueLock);
1687
1688 if (lastPending != nullptr)
1689 lastPending->next = b;
1690 else
1691 firstPending = b;
1692
1693 lastPending = b;
1694 ++numPending;
1695 }
1696
1697 QueuedBlock* removeFirstPending() noexcept
1698 {
1699 const juce::ScopedLock sl (pendingQueueLock);
1700
1701 if (auto b = firstPending)
1702 {
1703 firstPending = b->next;
1704
1705 if (firstPending == nullptr)
1706 lastPending = nullptr;
1707
1708 --numPending;
1709 return b;
1710 }
1711
1712 return {};
1713 }
1714
1715 void moveAnyPendingBlocksToFree() noexcept
1716 {
1717 while (auto b = removeFirstPending())
1718 addToFreeQueue (b);
1719
1720 jassert (numPending == 0);
1721 numPending = 0;
1722 }
1723
1724 bool isWriterInQueue (AudioFileWriter& writer) const
1725 {
1726 const juce::ScopedLock sl (pendingQueueLock);
1727
1728 for (auto b = firstPending; b != nullptr; b = b->next)
1729 if (b->writer == &writer)
1730 return true;
1731
1732 return false;
1733 }
1734
1735 void deleteFreeQueue() noexcept
1736 {
1737 auto b = firstFree;
1738 firstFree = nullptr;
1739
1740 while (b != nullptr)
1741 {
1742 std::unique_ptr<QueuedBlock> toDelete (b);
1743 b = b->next;
1744 }
1745 }
1746};
1747
1748//==============================================================================
1749WaveInputRecordingThread::WaveInputRecordingThread (Engine& e)
1750 : Thread ("WaveInputRecordingThread"),
1751 engine (e),
1752 queue (new BlockQueue())
1753{
1754}
1755
1756WaveInputRecordingThread::~WaveInputRecordingThread()
1757{
1758 flushAndStop();
1759 queue->deleteFreeQueue();
1760 queue.reset();
1761}
1762
1763void WaveInputRecordingThread::addUser()
1764{
1765 if (activeUsers++ == 0)
1766 prepareToStart();
1767}
1768
1769void WaveInputRecordingThread::removeUser()
1770{
1771 if (--activeUsers == 0)
1772 flushAndStop();
1773}
1774
1775//==============================================================================
1776void WaveInputRecordingThread::addBlockToRecord (AudioFileWriter& writer, const juce::AudioBuffer<float>& buffer,
1777 int start, int numSamples, const RecordingThumbnailManager::Thumbnail::Ptr& thumbnail)
1778{
1779 if (! threadShouldExit())
1780 {
1781 auto block = queue->findFreeBlock();
1782 block->load (writer, buffer, start, numSamples, thumbnail);
1783 queue->addToPendingQueue (block);
1784 notify();
1785 }
1786}
1787
1788void WaveInputRecordingThread::waitForWriterToFinish (AudioFileWriter& writer)
1789{
1790 while (queue->isWriterInQueue (writer) && isThreadRunning())
1791 Thread::sleep (2);
1792}
1793
1794void WaveInputRecordingThread::run()
1795{
1798
1799 for (;;)
1800 {
1801 if (queue->numPending > 500 && ! hasWarned)
1802 {
1803 hasWarned = true;
1804 TRACKTION_LOG_ERROR ("Audio recording can't keep up!");
1805 }
1806
1807 if (auto block = queue->removeFirstPending())
1808 {
1809 if (! block->writer.load()->appendBuffer (block->buffer, block->buffer.getNumSamples()))
1810 {
1811 if (! hasSentStop)
1812 {
1813 hasSentStop = true;
1814 TRACKTION_LOG_ERROR ("Audio recording failed to write to disk!");
1815 startTimer (1);
1816 }
1817 }
1818
1819 if (block->thumbnail != nullptr)
1820 {
1821 block->thumbnail->addBlock (block->buffer, 0, block->buffer.getNumSamples());
1822 block->thumbnail = nullptr;
1823 }
1824
1825 queue->addToFreeQueue (block);
1826 }
1827 else
1828 {
1829 if (threadShouldExit())
1830 break;
1831
1832 wait (401);
1833 }
1834 }
1835}
1836
1837void WaveInputRecordingThread::timerCallback()
1838{
1839 stopTimer();
1840 TransportControl::stopAllTransports (engine, false, false);
1841}
1842
1843void WaveInputRecordingThread::prepareToStart()
1844{
1845 flushAndStop();
1846 sleep (2);
1848 startThread (juce::Thread::Priority::normal);
1849}
1850
1851void WaveInputRecordingThread::flushAndStop()
1852{
1854 notify();
1855 stopThread (30000);
1856 queue->moveAnyPendingBlocksToFree();
1857 hasSentStop = false;
1858 hasWarned = false;
1859}
1860
1861}} // namespace tracktion { inline namespace engine
assert
int removeAllInstancesOf(ParameterType valueToRemove)
int size() const noexcept
void add(const ElementType &newElement)
bool addIfNotAlreadyThere(ParameterType newElement)
ElementType getLast() const noexcept
void setSize(int newNumChannels, int newNumSamples, bool keepExistingContent=false, bool clearExtraSpace=false, bool avoidReallocating=false)
int getNumChannels() const noexcept
int getNumSamples() const noexcept
void copyFrom(int destChannel, int destStartSample, const AudioBuffer &source, int sourceChannel, int sourceStartSample, int numSamples) noexcept
virtual Array< int > getPossibleBitDepths()=0
bool isDirectory() const
static String createLegalPathName(const String &pathNameToFix)
int64 getBytesFreeOnVolume() const
bool hasWriteAccess() const
const String & getFullPathName() const noexcept
static File getCurrentWorkingDirectory()
File getParentDirectory() const
static juce_wchar getSeparatorChar()
static String createLegalFileName(const String &fileNameToFix)
bool deleteFile() const
bool exists() const
Result createDirectory() const
static void JUCE_CALLTYPE disableDenormalisedNumberSupport(bool shouldDisable=true) noexcept
int size() const noexcept
void addArray(const ReferenceCountedArray &arrayToAddFrom, int startIndex=0, int numElementsToAdd=-1) noexcept
ObjectClass * add(ObjectClass *newObject)
int indexOf(StringRef stringToLookFor, bool ignoreCase=false, int startIndex=0) const
void add(String stringToAdd)
String trim() const
bool isEmpty() const noexcept
static String formatted(const String &formatStr, Args... args)
bool contains(StringRef text) const noexcept
String replace(StringRef stringToReplace, StringRef stringToInsertInstead, bool ignoreCase=false) const
bool isNotEmpty() const noexcept
void addJob(ThreadPoolJob *job, bool deleteJobWhenFinished)
bool removeJob(ThreadPoolJob *job, bool interruptIfRunning, int timeOutMilliseconds)
bool startThread()
bool threadShouldExit() const
bool stopThread(int timeOutMilliseconds)
void notify() const
void signalThreadShouldExit()
bool isThreadRunning() const
static Time JUCE_CALLTYPE getCurrentTime() noexcept
String getMonthName(bool threeLetterVersion) const
void stopTimer() noexcept
void startTimer(int intervalInMilliseconds) noexcept
Smart wrapper for writing to an audio file.
bool appendBuffer(juce::AudioBuffer< float > &buffer, int numSamples)
Appends an AudioBuffer to the file.
bool isOpen() const noexcept
Returns true if the file is open and ready to write to.
int getLatencySamples() const
Returns the overall latency of the currently prepared graph.
The Tracktion Edit class!
std::function< juce::File()> editFileRetriever
This callback can be set to return the file for this Edit.
TransportControl & getTransport() const noexcept
Returns the TransportControl which is used to stop/stop/position playback and recording.
juce::String getName()
Returns the name of the Edit if a ProjectItem can be found for it.
juce::CachedValue< bool > recordingPunchInOut
Whether recoridng only happens within the in/out markers.
ProjectItemID getProjectItemID() const noexcept
Returns the ProjectItemID of the Edit.
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.
WaveInputRecordingThread & getWaveInputRecordingThread() const
Returns the WaveInputRecordingThread instance.
AudioFileFormatManager & getAudioFileFormatManager() const
Returns the AudioFileFormatManager that maintains a list of available audio file formats.
UIBehaviour & getUIBehaviour() const
Returns the UIBehaviour class.
DeviceManager & getDeviceManager() const
Returns the DeviceManager instance for handling audio / MIDI devices.
AudioFileManager & getAudioFileManager() const
Returns the AudioFileManager instance.
RecordingThumbnailManager & getRecordingThumbnailManager() const
Returns the RecordingThumbnailManager instance.
BackgroundJobManager & getBackgroundJobs() const
Returns the BackgroundJobManager instance.
An instance of an InputDevice that's available to an Edit.
virtual bool isRecordingActive() const
Returns true if recording is enabled and the input is connected to any target.
bool discardRecordings
Whether to discard recordings or keep them.
std::vector< EditItemID > targetsToStop
The targets to stop, others will continue allowing you to punch out only specific targets.
Edit & edit
The Edit this instance belongs to.
std::vector< EditItemID > targets
The targets to record to, if this is empty, all armed targets will be added.
EditPlaybackContext & context
The EditPlaybackContext this instance belongs to.
InputDevice & owner
The state of this instance.
DestinationList destinations
The list of assigned destinations.
bool isLooping
Whether to treat the stopped recordings as looped or not.
TimeRange punchRange
The transport time range at which the recording should happen.
TimeRange markedRange
The marked range used for either loop or punch times.
TimePosition unloopedTimeToEndRecording
The TimePosition this recording should be stopped at.
Represents an input device.
@ automatic
Live input is audible when record is enabled.
juce::String getAlias() const
the alias is the name shown in the draggable input device components
juce::String getSelectableDescription() override
Subclasses must return a description of what they are.
Thumbnail::Ptr getThumbnailFor(const juce::File &f)
Returns the Thumbnail for a given audio file.
virtual void changed()
This should be called to send a change notification to any SelectableListeners that are registered wi...
Base class for tracks which contain clips and plugins and can be added to Edit[s].
void play(bool justSendMMCIfEnabled)
Starts playback of an Edit.
static void stopAllTransports(Engine &, bool discardRecordings, bool clearDevices)
Stops all TransportControl[s] in the Engine playing.
TimePosition getTimeWhenStarted() const
Returns the time when the transport was started.
TimeRange getLoopRange() const noexcept
Returns the loop range.
bool isPlaying() const
Returns true if the transport is playing.
virtual void showWarningMessage(const juce::String &message)
Should display a temporary warning message.
bool shouldTrackContentsBeMuted(const Track &t) override
Should return true if this input is currently actively recording into a track and it wants the existi...
void removeConsumer(Consumer *consumer) override
Base classes should override this to remove the Consumer internally.
void stopRecording(StopRecordingParameters params, std::function< void(tl::expected< Clip::Array, juce::String >)> callback) override
Stops a recording asyncronously.
bool isRecordingActive(EditItemID targetID) const override
Returns true if recording is enabled and the input is connected the given target.
bool isRecording(EditItemID targetID) override
Returns true if there are any active recordings for this device.
void addConsumer(Consumer *consumer) override
Base classes should override this to add any Consumers internally.
TimePosition getPunchInTime(EditItemID targetID) override
Returns the time that a given target started recording.
bool isRecordingActive() const override
Returns true if recording is enabled and the input is connected to any target.
juce::File getRecordingFile(EditItemID targetID) const override
Returns the File that the given target is currently recording to.
juce::Array< Clip * > applyRetrospectiveRecord(bool armedOnly) override
Takes the retrospective buffer and creates clips from it, as if recording had been triggered in the p...
std::vector< std::unique_ptr< RecordingContext > > startRecording(std::vector< std::unique_ptr< RecordingContext > > newContexts) override
Starts a recording.
bool isRecordingQueuedToStop(EditItemID targetID) override
Returns true if the async stopRecording function has been used and this target is waiting to stop.
tl::expected< Clip::Array, juce::String > stopRecording(StopRecordingParameters params) override
Stops a recording.
std::vector< tl::expected< std::unique_ptr< RecordingContext >, juce::String > > prepareToRecord(RecordingParameters params) override
Prepares a recording operation.
bool isRecording() override
Returns true if there are any active recordings for this device.
InputDeviceInstance * createInstance(EditPlaybackContext &) override
Creates an instance to use for a given playback context.
juce::String getSelectableDescription() override
Subclasses must return a description of what they are.
T emplace_back(T... args)
T format(T... args)
T is_pointer_v
#define JUCE_TRY
#define JUCE_CATCH_EXCEPTION
#define TRANS(stringLiteral)
#define jassert(expression)
#define JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(className)
#define jassertfalse
typedef int
T lock(T... args)
T lock_shared(T... args)
typedef float
T max(T... args)
T min(T... args)
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
bool isPositiveAndBelow(Type1 valueToTest, Type2 upperLimit) noexcept
int roundToInt(const FloatType value) noexcept
juce::String getName(LaunchQType t)
Retuns the name of a LaunchQType for display purposes.
juce::AudioChannelSet createChannelSet(const std::vector< ChannelIndex > &channels)
Creates an AudioChannelSet for a list of ChannelIndexes.
ClipOwner * findClipOwnerForID(const Edit &edit, EditItemID id)
Returns the ClipOwner with a given ID if it can be found in the Edit.
ClipSlot * findClipSlotForID(const Edit &edit, EditItemID id)
Returns the ClipSlot for the given ID.
juce::ReferenceCountedObjectPtr< WaveAudioClip > insertWaveClip(ClipOwner &parent, const juce::String &name, const juce::File &sourceFile, ClipPosition position, DeleteExistingClips deleteExistingClips)
Inserts a new WaveAudioClip into the ClipOwner's clip list.
Track * findTrackForID(const Edit &edit, EditItemID id)
Returns the Track with a given ID if contained in the Edit.
Project::Ptr getProjectForEdit(const Edit &e)
Tries to find the project that contains this edit (but may return nullptr!)
juce::Array< AudioTrack * > getTargetTracks(InputDeviceInstance &instance)
Returns the AudioTracks this instance is assigned to.
constexpr TimePosition toPosition(TimeDuration)
Converts a TimeDuration to a TimePosition.
RangeType< TimePosition > TimeRange
A RangeType based on real time (i.e.
T push_back(T... args)
T reserve(T... args)
T size(T... args)
sleep
Represents a duration in real-life time.
Represents a position in real-life time.
Represents the position of a clip on a timeline.
TimeDuration offset
The offset this ClipPosition has.
TimeRange time
The TimeRange this ClipPosition occupies.
ID for objects of type EditElement - e.g.
Base class for classes that want to listen to an InputDevice and get a callback for each block of inp...
Describes a WaveDevice from which the WaveOutputDevice and WaveInputDevice lists will be built.
TimeRange punchTimes
The Edit time range that the recorded clip should start/stop.
TimeRange recordingBlockRange
The Edit time range that blocks should be recorded for.
TimePosition unloopedStopTime
When the reecording is stopped, this should be the end time.
void closeFileWriter()
Blocks until there are no more pending samples to be written to this context.
TimeRange muteTimes
The Edit time range that the destination track should be muted for.
time
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.
T unlock(T... args)
T unlock_shared(T... args)
wait