1
0
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:
Paul Licameli 2018-06-01 04:31:40 -04:00
commit 9e2937feed
7 changed files with 249 additions and 37 deletions

View File

@ -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() ) );
}

View File

@ -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{};
};

View File

@ -251,4 +251,7 @@
// proper memory fences.
#define EXPERIMENTAL_REWRITE_RING_BUFFER
// PRL 1 Jun 2018
#define EXPERIMENTAL_PUNCH_AND_ROLL
#endif

View File

@ -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 &region, bool preserveFocus)
{
wxString title; // of label

View File

@ -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 );

View File

@ -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()

View File

@ -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.