Files
tenacity/libraries/lib-wave-track/WaveClip.cpp
Dmitry Makarenko ecbb09689a Fix #8559 Joining is broken for pitched clips when rendering is not required
(cherry picked from commit 22417b60bedbf48574998eef04f9bfa9a531bb12)
2025-06-16 22:08:19 -07:00

2091 lines
58 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
WaveClip.cpp
?? Dominic Mazzoni
?? Markus Meyer
*******************************************************************//**
\class WaveClip
\brief This allows multiple clips to be a part of one WaveTrack.
*//*******************************************************************/
#include "WaveClip.h"
#include <math.h>
#include <numeric>
#include <optional>
#include <vector>
#include <wx/log.h>
#include "BasicUI.h"
#include "Envelope.h"
#include "InconsistencyException.h"
#include "Resample.h"
#include "Sequence.h"
#include "TimeAndPitchInterface.h"
#include "UserException.h"
#ifdef _OPENMP
#include <omp.h>
#endif
const char *WaveClip::WaveClip_tag = "waveclip";
WaveClipListener::~WaveClipListener() = default;
void WaveClipListener::WriteXMLAttributes(XMLWriter &) const
{
}
bool WaveClipListener::HandleXMLAttribute(
const std::string_view &, const XMLAttributeValueView &)
{
return false;
}
void WaveClipListener::MakeStereo(WaveClipListener &&, bool)
{
}
void WaveClipListener::SwapChannels()
{
}
void WaveClipListener::Erase(size_t)
{
}
WaveClipChannel::~WaveClipChannel() = default;
Envelope &WaveClipChannel::GetEnvelope()
{
return GetClip().GetEnvelope();
}
const Envelope &WaveClipChannel::GetEnvelope() const
{
return GetClip().GetEnvelope();
}
bool WaveClipChannel::Intersects(double t0, double t1) const
{
return GetClip().IntersectsPlayRegion(t0, t1);
}
double WaveClipChannel::Start() const
{
return GetClip().GetPlayStartTime();
}
double WaveClipChannel::End() const
{
return GetClip().GetCommittedEndTime();
}
AudioSegmentSampleView
WaveClipChannel::GetSampleView(double t0, double t1, bool mayThrow) const
{
return GetClip().GetSampleView(miChannel, t0, t1, mayThrow);
}
bool WaveClipChannel::WithinPlayRegion(double t) const
{
return GetClip().WithinPlayRegion(t);
}
double WaveClipChannel::SamplesToTime(sampleCount s) const noexcept
{
return GetClip().SamplesToTime(s);
}
bool WaveClipChannel::HasPitchOrSpeed() const
{
return GetClip().HasPitchOrSpeed();
}
double WaveClipChannel::GetTrimLeft() const
{
return GetClip().GetTrimLeft();
}
bool WaveClipChannel::GetSamples(samplePtr buffer, sampleFormat format,
sampleCount start, size_t len, bool mayThrow) const
{
return GetClip().GetSamples(miChannel, buffer, format, start, len, mayThrow);
}
AudioSegmentSampleView WaveClipChannel::GetSampleView(
sampleCount start, size_t length, bool mayThrow) const
{
return GetClip().GetSampleView(miChannel, start, length, mayThrow);
}
const Sequence &WaveClipChannel::GetSequence() const
{
const auto pSequence = GetClip().GetSequence(miChannel);
// Assume sufficiently wide clip
assert(pSequence);
return *pSequence;
}
constSamplePtr WaveClipChannel::GetAppendBuffer() const
{
return GetClip().GetAppendBuffer(miChannel);
}
size_t WaveClipChannel::GetAppendBufferLen() const
{
return GetClip().GetAppendBufferLen(miChannel);
}
const BlockArray *WaveClipChannel::GetSequenceBlockArray() const
{
return GetClip().GetSequenceBlockArray(miChannel);
}
std::pair<float, float>
WaveClipChannel::GetMinMax(double t0, double t1, bool mayThrow) const
{
return GetClip().GetMinMax(miChannel, t0, t1, mayThrow);
}
float WaveClipChannel::GetRMS(double t0, double t1, bool mayThrow) const
{
return GetClip().GetRMS(miChannel, t0, t1, mayThrow);
}
sampleCount WaveClipChannel::GetPlayStartSample() const
{
return GetClip().GetPlayStartSample();
}
sampleCount WaveClipChannel::GetPlayEndSample() const
{
return GetClip().GetPlayEndSample();
}
void WaveClipChannel::SetSamples(constSamplePtr buffer, sampleFormat format,
sampleCount start, size_t len, sampleFormat effectiveFormat)
{
return GetClip().SetSamples(miChannel,
buffer, format, start, len, effectiveFormat);
}
void WaveClipChannel::WriteXML(XMLWriter &xmlFile) const
{
GetClip().WriteXML(miChannel, xmlFile);
}
double WaveClipChannel::GetTrimRight() const
{
return GetClip().GetTrimRight();
}
sampleCount WaveClipChannel::GetVisibleSampleCount() const
{
return GetClip().GetVisibleSampleCount();
}
int WaveClipChannel::GetRate() const
{
return GetClip().GetRate();
}
double WaveClipChannel::GetPlayStartTime() const
{
return GetClip().GetPlayStartTime();
}
double WaveClipChannel::GetPlayEndTime() const
{
return GetClip().GetPlayEndTime();
}
double WaveClipChannel::GetPlayDuration() const
{
return GetPlayEndTime() - GetPlayStartTime();
}
sampleCount WaveClipChannel::TimeToSamples(double time) const
{
return GetClip().TimeToSamples(time);
}
double WaveClipChannel::GetStretchRatio() const
{
return GetClip().GetStretchRatio();
}
WaveClip::WaveClip(size_t width,
const SampleBlockFactoryPtr &factory,
sampleFormat format, int rate)
{
assert(width > 0);
mRate = rate;
mSequences.resize(width);
for (auto &pSequence : mSequences)
pSequence = std::make_unique<Sequence>(factory,
SampleFormats{narrowestSampleFormat, format});
mEnvelope = std::make_unique<Envelope>(true, 1e-7, 2.0, 1.0);
assert(CheckInvariants());
}
WaveClip::WaveClip(
const WaveClip& orig, const SampleBlockFactoryPtr& factory,
bool copyCutlines, CreateToken token)
: mCentShift { orig.mCentShift }
, mPitchAndSpeedPreset { orig.mPitchAndSpeedPreset }
, mClipStretchRatio { orig.mClipStretchRatio }
, mRawAudioTempo { orig.mRawAudioTempo }
, mProjectTempo { orig.mProjectTempo }
{
// essentially a copy constructor - but you must pass in the
// current sample block factory, because we might be copying
// from one project to another
mSequenceOffset = orig.mSequenceOffset;
mTrimLeft = orig.mTrimLeft;
mTrimRight = orig.mTrimRight;
mRate = orig.mRate;
// Deep copy of attachments
Attachments &attachments = *this;
attachments = orig;
mSequences.reserve(orig.NChannels());
if (!token.emptyCopy)
for (auto &pSequence : orig.mSequences)
mSequences.push_back(std::make_unique<Sequence>(*pSequence, factory));
mEnvelope = std::make_unique<Envelope>(*orig.mEnvelope);
mName = orig.mName;
if (copyCutlines)
for (const auto &clip: orig.mCutLines)
mCutLines.push_back(
std::make_shared<WaveClip>(*clip, factory, true, token));
mIsPlaceholder = orig.GetIsPlaceholder();
assert(NChannels() == (token.emptyCopy ? 0 : orig.NChannels()));
assert(token.emptyCopy || CheckInvariants());
assert(!copyCutlines || NumCutLines() == orig.NumCutLines());
}
WaveClip::WaveClip(
const WaveClip& orig, const SampleBlockFactoryPtr& factory,
bool copyCutlines, double t0, double t1)
: mCentShift { orig.mCentShift }
, mClipStretchRatio { orig.mClipStretchRatio }
, mRawAudioTempo { orig.mRawAudioTempo }
, mProjectTempo { orig.mProjectTempo }
{
assert(orig.CountSamples(t0, t1) > 0);
mSequenceOffset = orig.mSequenceOffset;
//Adjust trim values to sample-boundary
if(t0 > orig.GetPlayStartTime()) {
const auto s0 = orig.TimeToSamples(t0 - orig.GetSequenceStartTime());
mTrimLeft = orig.SamplesToTime(s0);
}
else
mTrimLeft = orig.mTrimLeft;
if(t1 < orig.GetPlayEndTime())
{
const auto s1 = orig.TimeToSamples(orig.GetSequenceEndTime() - t1);
mTrimRight = orig.SamplesToTime(s1);
}
else
mTrimRight = orig.mTrimRight;
mRate = orig.mRate;
// Deep copy of attachments
Attachments &attachments = *this;
attachments = orig;
mIsPlaceholder = orig.GetIsPlaceholder();
mSequences.reserve(orig.NChannels());
for (auto &pSequence : orig.mSequences)
mSequences.push_back(
std::make_unique<Sequence>(*pSequence, factory));
mEnvelope = std::make_unique<Envelope>(*orig.mEnvelope);
if (copyCutlines)
for (const auto &cutline : orig.mCutLines)
mCutLines.push_back(
std::make_shared<WaveClip>(*cutline, factory, true));
assert(NChannels() == orig.NChannels());
assert(CheckInvariants());
}
WaveClip::~WaveClip()
{
Observer::Publisher<WaveClipDtorCalled>::Publish(WaveClipDtorCalled {});
}
double WaveClip::Start() const
{
return GetPlayStartTime();
}
double WaveClip::End() const
{
return GetPlayEndTime();
}
std::shared_ptr<ChannelInterval> WaveClip::DoGetChannel(size_t iChannel)
{
return std::make_shared<Channel>(*this, iChannel);
}
AudioSegmentSampleView WaveClip::GetSampleView(
size_t ii, sampleCount start, size_t length, bool mayThrow) const
{
assert(ii < NChannels());
return mSequences[ii]->GetFloatSampleView(
start + TimeToSamples(mTrimLeft), length, mayThrow);
}
AudioSegmentSampleView WaveClip::GetSampleView(
size_t iChannel, double t0, double t1, bool mayThrow) const
{
assert(iChannel < NChannels());
const auto start = TimeToSamples(std::max(0., t0));
const auto length =
(std::min(GetNumSamples(), TimeToSamples(t1)) - start).as_size_t();
return GetSampleView(iChannel, start, length, mayThrow);
}
size_t WaveClip::NChannels() const
{
return mSequences.size();
}
bool WaveClip::GetSamples(size_t ii,
samplePtr buffer, sampleFormat format,
sampleCount start, size_t len, bool mayThrow) const
{
assert(ii < NChannels());
return mSequences[ii]
->Get(buffer, format, start + TimeToSamples(mTrimLeft), len, mayThrow);
}
bool WaveClip::GetSamples(samplePtr buffers[], sampleFormat format,
sampleCount start, size_t len, bool mayThrow) const
{
bool result = true;
for (size_t ii = 0, width = NChannels(); result && ii < width; ++ii)
result = GetSamples(ii, buffers[ii], format, start, len, mayThrow);
return result;
}
/*! @excsafety{Strong} */
void WaveClip::SetSamples(size_t ii,
constSamplePtr buffer, sampleFormat format,
sampleCount start, size_t len, sampleFormat effectiveFormat)
{
StrongInvariantScope scope{ *this };
assert(ii < NChannels());
// use Strong-guarantee
mSequences[ii]->SetSamples(buffer, format,
start + TimeToSamples(mTrimLeft), len, effectiveFormat);
// use No-fail-guarantee
MarkChanged();
}
void WaveClip::SetEnvelope(std::unique_ptr<Envelope> p)
{
assert(p);
mEnvelope = std::move(p);
}
const BlockArray* WaveClip::GetSequenceBlockArray(size_t ii) const
{
assert(ii < NChannels());
return &mSequences[ii]->GetBlockArray();
}
size_t WaveClip::GetAppendBufferLen(size_t iChannel) const
{
assert(iChannel < NChannels());
return mSequences[iChannel]->GetAppendBufferLen();
}
void WaveClip::DiscardRightChannel()
{
mSequences.resize(1);
this->Attachments::ForEach([](WaveClipListener &attachment){
attachment.Erase(1);
});
for (auto &pCutline : mCutLines)
pCutline->DiscardRightChannel();
assert(NChannels() == 1);
assert(CheckInvariants());
}
void WaveClip::SwapChannels()
{
assert(NChannels() == 2);
this->Attachments::ForEach([](WaveClipListener &attachment){
attachment.SwapChannels();
});
std::swap(mSequences[0], mSequences[1]);
for (auto &pCutline : mCutLines)
pCutline->SwapChannels();
assert(CheckInvariants());
}
void WaveClip::TransferSequence(WaveClip &origClip, WaveClip &newClip)
{
// Move right channel into result
newClip.mSequences.resize(1);
newClip.mSequences[0] = std::move(origClip.mSequences[1]);
// Delayed satisfaction of the class invariants after the empty construction
newClip.CheckInvariants();
}
void WaveClip::FixSplitCutlines(
WaveClipHolders &myCutlines, WaveClipHolders &newCutlines)
{
auto beginMe = myCutlines.begin(),
endMe = myCutlines.end();
auto iterNew = newCutlines.begin(),
endNew = newCutlines.end();
for_each(beginMe, endMe, [&](const auto &myCutline){
assert(iterNew != endNew);
const auto pNew = *iterNew;
TransferSequence(*myCutline, *pNew);
// Recursion!
FixSplitCutlines(myCutline->mCutLines, pNew->mCutLines);
++iterNew;
});
assert(iterNew == endNew);
}
std::shared_ptr<WaveClip> WaveClip::SplitChannels()
{
assert(NChannels() == 2);
// Make empty copies of this and all cutlines
CreateToken token{ true };
auto result = std::make_shared<WaveClip>(*this, GetFactory(), true, token);
// Move one Sequence
TransferSequence(*this, *result);
// Must also do that for cutlines, which must be in correspondence, because
// of the post of the constructor.
// And possibly too for cutlines inside of cutlines!
FixSplitCutlines(mCutLines, result->mCutLines);
// Fix attachments in the new clip and assert consistency conditions between
// the clip and its cutlines
result->Attachments::ForEach([](WaveClipListener &attachment){
attachment.Erase(0);
});
assert(result->CheckInvariants());
// This call asserts invariants for this clip
DiscardRightChannel();
// Assert postconditions
assert(NChannels() == 1);
assert(result->NChannels() == 1);
return result;
}
void WaveClip::MakeStereo(WaveClip &&other, bool mustAlign)
{
assert(NChannels() == 1);
assert(other.NChannels() == 1);
assert(GetSampleFormats() == other.GetSampleFormats());
assert(GetFactory() == other.GetFactory());
assert(!mustAlign || GetNumSamples() == other.GetNumSamples());
mCutLines.clear();
mSequences.resize(2);
mSequences[1] = std::move(other.mSequences[0]);
this->Attachments::ForCorresponding(other,
[mustAlign](WaveClipListener *pLeft, WaveClipListener *pRight){
// Precondition of callback from ClientData::Site
assert(pLeft && pRight);
pLeft->MakeStereo(std::move(*pRight), mustAlign);
});
if (mustAlign)
assert(StrongInvariant());
else
assert(CheckInvariants());
}
size_t WaveClip::GreatestAppendBufferLen() const
{
size_t result = 0;
for (size_t iChannel = 0; iChannel < NChannels(); ++iChannel)
result = std::max(result, mSequences[iChannel]->GetAppendBufferLen());
return result;
}
void WaveClip::OnProjectTempoChange(
const std::optional<double>& oldTempo, double newTempo)
{
if (!mRawAudioTempo.has_value())
// When we have tempo detection ready (either by header-file
// read-up or signal analysis) we can use something smarter than that. In
// the meantime, use the tempo of the project when the clip is created as
// source tempo.
mRawAudioTempo = oldTempo.value_or(newTempo);
if (oldTempo.has_value())
{
const auto ratioChange = *oldTempo / newTempo;
mSequenceOffset *= ratioChange;
mTrimLeft *= ratioChange;
mTrimRight *= ratioChange;
StretchCutLines(ratioChange);
mEnvelope->RescaleTimesBy(ratioChange);
}
mProjectTempo = newTempo;
Observer::Publisher<StretchRatioChange>::Publish(
StretchRatioChange { GetStretchRatio() });
}
void WaveClip::StretchLeftTo(double to)
{
const auto pet = GetPlayEndTime();
if (to >= pet)
return;
const auto oldPlayDuration = pet - GetPlayStartTime();
const auto newPlayDuration = pet - to;
const auto ratioChange = newPlayDuration / oldPlayDuration;
mSequenceOffset = pet - (pet - mSequenceOffset) * ratioChange;
mTrimLeft *= ratioChange;
mTrimRight *= ratioChange;
mClipStretchRatio *= ratioChange;
mEnvelope->SetOffset(mSequenceOffset);
mEnvelope->RescaleTimesBy(ratioChange);
StretchCutLines(ratioChange);
Observer::Publisher<StretchRatioChange>::Publish(
StretchRatioChange { GetStretchRatio() });
}
void WaveClip::StretchRightTo(double to)
{
const auto pst = GetPlayStartTime();
if (to <= pst)
return;
const auto oldPlayDuration = GetPlayEndTime() - pst;
const auto newPlayDuration = to - pst;
const auto ratioChange = newPlayDuration / oldPlayDuration;
StretchBy(ratioChange);
}
void WaveClip::StretchBy(double ratio)
{
const auto pst = GetPlayStartTime();
mSequenceOffset = pst - mTrimLeft * ratio;
mTrimLeft *= ratio;
mTrimRight *= ratio;
mClipStretchRatio *= ratio;
mEnvelope->SetOffset(mSequenceOffset);
mEnvelope->RescaleTimesBy(ratio);
StretchCutLines(ratio);
Observer::Publisher<StretchRatioChange>::Publish(
StretchRatioChange { GetStretchRatio() });
}
void WaveClip::StretchCutLines(double ratioChange)
{
for (const auto& cutline : mCutLines)
{
cutline->mSequenceOffset *= ratioChange;
cutline->mTrimLeft *= ratioChange;
cutline->mTrimRight *= ratioChange;
cutline->mClipStretchRatio *= ratioChange;
cutline->mEnvelope->RescaleTimesBy(ratioChange);
}
}
double WaveClip::GetStretchRatio() const
{
const auto dstSrcRatio =
mProjectTempo.has_value() && mRawAudioTempo.has_value() ?
*mRawAudioTempo / *mProjectTempo :
1.0;
return mClipStretchRatio * dstSrcRatio;
}
int WaveClip::GetCentShift() const
{
return mCentShift;
}
Observer::Subscription
WaveClip::SubscribeToCentShiftChange(std::function<void(int)> cb) const
{
// Consider the list of subcribers as a mutable member that doesn't change
// real state
return const_cast<WaveClip*>(this)->
Observer::Publisher<CentShiftChange>::Subscribe(
[cb](const CentShiftChange& cents) { cb(cents.newValue); });
}
Observer::Subscription WaveClip::SubscribeToPitchAndSpeedPresetChange(
std::function<void(PitchAndSpeedPreset)> cb) const
{
// Consider the list of subcribers as a mutable member that doesn't change
// real state
return const_cast<WaveClip*>(this)->
Observer::Publisher<PitchAndSpeedPresetChange>::Subscribe(
[cb](const PitchAndSpeedPresetChange& formant) {
cb(formant.newValue);
});
}
bool WaveClip::HasEqualPitchAndSpeed(const WaveClip& other) const
{
return StretchRatioEquals(other.GetStretchRatio()) &&
GetCentShift() == other.GetCentShift();
}
bool WaveClip::HasPitchOrSpeed() const
{
return !StretchRatioEquals(1.0) || GetCentShift() != 0;
}
bool WaveClip::StretchRatioEquals(double value) const
{
return TimeAndPitchInterface::IsPassThroughMode(
1 + GetStretchRatio() - value);
}
sampleCount WaveClip::GetNumSamples() const
{
// Assume only the weak invariant
sampleCount result = 0;
for (auto &pSequence: mSequences)
result = std::max(result, pSequence->GetNumSamples());
return result;
}
SampleFormats WaveClip::GetSampleFormats() const
{
// All sequences have the same formats by class invariant
return mSequences[0]->GetSampleFormats();
}
size_t WaveClip::CountBlocks() const
{
return std::accumulate(mSequences.begin(), mSequences.end(), size_t{},
[](size_t acc, auto &pSequence){
return acc + pSequence->GetBlockArray().size(); });
}
//! A hint for sizing of well aligned fetches
size_t WaveClip::GetBestBlockSize(sampleCount t) const
{
return mSequences[0]->GetBestBlockSize(t);
}
size_t WaveClip::GetMaxBlockSize() const
{
return std::accumulate(mSequences.begin(), mSequences.end(), size_t{},
[](size_t acc, auto &pSequence){
return std::max(acc, pSequence->GetMaxBlockSize()); });
}
const SampleBlockFactoryPtr &WaveClip::GetFactory() const
{
// All sequences have the same factory by class invariant
return mSequences[0]->GetFactory();
}
std::vector<std::unique_ptr<Sequence>> WaveClip::GetEmptySequenceCopies() const
{
decltype(mSequences) newSequences;
newSequences.reserve(mSequences.size());
for (auto& pSequence : mSequences)
newSequences.push_back(std::make_unique<Sequence>(
pSequence->GetFactory(), pSequence->GetSampleFormats()));
return newSequences;
}
constSamplePtr WaveClip::GetAppendBuffer(size_t ii) const
{
assert(ii < NChannels());
return mSequences[ii]->GetAppendBuffer();
}
void WaveClip::MarkChanged() noexcept // NOFAIL-GUARANTEE
{
Attachments::ForEach(std::mem_fn(&WaveClipListener::MarkChanged));
}
std::pair<float, float> WaveClip::GetMinMax(size_t ii,
double t0, double t1, bool mayThrow) const
{
assert(ii < NChannels());
t0 = std::max(t0, GetPlayStartTime());
t1 = std::min(t1, GetPlayEndTime());
if (t0 > t1) {
if (mayThrow)
THROW_INCONSISTENCY_EXCEPTION;
return {
0.f, // harmless, but unused since Sequence::GetMinMax does not use these values
0.f // harmless, but unused since Sequence::GetMinMax does not use these values
};
}
if (t0 == t1)
return{ 0.f, 0.f };
auto s0 = TimeToSequenceSamples(t0);
auto s1 = TimeToSequenceSamples(t1);
return mSequences[ii]->GetMinMax(s0, s1 - s0, mayThrow);
}
float WaveClip::GetRMS(size_t ii, double t0, double t1, bool mayThrow) const
{
assert(ii < NChannels());
if (t0 > t1) {
if (mayThrow)
THROW_INCONSISTENCY_EXCEPTION;
return 0.f;
}
if (t0 == t1)
return 0.f;
auto s0 = TimeToSequenceSamples(t0);
auto s1 = TimeToSequenceSamples(t1);
return mSequences[ii]->GetRMS(s0, s1-s0, mayThrow);
}
void WaveClip::ConvertToSampleFormat(sampleFormat format,
const std::function<void(size_t)> & progressReport)
{
// This mutator does not require the strong invariant. It leaves sample
// counts unchanged in each sequence.
// Note: it is not necessary to do this recursively to cutlines.
// They get converted as needed when they are expanded.
Transaction transaction{ *this };
auto bChanged = mSequences[0]->ConvertToSampleFormat(format, progressReport);
for (size_t ii = 1, width = NChannels(); ii < width; ++ii) {
bool alsoChanged =
mSequences[ii]->ConvertToSampleFormat(format, progressReport);
// Class invariant implies:
assert(bChanged == alsoChanged);
}
if (bChanged)
MarkChanged();
transaction.Commit();
}
/*! @excsafety{No-fail} */
void WaveClip::UpdateEnvelopeTrackLen()
{
// The envelope time points account for stretching.
const auto len = GetNumSamples().as_double() * GetStretchRatio() / mRate;
if (len != mEnvelope->GetTrackLen())
mEnvelope->SetTrackLen(len, 1.0 / GetRate());
}
/*! @excsafety{Strong} */
std::shared_ptr<SampleBlock>
WaveClip::AppendToChannel(size_t iChannel,
constSamplePtr buffer, sampleFormat format, size_t len)
{
assert(iChannel < NChannels());
return mSequences[iChannel]->AppendNewBlock(buffer, format, len);
}
/*! @excsafety{Strong} */
std::shared_ptr<SampleBlock>
WaveClip::AppendLegacyNewBlock(constSamplePtr buffer, sampleFormat format, size_t len)
{
// This is a special use function for legacy files only and this assertion
// does not need to be relaxed. The clip is in a still unzipped track.
assert(NChannels() == 1);
return AppendToChannel(0, buffer, format, len);
}
/*! @excsafety{Strong} */
void WaveClip::AppendLegacySharedBlock(
const std::shared_ptr<SampleBlock> &pBlock)
{
// This is a special use function for legacy files only and this assertion
// does not need to be relaxed. The clip is in a still unzipped track.
assert(NChannels() == 1);
mSequences[0]->AppendSharedBlock( pBlock );
}
bool WaveClip::Append(size_t iChannel, const size_t nChannels,
constSamplePtr buffers[], sampleFormat format,
size_t len, unsigned int stride, sampleFormat effectiveFormat)
{
assert(iChannel < NChannels());
assert(iChannel + nChannels <= NChannels());
// No requirement or promise of the strong invariant, and therefore no
// need for Transaction
//wxLogDebug(wxT("Append: len=%lli"), (long long) len);
bool appended = false;
for (size_t ii = 0; ii < nChannels; ++ii)
appended = mSequences[iChannel + ii]->Append(
buffers[ii], format, len, stride, effectiveFormat)
|| appended;
// use No-fail-guarantee
UpdateEnvelopeTrackLen();
MarkChanged();
return appended;
}
bool WaveClip::Append(constSamplePtr buffers[], sampleFormat format,
size_t len, unsigned int stride, sampleFormat effectiveFormat)
{
StrongInvariantScope scope{ *this };
Transaction transaction{ *this };
//wxLogDebug(wxT("Append: len=%lli"), (long long) len);
size_t ii = 0;
bool appended = false;
for (auto &pSequence : mSequences)
appended =
pSequence->Append(buffers[ii++], format, len, stride, effectiveFormat)
|| appended;
transaction.Commit();
// use No-fail-guarantee
UpdateEnvelopeTrackLen();
MarkChanged();
return appended;
}
void WaveClip::Flush()
{
// Does not require or guarantee the strong invariant
//wxLogDebug(wxT("WaveClip::Flush"));
//wxLogDebug(wxT(" mAppendBufferLen=%lli"), (long long) mAppendBufferLen);
//wxLogDebug(wxT(" previous sample count %lli"), (long long) mSequence->GetNumSamples());
if (GreatestAppendBufferLen() > 0) {
Transaction transaction{ *this };
for (auto &pSequence : mSequences)
pSequence->Flush();
transaction.Commit();
// No-fail operations
UpdateEnvelopeTrackLen();
MarkChanged();
}
//wxLogDebug(wxT("now sample count %lli"), (long long) mSequence->GetNumSamples());
}
void WaveClip::RepairChannels()
{
if (NChannels() < 2)
return;
// Be sure of consistency of sample counts
// We may be here because the drive can't hold another megabyte, but
// note that InsertSilence makes silent blocks that don't occupy
// space in the database table of blocks.
// (However autosave may want to rewrite the document blob, so this solution
// may yet not be perfect.)
Transaction transaction{ *this };
const auto maxSamples = GetNumSamples();
for (const auto &pSequence: mSequences) {
const auto numSamples = pSequence->GetNumSamples();
if (pSequence->GetNumSamples() != maxSamples)
pSequence->InsertSilence(numSamples, maxSamples - numSamples);
}
transaction.Commit();
}
static constexpr auto Offset_attr = "offset";
static constexpr auto TrimLeft_attr = "trimLeft";
static constexpr auto TrimRight_attr = "trimRight";
static constexpr auto CentShiftAttr = "centShift";
static constexpr auto PitchAndSpeedPreset_attr = "pitchAndSpeedPreset";
static constexpr auto RawAudioTempo_attr = "rawAudioTempo";
static constexpr auto ClipStretchRatio_attr = "clipStretchRatio";
static constexpr auto Name_attr = "name";
bool WaveClip::HandleXMLTag(const std::string_view& tag, const AttributesList &attrs)
{
if (tag == WaveClip_tag)
{
double dblValue;
long longValue;
for (auto pair : attrs)
{
auto attr = pair.first;
auto value = pair.second;
if (attr == Offset_attr)
{
if (!value.TryGet(dblValue))
return false;
SetSequenceStartTime(dblValue);
}
else if (attr == TrimLeft_attr)
{
if (!value.TryGet(dblValue))
return false;
SetTrimLeft(dblValue);
}
else if (attr == TrimRight_attr)
{
if (!value.TryGet(dblValue))
return false;
SetTrimRight(dblValue);
}
else if (attr == CentShiftAttr)
{
if (!value.TryGet(dblValue))
return false;
mCentShift = dblValue;
}
else if (attr == PitchAndSpeedPreset_attr)
{
if (!value.TryGet(longValue))
return false;
mPitchAndSpeedPreset = static_cast<PitchAndSpeedPreset>(longValue);
}
else if (attr == RawAudioTempo_attr)
{
if (!value.TryGet(dblValue))
return false;
if (dblValue == 0)
mRawAudioTempo.reset();
else
mRawAudioTempo = dblValue;
}
else if (attr == ClipStretchRatio_attr)
{
if (!value.TryGet(dblValue))
return false;
mClipStretchRatio = dblValue;
}
else if (attr == Name_attr)
{
if(value.IsStringView())
SetName(value.ToWString());
}
else if (Attachments::FindIf(
[&](WaveClipListener &listener){
return listener.HandleXMLAttribute(attr, value); }
))
;
}
return true;
}
return false;
}
void WaveClip::HandleXMLEndTag(const std::string_view& tag)
{
// All blocks were deserialized into new sequences; remove the one made
// by the constructor which remains empty.
mSequences.erase(mSequences.begin());
mSequences.shrink_to_fit();
if (tag == WaveClip_tag)
UpdateEnvelopeTrackLen();
// A proof of this assertion assumes that nothing has happened since
// construction of this, besides calls to the other deserialization
// functions
assert(CheckInvariants());
}
XMLTagHandler *WaveClip::HandleXMLChild(const std::string_view& tag)
{
auto &pFirst = mSequences[0];
if (tag == Sequence::Sequence_tag) {
// Push back a new sequence prototyped from the empty sequence made
// by the constructor. See also HandleXMLEndTag above.
// Assume sequences were serialized in channel iteration order.
mSequences.push_back(std::make_unique<Sequence>(
pFirst->GetFactory(), pFirst->GetSampleFormats()));
return mSequences.back().get();
}
else if (tag == "envelope")
return mEnvelope.get();
else if (tag == WaveClip_tag)
{
// Nested wave clips are cut lines
auto format = pFirst->GetSampleFormats().Stored();
// The format is not stored in WaveClip itself but passed to
// Sequence::Sequence; but then the Sequence will deserialize format
// again
mCutLines.push_back(
std::make_shared<WaveClip>(
// Make only one channel now, but recursive deserialization
// increases the width later
1, pFirst->GetFactory(),
format, mRate));
return mCutLines.back().get();
}
else
return nullptr;
}
void WaveClip::WriteXML(size_t ii, XMLWriter &xmlFile) const
// may throw
{
assert(ii < NChannels());
if (GetSequenceSamplesCount() <= 0)
// Oops, I'm empty? How did that happen? Anyway, I do nothing but causing
// problems, don't save me.
return;
xmlFile.StartTag(WaveClip_tag);
xmlFile.WriteAttr(Offset_attr, mSequenceOffset, 8);
xmlFile.WriteAttr(TrimLeft_attr, mTrimLeft, 8);
xmlFile.WriteAttr(TrimRight_attr, mTrimRight, 8);
xmlFile.WriteAttr(CentShiftAttr, mCentShift);
xmlFile.WriteAttr(PitchAndSpeedPreset_attr,
static_cast<long>(mPitchAndSpeedPreset));
xmlFile.WriteAttr(RawAudioTempo_attr, mRawAudioTempo.value_or(0.), 8);
xmlFile.WriteAttr(ClipStretchRatio_attr, mClipStretchRatio, 8);
xmlFile.WriteAttr(Name_attr, mName);
Attachments::ForEach([&](const WaveClipListener &listener){
listener.WriteXMLAttributes(xmlFile);
});
mSequences[ii]->WriteXML(xmlFile);
mEnvelope->WriteXML(xmlFile);
for (const auto &clip: mCutLines)
clip->WriteXML(ii, xmlFile);
xmlFile.EndTag(WaveClip_tag);
}
/*! @excsafety{Strong} */
bool WaveClip::Paste(double t0, const WaveClip& o)
{
const WaveClip *pOther = &o;
WaveClipHolder dup;
if (!o.StrongInvariant()) {
assert(false); // precondition not honored
// But try to repair it and continue in release
dup = std::make_shared<WaveClip>(o, o.GetFactory(), true);
dup->RepairChannels();
pOther = dup.get();
}
auto &other = *pOther;
if (NChannels() != other.NChannels())
// post is satisfied
return false;
if (GetSequenceSamplesCount() == 0)
{
// Empty clip: we're flexible and adopt the other's pitch and stretch.
mPitchAndSpeedPreset = other.mPitchAndSpeedPreset;
mCentShift = other.mCentShift;
mClipStretchRatio = other.mClipStretchRatio;
mRawAudioTempo = other.mRawAudioTempo;
mProjectTempo = other.mProjectTempo;
}
else if (!HasEqualPitchAndSpeed(other))
// post is satisfied
return false;
StrongInvariantScope scope{ *this };
Transaction transaction{ *this };
const bool clipNeedsResampling = other.mRate != mRate;
const bool clipNeedsNewFormat =
other.GetSampleFormats().Stored() != GetSampleFormats().Stored();
std::shared_ptr<WaveClip> newClip;
t0 = std::clamp(t0, GetPlayStartTime(), GetPlayEndTime());
// Delay the finish of the clearing of this clip
ClearSequenceFinisher finisher;
//seems like edge cases cannot happen, see WaveTrack::PasteWaveTrack
double pastePositionShift = 0.0;
double startPositionShift = 0.0;
auto &factory = GetFactory();
if (t0 == GetPlayStartTime())
{
pastePositionShift = t0;
startPositionShift = GetTrimLeft() - other.GetTrimLeft();
finisher = ClearSequence(GetSequenceStartTime(), t0);
SetTrimLeft(other.GetTrimLeft());
auto copy = std::make_shared<WaveClip>(other, factory, true);
copy->ClearSequence(copy->GetPlayEndTime(), copy->GetSequenceEndTime())
.Commit();
newClip = std::move(copy);
}
else if (t0 == GetPlayEndTime())
{
finisher = ClearSequence(GetPlayEndTime(), GetSequenceEndTime());
SetTrimRight(other.GetTrimRight());
auto copy = std::make_shared<WaveClip>(other, factory, true);
copy->ClearSequence(copy->GetSequenceStartTime(), copy->GetPlayStartTime())
.Commit();
newClip = std::move(copy);
}
else
{
newClip = std::make_shared<WaveClip>(other, factory, true);
newClip->ClearSequence(newClip->GetPlayEndTime(), newClip->GetSequenceEndTime())
.Commit();
newClip->ClearSequence(newClip->GetSequenceStartTime(), newClip->GetPlayStartTime())
.Commit();
newClip->SetTrimLeft(0);
newClip->SetTrimRight(0);
}
if (clipNeedsResampling || clipNeedsNewFormat)
{
auto copy = std::make_shared<WaveClip>(*newClip.get(), factory, true);
if (clipNeedsResampling)
// The other clip's rate is different from ours, so resample
copy->Resample(mRate);
if (clipNeedsNewFormat)
// Force sample formats to match.
copy->ConvertToSampleFormat(GetSampleFormats().Stored());
newClip = std::move(copy);
}
// Paste cut lines contained in pasted clip
WaveClipHolders newCutlines;
for (const auto &cutline: newClip->mCutLines)
{
auto cutlineCopy = std::make_shared<WaveClip>(*cutline, factory,
// Recursively copy cutlines of cutlines. They don't need
// their offsets adjusted.
true);
cutlineCopy->ShiftBy(t0 - GetSequenceStartTime());
newCutlines.push_back(std::move(cutlineCopy));
}
sampleCount s0 = TimeToSequenceSamples(t0 - pastePositionShift);
// Because newClip was made above as a copy of (a copy of) other
assert(other.NChannels() == newClip->NChannels());
// And other has the same width as this, so this loop is safe
// Assume Strong-guarantee from Sequence::Paste
for (size_t ii = 0, width = NChannels(); ii < width; ++ii)
mSequences[ii]->Paste(s0, newClip->mSequences[ii].get());
// Assume No-fail-guarantee in the remaining
finisher.Commit();
transaction.Commit();
MarkChanged();
SetSequenceStartTime(GetSequenceStartTime() + startPositionShift);
const auto sampleTime = 1.0 / GetRate();
const auto timeOffsetInEnvelope =
s0.as_double() * GetStretchRatio() / mRate + GetSequenceStartTime();
mEnvelope->PasteEnvelope(
timeOffsetInEnvelope, newClip->mEnvelope.get(), sampleTime);
OffsetCutLines(t0, newClip->GetPlayEndTime() - newClip->GetPlayStartTime());
for (auto &holder : newCutlines)
mCutLines.push_back(std::move(holder));
return true;
}
/*! @excsafety{Strong} */
void WaveClip::InsertSilence( double t, double len, double *pEnvelopeValue )
{
StrongInvariantScope scope{ *this };
Transaction transaction{ *this };
ClearSequenceFinisher finisher;
if (t == GetPlayStartTime() && t > GetSequenceStartTime())
finisher = ClearSequence(GetSequenceStartTime(), t);
else if (t == GetPlayEndTime() && t < GetSequenceEndTime()) {
finisher = ClearSequence(t, GetSequenceEndTime());
SetTrimRight(.0);
}
const auto s0 = TimeToSequenceSamples(t);
const auto slen = TimeToSamples(len);
// use Strong-guarantee
for (auto &pSequence : mSequences)
pSequence->InsertSilence(s0, slen);
// use No-fail-guarantee in the rest
finisher.Commit();
transaction.Commit();
OffsetCutLines(t, len);
const auto sampleTime = 1.0 / GetRate();
auto &envelope = GetEnvelope();
if ( pEnvelopeValue ) {
// Preserve limit value at the end
auto oldLen = envelope.GetTrackLen();
auto newLen = oldLen + len;
envelope.Cap( sampleTime );
// Ramp across the silence to the given value
envelope.SetTrackLen( newLen, sampleTime );
envelope.InsertOrReplace
( envelope.GetOffset() + newLen, *pEnvelopeValue );
}
else
envelope.InsertSpace( t, len );
MarkChanged();
}
/*! @excsafety{Strong} */
void WaveClip::AppendSilence( double len, double envelopeValue )
{
auto t = GetPlayEndTime();
InsertSilence( t, len, &envelopeValue );
}
/*! @excsafety{Strong} */
void WaveClip::Clear(double t0, double t1)
{
auto st0 = t0;
auto st1 = t1;
auto offset = .0;
if (st0 <= GetPlayStartTime())
{
offset = (t0 - GetPlayStartTime()) + GetTrimLeft();
st0 = GetSequenceStartTime();
SetTrimLeft(.0);
}
if (st1 >= GetPlayEndTime())
{
st1 = GetSequenceEndTime();
SetTrimRight(.0);
}
Transaction transaction{ *this };
ClearSequence(st0, st1)
.Commit();
transaction.Commit();
MarkChanged();
if (offset != .0)
ShiftBy(offset);
}
void WaveClip::ClearLeft(double t)
{
if (t > GetPlayStartTime() && t < GetPlayEndTime())
{
Transaction transaction{ *this };
ClearSequence(GetSequenceStartTime(), t)
.Commit();
transaction.Commit();
SetTrimLeft(.0);
SetSequenceStartTime(t);
MarkChanged();
}
}
void WaveClip::ClearRight(double t)
{
if (t > GetPlayStartTime() && t < GetPlayEndTime())
{
Transaction transaction{ *this };
ClearSequence(t, GetSequenceEndTime())
.Commit();
transaction.Commit();
SetTrimRight(.0);
MarkChanged();
}
}
auto WaveClip::ClearSequence(double t0, double t1) -> ClearSequenceFinisher
{
StrongInvariantScope scope{ *this };
auto clip_t0 = std::max(t0, GetSequenceStartTime());
auto clip_t1 = std::min(t1, GetSequenceEndTime());
auto s0 = TimeToSequenceSamples(clip_t0);
auto s1 = TimeToSequenceSamples(clip_t1);
if (s0 == s1)
return {};
// use Strong-guarantee
for (auto &pSequence : mSequences)
pSequence->Delete(s0, s1 - s0);
return { this, t0, t1, clip_t0, clip_t1 };
}
WaveClip::ClearSequenceFinisher::~ClearSequenceFinisher() noexcept
{
if (!pClip || !committed)
return;
// use No-fail-guarantee in the remaining
// msmeyer
//
// Delete all cutlines that are within the given area, if any.
//
// Note that when cutlines are active, two functions are used:
// Clear() and ClearAndAddCutLine(). ClearAndAddCutLine() is called
// whenever the user directly calls a command that removes some audio, e.g.
// "Cut" or "Clear" from the menu. This command takes care about recursive
// preserving of cutlines within clips. Clear() is called when internal
// operations want to remove audio. In the latter case, it is the right
// thing to just remove all cutlines within the area.
//
// May DELETE as we iterate, so don't use range-for
for (auto it = pClip->mCutLines.begin(); it != pClip->mCutLines.end();)
{
WaveClip* clip = it->get();
double cutlinePosition =
pClip->GetSequenceStartTime() + clip->GetSequenceStartTime();
if (cutlinePosition >= t0 && cutlinePosition <= t1)
{
// This cutline is within the area, DELETE it
it = pClip->mCutLines.erase(it);
}
else
{
if (cutlinePosition >= t1)
{
clip->ShiftBy(clip_t0 - clip_t1);
}
++it;
}
}
// Collapse envelope
auto sampleTime = 1.0 / pClip->GetRate();
pClip->GetEnvelope().CollapseRegion(t0, t1, sampleTime);
}
/*! @excsafety{Weak}
-- This WaveClip remains destructible in case of AudacityException.
But some cutlines may be deleted */
void WaveClip::ClearAndAddCutLine(double t0, double t1)
{
StrongInvariantScope scope{ *this };
if (t0 > GetPlayEndTime() || t1 < GetPlayStartTime() || CountSamples(t0, t1) == 0)
return; // no samples to remove
Transaction transaction{ *this };
const double clip_t0 = std::max( t0, GetPlayStartTime() );
const double clip_t1 = std::min( t1, GetPlayEndTime() );
auto newClip = std::make_shared<WaveClip>(
*this, GetFactory(), true, clip_t0, clip_t1);
if(t1 < GetPlayEndTime())
{
newClip->ClearSequence(t1, newClip->GetSequenceEndTime())
.Commit();
newClip->SetTrimRight(.0);
}
if(t0 > GetPlayStartTime())
{
newClip->ClearSequence(newClip->GetSequenceStartTime(), t0)
.Commit();
newClip->SetTrimLeft(.0);
}
newClip->SetSequenceStartTime( clip_t0 - GetSequenceStartTime() );
// Remove cutlines from this clip that were in the selection, shift
// left those that were after the selection
// May DELETE as we iterate, so don't use range-for
for (auto it = mCutLines.begin(); it != mCutLines.end();)
{
WaveClip* clip = it->get();
double cutlinePosition = GetSequenceStartTime() + clip->GetSequenceStartTime();
if (cutlinePosition >= t0 && cutlinePosition <= t1)
it = mCutLines.erase(it);
else
{
if (cutlinePosition >= t1)
{
clip->ShiftBy(clip_t0 - clip_t1);
}
++it;
}
}
// Clear actual audio data
auto s0 = TimeToSequenceSamples(t0);
auto s1 = TimeToSequenceSamples(t1);
// use Weak-guarantee
for (auto &pSequence : mSequences)
pSequence->Delete(s0, s1-s0);
// Collapse envelope
auto sampleTime = 1.0 / GetRate();
GetEnvelope().CollapseRegion( t0, t1, sampleTime );
transaction.Commit();
MarkChanged();
AddCutLine(std::move(newClip));
}
void WaveClip::AddCutLine(WaveClipHolder pClip)
{
assert(NChannels() == pClip->NChannels());
mCutLines.push_back(std::move(pClip));
// New clip is assumed to have correct width
assert(CheckInvariants());
}
bool WaveClip::FindCutLine(double cutLinePosition,
double* cutlineStart /* = NULL */,
double* cutlineEnd /* = NULL */) const
{
for (const auto &cutline: mCutLines)
{
if (fabs(GetSequenceStartTime() + cutline->GetSequenceStartTime() - cutLinePosition) < 0.0001)
{
auto startTime = GetSequenceStartTime() + cutline->GetSequenceStartTime();
if (cutlineStart)
*cutlineStart = startTime;
if (cutlineEnd)
*cutlineEnd = startTime + cutline->SamplesToTime(cutline->GetVisibleSampleCount());
return true;
}
}
return false;
}
/*! @excsafety{Strong} */
void WaveClip::ExpandCutLine(double cutLinePosition)
{
auto end = mCutLines.end();
auto it = std::find_if( mCutLines.begin(), end,
[&](const WaveClipHolder &cutline) {
return fabs(GetSequenceStartTime() + cutline->GetSequenceStartTime() - cutLinePosition) < 0.0001;
} );
if ( it != end ) {
auto *cutline = it->get();
// assume Strong-guarantee from Paste
// Envelope::Paste takes offset into account, WaveClip::Paste doesn't!
// Do this to get the right result:
cutline->mEnvelope->SetOffset(0);
bool success = Paste(
GetSequenceStartTime() + cutline->GetSequenceStartTime(), *cutline);
assert(success); // class invariant promises cutlines have correct width
// Now erase the cutline,
// but be careful to find it again, because Paste above may
// have modified the array of cutlines (if our cutline contained
// another cutline!), invalidating the iterator we had.
end = mCutLines.end();
it = std::find_if(mCutLines.begin(), end,
[=](const WaveClipHolder &p) { return p.get() == cutline; });
if (it != end)
mCutLines.erase(it); // deletes cutline!
else {
wxASSERT(false);
}
}
}
bool WaveClip::RemoveCutLine(double cutLinePosition)
{
for (auto it = mCutLines.begin(); it != mCutLines.end(); ++it)
{
const auto &cutline = *it;
//std::numeric_limits<double>::epsilon() or (1.0 / static_cast<double>(mRate))?
if (fabs(GetSequenceStartTime() + cutline->GetSequenceStartTime() - cutLinePosition) < 0.0001)
{
mCutLines.erase(it); // deletes cutline!
return true;
}
}
return false;
}
/*! @excsafety{No-fail} */
void WaveClip::OffsetCutLines(double t0, double len)
{
for (const auto &cutLine : mCutLines)
{
if (GetSequenceStartTime() + cutLine->GetSequenceStartTime() >= t0)
cutLine->ShiftBy(len);
}
}
void WaveClip::CloseLock() noexcept
{
// Don't need a Transaction for noexcept operations
for (auto &pSequence : mSequences)
pSequence->CloseLock();
for (const auto &cutline: mCutLines)
cutline->CloseLock();
}
void WaveClip::SetRate(int rate)
{
const auto trimLeftSampleNum = TimeToSamples(mTrimLeft);
const auto trimRightSampleNum = TimeToSamples(mTrimRight);
auto ratio = static_cast<double>(mRate) / rate;
mRate = rate;
mTrimLeft = SamplesToTime(trimLeftSampleNum);
mTrimRight = SamplesToTime(trimRightSampleNum);
const auto newLength =
GetNumSamples().as_double() * GetStretchRatio() / mRate;
mEnvelope->RescaleTimes(newLength);
MarkChanged();
SetSequenceStartTime(GetSequenceStartTime() * ratio);
}
void WaveClip::SetRawAudioTempo(double tempo)
{
mRawAudioTempo = tempo;
}
bool WaveClip::SetCentShift(int cents)
{
if (
cents < TimeAndPitchInterface::MinCents ||
cents > TimeAndPitchInterface::MaxCents)
return false;
mCentShift = cents;
Observer::Publisher<CentShiftChange>::Publish(CentShiftChange { cents });
return true;
}
void WaveClip::SetPitchAndSpeedPreset(PitchAndSpeedPreset preset)
{
mPitchAndSpeedPreset = preset;
Observer::Publisher<PitchAndSpeedPresetChange>::Publish(
PitchAndSpeedPresetChange { mPitchAndSpeedPreset });
}
PitchAndSpeedPreset WaveClip::GetPitchAndSpeedPreset() const
{
return mPitchAndSpeedPreset;
}
/*! @excsafety{Strong} */
void WaveClip::Resample(int rate, BasicUI::ProgressDialog *progress)
{
// This mutator does not require the strong invariant.
// Note: it is not necessary to do this recursively to cutlines.
// They get resampled as needed when they are expanded.
if (rate == mRate)
return; // Nothing to do
// This function does its own RAII without a Transaction
double factor = (double)rate / (double)mRate;
//Resample is always configured to have single channel.
//Create Resample instance per each channel in the clip
std::vector<::Resample> resample;
for (unsigned n = 0; n < mSequences.size(); ++n)
resample.emplace_back(true, factor, factor);// constant rate resampling
const size_t bufsize = 65536;
Floats inBuffer{ bufsize };
Floats outBuffer{ bufsize };
sampleCount pos = 0;
bool error = false;
int outGenerated = 0;
const auto numSamples = GetNumSamples();
// These sequences are appended to below
auto newSequences = GetEmptySequenceCopies();
/**
* We want to keep going as long as we have something to feed the resampler
* with OR as long as the resampler spews out samples (which could continue
* for a few iterations after we stop feeding it)
*/
while (pos < numSamples || outGenerated > 0) {
const auto inLen = limitSampleBufferSize( bufsize, numSamples - pos );
bool isLast = ((pos + inLen) == numSamples);
auto ppNewSequence = newSequences.begin();
std::optional<std::pair<size_t, size_t>> results{};
size_t nSequence = 0;
for (auto &pSequence : mSequences) {
auto &pNewSequence = *ppNewSequence++;
if (
inLen > 0 &&
!pSequence->Get(
(samplePtr)inBuffer.get(), floatSample, pos, inLen, true))
{
error = true;
break;
}
// Expect the same results for all channels, or else fail
auto newResults = resample[nSequence].Process(factor, inBuffer.get(), inLen,
isLast, outBuffer.get(), bufsize);
if (!results)
results.emplace(newResults);
else if (*results != newResults) {
error = true;
break;
}
outGenerated = results->second;
if (outGenerated < 0) {
error = true;
break;
}
pNewSequence->Append((samplePtr)outBuffer.get(), floatSample,
outGenerated, 1,
widestSampleFormat /* computed samples need dither */
);
++nSequence;
}
if (results)
pos += results->first;
if (progress)
{
auto updateResult = progress->Poll(
pos.as_long_long(),
numSamples.as_long_long()
);
error = (updateResult != BasicUI::ProgressResult::Success);
if (error)
throw UserException{};
}
}
if (error)
throw SimpleMessageBoxException{
ExceptionType::Internal,
XO("Resampling failed."),
XO("Warning"),
"Error:_Resampling"
};
else
{
// Use No-fail-guarantee in these steps
mSequences = std::move(newSequences);
mRate = rate;
Flush();
Attachments::ForEach( std::mem_fn( &WaveClipListener::Invalidate ) );
MarkChanged();
}
}
void WaveClip::SetName(const wxString& name)
{
mName = name;
}
const wxString& WaveClip::GetName() const
{
return mName;
}
sampleCount WaveClip::TimeToSamples(double time) const
{
return sampleCount(floor(time * mRate / GetStretchRatio() + 0.5));
}
double WaveClip::SamplesToTime(sampleCount s) const noexcept
{
return s.as_double() * GetStretchRatio() / mRate;
}
double WaveClip::SnapToTrackSample(double t) const noexcept
{
return std::round(t * mRate) / mRate;
}
void WaveClip::SetSilence(sampleCount offset, sampleCount length)
{
StrongInvariantScope scope{ *this };
const auto start = TimeToSamples(mTrimLeft) + offset;
Transaction transaction{ *this };
for (auto &pSequence : mSequences)
pSequence->SetSilence(start, length);
transaction.Commit();
MarkChanged();
}
sampleCount WaveClip::GetSequenceSamplesCount() const
{
return GetNumSamples() * NChannels();
}
double WaveClip::GetPlayStartTime() const noexcept
{
return SnapToTrackSample(mSequenceOffset + mTrimLeft);
}
void WaveClip::SetPlayStartTime(double time)
{
SetSequenceStartTime(time - mTrimLeft);
}
double WaveClip::GetPlayEndTime() const
{
const auto numSamples = GetNumSamples();
double maxLen = mSequenceOffset +
((numSamples + GreatestAppendBufferLen()).as_double()) *
GetStretchRatio() / mRate -
mTrimRight;
// JS: calculated value is not the length;
// it is a maximum value and can be negative; no clipping to 0
return SnapToTrackSample(maxLen);
}
double WaveClip::GetCommittedEndTime() const
{
const auto numSamples = GetNumSamples();
double maxLen = mSequenceOffset - mTrimRight +
numSamples.as_double() * GetStretchRatio() / mRate;
// JS: calculated value is not the length;
// it is a maximum value and can be negative; no clipping to 0
return SnapToTrackSample(maxLen);
}
double WaveClip::GetPlayDuration() const
{
return GetPlayEndTime() - GetPlayStartTime();
}
bool WaveClip::IsEmpty() const
{
return std::floor(GetPlayDuration() * mRate + 0.5) < 2.0;
}
sampleCount WaveClip::GetPlayStartSample() const
{
return sampleCount { GetPlayStartTime() * mRate + 0.5 };
}
sampleCount WaveClip::GetPlayEndSample() const
{
return sampleCount { GetPlayEndTime() * mRate + 0.5 };
}
sampleCount WaveClip::GetVisibleSampleCount() const
{
return GetNumSamples()
- TimeToSamples(mTrimRight) - TimeToSamples(mTrimLeft);
}
void WaveClip::SetTrimLeft(double trim)
{
mTrimLeft = std::max(.0, trim);
}
double WaveClip::GetTrimLeft() const noexcept
{
return mTrimLeft;
}
void WaveClip::SetTrimRight(double trim)
{
mTrimRight = std::max(.0, trim);
}
double WaveClip::GetTrimRight() const noexcept
{
return mTrimRight;
}
void WaveClip::TrimLeft(double deltaTime)
{
SetTrimLeft(mTrimLeft + deltaTime);
}
void WaveClip::TrimRight(double deltaTime)
{
SetTrimRight(mTrimRight + deltaTime);
}
void WaveClip::TrimQuarternotesFromRight(double quarters)
{
assert(mRawAudioTempo.has_value());
if (!mRawAudioTempo.has_value())
return;
const auto secondsPerQuarter = 60 * GetStretchRatio() / *mRawAudioTempo;
// MH https://github.com/audacity/audacity/issues/5878: Clip boundaries are
// quantized to the sample period. Music durations aren't, though.
// `quarters` was probably chosen such that the clip ends exactly at some
// musical grid snapping point. However, if we right-trim by `quarters`,
// the clip's play end time might be rounded up to the next sample period,
// overlapping the next snapping point on the musical grid. We don't want
// this, or it would disturb music producers who want to horizontally
// duplicate loops.
const auto quantizedTrim =
std::ceil(quarters * secondsPerQuarter * GetRate()) / GetRate();
TrimRight(quantizedTrim);
}
void WaveClip::TrimLeftTo(double to)
{
mTrimLeft =
std::clamp(to, SnapToTrackSample(mSequenceOffset), GetPlayEndTime()) -
mSequenceOffset;
}
void WaveClip::TrimRightTo(double to)
{
const auto endTime = SnapToTrackSample(GetSequenceEndTime());
mTrimRight = endTime - std::clamp(to, GetPlayStartTime(), endTime);
}
double WaveClip::GetSequenceStartTime() const noexcept
{
// JS: mSequenceOffset is the minimum value and it is returned; no clipping to 0
// Do we need to `SnapToTrackSample` before returning?
return mSequenceOffset;
}
void WaveClip::SetSequenceStartTime(double startTime)
{
mSequenceOffset = startTime;
mEnvelope->SetOffset(startTime);
}
double WaveClip::GetSequenceEndTime() const
{
const auto numSamples = GetNumSamples();
double maxLen = GetSequenceStartTime() +
numSamples.as_double() * GetStretchRatio() / mRate;
return maxLen;
}
sampleCount WaveClip::GetSequenceStartSample() const
{
return TimeToSamples(mSequenceOffset);
}
void WaveClip::ShiftBy(double delta) noexcept
{
SetSequenceStartTime(GetSequenceStartTime() + delta);
MarkChanged();
}
bool WaveClip::SplitsPlayRegion(double t) const
{
return GetPlayStartTime() < t && t < GetPlayEndTime();
}
bool WaveClip::WithinPlayRegion(double t) const
{
return GetPlayStartTime() <= t && t < GetPlayEndTime();
}
bool WaveClip::EntirelyWithinPlayRegion(double t0, double t1) const
{
assert(t0 <= t1);
// t1 is the open end of the interval, hence it's ok if it's equal to the
// open end of the play region.
return !BeforePlayRegion(t0) && t1 <= GetPlayEndTime();
}
bool WaveClip::PartlyWithinPlayRegion(double t0, double t1) const
{
assert(t0 <= t1);
return WithinPlayRegion(t0) != WithinPlayRegion(t1);
}
bool WaveClip::IntersectsPlayRegion(double t0, double t1) const
{
assert(t0 <= t1);
// t1 is the open end of the interval, so it must be excluded from the closed
// begin of the play region.
return t0 < GetPlayEndTime() && GetPlayStartTime() < t1;
}
bool WaveClip::CoversEntirePlayRegion(double t0, double t1) const
{
assert(t0 <= t1);
return t0 <= GetPlayStartTime() && GetPlayEndTime() <= t1;
}
bool WaveClip::BeforePlayRegion(double t) const
{
return t < GetPlayStartTime();
}
bool WaveClip::AtOrBeforePlayRegion(double t) const
{
return t <= GetPlayStartTime();
}
bool WaveClip::AfterPlayRegion(double t) const
{
return GetPlayEndTime() <= t;
}
sampleCount WaveClip::CountSamples(double t0, double t1) const
{
if(t0 < t1)
{
t0 = std::max(t0, GetPlayStartTime());
t1 = std::min(t1, GetPlayEndTime());
const auto s0 = TimeToSamples(t0 - GetPlayStartTime());
const auto s1 = TimeToSamples(t1 - GetPlayStartTime());
return s1 - s0;
}
return { 0 };
}
sampleCount WaveClip::TimeToSequenceSamples(double t) const
{
if (t < GetSequenceStartTime())
return 0;
else if (t > GetSequenceEndTime())
return GetNumSamples();
return TimeToSamples(t - GetSequenceStartTime());
}
bool WaveClip::CheckInvariants() const
{
const auto width = NChannels();
auto iter = mSequences.begin(),
end = mSequences.end();
// There must be at least one pointer
if (iter != end) {
// All pointers mut be non-null
auto &pFirst = *iter++;
if (pFirst) {
// All sequences must have the same sample formats, and sample block
// factory
return
std::all_of(iter, end, [&](decltype(pFirst) pSequence) {
return pSequence &&
pSequence->GetSampleFormats() == pFirst->GetSampleFormats() &&
pSequence->GetFactory() == pFirst->GetFactory();
}) &&
// All cut lines are non-null, satisfy the invariants, and match width
std::all_of(mCutLines.begin(), mCutLines.end(),
[width](const WaveClipHolder &pCutLine) {
if (!(pCutLine && pCutLine->NChannels() == width))
return false;
if (!pCutLine->StrongInvariant()) {
pCutLine->AssertOrRepairStrongInvariant();
return false;
}
return true;
});
}
}
return false;
}
bool WaveClip::StrongInvariant() const
{
if (!CheckInvariants())
return false;
const auto width = NChannels();
auto iter = mSequences.begin(),
end = mSequences.end();
assert(iter != end); // because CheckInvariants is true
auto &pFirst = *iter++;
assert(pFirst); // likewise
// All sequences must have the same lengths
return all_of(iter, end, [&](decltype(pFirst) pSequence) {
assert(pSequence); // likewise
return pSequence->GetNumSamples() == pFirst->GetNumSamples();
});
}
void WaveClip::AssertOrRepairStrongInvariant()
{
if (!StrongInvariant()) {
assert(false);
RepairChannels();
assert(StrongInvariant());
}
}
WaveClip::StrongInvariantScope::StrongInvariantScope(WaveClip &clip)
: mClip{ clip }
{
mClip.AssertOrRepairStrongInvariant();
}
WaveClip::StrongInvariantScope::~StrongInvariantScope()
{
assert(mClip.StrongInvariant());
}
WaveClip::Transaction::Transaction(WaveClip &clip)
: clip{ clip }
, mTrimLeft{ clip.mTrimLeft }
, mTrimRight{ clip.mTrimRight }
{
sequences.reserve(clip.mSequences.size());
auto &factory = clip.GetFactory();
for (auto &pSequence : clip.mSequences)
sequences.push_back(
//! Does not copy un-flushed append buffer data
std::make_unique<Sequence>(*pSequence, factory));
}
WaveClip::Transaction::~Transaction()
{
if (!committed) {
clip.mSequences.swap(sequences);
clip.mTrimLeft = mTrimLeft;
clip.mTrimRight = mTrimRight;
}
}