11namespace tracktion {
inline namespace engine
35 if (bytesFree > 0 && bytesFree < 1024 * 1024 * 50)
41 void handleAsyncUpdate()
override
47 void timerCallback()
override
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%";
80 projDir = proj->getDirectoryForMedia (ProjectItem::Category::recorded).getFullPathName();
87 projDir = editFile.getParentDirectory().getFullPathName();
94 date << now.getDayOfMonth()
111 .
replace (editPattern, editName,
true)
112 .
replace (trackPattern, trackName,
true)
113 .
replace (datePattern, date,
true)
114 .
replace (timePattern, time,
true)
125 lengthInSeconds = e.
getPropertyStorage().getProperty (SettingID::retrospectiveRecord, 30);
128 void updateSizeIfNeeded (
int newNumChannels,
double newSampleRate)
132 if (newNumChannels != numChannels || newNumSamples != numSamples || newSampleRate != sampleRate)
134 numChannels = newNumChannels;
135 numSamples = newNumSamples;
136 sampleRate = newSampleRate;
138 fifo.setSize (numChannels,
std::max (1, numSamples));
145 if (numSamplesIn < numSamples)
147 lastStreamTime = streamTime;
149 fifo.ensureFreeSpace (numSamplesIn);
150 fifo.write (inputBuffer);
159 if (context.isPlaying())
162 pei.lastEditTime = context.globalStreamTimeToEditTime (streamTime);
166 pei.pausedTime = pei.pausedTime + TimeDuration::fromSamples (numSamplesIn, sampleRate);
170 bool wasRecentlyPlaying (
Edit& edit)
175 return (pei.lastEditTime >= 0s && pei.pausedTime < 20s);
178 void removeEditSync (
Edit& edit)
183 if (itr != editInfo.end())
184 editInfo.erase (itr);
187 double lengthInSeconds = 30.0;
190 double lastStreamTime = 0;
194 double sampleRate = 0;
218 getWaveInput().addInstance (
this);
229 auto& wi = getWaveInput();
231 if (wi.retrospectiveBuffer)
232 wi.retrospectiveBuffer->removeEditSync (
edit);
234 getWaveInput().removeInstance (
this);
249 return getRecordStopper().isQueued (targetID);
254 bool isTrackRecordingWithPunch =
false, muteTrackNow =
false,
255 muteTrackContentsWhilstRecording =
false, isActivelyRecording =
false;
260 if (recordingContexts.empty())
263 if (
auto recContext = getContextForID (t.itemID))
265 isTrackRecordingWithPunch = recContext->recordingWithPunch;
266 muteTrackNow = recContext->muteTargetNow;
267 muteTrackContentsWhilstRecording = recContext->muteTrackContentsWhilstRecording;
268 isActivelyRecording = recContext->hasHitThreshold;
272 if (muteTrackContentsWhilstRecording && isActivelyRecording)
275 if (isTrackRecordingWithPunch
277 && getWaveInput().mergeMode == 1)
288 static tl::expected<juce::File, juce::String> getDestinationRecordingFile (
Edit& ed,
EditItemID targetID,
302 recordedFile =
juce::File (expandPatterns (ed, filenameMask, track, take++)
303 + format.getFileExtensions()[0]);
304 }
while (recordedFile.
exists());
308 TRACKTION_LOG_ERROR (
"Record fail: can't create parent directory: " + recordedFile.
getFullPathName());
310 return TRANS(
"The directory\nXZZX\ndoesn't exist")
316 TRACKTION_LOG_ERROR (
"Record fail: directory is read-only: " + recordedFile.
getFullPathName());
318 return TRANS(
"The directory\nXZZX\n doesn't have write-access")
324 TRACKTION_LOG_ERROR (
"Record fail: can't overwrite file: " + recordedFile.
getFullPathName());
332 tl::expected<std::unique_ptr<RecordingContext>,
juce::String> prepareToRecordTarget (EditItemID targetID, TimeRange punchRange)
335 TRACKTION_ASSERT_MESSAGE_THREAD
339 if (getContextForID (targetID))
340 return tl::unexpected (
TRANS(
"Recording already in progress"));
343 if (proj->isReadOnly())
344 return tl::unexpected (
TRANS(
"The current project is read-only, so new clips can't be recorded into it!"));
346 auto format = getFormatToUse();
347 const auto res = getDestinationRecordingFile (
edit, targetID, *format, getWaveInput().filenameMask);
350 return tl::unexpected (res.error());
352 auto recordedFile = res.value();
357 AudioFileUtils::addBWAVStartToMetadata (metadata, (SampleCount) tracktion::toSamples (punchRange.getStart(), rc->sampleRate));
358 auto& wi = getWaveInput();
361 wi.isStereoPair() ? 2 : 1,
362 rc->sampleRate, wi.bitDepth, metadata, 0);
364 if (rc->fileWriter->isOpen())
367 rc->firstRecCallback =
true;
369 const auto adjustSeconds = wi.getAdjustmentSeconds();
370 rc->adjustSamples = (
int) tracktion::toSamples (adjustSeconds, rc->sampleRate);
372 if (!
owner.isTrackDevice())
375 rc->adjustDurationAtStart = TimeDuration::fromSamples (rc->adjustSamples, rc->sampleRate);
379 rc->recordingWithPunch =
true;
382 auto muteStart =
std::max (punchRange.getStart(), loopRange.getStart());
383 auto muteEnd = punchRange.getEnd();
385 if (punchRange.getStart() < loopRange.getEnd() - 0.5s)
387 punchRange = punchRange.withEnd (punchRange.getEnd() + 0.8s);
388 muteEnd = loopRange.getEnd();
391 rc->muteTimes = { muteStart, muteEnd };
394 rc->punchTimes = punchRange;
395 rc->recordingBlockRange = rc->punchTimes.withEnd (rc->punchTimes.getEnd() + adjustSeconds);
396 rc->hasHitThreshold = (wi.recordTriggerDb <= -50.0f);
402 rc->thumbnail->reset (wi.isStereoPair() ? 2 : 1, rc->sampleRate);
403 rc->thumbnail->punchInTime = punchRange.getStart();
411 TRACKTION_LOG_ERROR (
"Record fail: couldn't write to file: " + recordedFile.
getFullPathName());
413 return tl::unexpected (
TRANS(
"Couldn't record!") +
"\n\n"
419 return tl::unexpected (
TRANS(
"Unable to start recording"));
425 TRACKTION_ASSERT_MESSAGE_THREAD
432 if (dest->recordEnabled)
433 params.
targets.push_back (dest->targetID);
435 for (
auto target : params.
targets)
443 TRACKTION_ASSERT_MESSAGE_THREAD
444 bool hasAddedContexts =
false;
446 for (
auto& recContext : newContexts)
450 const auto targetID = midiContext->targetID;
457 hasAddedContexts =
true;
458 recContext.release();
459 context.transport.callRecordingAboutToStartListeners (*
this, targetID);
467 return std::move (erase_if_null (newContexts));
475 for (
auto& recContext : recordingContexts)
476 if (recContext->targetID == targetID)
477 return recContext->punchTimes.getStart();
485 return getContextForID (targetID) !=
nullptr;
491 return ! recordingContexts.empty();
496 TRACKTION_ASSERT_MESSAGE_THREAD
498 const auto numContextsRecording = [
this]
501 return recordingContexts.size();
505 contextsToStop.
reserve (numContextsRecording);
511 for (
auto& recContext : recordingContexts)
514 if (! contains_v (params.
targetsToStop, recContext->targetID))
518 contextsToStop.
push_back (std::move (recContext));
522 erase_if_null (recordingContexts);
523 assert ((recordingContexts.size() + contextsToStop.
size()) == numContextsRecording);
530 for (
auto& recContext : contextsToStop)
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),
539 context.transport.callRecordingFinishedListeners (*
this, targetID,
545 res.map ([&] (
auto c) { clips.
addArray (std::move (c)); })
546 .map_error ([&] (
auto err) { error = err; });
550 return tl::unexpected (error);
556 std::function<
void (tl::expected<Clip::Array, juce::String>)> callback)
override
558 TRACKTION_ASSERT_MESSAGE_THREAD
560 const auto getNumContextsRecording = [
this]
563 return recordingContexts.size();
572 for (
auto& recContext : recordingContexts)
580 for (
auto& recContext : recordingContexts)
582 if (! contains_v (params.
targetsToStop, recContext->targetID))
590 recContext->recordingBlockRange = recContext->recordingBlockRange.withEnd (recContext->unloopedStopTime
591 + recContext->adjustDurationAtStart);
596 recContext->stopCallback = callback;
597 recContext->stopParams = params;
598 recContext->stopParams.
targetsToStop = { recContext->targetID };
606 getRecordStopper().addTargetToStop (targetID);
613 if (
auto rc = getContextForID (targetID))
623 editPlaybackContext (epc), file (f)
629 double sampleRate = 44100.0;
637 bool firstRecCallback =
false, recordingWithPunch =
false;
638 int adjustSamples = 0;
640 const bool muteTrackContentsWhilstRecording = engine.getEngineBehaviour().muteTrackContentsWhilstRecording();
646 const detail::ScopedActiveRecordingDevice scopedActiveRecordingDevice { editPlaybackContext };
648 std::function<void (tl::expected<Clip::Array, juce::String>)> stopCallback;
649 StopRecordingParameters stopParams;
653 if (fileWriter !=
nullptr)
655 start, numSamples, thumbnail);
665 if (
auto localCopy = std::move (fileWriter))
672 bool isLooping, TimeRange loopRange,
673 bool discardRecordings)
675 TRACKTION_ASSERT_MESSAGE_THREAD
678 if (! rc || discardRecordings)
679 for (
auto c : consumers)
680 c->discardRecordings (rc ? rc->targetID :
EditItemID());
685 rc->closeFileWriter();
688 if (rc->punchTimes.getStart() >=
context.getUnloopedPosition())
690 rc->file.deleteFile();
694 if (! rc->file.existsAsFile() || rc->file.getSize() == 0)
697 const AudioFile recordedFile (
edit.
engine, rc->file);
700 if (discardRecordings || ! clipOwner)
707 const bool isClipSlot =
dynamic_cast<ClipSlot*
> (clipOwner) !=
nullptr;
709 const bool wasLoopRecording = isClipSlot ? false : isLooping;
711 return applyLastRecording (*rc, recordedFile, *clipOwner,
712 { rc->punchTimes.getStart(), unloopedEndTime },
713 wasLoopRecording, wasPunchRecording, loopRange.getEnd());
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)
724 afm.forceFileUpdate (recordedFile);
726 auto recordedFileLength = TimeDuration::fromSeconds (recordedFile.getLength());
728 if (recordedFileLength <= 1ms)
731 auto newClipLen =
std::min (recordedFileLength,
732 recordedRange.getLength());
734 if (newClipLen <= 0.01s)
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());
747 s =
TRANS(
"The device \"XZZX\" \nrecorded a zero-length file which won't be added to the edit")
748 .replace (
"XZZX", getWaveInput().
getName());
750 recordedFile.deleteFile();
752 return tl::unexpected (s);
757 if (
auto projectItem = proj->createNewItem (recordedFile.getFile(),
758 ProjectItem::waveItemType(),
759 recordedFile.getFile().getFileNameWithoutExtension(),
760 {}, ProjectItem::Category::recorded,
true))
762 return applyLastRecording (rc, projectItem, recordedFile, destClipOwner,
763 recordedFileLength, newClipLen, isLooping, isPunching, loopEnd);
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!"));
771 return applyLastRecording (rc,
nullptr, recordedFile, destClipOwner,
772 recordedFileLength, newClipLen, isLooping, isPunching, loopEnd);
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)
784 jassert (projectItem ==
nullptr || projectItem->getID().isValid());
790 filesCreated.
add (recordedFile.getFile());
793 if (! splitRecordingIntoMultipleTakes (
context, recordedFile, projectItem, recordedFileLength, extraTakes, filesCreated))
794 return tl::unexpected (
TRANS(
"Couldn't create audio files for multiple takes"));
796 auto endPos = rc.punchTimes.getStart() + newClipLen;
798 if (isPunching ||
context.transport.looping)
799 endPos =
juce::jlimit (rc.punchTimes.getStart() + 0.5s, loopEnd, endPos);
802 bool replaceOldClips = getWaveInput().mergeMode == 1;
805 if (replaceOldClips && isPunching)
807 if (projectItem !=
nullptr)
808 newClip =
insertWaveClip (destClipOwner, getNameForNewClip (destClipOwner), projectItem->getID(),
809 { { loopRange.getStart(), endPos }, {} }, DeleteExistingClips::yes);
811 newClip =
insertWaveClip (destClipOwner, getNameForNewClip (destClipOwner), recordedFile.getFile(),
812 { { loopRange.getStart(), endPos }, {} }, DeleteExistingClips::yes);
814 if (newClip !=
nullptr)
815 newClip->setStart (rc.punchTimes.getStart(),
false,
false);
819 if (projectItem !=
nullptr)
820 newClip =
insertWaveClip (destClipOwner, getNameForNewClip (destClipOwner), projectItem->getID(),
821 { { rc.punchTimes.getStart(), endPos }, {} },
822 replaceOldClips ? DeleteExistingClips::yes : DeleteExistingClips::no);
824 newClip =
insertWaveClip (destClipOwner, getNameForNewClip (destClipOwner), recordedFile.getFile(),
825 { { rc.punchTimes.getStart(), endPos }, {} },
826 replaceOldClips ? DeleteExistingClips::yes : DeleteExistingClips::no);
829 if (newClip ==
nullptr)
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;
836 TRACKTION_LOG_ERROR (s);
838 engine.getUIBehaviour().showWarningMessage (
TRANS(
"Couldn't add the new recording to the project!"));
846 if (newClip->getPosition().getStart() < loopRange.getStart())
847 newClip->setStart (loopRange.getStart(),
true,
false);
849 if (newClip->getPosition().getEnd() > loopRange.getEnd()
850 && newClip->getPosition().getStart() < loopRange.getEnd() - 0.1s)
851 newClip->setEnd (loopRange.getEnd(),
true);
854 for (
auto& f : filesCreated)
856 AudioFileUtils::applyBWAVStartTime (f, (SampleCount) tracktion::toSamples (newClip->getPosition().getStartOfSource(), rc.sampleRate));
857 afm.forceFileUpdate (AudioFile (edit.engine, f));
860 if (
auto wc =
dynamic_cast<WaveAudioClip*
> (newClip.get()))
862 if (extraTakes.
size() > 0)
864 for (
auto& take : extraTakes)
865 wc->addTake (take->getID());
867 else if (filesCreated.
size() > 1)
869 for (
auto& f : filesCreated)
875 clips.add (newClip.get());
879 static bool splitRecordingIntoMultipleTakes (EditPlaybackContext& epc,
880 const AudioFile& recordedFile,
881 const ProjectItem::Ptr& projectItem,
882 TimeDuration recordedFileLength,
886 auto& edit = epc.edit;
887 auto& afm = edit.engine.getAudioFileManager();
890 if (projectItem !=
nullptr)
891 extraTakes.
add (projectItem);
894 auto loopLength = epc.transport.getLoopRange().getLength();
898 const auto takeStart =
toPosition (loopLength * take);
900 recordedFileLength));
902 if ((takeEnd - takeStart) < 0.1s)
905 const auto takeRange =
TimeRange (takeStart, takeEnd);
906 auto takeFile = recordedFile.getFile()
907 .getSiblingFile (recordedFile.getFile().getFileNameWithoutExtension()
909 .withFileExtension (recordedFile.getFile().getFileExtension())
910 .getNonexistentSibling (
false);
912 afm.releaseFile (recordedFile);
914 if (AudioFileUtils::copySectionToNewFile (edit.engine, recordedFile.getFile(), takeFile, takeRange) < 0)
917 if (projectItem !=
nullptr)
919 if (
auto takeObject = projectItem->getProject()->createNewItem (takeFile, ProjectItem::waveItemType(),
920 recordedFile.getFile().getFileNameWithoutExtension()
923 ProjectItem::Category::recorded,
true))
925 extraTakes.
add (takeObject);
926 filesCreated.
add (takeFile);
931 filesCreated.
add (takeFile);
938 auto tempFile = recordedFile.getFile().getNonexistentSibling (
false);
940 if (AudioFileUtils::copySectionToNewFile (edit.engine, recordedFile.getFile(), tempFile,
TimeRange (0.0s, loopLength)) > 0)
942 afm.releaseFile (recordedFile);
943 tempFile.moveFileTo (recordedFile.getFile());
944 filesCreated.
add (recordedFile.getFile());
945 afm.forceFileUpdate (recordedFile);
947 if (projectItem !=
nullptr)
948 projectItem->verifyLength();
962 if (armedOnly && ! isRecordingActive (dstTrack->itemID))
965 auto& wi = getWaveInput();
967 auto recordBuffer = wi.getRetrospectiveRecordBuffer();
969 if (recordBuffer ==
nullptr)
972 auto format = getFormatToUse();
973 const auto res = getDestinationRecordingFile (edit, dstTrack->itemID, *format, getWaveInput().filenameMask);
978 const auto recordedFile = res.value();
983 recordBuffer->numChannels,
984 recordBuffer->sampleRate,
985 wi.bitDepth, metadata, 0);
992 while ((numReady = recordBuffer->fifo.getNumReady()) > 0)
996 if (! recordBuffer->fifo.read (scratchBuffer, 0, toRead)
1005 if (proj ==
nullptr)
1011 auto projectItem = proj->createNewItem (recordedFile, ProjectItem::waveItemType(),
1012 recordedFile.getFileNameWithoutExtension(),
1013 {}, ProjectItem::Category::recorded,
true);
1015 if (projectItem ==
nullptr)
1018 jassert (projectItem->getID().isValid());
1020 auto clipName = getNameForNewClip (*dstTrack);
1022 const auto recordedLength = TimeDuration::fromSeconds (
AudioFile (dstTrack->edit.engine, recordedFile).getLength());
1024 if (context.isPlaying() || recordBuffer->wasRecentlyPlaying (edit))
1026 const auto blockSizeSeconds = edit.engine.getDeviceManager().getBlockLength();
1027 auto adjust = -wi.getAdjustmentSeconds() + blockSizeSeconds;
1029 adjust = adjust - TimeDuration::fromSamples (context.getLatencySamples(), edit.engine.getDeviceManager().getSampleRate());
1033 if (context.getNodePlayHead() !=
nullptr)
1034 adjust = adjust + blockSizeSeconds;
1036 if (context.isPlaying())
1038 start = context.globalStreamTimeToEditTime (recordBuffer->lastStreamTime) - recordedLength + adjust;
1042 auto& pei = recordBuffer->editInfo[edit.getProjectItemID()];
1043 start = pei.lastEditTime + pei.pausedTime - recordedLength + adjust;
1044 pei.lastEditTime = -1s;
1049 auto position = context.getPosition();
1052 start = position - recordedLength;
1057 ClipPosition clipPos = { { start, start + recordedLength }, {} };
1061 clipPos.
offset = toDuration (-start);
1062 clipPos.
time = clipPos.
time.withStart (0s);
1065 auto newClip = dstTrack->insertWaveClip (clipName, projectItem->getID(), clipPos,
false);
1067 if (newClip ==
nullptr)
1072 AudioFileUtils::applyBWAVStartTime (recordedFile, (SampleCount) tracktion::toSamples (newClip->getPosition().getStartOfSource(), recordBuffer->sampleRate));
1074 edit.engine.getAudioFileManager().forceFileUpdate (
AudioFile (dstTrack->edit.engine, recordedFile));
1076 if (dstTrack->playSlotClips.get())
1078 if (
auto slot = getFreeSlot (*dstTrack))
1080 newClip->setUsesProxy (
false);
1081 newClip->setStart (0_tp,
false,
true);
1083 if (! newClip->isLooping())
1084 newClip->setLoopRangeBeats ({ 0_bp, newClip->getLengthInBeats() });
1086 newClip->removeFromParent();
1087 slot->setClip (newClip.get());
1091 clips.
add (newClip.get());
1097 void copyIncomingDataIntoBuffer (
const float*
const* allChannels,
int numChannels,
int numSamples)
1099 auto& wi = getWaveInput();
1100 auto& channelSet = wi.getChannelSet();
1101 inputBuffer.setSize (channelSet.size(), numSamples);
1103 if (numChannels == 0)
1105 inputBuffer.clear();
1109 for (
const auto& ci : wi.getChannels())
1113 auto inputIndex = channelSet.getChannelIndexForType (ci.channel);
1114 juce::FloatVectorOperations::copy (inputBuffer.getWritePointer (inputIndex),
1115 allChannels[ci.indexInDevice], numSamples);
1124 void acceptInputBuffer (
const float*
const* allChannels,
int numChannels,
int numSamples,
1125 double streamTime, LevelMeasurer* measurerToUpdate,
1126 RetrospectiveRecordBuffer* retrospectiveBuffer,
bool addToRetrospective)
1129 copyIncomingDataIntoBuffer (allChannels, numChannels, numSamples);
1131 auto inputGainDb = getWaveInput().inputGainDb;
1133 if (inputGainDb > 0.01f || inputGainDb < -0.01f)
1134 inputBuffer.applyGain (0, numSamples, dbToGain (inputGainDb));
1136 if (measurerToUpdate !=
nullptr)
1137 measurerToUpdate->processBuffer (inputBuffer, 0, numSamples);
1139 if (retrospectiveBuffer !=
nullptr)
1141 if (addToRetrospective)
1143 retrospectiveBuffer->updateSizeIfNeeded (inputBuffer.getNumChannels(),
1144 edit.engine.getDeviceManager().getSampleRate());
1145 retrospectiveBuffer->processBuffer (streamTime, inputBuffer, numSamples);
1148 retrospectiveBuffer->syncToEdit (edit, context, streamTime, numSamples);
1154 for (
auto n : consumers)
1155 n->acceptInputBuffer (choc::buffer::createChannelArrayView (inputBuffer.getArrayOfWritePointers(),
1156 (choc::buffer::ChannelCount) inputBuffer.getNumChannels(),
1157 (choc::buffer::FrameCount) numSamples));
1161 const auto blockStart = context.globalStreamTimeToEditTimeUnlooped (streamTime);
1164 for (
auto& recordingContext : recordingContexts)
1166 const TimeRange blockRange (blockStart, TimeDuration::fromSamples (numSamples, recordingContext->sampleRate));
1168 recordingContext->muteTargetNow = recordingContext->muteTimes.overlaps (blockRange);
1170 if (recordingContext->recordingBlockRange.overlaps (blockRange))
1172 if (! recordingContext->hasHitThreshold)
1174 auto bufferLevelDb = gainToDb (inputBuffer.getMagnitude (0, numSamples));
1175 recordingContext->hasHitThreshold = bufferLevelDb > getWaveInput().recordTriggerDb;
1177 if (! recordingContext->hasHitThreshold)
1180 recordingContext->punchTimes = recordingContext->punchTimes.withStart (blockRange.getStart());
1182 if (recordingContext->thumbnail !=
nullptr)
1183 recordingContext->thumbnail->punchInTime = blockRange.getStart();
1186 if (recordingContext->firstRecCallback)
1188 recordingContext->firstRecCallback =
false;
1190 auto timeDiff = blockRange.getStart() - recordingContext->recordingBlockRange.getStart();
1191 recordingContext->adjustSamples -= (
int) tracktion::toSamples (timeDiff, recordingContext->sampleRate);
1194 const int adjustSamples = recordingContext->adjustSamples;
1196 if (adjustSamples < 0)
1199 AudioScratchBuffer silence (inputBuffer.getNumChannels(), -adjustSamples);
1200 silence.buffer.clear();
1202 recordingContext->addBlockToRecord (silence.buffer, 0, silence.buffer.getNumSamples());
1203 recordingContext->adjustSamples = 0;
1205 else if (adjustSamples > 0)
1208 if (adjustSamples >= numSamples)
1210 recordingContext->adjustSamples -= numSamples;
1214 recordingContext->addBlockToRecord (inputBuffer, adjustSamples, numSamples - adjustSamples);
1215 recordingContext->adjustSamples = 0;
1220 recordingContext->addBlockToRecord (inputBuffer, 0, numSamples);
1234 WaveRecordingContext* getContextForID (EditItemID targetID)
const
1238 for (
auto& recContext : recordingContexts)
1239 if (recContext->targetID == targetID)
1240 return recContext.get();
1245 RecordStopper& getRecordStopper()
1247 TRACKTION_ASSERT_MESSAGE_THREAD
1248 if (! recordStopper)
1251 const auto unloopedTimeNow = context.getUnloopedPosition();
1255 if (
auto recContext = getContextForID (targetID))
1257 if (unloopedTimeNow >= recContext->recordingBlockRange.getEnd())
1259 auto stopParams = recContext->stopParams;
1263 auto res = stopRecording (stopParams);
1266 return RecordStopper::HasFinished::yes;
1270 return RecordStopper::HasFinished::no;
1273 return *recordStopper;
1276 WaveInputDevice& getWaveInput() const noexcept {
return static_cast<WaveInputDevice&
> (owner); }
1300 :
InputDevice (e, devType, desc.name,
"wavein_" +
juce::String::toHexString (desc.name.hashCode())),
1301 deviceChannels (desc.channels),
1305 enabled = desc.enabled;
1309WaveInputDevice::~WaveInputDevice()
1311 notifyListenersOfDeletion();
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"));
1329 auto& afm = engine.getAudioFileFormatManager();
1330 s.
add (afm.getWavFormat()->getFormatName());
1331 s.
add (afm.getAiffFormat()->getFormatName());
1332 s.
add (afm.getFlacFormat()->getFormatName());
1339 if (! isTrackDevice() && retrospectiveBuffer ==
nullptr)
1345void WaveInputDevice::resetToDefault()
1347 juce::String propName = isTrackDevice() ?
"TRACKTION_TRACK_DEVICE" : getName();
1348 engine.getPropertyStorage().removePropertyItem (SettingID::wavein, propName);
1352void WaveInputDevice::setEnabled (
bool b)
1359 if (! isTrackDevice())
1361 engine.getDeviceManager().setWaveInChannelsEnabled (deviceChannels, b);
1377void WaveInputDevice::closeDevice()
1382void WaveInputDevice::loadProps()
1384 filenameMask = getDefaultMask();
1387 outputFormat = engine.getAudioFileFormatManager().getDefaultFormat()->getFormatName();
1389 recordTriggerDb = -50.0f;
1394 juce::String propName = isTrackDevice() ?
"TRACKTION_TRACK_DEVICE" : getName();
1396 if (
auto n = engine.getPropertyStorage().getXmlPropertyItem (SettingID::wavein, propName))
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);
1402 outputFormat = n->getStringAttribute (
"format", outputFormat);
1403 bitDepth = n->getIntAttribute (
"bits", bitDepth);
1405 if (! getRecordFormatNames().contains (outputFormat))
1406 outputFormat = engine.getAudioFileFormatManager().getDefaultFormat()->getFormatName();
1408 recordTriggerDb = (
float) n->getDoubleAttribute (
"triggerDb", recordTriggerDb);
1409 mergeMode = n->getIntAttribute (
"mode", mergeMode);
1410 recordAdjustMs = n->getDoubleAttribute (
"adjustMs", recordAdjustMs);
1412 if (recordAdjustMs != 0)
1413 TRACKTION_LOG (
"Manual record adjustment: " +
juce::String (recordAdjustMs) +
"ms");
1419void WaveInputDevice::saveProps()
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);
1432 juce::String propName = isTrackDevice() ?
"TRACKTION_TRACK_DEVICE" : getName();
1434 engine.getPropertyStorage().setXmlPropertyItem (SettingID::wavein, propName, n);
1440 if (getDeviceType() == trackWaveDevice)
1441 return getAlias() +
" (" + getType() +
")";
1446bool WaveInputDevice::isStereoPair()
const
1448 return deviceChannels.size() == 2;
1451void WaveInputDevice::setStereoPair (
bool stereo)
1453 if (isTrackDevice())
1459 auto& dm = engine.getDeviceManager();
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);
1467void WaveInputDevice::setRecordAdjustmentMs (
double ms)
1474void WaveInputDevice::setInputGainDb (
float newGain)
1476 if (newGain != inputGainDb)
1478 inputGainDb = newGain;
1484void WaveInputDevice::setRecordTriggerDb (
float newDB)
1486 if (recordTriggerDb != newDB)
1488 recordTriggerDb = newDB;
1498 << trackPattern<<
'_' <<
TRANS(
"Take") <<
'_' << takePattern;
1503void WaveInputDevice::setFilenameMask (
const juce::String& newMask)
1505 if (filenameMask != newMask)
1507 filenameMask = newMask.
isNotEmpty() ? newMask
1514void WaveInputDevice::setFilenameMaskToDefault()
1516 if (getDefaultMask() != filenameMask)
1517 setFilenameMask ({});
1520void WaveInputDevice::setBitDepth (
int newDepth)
1522 if (bitDepth != newDepth)
1524 bitDepth = newDepth;
1530void WaveInputDevice::checkBitDepth()
1532 if (! getAvailableBitDepths().contains (bitDepth))
1533 bitDepth = getAvailableBitDepths().
getLast();
1543 return engine.getAudioFileFormatManager().getNamedFormat (outputFormat);
1546void WaveInputDevice::setOutputFormat (
const juce::String& newFormat)
1548 if (outputFormat != newFormat)
1550 outputFormat = newFormat;
1559 return getMergeModes() [mergeMode];
1562void WaveInputDevice::setMergeMode (
const juce::String& newMode)
1564 int newIndex = getMergeModes().
indexOf (newMode);
1566 if (mergeMode != newIndex)
1568 mergeMode = newIndex;
1574TimeDuration WaveInputDevice::getAdjustmentSeconds()
1576 auto& dm = engine.getDeviceManager();
1577 const double autoAdjustMs = isTrackDevice() ? 0.0 : dm.getRecordAdjustmentMs();
1579 return TimeDuration::fromSeconds (
juce::jlimit (-3.0, 3.0, 0.001 * (recordAdjustMs + autoAdjustMs)));
1583void WaveInputDevice::addInstance (WaveInputDeviceInstance* i)
1586 instances.addIfNotAlreadyThere (i);
1589void WaveInputDevice::removeInstance (WaveInputDeviceInstance* i)
1592 instances.removeAllInstancesOf (i);
1596void WaveInputDevice::consumeNextAudioBlock (
const float*
const* allChannels,
int numChannels,
int numSamples,
double streamTime)
1600 bool isFirst =
true;
1604 for (
auto i : instances)
1606 i->acceptInputBuffer (allChannels, numChannels, numSamples, streamTime,
1607 isFirst ? &levelMeasurer : nullptr,
1608 (! retrospectiveRecordLock) ? retrospectiveBuffer.get() : nullptr, isFirst);
1615void WaveInputDevice::updateRetrospectiveBufferLength (
double length)
1617 if (retrospectiveBuffer !=
nullptr)
1618 retrospectiveBuffer->lengthInSeconds = length;
1626 for (
int i = 32; --i >= 0;)
1640 buffer.
copyFrom (i, 0, newBuffer, i, start, numSamples);
1660 QueuedBlock* findFreeBlock()
1664 if (firstFree ==
nullptr)
1665 return new QueuedBlock();
1667 auto* b = firstFree;
1668 firstFree = b->next;
1673 void addToFreeQueue (QueuedBlock* b)
noexcept
1677 b->writer =
nullptr;
1678 b->next = firstFree;
1682 void addToPendingQueue (QueuedBlock* b)
noexcept
1688 if (lastPending !=
nullptr)
1689 lastPending->next = b;
1697 QueuedBlock* removeFirstPending() noexcept
1701 if (
auto b = firstPending)
1703 firstPending = b->next;
1705 if (firstPending ==
nullptr)
1706 lastPending =
nullptr;
1715 void moveAnyPendingBlocksToFree() noexcept
1717 while (
auto b = removeFirstPending())
1724 bool isWriterInQueue (AudioFileWriter& writer)
const
1728 for (
auto b = firstPending; b !=
nullptr; b = b->next)
1729 if (b->writer == &writer)
1735 void deleteFreeQueue() noexcept
1738 firstFree =
nullptr;
1740 while (b !=
nullptr)
1749WaveInputRecordingThread::WaveInputRecordingThread (
Engine& e)
1750 : Thread (
"WaveInputRecordingThread"),
1752 queue (new BlockQueue())
1756WaveInputRecordingThread::~WaveInputRecordingThread()
1759 queue->deleteFreeQueue();
1763void WaveInputRecordingThread::addUser()
1765 if (activeUsers++ == 0)
1769void WaveInputRecordingThread::removeUser()
1771 if (--activeUsers == 0)
1777 int start,
int numSamples,
const RecordingThumbnailManager::Thumbnail::Ptr& thumbnail)
1781 auto block = queue->findFreeBlock();
1782 block->load (writer, buffer, start, numSamples, thumbnail);
1783 queue->addToPendingQueue (block);
1788void WaveInputRecordingThread::waitForWriterToFinish (AudioFileWriter& writer)
1794void WaveInputRecordingThread::run()
1801 if (queue->numPending > 500 && ! hasWarned)
1804 TRACKTION_LOG_ERROR (
"Audio recording can't keep up!");
1807 if (
auto block = queue->removeFirstPending())
1809 if (! block->writer.load()->appendBuffer (block->buffer, block->buffer.getNumSamples()))
1814 TRACKTION_LOG_ERROR (
"Audio recording failed to write to disk!");
1819 if (block->thumbnail !=
nullptr)
1821 block->thumbnail->addBlock (block->buffer, 0, block->buffer.getNumSamples());
1822 block->thumbnail =
nullptr;
1825 queue->addToFreeQueue (block);
1837void WaveInputRecordingThread::timerCallback()
1843void WaveInputRecordingThread::prepareToStart()
1851void WaveInputRecordingThread::flushAndStop()
1856 queue->moveAnyPendingBlocksToFree();
1857 hasSentStop =
false;
int removeAllInstancesOf(ParameterType valueToRemove)
int size() const noexcept
void add(const ElementType &newElement)
bool addIfNotAlreadyThere(ParameterType newElement)
ElementType getLast() const noexcept
void triggerAsyncUpdate()
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
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)
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)
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 threadShouldExit() const
bool stopThread(int timeOutMilliseconds)
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.
Base class for RecordingContexts.
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.
T emplace_back(T... args)
#define JUCE_CATCH_EXCEPTION
#define TRANS(stringLiteral)
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.
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.
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.
#define CRASH_TRACER
This macro adds the current location to a stack which gets logged if a crash happens.
T unlock_shared(T... args)