mirror of
https://github.com/cookiengineer/audacity
synced 2025-06-16 16:10:06 +02:00
Punch and roll recording
menu item, tentative shortcut Shift+D ("do-over") Add to recording preferences for pre-roll and crossfade Define EXPERIMENTAL flag Recording options allow crossfade data for start of recording Stop playback of pre-rolled tracks at the right time Allow for preRoll in start-stream options
This commit is contained in:
commit
9e2937feed
127
src/AudioIO.cpp
127
src/AudioIO.cpp
@ -1929,22 +1929,27 @@ int AudioIO::StartStream(const TransportTracks &tracks,
|
||||
mTimeTrack = options.timeTrack;
|
||||
}
|
||||
|
||||
mT0 = t0;
|
||||
// Clamp pre-roll so we don't play before time 0
|
||||
const auto preRoll = std::max(0.0, std::min(t0, options.preRoll));
|
||||
mT0 = t0 - preRoll;
|
||||
mT1 = t1;
|
||||
mRecordingSchedule = {};
|
||||
mRecordingSchedule.mPreRoll = preRoll;
|
||||
mRecordingSchedule.mLatencyCorrection =
|
||||
(gPrefs->ReadDouble(wxT("/AudioIO/LatencyCorrection"),
|
||||
DEFAULT_LATENCY_CORRECTION))
|
||||
/ 1000.0;
|
||||
mRecordingSchedule.mDuration = mT1 - mT0;
|
||||
mRecordingSchedule.mDuration = t1 - t0;
|
||||
if (tracks.captureTracks.size() > 0)
|
||||
// adjust mT1 so that we don't give paComplete too soon to fill up the
|
||||
// desired length of recording
|
||||
mT1 -= mRecordingSchedule.mLatencyCorrection;
|
||||
if (options.pCrossfadeData)
|
||||
mRecordingSchedule.mCrossfadeData.swap( *options.pCrossfadeData );
|
||||
|
||||
mListener = options.listener;
|
||||
mRate = sampleRate;
|
||||
mTime = t0;
|
||||
mTime = mT0;
|
||||
mSeek = 0;
|
||||
mLastRecordingOffset = 0;
|
||||
mCaptureTracks = tracks.captureTracks;
|
||||
@ -2150,18 +2155,26 @@ int AudioIO::StartStream(const TransportTracks &tracks,
|
||||
mPlaybackBuffers[i] = std::make_unique<RingBuffer>(floatSample, playbackBufferSize);
|
||||
|
||||
// MB: use normal time for the end time, not warped time!
|
||||
WaveTrackConstArray tracks;
|
||||
tracks.push_back(mPlaybackTracks[i]);
|
||||
WaveTrackConstArray mixTracks;
|
||||
mixTracks.push_back(mPlaybackTracks[i]);
|
||||
|
||||
double endTime;
|
||||
if (make_iterator_range(tracks.prerollTracks).contains(mPlaybackTracks[i]))
|
||||
// Stop playing this track after pre-roll
|
||||
endTime = t0;
|
||||
else
|
||||
// Pass t1 -- not mT1 as may have been adjusted for latency
|
||||
// -- so that overdub recording stops playing back samples
|
||||
// at the right time, though transport may continue to record
|
||||
endTime = t1;
|
||||
|
||||
mPlaybackMixers[i] = std::make_unique<Mixer>
|
||||
(tracks,
|
||||
(mixTracks,
|
||||
// Don't throw for read errors, just play silence:
|
||||
false,
|
||||
warpOptions,
|
||||
mT0,
|
||||
// Pass t1 -- not mT1 as may have been adjusted for latency
|
||||
// -- so that overdub recording stops playing back samples
|
||||
// at the right time, though transport may continue to record
|
||||
t1,
|
||||
endTime,
|
||||
1,
|
||||
playbackMixBufferSize, false,
|
||||
mRate, floatSample, false);
|
||||
@ -2548,7 +2561,10 @@ void AudioIO::SetMeters()
|
||||
|
||||
void AudioIO::StopStream()
|
||||
{
|
||||
auto cleanup = finally ( [this] { ClearRecordingException(); } );
|
||||
auto cleanup = finally ( [this] {
|
||||
ClearRecordingException();
|
||||
mRecordingSchedule = {}; // free arrays
|
||||
} );
|
||||
|
||||
if( mPortStreamV19 == NULL
|
||||
#ifdef EXPERIMENTAL_MIDI_OUT
|
||||
@ -3997,12 +4013,12 @@ void AudioIO::FillBuffers()
|
||||
size_t discarded = 0;
|
||||
|
||||
if (!mRecordingSchedule.mLatencyCorrected) {
|
||||
if(mRecordingSchedule.mLatencyCorrection >= 0) {
|
||||
const auto correction = mRecordingSchedule.TotalCorrection();
|
||||
if (correction >= 0) {
|
||||
// Rightward shift
|
||||
// Once only (per track per recording), insert some initial
|
||||
// silence.
|
||||
size_t size = floor(
|
||||
mRecordingSchedule.mLatencyCorrection * mRate * mFactor);
|
||||
size_t size = floor( correction * mRate * mFactor);
|
||||
SampleBuffer temp(size, trackFormat);
|
||||
ClearSamples(temp.ptr(), trackFormat, 0, size);
|
||||
mCaptureTracks[i]->Append(temp.ptr(), trackFormat,
|
||||
@ -4021,28 +4037,50 @@ void AudioIO::FillBuffers()
|
||||
}
|
||||
}
|
||||
|
||||
const float *pCrossfadeSrc = nullptr;
|
||||
size_t crossfadeStart = 0, totalCrossfadeLength = 0;
|
||||
if (i < mRecordingSchedule.mCrossfadeData.size())
|
||||
{
|
||||
// Do crossfading
|
||||
// The supplied crossfade samples are at the same rate as the track
|
||||
const auto &data = mRecordingSchedule.mCrossfadeData[i];
|
||||
totalCrossfadeLength = data.size();
|
||||
if (totalCrossfadeLength) {
|
||||
crossfadeStart =
|
||||
floor(mRecordingSchedule.Consumed() * mCaptureTracks[i]->GetRate());
|
||||
if (crossfadeStart < totalCrossfadeLength)
|
||||
pCrossfadeSrc = data.data() + crossfadeStart;
|
||||
}
|
||||
}
|
||||
|
||||
size_t toGet = avail - discarded;
|
||||
SampleBuffer temp;
|
||||
size_t size;
|
||||
sampleFormat format;
|
||||
if( mFactor == 1.0 )
|
||||
{
|
||||
SampleBuffer temp(toGet, trackFormat);
|
||||
// Take captured samples directly
|
||||
size = toGet;
|
||||
if (pCrossfadeSrc)
|
||||
// Change to float for crossfade calculation
|
||||
format = floatSample;
|
||||
else
|
||||
format = trackFormat;
|
||||
temp.Allocate(size, format);
|
||||
const auto got =
|
||||
mCaptureBuffers[i]->Get(temp.ptr(), trackFormat, toGet);
|
||||
mCaptureBuffers[i]->Get(temp.ptr(), format, toGet);
|
||||
// wxASSERT(got == toGet);
|
||||
// but we can't assert in this thread
|
||||
wxUnusedVar(got);
|
||||
// see comment in second handler about guarantee
|
||||
size_t size = toGet;
|
||||
if (double(size) > remainingSamples)
|
||||
size = floor(remainingSamples);
|
||||
mCaptureTracks[i]-> Append(temp.ptr(), trackFormat,
|
||||
size, 1,
|
||||
&appendLog);
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t size = lrint(toGet * mFactor);
|
||||
size = lrint(toGet * mFactor);
|
||||
format = floatSample;
|
||||
SampleBuffer temp1(toGet, floatSample);
|
||||
SampleBuffer temp2(size, floatSample);
|
||||
temp.Allocate(size, format);
|
||||
const auto got =
|
||||
mCaptureBuffers[i]->Get(temp1.ptr(), floatSample, toGet);
|
||||
// wxASSERT(got == toGet);
|
||||
@ -4057,15 +4095,34 @@ void AudioIO::FillBuffers()
|
||||
toGet = floor(remainingSamples);
|
||||
const auto results =
|
||||
mResample[i]->Process(mFactor, (float *)temp1.ptr(), toGet,
|
||||
!IsStreamActive(), (float *)temp2.ptr(), size);
|
||||
!IsStreamActive(), (float *)temp.ptr(), size);
|
||||
size = results.second;
|
||||
// see comment in second handler about guarantee
|
||||
mCaptureTracks[i]-> Append(temp2.ptr(), floatSample,
|
||||
size, 1,
|
||||
&appendLog);
|
||||
}
|
||||
}
|
||||
|
||||
if (pCrossfadeSrc) {
|
||||
wxASSERT(format == floatSample);
|
||||
size_t crossfadeLength = std::min(size, totalCrossfadeLength - crossfadeStart);
|
||||
if (crossfadeLength) {
|
||||
auto ratio = double(crossfadeStart) / totalCrossfadeLength;
|
||||
auto ratioStep = 1.0 / totalCrossfadeLength;
|
||||
auto pCrossfadeDst = (float*)temp.ptr();
|
||||
|
||||
// Crossfade loop here
|
||||
for (size_t ii = 0; ii < crossfadeLength; ++ii) {
|
||||
*pCrossfadeDst = ratio * *pCrossfadeDst + (1.0 - ratio) * *pCrossfadeSrc;
|
||||
++pCrossfadeSrc, ++pCrossfadeDst;
|
||||
ratio += ratioStep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now append
|
||||
// see comment in second handler about guarantee
|
||||
mCaptureTracks[i]->Append(temp.ptr(), format,
|
||||
size, 1,
|
||||
&appendLog);
|
||||
|
||||
if (!appendLog.IsEmpty())
|
||||
{
|
||||
blockFileLog.StartTag(wxT("recordingrecovery"));
|
||||
@ -5403,15 +5460,15 @@ int audacityAudioCallback(const void *inputBuffer, void *outputBuffer,
|
||||
|
||||
double AudioIO::RecordingSchedule::ToConsume() const
|
||||
{
|
||||
return mDuration - mLatencyCorrection - mPosition;
|
||||
return mDuration - Consumed();
|
||||
}
|
||||
|
||||
double AudioIO::RecordingSchedule::Consumed() const
|
||||
{
|
||||
return std::max( 0.0, mPosition + TotalCorrection() );
|
||||
}
|
||||
|
||||
double AudioIO::RecordingSchedule::ToDiscard() const
|
||||
{
|
||||
if (mLatencyCorrection >= 0)
|
||||
return 0.0;
|
||||
else if (mPosition >= -mLatencyCorrection)
|
||||
return 0.0;
|
||||
else
|
||||
return (-mLatencyCorrection) - mPosition;
|
||||
return std::max(0.0, -( mPosition + TotalCorrection() ) );
|
||||
}
|
||||
|
@ -85,6 +85,12 @@ class AudioIOListener;
|
||||
#define DEFAULT_LATENCY_DURATION 100.0
|
||||
#define DEFAULT_LATENCY_CORRECTION -130.0
|
||||
|
||||
#define AUDIO_PRE_ROLL_KEY (wxT("/AudioIO/PreRoll"))
|
||||
#define DEFAULT_PRE_ROLL_SECONDS 5.0
|
||||
|
||||
#define AUDIO_ROLL_CROSSFADE_KEY (wxT("/AudioIO/Crossfade"))
|
||||
#define DEFAULT_ROLL_CROSSFADE_MS 10.0
|
||||
|
||||
#ifdef EXPERIMENTAL_AUTOMATED_INPUT_LEVEL_ADJUSTMENT
|
||||
#define AILA_DEF_TARGET_PEAK 92
|
||||
#define AILA_DEF_DELTA_PEAK 2
|
||||
@ -111,6 +117,8 @@ wxDECLARE_EXPORTED_EVENT(AUDACITY_DLL_API,
|
||||
|
||||
struct ScrubbingOptions;
|
||||
|
||||
using PRCrossfadeData = std::vector< std::vector < float > >;
|
||||
|
||||
// To avoid growing the argument list of StartStream, add fields here
|
||||
struct AudioIOStartStreamOptions
|
||||
{
|
||||
@ -123,6 +131,7 @@ struct AudioIOStartStreamOptions
|
||||
, cutPreviewGapStart(0.0)
|
||||
, cutPreviewGapLen(0.0)
|
||||
, pStartTime(NULL)
|
||||
, preRoll(0.0)
|
||||
{}
|
||||
|
||||
TimeTrack *timeTrack;
|
||||
@ -132,6 +141,7 @@ struct AudioIOStartStreamOptions
|
||||
double cutPreviewGapStart;
|
||||
double cutPreviewGapLen;
|
||||
double * pStartTime;
|
||||
double preRoll;
|
||||
|
||||
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
|
||||
// Non-null value indicates that scrubbing will happen
|
||||
@ -139,6 +149,9 @@ struct AudioIOStartStreamOptions
|
||||
// are all incompatible with scrubbing):
|
||||
ScrubbingOptions *pScrubbingOptions {};
|
||||
#endif
|
||||
|
||||
// contents may get swapped with empty vector
|
||||
PRCrossfadeData *pCrossfadeData{};
|
||||
};
|
||||
|
||||
struct TransportTracks {
|
||||
@ -147,6 +160,9 @@ struct TransportTracks {
|
||||
#ifdef EXPERIMENTAL_MIDI_OUT
|
||||
NoteTrackConstArray midiTracks;
|
||||
#endif
|
||||
|
||||
// This is a subset of playbackTracks
|
||||
WaveTrackConstArray prerollTracks;
|
||||
};
|
||||
|
||||
// This workaround makes pause and stop work when output is to GarageBand,
|
||||
@ -544,6 +560,8 @@ private:
|
||||
* If bOnlyBuffers is specified, it only cleans up the buffers. */
|
||||
void StartStreamCleanup(bool bOnlyBuffers = false);
|
||||
|
||||
PRCrossfadeData mCrossfadeData{};
|
||||
|
||||
#ifdef EXPERIMENTAL_MIDI_OUT
|
||||
// MIDI_PLAYBACK:
|
||||
PmStream *mMidiStream;
|
||||
@ -823,15 +841,19 @@ public:
|
||||
|
||||
private:
|
||||
struct RecordingSchedule {
|
||||
double mLatencyCorrection{};
|
||||
double mPreRoll{};
|
||||
double mLatencyCorrection{}; // negative value usually
|
||||
double mDuration{};
|
||||
PRCrossfadeData mCrossfadeData;
|
||||
|
||||
// These are initialized by the main thread, then updated
|
||||
// only by the thread calling FillBuffers:
|
||||
double mPosition{};
|
||||
bool mLatencyCorrected{};
|
||||
|
||||
double TotalCorrection() const { return mLatencyCorrection - mPreRoll; }
|
||||
double ToConsume() const;
|
||||
double Consumed() const;
|
||||
double ToDiscard() const;
|
||||
} mRecordingSchedule{};
|
||||
};
|
||||
|
@ -251,4 +251,7 @@
|
||||
// proper memory fences.
|
||||
#define EXPERIMENTAL_REWRITE_RING_BUFFER
|
||||
|
||||
// PRL 1 Jun 2018
|
||||
#define EXPERIMENTAL_PUNCH_AND_ROLL
|
||||
|
||||
#endif
|
||||
|
@ -924,6 +924,12 @@ void AudacityProject::CreateMenusAndCommands()
|
||||
AudioIONotBusyFlag | CanStopAudioStreamFlag,
|
||||
AudioIONotBusyFlag | CanStopAudioStreamFlag);
|
||||
|
||||
#ifdef EXPERIMENTAL_PUNCH_AND_ROLL
|
||||
c->AddItem(wxT("PunchAndRoll"), XXO("Punch and Rol&l Record"), FN(OnPunchAndRoll), wxT("Shift+D"),
|
||||
WaveTracksExistFlag | AudioIONotBusyFlag,
|
||||
WaveTracksExistFlag | AudioIONotBusyFlag);
|
||||
#endif
|
||||
|
||||
c->BeginSubMenu(_("Transport &Options"));
|
||||
// Sound Activated recording options
|
||||
c->AddItem(wxT("SoundActivationLevel"), XXO("Sound Activation Le&vel..."), FN(OnSoundActivated),
|
||||
@ -8507,6 +8513,93 @@ int AudacityProject::DialogForLabelName(const wxString& initialValue, wxString&
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
#ifdef EXPERIMENTAL_PUNCH_AND_ROLL
|
||||
void AudacityProject::OnPunchAndRoll(const CommandContext &WXUNUSED(context))
|
||||
{
|
||||
if (!gAudioIO->IsBusy()) {
|
||||
|
||||
// Ignore all but left edge of the selection.
|
||||
mViewInfo.selectedRegion.collapseToT0();
|
||||
const double t1 = std::max(0.0, mViewInfo.selectedRegion.t1());
|
||||
|
||||
// Decide which tracks to record in.
|
||||
auto pBar = GetControlToolBar();
|
||||
auto tracks = pBar->ChooseExistingRecordingTracks(*this, true);
|
||||
if (tracks.empty()) {
|
||||
int recordingChannels =
|
||||
std::max(0L, gPrefs->Read(wxT("/AudioIO/RecordChannels"), 2));
|
||||
auto format = wxPLURAL(
|
||||
"Please select at least %d channel.",
|
||||
"Please select at least %d channels.",
|
||||
recordingChannels
|
||||
);
|
||||
auto message =
|
||||
wxString::Format(format, recordingChannels);
|
||||
AudacityMessageBox(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete the portion of the target tracks right of the selection, but first,
|
||||
// remember a part of the deletion for crossfading with the new recording.
|
||||
PRCrossfadeData crossfadeData;
|
||||
const double crossFadeDuration =
|
||||
gPrefs->Read(AUDIO_ROLL_CROSSFADE_KEY, DEFAULT_ROLL_CROSSFADE_MS)
|
||||
/ 1000.0;
|
||||
for (const auto &wt : tracks) {
|
||||
if (!wt->GetClipAtSample(sampleCount(floor(t1 * wt->GetRate())))) {
|
||||
auto message = _("Please select a time within a clip.");
|
||||
AudacityMessageBox(message);
|
||||
return;
|
||||
}
|
||||
const auto endTime = wt->GetEndTime();
|
||||
const auto duration = std::max(0.0, std::min(crossFadeDuration, endTime - t1));
|
||||
const size_t getLen = floor(duration * wt->GetRate());
|
||||
std::vector<float> data(getLen);
|
||||
if (getLen > 0) {
|
||||
float *const samples = data.data();
|
||||
const sampleCount pos = wt->TimeToLongSamples(t1);
|
||||
wt->Get((samplePtr)samples, floatSample, pos, getLen);
|
||||
}
|
||||
crossfadeData.push_back(std::move(data));
|
||||
}
|
||||
|
||||
// Change tracks only after passing the error checks above
|
||||
for (const auto &wt : tracks) {
|
||||
wt->Clear(t1, wt->GetEndTime());
|
||||
}
|
||||
|
||||
// Choose the tracks for playback.
|
||||
TransportTracks transportTracks;
|
||||
const auto duplex = ControlToolBar::UseDuplex();
|
||||
if (duplex)
|
||||
// play all
|
||||
transportTracks = GetAllPlaybackTracks(*GetTracks(), false, true);
|
||||
else
|
||||
// play recording tracks only
|
||||
std::copy(tracks.begin(), tracks.end(), std::back_inserter(transportTracks.playbackTracks));
|
||||
|
||||
// Unlike with the usual recording, a track may be chosen both for playback and recording.
|
||||
transportTracks.captureTracks = std::move(tracks);
|
||||
|
||||
// Try to start recording
|
||||
AudioIOStartStreamOptions options(GetDefaultPlayOptions());
|
||||
options.preRoll = gPrefs->Read(AUDIO_PRE_ROLL_KEY, DEFAULT_PRE_ROLL_SECONDS);
|
||||
options.pCrossfadeData = &crossfadeData;
|
||||
bool success = GetControlToolBar()->DoRecord(*this,
|
||||
transportTracks,
|
||||
t1, DBL_MAX, options);
|
||||
|
||||
if (success)
|
||||
// Undo state will get pushed elsewhere, when record finishes
|
||||
;
|
||||
else
|
||||
// Roll back the deletions
|
||||
RollbackState();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
int AudacityProject::DoAddLabel(const SelectedRegion ®ion, bool preserveFocus)
|
||||
{
|
||||
wxString title; // of label
|
||||
|
@ -403,6 +403,10 @@ void OnToggleSWPlaythrough(const CommandContext &context );
|
||||
#endif
|
||||
void OnRescanDevices(const CommandContext &context );
|
||||
|
||||
#ifdef EXPERIMENTAL_PUNCH_AND_ROLL
|
||||
void OnPunchAndRoll(const CommandContext &context);
|
||||
#endif
|
||||
|
||||
// Import Submenu
|
||||
void OnImport(const CommandContext &context );
|
||||
void OnImportLabels(const CommandContext &context );
|
||||
|
@ -19,6 +19,7 @@
|
||||
*//********************************************************************/
|
||||
|
||||
#include "../Audacity.h"
|
||||
#include "../Experimental.h"
|
||||
#include "RecordingPrefs.h"
|
||||
|
||||
#include <wx/defs.h>
|
||||
@ -222,8 +223,33 @@ void RecordingPrefs::PopulateOrExchange(ShuttleGui & S)
|
||||
}
|
||||
S.EndStatic();
|
||||
#endif
|
||||
S.EndScroller();
|
||||
|
||||
#ifdef EXPERIMENTAL_PUNCH_AND_ROLL
|
||||
S.StartStatic(_("Punch and Roll Recording"));
|
||||
{
|
||||
S.StartThreeColumn();
|
||||
{
|
||||
auto w = S.TieNumericTextBox(_("Pre-ro&ll duration:"),
|
||||
AUDIO_PRE_ROLL_KEY,
|
||||
DEFAULT_PRE_ROLL_SECONDS,
|
||||
9);
|
||||
S.AddUnits(_("seconds"));
|
||||
w->SetName(w->GetName() + wxT(" ") + _("seconds"));
|
||||
}
|
||||
{
|
||||
auto w = S.TieNumericTextBox(_("Cross&fade:"),
|
||||
AUDIO_ROLL_CROSSFADE_KEY,
|
||||
DEFAULT_ROLL_CROSSFADE_MS,
|
||||
9);
|
||||
S.AddUnits(_("milliseconds"));
|
||||
w->SetName(w->GetName() + wxT(" ") + _("milliseconds"));
|
||||
}
|
||||
S.EndThreeColumn();
|
||||
}
|
||||
S.EndStatic();
|
||||
#endif
|
||||
|
||||
S.EndScroller();
|
||||
}
|
||||
|
||||
bool RecordingPrefs::Commit()
|
||||
|
@ -1125,6 +1125,13 @@ bool ControlToolBar::DoRecord(AudacityProject &project,
|
||||
{
|
||||
t1 = wt->GetEndTime();
|
||||
|
||||
// If the track was chosen for recording and playback both,
|
||||
// remember the original in preroll tracks, before making the
|
||||
// pending replacement.
|
||||
bool prerollTrack = make_iterator_range(transportTracks.playbackTracks).contains(wt);
|
||||
if (prerollTrack)
|
||||
transportTracks.prerollTracks.push_back(wt);
|
||||
|
||||
// A function that copies all the non-sample data between
|
||||
// wave tracks; in case the track recorded to changes scale
|
||||
// type (for instance), during the recording.
|
||||
|
Loading…
x
Reference in New Issue
Block a user