/********************************************************************** Audacity: A Digital Audio Editor WaveTrack.cpp Dominic Mazzoni *******************************************************************//** \class WaveTrack \brief A Track that contains audio waveform data. *//****************************************************************//** \class WaveTrack::Location \brief Used only by WaveTrack, a special way to hold location that can accommodate merged regions. *//****************************************************************/ /*! @class WaveTrackFactory @brief Used to create or clone a WaveTrack, with appropriate context from the project that will own the track. */ #include "WaveTrack.h" #include "WaveClip.h" #include #include #include #include #include #include #include "float_cast.h" #include "Envelope.h" #include "Sequence.h" #include "ProjectFileIORegistry.h" #include "ProjectSettings.h" #include "Prefs.h" #include "effects/TimeWarper.h" #include "prefs/QualitySettings.h" #include "prefs/SpectrogramSettings.h" #include "prefs/TracksPrefs.h" #include "prefs/TracksBehaviorsPrefs.h" #include "prefs/WaveformSettings.h" #include "InconsistencyException.h" #include "tracks/ui/TrackView.h" #include "tracks/ui/TrackControls.h" using std::max; static ProjectFileIORegistry::Entry registerFactory{ wxT( "wavetrack" ), []( AudacityProject &project ){ auto &trackFactory = WaveTrackFactory::Get( project ); auto &tracks = TrackList::Get( project ); auto result = tracks.Add(trackFactory.NewWaveTrack()); TrackView::Get( *result ); TrackControls::Get( *result ); return result; } }; WaveTrack::Holder WaveTrackFactory::DuplicateWaveTrack(const WaveTrack &orig) { return std::static_pointer_cast( orig.Duplicate() ); } WaveTrack::Holder WaveTrackFactory::NewWaveTrack(sampleFormat format, double rate) { if (format == (sampleFormat)0) format = QualitySettings::SampleFormatChoice(); if (rate == 0) rate = mSettings.GetRate(); return std::make_shared ( mpFactory, format, rate ); } WaveTrack::WaveTrack( const SampleBlockFactoryPtr &pFactory, sampleFormat format, double rate ) : PlayableTrack() , mpFactory(pFactory) { mLegacyProjectFileOffset = 0; mFormat = format; mRate = (int) rate; mGain = 1.0; mPan = 0.0; mOldGain[0] = 0.0; mOldGain[1] = 0.0; mWaveColorIndex = 0; SetDefaultName(TracksPrefs::GetDefaultAudioTrackNamePreference()); SetName(GetDefaultName()); mDisplayMin = -1.0; mDisplayMax = 1.0; mSpectrumMin = mSpectrumMax = -1; // so values will default to settings mLastScaleType = -1; mLastdBRange = -1; } WaveTrack::WaveTrack(const WaveTrack &orig): PlayableTrack(orig) , mpFactory( orig.mpFactory ) , mpSpectrumSettings(orig.mpSpectrumSettings ? std::make_unique(*orig.mpSpectrumSettings) : nullptr ) , mpWaveformSettings(orig.mpWaveformSettings ? std::make_unique(*orig.mpWaveformSettings) : nullptr ) { mLastScaleType = -1; mLastdBRange = -1; mLegacyProjectFileOffset = 0; Init(orig); for (const auto &clip : orig.mClips) mClips.push_back ( std::make_unique( *clip, mpFactory, true ) ); } // Copy the track metadata but not the contents. void WaveTrack::Init(const WaveTrack &orig) { PlayableTrack::Init(orig); mpFactory = orig.mpFactory; mFormat = orig.mFormat; mWaveColorIndex = orig.mWaveColorIndex; mRate = orig.mRate; mGain = orig.mGain; mPan = orig.mPan; mOldGain[0] = 0.0; mOldGain[1] = 0.0; SetDefaultName(orig.GetDefaultName()); SetName(orig.GetName()); mDisplayMin = orig.mDisplayMin; mDisplayMax = orig.mDisplayMax; mSpectrumMin = orig.mSpectrumMin; mSpectrumMax = orig.mSpectrumMax; mDisplayLocationsCache.clear(); } void WaveTrack::Reinit(const WaveTrack &orig) { Init(orig); { auto &settings = orig.mpSpectrumSettings; if (settings) mpSpectrumSettings = std::make_unique(*settings); else mpSpectrumSettings.reset(); } { auto &settings = orig.mpWaveformSettings; if (settings) mpWaveformSettings = std::make_unique(*settings); else mpWaveformSettings.reset(); } this->SetOffset(orig.GetOffset()); } void WaveTrack::Merge(const Track &orig) { orig.TypeSwitch( [&](const WaveTrack *pwt) { const WaveTrack &wt = *pwt; mGain = wt.mGain; mPan = wt.mPan; mDisplayMin = wt.mDisplayMin; mDisplayMax = wt.mDisplayMax; SetSpectrogramSettings(wt.mpSpectrumSettings ? std::make_unique(*wt.mpSpectrumSettings) : nullptr); SetWaveformSettings (wt.mpWaveformSettings ? std::make_unique(*wt.mpWaveformSettings) : nullptr); }); PlayableTrack::Merge(orig); } WaveTrack::~WaveTrack() { } double WaveTrack::GetOffset() const { return GetStartTime(); } /*! @excsafety{No-fail} */ void WaveTrack::SetOffset(double o) { double delta = o - GetOffset(); for (const auto &clip : mClips) // assume No-fail-guarantee clip->SetOffset(clip->GetOffset() + delta); mOffset = o; } auto WaveTrack::GetChannelIgnoringPan() const -> ChannelType { return mChannel; } auto WaveTrack::GetChannel() const -> ChannelType { if( mChannel != Track::MonoChannel ) return mChannel; auto pan = GetPan(); if( pan < -0.99 ) return Track::LeftChannel; if( pan > 0.99 ) return Track::RightChannel; return mChannel; } void WaveTrack::SetPanFromChannelType() { if( mChannel == Track::LeftChannel ) SetPan( -1.0f ); else if( mChannel == Track::RightChannel ) SetPan( 1.0f ); }; void WaveTrack::SetLastScaleType() const { mLastScaleType = GetWaveformSettings().scaleType; } void WaveTrack::SetLastdBRange() const { mLastdBRange = GetWaveformSettings().dBRange; } void WaveTrack::GetDisplayBounds(float *min, float *max) const { *min = mDisplayMin; *max = mDisplayMax; } void WaveTrack::SetDisplayBounds(float min, float max) const { mDisplayMin = min; mDisplayMax = max; } void WaveTrack::GetSpectrumBounds(float *min, float *max) const { const double rate = GetRate(); const SpectrogramSettings &settings = GetSpectrogramSettings(); const SpectrogramSettings::ScaleType type = settings.scaleType; const float top = (rate / 2.); float bottom; if (type == SpectrogramSettings::stLinear) bottom = 0.0f; else if (type == SpectrogramSettings::stPeriod) { // special case const auto half = settings.GetFFTLength() / 2; // EAC returns no data for below this frequency: const float bin2 = rate / half; bottom = bin2; } else // logarithmic, etc. bottom = 1.0f; { float spectrumMax = mSpectrumMax; if (spectrumMax < 0) spectrumMax = settings.maxFreq; if (spectrumMax < 0) *max = top; else *max = std::max(bottom, std::min(top, spectrumMax)); } { float spectrumMin = mSpectrumMin; if (spectrumMin < 0) spectrumMin = settings.minFreq; if (spectrumMin < 0) *min = std::max(bottom, top / 1000.0f); else *min = std::max(bottom, std::min(top, spectrumMin)); } } void WaveTrack::SetSpectrumBounds(float min, float max) const { mSpectrumMin = min; mSpectrumMax = max; } int WaveTrack::ZeroLevelYCoordinate(wxRect rect) const { return rect.GetTop() + (int)((mDisplayMax / (mDisplayMax - mDisplayMin)) * rect.height); } template< typename Container > static Container MakeIntervals(const std::vector &clips) { Container result; for (const auto &clip: clips) { result.emplace_back( clip->GetStartTime(), clip->GetEndTime(), std::make_unique( clip ) ); } return result; } Track::Holder WaveTrack::PasteInto( AudacityProject &project ) const { auto &trackFactory = WaveTrackFactory::Get( project ); auto &pSampleBlockFactory = trackFactory.GetSampleBlockFactory(); auto pNewTrack = EmptyCopy( pSampleBlockFactory ); pNewTrack->Paste(0.0, this); return pNewTrack; } auto WaveTrack::GetIntervals() const -> ConstIntervals { return MakeIntervals( mClips ); } auto WaveTrack::GetIntervals() -> Intervals { return MakeIntervals( mClips ); } Track::Holder WaveTrack::Clone() const { return std::make_shared( *this ); } double WaveTrack::GetRate() const { return mRate; } void WaveTrack::SetRate(double newRate) { wxASSERT( newRate > 0 ); newRate = std::max( 1.0, newRate ); auto ratio = mRate / newRate; mRate = (int) newRate; for (const auto &clip : mClips) { clip->SetRate((int)newRate); clip->SetOffset( clip->GetOffset() * ratio ); } } float WaveTrack::GetGain() const { return mGain; } void WaveTrack::SetGain(float newGain) { if (mGain != newGain) { mGain = newGain; Notify(); } } float WaveTrack::GetPan() const { return mPan; } void WaveTrack::SetPan(float newPan) { if (newPan > 1.0) newPan = 1.0; else if (newPan < -1.0) newPan = -1.0; if ( mPan != newPan ) { mPan = newPan; Notify(); } } float WaveTrack::GetChannelGain(int channel) const { float left = 1.0; float right = 1.0; if (mPan < 0) right = (mPan + 1.0); else if (mPan > 0) left = 1.0 - mPan; if ((channel%2) == 0) return left*mGain; else return right*mGain; } float WaveTrack::GetOldChannelGain(int channel) const { return mOldGain[channel%2]; } void WaveTrack::SetOldChannelGain(int channel, float gain) { mOldGain[channel % 2] = gain; } /*! @excsafety{Strong} */ void WaveTrack::SetWaveColorIndex(int colorIndex) { for (const auto &clip : mClips) clip->SetColourIndex( colorIndex ); mWaveColorIndex = colorIndex; } sampleCount WaveTrack::GetNumSamples() const { sampleCount result{ 0 }; for (const auto& clip : mClips) result += clip->GetNumSamples(); return result; } /*! @excsafety{Weak} -- Might complete on only some clips */ void WaveTrack::ConvertToSampleFormat(sampleFormat format, const std::function & progressReport) { for (const auto& clip : mClips) clip->ConvertToSampleFormat(format, progressReport); mFormat = format; } bool WaveTrack::IsEmpty(double t0, double t1) const { if (t0 > t1) return true; //wxPrintf("Searching for overlap in %.6f...%.6f\n", t0, t1); for (const auto &clip : mClips) { if (!clip->BeforeClip(t1) && !clip->AfterClip(t0)) { //wxPrintf("Overlapping clip: %.6f...%.6f\n", // clip->GetStartTime(), // clip->GetEndTime()); // We found a clip that overlaps this region return false; } } //wxPrintf("No overlap found\n"); // Otherwise, no clips overlap this region return true; } Track::Holder WaveTrack::Cut(double t0, double t1) { if (t1 < t0) THROW_INCONSISTENCY_EXCEPTION; auto tmp = Copy(t0, t1); Clear(t0, t1); return tmp; } /*! @excsafety{Strong} */ Track::Holder WaveTrack::SplitCut(double t0, double t1) { if (t1 < t0) THROW_INCONSISTENCY_EXCEPTION; // SplitCut is the same as 'Copy', then 'SplitDelete' auto tmp = Copy(t0, t1); SplitDelete(t0, t1); return tmp; } #if 0 Track::Holder WaveTrack::CutAndAddCutLine(double t0, double t1) { if (t1 < t0) THROW_INCONSISTENCY_EXCEPTION; // Cut is the same as 'Copy', then 'Delete' auto tmp = Copy(t0, t1); ClearAndAddCutLine(t0, t1); return tmp; } #endif //Trim trims within a clip, rather than trimming everything. //If a bound is outside a clip, it trims everything. /*! @excsafety{Weak} */ void WaveTrack::Trim (double t0, double t1) { bool inside0 = false; bool inside1 = false; //Keeps track of the offset of the first clip greater than // the left selection t0. double firstGreaterOffset = -1; for (const auto &clip : mClips) { //Find the first clip greater than the offset. //If we end up clipping the entire track, this is useful. if(firstGreaterOffset < 0 && clip->GetStartTime() >= t0) firstGreaterOffset = clip->GetStartTime(); if(t1 > clip->GetStartTime() && t1 < clip->GetEndTime()) { clip->Clear(t1,clip->GetEndTime()); inside1 = true; } if(t0 > clip->GetStartTime() && t0 < clip->GetEndTime()) { clip->Clear(clip->GetStartTime(),t0); clip->SetOffset(t0); inside0 = true; } } //if inside0 is false, then the left selector was between //clips, so DELETE everything to its left. if(!inside1 && t1 < GetEndTime()) Clear(t1,GetEndTime()); if(!inside0 && t0 > GetStartTime()) SplitDelete(GetStartTime(), t0); } WaveTrack::Holder WaveTrack::EmptyCopy( const SampleBlockFactoryPtr &pFactory ) const { auto result = std::make_shared( pFactory, mFormat, mRate ); result->Init(*this); result->mpFactory = pFactory ? pFactory : mpFactory; return result; } Track::Holder WaveTrack::Copy(double t0, double t1, bool forClipboard) const { if (t1 < t0) THROW_INCONSISTENCY_EXCEPTION; auto result = EmptyCopy(); WaveTrack *newTrack = result.get(); // PRL: Why shouldn't cutlines be copied and pasted too? I don't know, but // that was the old behavior. But this function is also used by the // Duplicate command and I changed its behavior in that case. for (const auto &clip : mClips) { if (t0 <= clip->GetStartTime() && t1 >= clip->GetEndTime()) { // Whole clip is in copy region //wxPrintf("copy: clip %i is in copy region\n", (int)clip); newTrack->mClips.push_back (std::make_unique(*clip, mpFactory, ! forClipboard)); WaveClip *const newClip = newTrack->mClips.back().get(); newClip->Offset(-t0); } else if (t1 > clip->GetStartTime() && t0 < clip->GetEndTime()) { // Clip is affected by command //wxPrintf("copy: clip %i is affected by command\n", (int)clip); const double clip_t0 = std::max(t0, clip->GetStartTime()); const double clip_t1 = std::min(t1, clip->GetEndTime()); auto newClip = std::make_unique (*clip, mpFactory, ! forClipboard, clip_t0, clip_t1); //wxPrintf("copy: clip_t0=%f, clip_t1=%f\n", clip_t0, clip_t1); newClip->Offset(-t0); if (newClip->GetOffset() < 0) newClip->SetOffset(0); newTrack->mClips.push_back(std::move(newClip)); // transfer ownership } } // AWD, Oct 2009: If the selection ends in whitespace, create a placeholder // clip representing that whitespace // PRL: Only if we want the track for pasting into other tracks. Not if it // goes directly into a project as in the Duplicate command. if (forClipboard && newTrack->GetEndTime() + 1.0 / newTrack->GetRate() < t1 - t0) { auto placeholder = std::make_unique(mpFactory, newTrack->GetSampleFormat(), static_cast(newTrack->GetRate()), 0 /*colourindex*/); placeholder->SetIsPlaceholder(true); placeholder->InsertSilence(0, (t1 - t0) - newTrack->GetEndTime()); placeholder->Offset(newTrack->GetEndTime()); newTrack->mClips.push_back(std::move(placeholder)); // transfer ownership } return result; } Track::Holder WaveTrack::CopyNonconst(double t0, double t1) { return Copy(t0, t1); } /*! @excsafety{Strong} */ void WaveTrack::Clear(double t0, double t1) { HandleClear(t0, t1, false, false); } /*! @excsafety{Strong} */ void WaveTrack::ClearAndAddCutLine(double t0, double t1) { HandleClear(t0, t1, true, false); } const SpectrogramSettings &WaveTrack::GetSpectrogramSettings() const { if (mpSpectrumSettings) return *mpSpectrumSettings; else return SpectrogramSettings::defaults(); } SpectrogramSettings &WaveTrack::GetSpectrogramSettings() { if (mpSpectrumSettings) return *mpSpectrumSettings; else return SpectrogramSettings::defaults(); } SpectrogramSettings &WaveTrack::GetIndependentSpectrogramSettings() { if (!mpSpectrumSettings) mpSpectrumSettings = std::make_unique(SpectrogramSettings::defaults()); return *mpSpectrumSettings; } void WaveTrack::SetSpectrogramSettings(std::unique_ptr &&pSettings) { if (mpSpectrumSettings != pSettings) { mpSpectrumSettings = std::move(pSettings); } } void WaveTrack::UseSpectralPrefs( bool bUse ) { if( bUse ){ if( !mpSpectrumSettings ) return; // reset it, and next we will be getting the defaults. mpSpectrumSettings.reset(); } else { if( mpSpectrumSettings ) return; GetIndependentSpectrogramSettings(); } } const WaveformSettings &WaveTrack::GetWaveformSettings() const { // Create on demand return const_cast(this)->GetWaveformSettings(); } WaveformSettings &WaveTrack::GetWaveformSettings() { // Create on demand if (!mpWaveformSettings) mpWaveformSettings = std::make_unique(WaveformSettings::defaults()); return *mpWaveformSettings; } void WaveTrack::SetWaveformSettings(std::unique_ptr &&pSettings) { if (mpWaveformSettings != pSettings) { mpWaveformSettings = std::move(pSettings); } } // // ClearAndPaste() is a specialized version of HandleClear() // followed by Paste() and is used mostly by effects that // can't replace track data directly using Get()/Set(). // // HandleClear() removes any cut/split lines with the // cleared range, but, in most cases, effects want to preserve // the existing cut/split lines, so they are saved before the // HandleClear()/Paste() and restored after. // // If the pasted track overlaps two or more clips, then it will // be pasted with visible split lines. Normally, effects do not // want these extra lines, so they may be merged out. // /*! @excsafety{Weak} -- This WaveTrack remains destructible in case of AudacityException. But some of its cutline clips may have been destroyed. */ void WaveTrack::ClearAndPaste(double t0, // Start of time to clear double t1, // End of time to clear const Track *src, // What to paste bool preserve, // Whether to reinsert splits/cuts bool merge, // Whether to remove 'extra' splits const TimeWarper *effectWarper // How does time change ) { double dur = std::min(t1 - t0, src->GetEndTime()); // If duration is 0, then it's just a plain paste if (dur == 0.0) { // use Weak-guarantee Paste(t0, src); return; } std::vector envPoints; std::vector splits; WaveClipHolders cuts; // If provided time warper was NULL, use a default one that does nothing IdentityTimeWarper localWarper; const TimeWarper *warper = (effectWarper ? effectWarper : &localWarper); // Align to a sample t0 = LongSamplesToTime(TimeToLongSamples(t0)); t1 = LongSamplesToTime(TimeToLongSamples(t1)); // Save the cut/split lines whether preserving or not since merging // needs to know if a clip boundary is being crossed since Paste() // will add split lines around the pasted clip if so. for (const auto &clip : mClips) { double st; // Remember clip boundaries as locations to split st = LongSamplesToTime(TimeToLongSamples(clip->GetStartTime())); if (st >= t0 && st <= t1 && !make_iterator_range(splits).contains(st)) { splits.push_back(st); } st = LongSamplesToTime(TimeToLongSamples(clip->GetEndTime())); if (st >= t0 && st <= t1 && !make_iterator_range(splits).contains(st)) { splits.push_back(st); } // Search for cut lines auto &cutlines = clip->GetCutLines(); // May erase from cutlines, so don't use range-for for (auto it = cutlines.begin(); it != cutlines.end(); ) { WaveClip *cut = it->get(); double cs = LongSamplesToTime(TimeToLongSamples(clip->GetOffset() + cut->GetOffset())); // Remember cut point if (cs >= t0 && cs <= t1) { // Remember the absolute offset and add to our cuts array. cut->SetOffset(cs); cuts.push_back(std::move(*it)); // transfer ownership! it = cutlines.erase(it); } else ++it; } // Save the envelope points const auto &env = *clip->GetEnvelope(); for (size_t i = 0, numPoints = env.GetNumberOfPoints(); i < numPoints; ++i) { envPoints.push_back(env[i]); } } const auto tolerance = 2.0 / GetRate(); // Now, clear the selection HandleClear(t0, t1, false, false); { // And paste in the NEW data Paste(t0, src); { // First, merge the NEW clip(s) in with the existing clips if (merge && splits.size() > 0) { // Now t1 represents the absolute end of the pasted data. t1 = t0 + src->GetEndTime(); // Get a sorted array of the clips auto clips = SortedClipArray(); // Scan the sorted clips for the first clip whose start time // exceeds the pasted regions end time. { WaveClip *prev = nullptr; for (const auto clip : clips) { // Merge this clip and the previous clip if the end time // falls within it and this isn't the first clip in the track. if (fabs(t1 - clip->GetStartTime()) < tolerance) { if (prev) MergeClips(GetClipIndex(prev), GetClipIndex(clip)); break; } prev = clip; } } } // Refill the array since clips have changed. auto clips = SortedClipArray(); { // Scan the sorted clips to look for the start of the pasted // region. WaveClip *prev = nullptr; for (const auto clip : clips) { if (prev) { // It must be that clip is what was pasted and it begins where // prev ends. // use Weak-guarantee MergeClips(GetClipIndex(prev), GetClipIndex(clip)); break; } if (fabs(t0 - clip->GetEndTime()) < tolerance) // Merge this clip and the next clip if the start time // falls within it and this isn't the last clip in the track. prev = clip; else prev = nullptr; } } } // Restore cut/split lines if (preserve) { // Restore the split lines, transforming the position appropriately for (const auto split: splits) { SplitAt(warper->Warp(split)); } // Restore the saved cut lines, also transforming if time altered for (const auto &clip : mClips) { double st; double et; st = clip->GetStartTime(); et = clip->GetEndTime(); // Scan the cuts for any that live within this clip for (auto it = cuts.begin(); it != cuts.end();) { WaveClip *cut = it->get(); double cs = cut->GetOffset(); // Offset the cut from the start of the clip and add it to // this clips cutlines. if (cs >= st && cs <= et) { cut->SetOffset(warper->Warp(cs) - st); clip->GetCutLines().push_back( std::move(*it) ); // transfer ownership! it = cuts.erase(it); } else ++it; } } } // Restore the envelope points for (auto point : envPoints) { auto t = warper->Warp(point.GetT()); if (auto clip = GetClipAtTime(t)) clip->GetEnvelope()->Insert(t, point.GetVal()); } } } /*! @excsafety{Strong} */ void WaveTrack::SplitDelete(double t0, double t1) { bool addCutLines = false; bool split = true; HandleClear(t0, t1, addCutLines, split); } namespace { WaveClipHolders::const_iterator FindClip(const WaveClipHolders &list, const WaveClip *clip, int *distance = nullptr) { if (distance) *distance = 0; auto it = list.begin(); for (const auto end = list.end(); it != end; ++it) { if (it->get() == clip) break; if (distance) ++*distance; } return it; } WaveClipHolders::iterator FindClip(WaveClipHolders &list, const WaveClip *clip, int *distance = nullptr) { if (distance) *distance = 0; auto it = list.begin(); for (const auto end = list.end(); it != end; ++it) { if (it->get() == clip) break; if (distance) ++*distance; } return it; } } std::shared_ptr WaveTrack::RemoveAndReturnClip(WaveClip* clip) { // Be clear about who owns the clip!! auto it = FindClip(mClips, clip); if (it != mClips.end()) { auto result = std::move(*it); // Array stops owning the clip, before we shrink it mClips.erase(it); return result; } else return {}; } bool WaveTrack::AddClip(const std::shared_ptr &clip) { if (clip->GetSequence()->GetFactory() != this->mpFactory) return false; // Uncomment the following line after we correct the problem of zero-length clips //if (CanInsertClip(clip)) mClips.push_back(clip); // transfer ownership return true; } /*! @excsafety{Strong} */ void WaveTrack::HandleClear(double t0, double t1, bool addCutLines, bool split) { // For debugging, use an ASSERT so that we stop // closer to the problem. wxASSERT( t1 >= t0 ); if (t1 < t0) THROW_INCONSISTENCY_EXCEPTION; bool editClipCanMove = GetEditClipsCanMove(); WaveClipPointers clipsToDelete; WaveClipHolders clipsToAdd; // We only add cut lines when deleting in the middle of a single clip // The cut line code is not really prepared to handle other situations if (addCutLines) { for (const auto &clip : mClips) { if (!clip->BeforeClip(t1) && !clip->AfterClip(t0) && (clip->BeforeClip(t0) || clip->AfterClip(t1))) { addCutLines = false; break; } } } for (const auto &clip : mClips) { if (clip->BeforeClip(t0) && clip->AfterClip(t1)) { // Whole clip must be deleted - remember this clipsToDelete.push_back(clip.get()); } else if (!clip->BeforeClip(t1) && !clip->AfterClip(t0)) { // Clip data is affected by command if (addCutLines) { // Don't modify this clip in place, because we want a strong // guarantee, and might modify another clip clipsToDelete.push_back( clip.get() ); auto newClip = std::make_unique( *clip, mpFactory, true ); newClip->ClearAndAddCutLine( t0, t1 ); clipsToAdd.push_back( std::move( newClip ) ); } else { if (split) { // Three cases: if (clip->BeforeClip(t0)) { // Delete from the left edge // Don't modify this clip in place, because we want a strong // guarantee, and might modify another clip clipsToDelete.push_back( clip.get() ); auto newClip = std::make_unique( *clip, mpFactory, true ); newClip->Clear(clip->GetStartTime(), t1); newClip->Offset(t1-clip->GetStartTime()); clipsToAdd.push_back( std::move( newClip ) ); } else if (clip->AfterClip(t1)) { // Delete to right edge // Don't modify this clip in place, because we want a strong // guarantee, and might modify another clip clipsToDelete.push_back( clip.get() ); auto newClip = std::make_unique( *clip, mpFactory, true ); newClip->Clear(t0, clip->GetEndTime()); clipsToAdd.push_back( std::move( newClip ) ); } else { // Delete in the middle of the clip...we actually create two // NEW clips out of the left and right halves... // left clipsToAdd.push_back ( std::make_unique( *clip, mpFactory, true ) ); clipsToAdd.back()->Clear(t0, clip->GetEndTime()); // right clipsToAdd.push_back ( std::make_unique( *clip, mpFactory, true ) ); WaveClip *const right = clipsToAdd.back().get(); right->Clear(clip->GetStartTime(), t1); right->Offset(t1 - clip->GetStartTime()); clipsToDelete.push_back(clip.get()); } } else { // (We are not doing a split cut) // Don't modify this clip in place, because we want a strong // guarantee, and might modify another clip clipsToDelete.push_back( clip.get() ); auto newClip = std::make_unique( *clip, mpFactory, true ); // clip->Clear keeps points < t0 and >= t1 via Envelope::CollapseRegion newClip->Clear(t0,t1); clipsToAdd.push_back( std::move( newClip ) ); } } } } // Only now, change the contents of this track // use No-fail-guarantee for the rest for (const auto &clip : mClips) { if (clip->BeforeClip(t1)) { // Clip is "behind" the region -- offset it unless we're splitting // or we're using the "don't move other clips" mode if (!split && editClipCanMove) clip->Offset(-(t1-t0)); } } for (const auto &clip: clipsToDelete) { auto myIt = FindClip(mClips, clip); if (myIt != mClips.end()) mClips.erase(myIt); // deletes the clip! else wxASSERT(false); } for (auto &clip: clipsToAdd) mClips.push_back(std::move(clip)); // transfer ownership } void WaveTrack::SyncLockAdjust(double oldT1, double newT1) { if (newT1 > oldT1) { // Insert space within the track // JKC: This is a rare case where using >= rather than > on a float matters. // GetEndTime() looks through the clips and may give us EXACTLY the same // value as T1, when T1 was set to be at the end of one of those clips. if (oldT1 >= GetEndTime()) return; // If track is empty at oldT1 insert whitespace; otherwise, silence if (IsEmpty(oldT1, oldT1)) { // Check if clips can move bool clipsCanMove = true; gPrefs->Read(wxT("/GUI/EditClipCanMove"), &clipsCanMove); if (clipsCanMove) { auto tmp = Cut (oldT1, GetEndTime() + 1.0/GetRate()); Paste(newT1, tmp.get()); } return; } else { // AWD: Could just use InsertSilence() on its own here, but it doesn't // follow EditClipCanMove rules (Paste() does it right) auto tmp = std::make_shared( mpFactory, GetSampleFormat(), GetRate() ); tmp->InsertSilence(0.0, newT1 - oldT1); tmp->Flush(); Paste(oldT1, tmp.get()); } } else if (newT1 < oldT1) { Clear(newT1, oldT1); } } /*! @excsafety{Weak} */ void WaveTrack::Paste(double t0, const Track *src) { bool editClipCanMove = GetEditClipsCanMove(); bool bOk = src && src->TypeSwitch< bool >( [&](const WaveTrack *other) { // // Pasting is a bit complicated, because with the existence of multiclip mode, // we must guess the behaviour the user wants. // // Currently, two modes are implemented: // // - If a single clip should be pasted, and it should be pasted inside another // clip, no NEW clips are generated. The audio is simply inserted. // This resembles the old (pre-multiclip support) behaviour. However, if // the clip is pasted outside of any clip, a NEW clip is generated. This is // the only behaviour which is different to what was done before, but it // shouldn't confuse users too much. // // - If multiple clips should be pasted, or a single clip that does not fill // the duration of the pasted track, these are always pasted as single // clips, and the current clip is split, when necessary. This may seem // strange at first, but it probably is better than trying to auto-merge // anything. The user can still merge the clips by hand (which should be a // simple command reachable by a hotkey or single mouse click). // if (other->GetNumClips() == 0) return true; //wxPrintf("paste: we have at least one clip\n"); bool singleClipMode = (other->GetNumClips() == 1 && other->GetStartTime() == 0.0); const double insertDuration = other->GetEndTime(); if( insertDuration != 0 && insertDuration < 1.0/mRate ) // PRL: I added this check to avoid violations of preconditions in other WaveClip and Sequence // methods, but allow the value 0 so I don't subvert the purpose of commit // 739422ba70ceb4be0bb1829b6feb0c5401de641e which causes append-recording always to make // a new clip. return true; //wxPrintf("Check if we need to make room for the pasted data\n"); // Make room for the pasted data if (editClipCanMove) { if (!singleClipMode) { // We need to insert multiple clips, so split the current clip and // move everything to the right, then try to paste again if (!IsEmpty(t0, GetEndTime())) { auto tmp = Cut(t0, GetEndTime()+1.0/mRate); Paste(t0 + insertDuration, tmp.get()); } } else { // We only need to insert one single clip, so just move all clips // to the right of the paste point out of the way for (const auto &clip : mClips) { if (clip->GetStartTime() > t0-(1.0/mRate)) clip->Offset(insertDuration); } } } if (singleClipMode) { // Single clip mode // wxPrintf("paste: checking for single clip mode!\n"); WaveClip *insideClip = NULL; for (const auto &clip : mClips) { if (editClipCanMove) { if (clip->WithinClip(t0)) { //wxPrintf("t0=%.6f: inside clip is %.6f ... %.6f\n", // t0, clip->GetStartTime(), clip->GetEndTime()); insideClip = clip.get(); break; } } else { // If clips are immovable we also allow prepending to clips if (clip->WithinClip(t0) || TimeToLongSamples(t0) == clip->GetStartSample()) { insideClip = clip.get(); break; } } } if (insideClip) { // Exhibit traditional behaviour //wxPrintf("paste: traditional behaviour\n"); if (!editClipCanMove) { // We did not move other clips out of the way already, so // check if we can paste without having to move other clips for (const auto &clip : mClips) { if (clip->GetStartTime() > insideClip->GetStartTime() && insideClip->GetEndTime() + insertDuration > clip->GetStartTime()) // Strong-guarantee in case of this path // not that it matters. throw SimpleMessageBoxException{ ExceptionType::BadUserAction, XO("There is not enough room available to paste the selection"), XO("Warning"), "Error:_Insufficient_space_in_track" }; } } insideClip->Paste(t0, other->GetClipByIndex(0)); return true; } // Just fall through and exhibit NEW behaviour } // Insert NEW clips //wxPrintf("paste: multi clip mode!\n"); if (!editClipCanMove && !IsEmpty(t0, t0+insertDuration-1.0/mRate)) // Strong-guarantee in case of this path // not that it matters. throw SimpleMessageBoxException{ ExceptionType::BadUserAction, XO("There is not enough room available to paste the selection"), XO("Warning"), "Error:_Insufficient_space_in_track" }; for (const auto &clip : other->mClips) { // AWD Oct. 2009: Don't actually paste in placeholder clips if (!clip->GetIsPlaceholder()) { auto newClip = std::make_unique( *clip, mpFactory, true ); newClip->Resample(mRate); newClip->Offset(t0); newClip->MarkChanged(); mClips.push_back(std::move(newClip)); // transfer ownership } } return true; } ); if( !bOk ) // THROW_INCONSISTENCY_EXCEPTION; // ? (void)0;// Empty if intentional. } void WaveTrack::Silence(double t0, double t1) { if (t1 < t0) THROW_INCONSISTENCY_EXCEPTION; auto start = (sampleCount)floor(t0 * mRate + 0.5); auto len = (sampleCount)floor(t1 * mRate + 0.5) - start; for (const auto &clip : mClips) { auto clipStart = clip->GetStartSample(); auto clipEnd = clip->GetEndSample(); if (clipEnd > start && clipStart < start+len) { // Clip sample region and Get/Put sample region overlap auto samplesToCopy = start+len - clipStart; if (samplesToCopy > clip->GetNumSamples()) samplesToCopy = clip->GetNumSamples(); auto startDelta = clipStart - start; decltype(startDelta) inclipDelta = 0; if (startDelta < 0) { inclipDelta = -startDelta; // make positive value samplesToCopy -= inclipDelta; startDelta = 0; } clip->GetSequence()->SetSilence(inclipDelta, samplesToCopy); clip->MarkChanged(); } } } /*! @excsafety{Strong} */ void WaveTrack::InsertSilence(double t, double len) { // Nothing to do, if length is zero. // Fixes Bug 1626 if( len == 0 ) return; if (len <= 0) THROW_INCONSISTENCY_EXCEPTION; if (mClips.empty()) { // Special case if there is no clip yet auto clip = std::make_unique(mpFactory, mFormat, mRate, this->GetWaveColorIndex()); clip->InsertSilence(0, len); // use No-fail-guarantee mClips.push_back( std::move( clip ) ); return; } else { // Assume at most one clip contains t const auto end = mClips.end(); const auto it = std::find_if( mClips.begin(), end, [&](const WaveClipHolder &clip) { return clip->WithinClip(t); } ); // use Strong-guarantee if (it != end) it->get()->InsertSilence(t, len); // use No-fail-guarantee for (const auto &clip : mClips) { if (clip->BeforeClip(t)) clip->Offset(len); } } } //Performs the opposite of Join //Analyses selected region for possible Joined clips and disjoins them /*! @excsafety{Weak} */ void WaveTrack::Disjoin(double t0, double t1) { auto minSamples = TimeToLongSamples( WAVETRACK_MERGE_POINT_TOLERANCE ); const size_t maxAtOnce = 1048576; Floats buffer{ maxAtOnce }; Regions regions; wxBusyCursor busy; for (const auto &clip : mClips) { double startTime = clip->GetStartTime(); double endTime = clip->GetEndTime(); if( endTime < t0 || startTime > t1 ) continue; if( t0 > startTime ) startTime = t0; if( t1 < endTime ) endTime = t1; //simply look for a sequence of zeroes and if the sequence //is greater than minimum number, split-DELETE the region sampleCount seqStart = -1; sampleCount start, end; clip->TimeToSamplesClip( startTime, &start ); clip->TimeToSamplesClip( endTime, &end ); auto len = ( end - start ); for( decltype(len) done = 0; done < len; done += maxAtOnce ) { auto numSamples = limitSampleBufferSize( maxAtOnce, len - done ); clip->GetSamples( ( samplePtr )buffer.get(), floatSample, start + done, numSamples ); for( decltype(numSamples) i = 0; i < numSamples; i++ ) { auto curSamplePos = start + done + i; //start a NEW sequence if( buffer[ i ] == 0.0 && seqStart == -1 ) seqStart = curSamplePos; else if( buffer[ i ] != 0.0 || curSamplePos == end - 1 ) { if( seqStart != -1 ) { decltype(end) seqEnd; //consider the end case, where selection ends in zeroes if( curSamplePos == end - 1 && buffer[ i ] == 0.0 ) seqEnd = end; else seqEnd = curSamplePos; if( seqEnd - seqStart + 1 > minSamples ) { regions.push_back(Region( seqStart.as_double() / GetRate() + clip->GetStartTime(), seqEnd.as_double() / GetRate() + clip->GetStartTime())); } seqStart = -1; } } } } } for( unsigned int i = 0; i < regions.size(); i++ ) { const Region ®ion = regions.at(i); SplitDelete(region.start, region.end ); } } /*! @excsafety{Weak} */ void WaveTrack::Join(double t0, double t1) { // Merge all WaveClips overlapping selection into one WaveClipPointers clipsToDelete; WaveClip *newClip; for (const auto &clip: mClips) { if (clip->GetStartTime() < t1-(1.0/mRate) && clip->GetEndTime()-(1.0/mRate) > t0) { // Put in sorted order auto it = clipsToDelete.begin(), end = clipsToDelete.end(); for (; it != end; ++it) if ((*it)->GetStartTime() > clip->GetStartTime()) break; //wxPrintf("Insert clip %.6f at position %d\n", clip->GetStartTime(), i); clipsToDelete.insert(it, clip.get()); } } //if there are no clips to DELETE, nothing to do if( clipsToDelete.size() == 0 ) return; newClip = CreateClip(); double t = clipsToDelete[0]->GetOffset(); newClip->SetOffset(t); for (const auto &clip : clipsToDelete) { //wxPrintf("t=%.6f adding clip (offset %.6f, %.6f ... %.6f)\n", // t, clip->GetOffset(), clip->GetStartTime(), clip->GetEndTime()); if (clip->GetOffset() - t > (1.0 / mRate)) { double addedSilence = (clip->GetOffset() - t); //wxPrintf("Adding %.6f seconds of silence\n"); auto offset = clip->GetOffset(); auto value = clip->GetEnvelope()->GetValue( offset ); newClip->AppendSilence( addedSilence, value ); t += addedSilence; } //wxPrintf("Pasting at %.6f\n", t); newClip->Paste(t, clip); t = newClip->GetEndTime(); auto it = FindClip(mClips, clip); mClips.erase(it); // deletes the clip } } /*! @excsafety{Partial} -- Some prefix (maybe none) of the buffer is appended, and no content already flushed to disk is lost. */ bool WaveTrack::Append(constSamplePtr buffer, sampleFormat format, size_t len, unsigned int stride /* = 1 */) { return RightmostOrNewClip()->Append(buffer, format, len, stride); } sampleCount WaveTrack::GetBlockStart(sampleCount s) const { for (const auto &clip : mClips) { const auto startSample = (sampleCount)floor(0.5 + clip->GetStartTime()*mRate); const auto endSample = startSample + clip->GetNumSamples(); if (s >= startSample && s < endSample) return startSample + clip->GetSequence()->GetBlockStart(s - startSample); } return -1; } size_t WaveTrack::GetBestBlockSize(sampleCount s) const { auto bestBlockSize = GetMaxBlockSize(); for (const auto &clip : mClips) { auto startSample = (sampleCount)floor(clip->GetStartTime()*mRate + 0.5); auto endSample = startSample + clip->GetNumSamples(); if (s >= startSample && s < endSample) { bestBlockSize = clip->GetSequence()->GetBestBlockSize(s - startSample); break; } } return bestBlockSize; } size_t WaveTrack::GetMaxBlockSize() const { decltype(GetMaxBlockSize()) maxblocksize = 0; for (const auto &clip : mClips) { maxblocksize = std::max(maxblocksize, clip->GetSequence()->GetMaxBlockSize()); } if (maxblocksize == 0) { // We really need the maximum block size, so create a // temporary sequence to get it. maxblocksize = Sequence{ mpFactory, mFormat }.GetMaxBlockSize(); } wxASSERT(maxblocksize > 0); return maxblocksize; } size_t WaveTrack::GetIdealBlockSize() { return NewestOrNewClip()->GetSequence()->GetIdealBlockSize(); } /*! @excsafety{Mixed} */ /*! @excsafety{No-fail} -- The rightmost clip will be in a flushed state. */ /*! @excsafety{Partial} -- Some initial portion (maybe none) of the append buffer of the rightmost clip gets appended; no previously saved contents are lost. */ void WaveTrack::Flush() { // After appending, presumably. Do this to the clip that gets appended. RightmostOrNewClip()->Flush(); } bool WaveTrack::HandleXMLTag(const wxChar *tag, const wxChar **attrs) { if (!wxStrcmp(tag, wxT("wavetrack"))) { double dblValue; long nValue; while(*attrs) { const wxChar *attr = *attrs++; const wxChar *value = *attrs++; if (!value) break; const wxString strValue = value; if (!wxStrcmp(attr, wxT("rate"))) { // mRate is an int, but "rate" in the project file is a float. if (!XMLValueChecker::IsGoodString(strValue) || !Internat::CompatibleToDouble(strValue, &dblValue) || (dblValue < 1.0) || (dblValue > 1000000.0)) // allow a large range to be read return false; mRate = lrint(dblValue); } else if (!wxStrcmp(attr, wxT("offset")) && XMLValueChecker::IsGoodString(strValue) && Internat::CompatibleToDouble(strValue, &dblValue)) { // Offset is only relevant for legacy project files. The value // is cached until the actual WaveClip containing the legacy // track is created. mLegacyProjectFileOffset = dblValue; } else if (this->PlayableTrack::HandleXMLAttribute(attr, value)) {} else if (this->Track::HandleCommonXMLAttribute(attr, strValue)) ; else if (!wxStrcmp(attr, wxT("gain")) && XMLValueChecker::IsGoodString(strValue) && Internat::CompatibleToDouble(strValue, &dblValue)) mGain = dblValue; else if (!wxStrcmp(attr, wxT("pan")) && XMLValueChecker::IsGoodString(strValue) && Internat::CompatibleToDouble(strValue, &dblValue) && (dblValue >= -1.0) && (dblValue <= 1.0)) mPan = dblValue; else if (!wxStrcmp(attr, wxT("channel"))) { if (!XMLValueChecker::IsGoodInt(strValue) || !strValue.ToLong(&nValue) || !XMLValueChecker::IsValidChannel(nValue)) return false; mChannel = static_cast( nValue ); } else if (!wxStrcmp(attr, wxT("linked")) && XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue)) SetLinked(nValue != 0); else if (!wxStrcmp(attr, wxT("colorindex")) && XMLValueChecker::IsGoodString(strValue) && strValue.ToLong(&nValue)) // Don't use SetWaveColorIndex as it sets the clips too. mWaveColorIndex = nValue; else if (!wxStrcmp(attr, wxT("sampleformat")) && XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue) && XMLValueChecker::IsValidSampleFormat(nValue)) mFormat = static_cast(nValue); } // while return true; } return false; } void WaveTrack::HandleXMLEndTag(const wxChar * WXUNUSED(tag)) { // In case we opened a pre-multiclip project, we need to // simulate closing the waveclip tag. NewestOrNewClip()->HandleXMLEndTag(wxT("waveclip")); } XMLTagHandler *WaveTrack::HandleXMLChild(const wxChar *tag) { // // This is legacy code (1.2 and previous) and is not called for NEW projects! // if (!wxStrcmp(tag, wxT("sequence")) || !wxStrcmp(tag, wxT("envelope"))) { // This is a legacy project, so set the cached offset NewestOrNewClip()->SetOffset(mLegacyProjectFileOffset); // Legacy project file tracks are imported as one single wave clip if (!wxStrcmp(tag, wxT("sequence"))) return NewestOrNewClip()->GetSequence(); else if (!wxStrcmp(tag, wxT("envelope"))) return NewestOrNewClip()->GetEnvelope(); } // JKC... for 1.1.0, one step better than what we had, but still badly broken. //If we see a waveblock at this level, we'd better generate a sequence. if( !wxStrcmp( tag, wxT("waveblock" ))) { // This is a legacy project, so set the cached offset NewestOrNewClip()->SetOffset(mLegacyProjectFileOffset); Sequence *pSeq = NewestOrNewClip()->GetSequence(); return pSeq; } // // This is for the NEW file format (post-1.2) // if (!wxStrcmp(tag, wxT("waveclip"))) return CreateClip(); else return NULL; } void WaveTrack::WriteXML(XMLWriter &xmlFile) const // may throw { xmlFile.StartTag(wxT("wavetrack")); this->Track::WriteCommonXMLAttributes( xmlFile ); xmlFile.WriteAttr(wxT("channel"), mChannel); xmlFile.WriteAttr(wxT("linked"), mLinked); this->PlayableTrack::WriteXMLAttributes(xmlFile); xmlFile.WriteAttr(wxT("rate"), mRate); xmlFile.WriteAttr(wxT("gain"), (double)mGain); xmlFile.WriteAttr(wxT("pan"), (double)mPan); xmlFile.WriteAttr(wxT("colorindex"), mWaveColorIndex ); xmlFile.WriteAttr(wxT("sampleformat"), static_cast(mFormat) ); for (const auto &clip : mClips) { clip->WriteXML(xmlFile); } xmlFile.EndTag(wxT("wavetrack")); } bool WaveTrack::GetErrorOpening() { for (const auto &clip : mClips) if (clip->GetSequence()->GetErrorOpening()) return true; return false; } bool WaveTrack::CloseLock() { for (const auto &clip : mClips) clip->CloseLock(); return true; } AUDACITY_DLL_API sampleCount WaveTrack::TimeToLongSamples(double t0) const { return sampleCount( floor(t0 * mRate + 0.5) ); } double WaveTrack::LongSamplesToTime(sampleCount pos) const { return pos.as_double() / mRate; } double WaveTrack::GetStartTime() const { bool found = false; double best = 0.0; if (mClips.empty()) return 0; for (const auto &clip : mClips) if (!found) { found = true; best = clip->GetStartTime(); } else if (clip->GetStartTime() < best) best = clip->GetStartTime(); return best; } double WaveTrack::GetEndTime() const { bool found = false; double best = 0.0; if (mClips.empty()) return 0; for (const auto &clip : mClips) if (!found) { found = true; best = clip->GetEndTime(); } else if (clip->GetEndTime() > best) best = clip->GetEndTime(); return best; } // // Getting/setting samples. The sample counts here are // expressed relative to t=0.0 at the track's sample rate. // std::pair WaveTrack::GetMinMax( double t0, double t1, bool mayThrow) const { std::pair results { // we need these at extremes to make sure we find true min and max FLT_MAX, -FLT_MAX }; bool clipFound = false; if (t0 > t1) { if (mayThrow) THROW_INCONSISTENCY_EXCEPTION; return results; } if (t0 == t1) return results; for (const auto &clip: mClips) { if (t1 >= clip->GetStartTime() && t0 <= clip->GetEndTime()) { clipFound = true; auto clipResults = clip->GetMinMax(t0, t1, mayThrow); if (clipResults.first < results.first) results.first = clipResults.first; if (clipResults.second > results.second) results.second = clipResults.second; } } if(!clipFound) { results = { 0.f, 0.f }; // sensible defaults if no clips found } return results; } float WaveTrack::GetRMS(double t0, double t1, bool mayThrow) const { if (t0 > t1) { if (mayThrow) THROW_INCONSISTENCY_EXCEPTION; return 0.f; } if (t0 == t1) return 0.f; double sumsq = 0.0; sampleCount length = 0; for (const auto &clip: mClips) { // If t1 == clip->GetStartTime() or t0 == clip->GetEndTime(), then the clip // is not inside the selection, so we don't want it. // if (t1 >= clip->GetStartTime() && t0 <= clip->GetEndTime()) if (t1 >= clip->GetStartTime() && t0 <= clip->GetEndTime()) { sampleCount clipStart, clipEnd; float cliprms = clip->GetRMS(t0, t1, mayThrow); clip->TimeToSamplesClip(wxMax(t0, clip->GetStartTime()), &clipStart); clip->TimeToSamplesClip(wxMin(t1, clip->GetEndTime()), &clipEnd); sumsq += cliprms * cliprms * (clipEnd - clipStart).as_float(); length += (clipEnd - clipStart); } } return length > 0 ? sqrt(sumsq / length.as_double()) : 0.0; } bool WaveTrack::Get(samplePtr buffer, sampleFormat format, sampleCount start, size_t len, fillFormat fill, bool mayThrow, sampleCount * pNumWithinClips) const { // Simple optimization: When this buffer is completely contained within one clip, // don't clear anything (because we won't have to). Otherwise, just clear // everything to be on the safe side. bool doClear = true; bool result = true; sampleCount samplesCopied = 0; for (const auto &clip: mClips) { if (start >= clip->GetStartSample() && start+len <= clip->GetEndSample()) { doClear = false; break; } } if (doClear) { // Usually we fill in empty space with zero if( fill == fillZero ) ClearSamples(buffer, format, 0, len); // but we don't have to. else if( fill==fillTwo ) { wxASSERT( format==floatSample ); float * pBuffer = (float*)buffer; for(size_t i=0;iGetStartSample(); auto clipEnd = clip->GetEndSample(); if (clipEnd > start && clipStart < start+len) { // Clip sample region and Get/Put sample region overlap auto samplesToCopy = std::min( start+len - clipStart, clip->GetNumSamples() ); auto startDelta = clipStart - start; decltype(startDelta) inclipDelta = 0; if (startDelta < 0) { inclipDelta = -startDelta; // make positive value samplesToCopy -= inclipDelta; // samplesToCopy is now either len or // (clipEnd - clipStart) - (start - clipStart) // == clipEnd - start > 0 // samplesToCopy is not more than len // startDelta = 0; // startDelta is zero } else { // startDelta is nonnegative and less than len // samplesToCopy is positive and not more than len } if (!clip->GetSamples( (samplePtr)(((char*)buffer) + startDelta.as_size_t() * SAMPLE_SIZE(format)), format, inclipDelta, samplesToCopy.as_size_t(), mayThrow )) result = false; else samplesCopied += samplesToCopy; } } if( pNumWithinClips ) *pNumWithinClips = samplesCopied; return result; } /*! @excsafety{Weak} */ void WaveTrack::Set(constSamplePtr buffer, sampleFormat format, sampleCount start, size_t len) { for (const auto &clip: mClips) { auto clipStart = clip->GetStartSample(); auto clipEnd = clip->GetEndSample(); if (clipEnd > start && clipStart < start+len) { // Clip sample region and Get/Put sample region overlap auto samplesToCopy = std::min( start+len - clipStart, clip->GetNumSamples() ); auto startDelta = clipStart - start; decltype(startDelta) inclipDelta = 0; if (startDelta < 0) { inclipDelta = -startDelta; // make positive value samplesToCopy -= inclipDelta; // samplesToCopy is now either len or // (clipEnd - clipStart) - (start - clipStart) // == clipEnd - start > 0 // samplesToCopy is not more than len // startDelta = 0; // startDelta is zero } else { // startDelta is nonnegative and less than len // samplesToCopy is positive and not more than len } clip->SetSamples( (constSamplePtr)(((const char*)buffer) + startDelta.as_size_t() * SAMPLE_SIZE(format)), format, inclipDelta, samplesToCopy.as_size_t() ); clip->MarkChanged(); } } } void WaveTrack::GetEnvelopeValues(double *buffer, size_t bufferLen, double t0) const { // The output buffer corresponds to an unbroken span of time which the callers expect // to be fully valid. As clips are processed below, the output buffer is updated with // envelope values from any portion of a clip, start, end, middle, or none at all. // Since this does not guarantee that the entire buffer is filled with values we need // to initialize the entire buffer to a default value. // // This does mean that, in the cases where a usable clip is located, the buffer value will // be set twice. Unfortunately, there is no easy way around this since the clips are not // stored in increasing time order. If they were, we could just track the time as the // buffer is filled. for (decltype(bufferLen) i = 0; i < bufferLen; i++) { buffer[i] = 1.0; } double startTime = t0; auto tstep = 1.0 / mRate; double endTime = t0 + tstep * bufferLen; for (const auto &clip: mClips) { // IF clip intersects startTime..endTime THEN... auto dClipStartTime = clip->GetStartTime(); auto dClipEndTime = clip->GetEndTime(); if ((dClipStartTime < endTime) && (dClipEndTime > startTime)) { auto rbuf = buffer; auto rlen = bufferLen; auto rt0 = t0; if (rt0 < dClipStartTime) { // This is not more than the number of samples in // (endTime - startTime) which is bufferLen: auto nDiff = (sampleCount)floor((dClipStartTime - rt0) * mRate + 0.5); auto snDiff = nDiff.as_size_t(); rbuf += snDiff; wxASSERT(snDiff <= rlen); rlen -= snDiff; rt0 = dClipStartTime; } if (rt0 + rlen*tstep > dClipEndTime) { auto nClipLen = clip->GetEndSample() - clip->GetStartSample(); if (nClipLen <= 0) // Testing for bug 641, this problem is consistently '== 0', but doesn't hurt to check <. return; // This check prevents problem cited in http://bugzilla.audacityteam.org/show_bug.cgi?id=528#c11, // Gale's cross_fade_out project, which was already corrupted by bug 528. // This conditional prevents the previous write past the buffer end, in clip->GetEnvelope() call. // Never increase rlen here. // PRL bug 827: rewrote it again rlen = limitSampleBufferSize( rlen, nClipLen ); rlen = std::min(rlen, size_t(floor(0.5 + (dClipEndTime - rt0) / tstep))); } // Samples are obtained for the purpose of rendering a wave track, // so quantize time clip->GetEnvelope()->GetValues(rbuf, rlen, rt0, tstep); } } } WaveClip* WaveTrack::GetClipAtSample(sampleCount sample) { for (const auto &clip: mClips) { auto start = clip->GetStartSample(); auto len = clip->GetNumSamples(); if (sample >= start && sample < start + len) return clip.get(); } return NULL; } // When the time is both the end of a clip and the start of the next clip, the // latter clip is returned. WaveClip* WaveTrack::GetClipAtTime(double time) { const auto clips = SortedClipArray(); auto p = std::find_if(clips.rbegin(), clips.rend(), [&] (WaveClip* const& clip) { return time >= clip->GetStartTime() && time <= clip->GetEndTime(); }); // When two clips are immediately next to each other, the GetEndTime() of the first clip // and the GetStartTime() of the second clip may not be exactly equal due to rounding errors. // If "time" is the end time of the first of two such clips, and the end time is slightly // less than the start time of the second clip, then the first rather than the // second clip is found by the above code. So correct this. if (p != clips.rend() && p != clips.rbegin() && time == (*p)->GetEndTime() && (*p)->SharesBoundaryWithNextClip(*(p-1))) { p--; } return p != clips.rend() ? *p : nullptr; } Envelope* WaveTrack::GetEnvelopeAtTime(double time) { WaveClip* clip = GetClipAtTime(time); if (clip) return clip->GetEnvelope(); else return NULL; } Sequence* WaveTrack::GetSequenceAtTime(double time) { WaveClip* clip = GetClipAtTime(time); if (clip) return clip->GetSequence(); else return NULL; } WaveClip* WaveTrack::CreateClip() { mClips.push_back(std::make_unique(mpFactory, mFormat, mRate, GetWaveColorIndex())); return mClips.back().get(); } WaveClip* WaveTrack::NewestOrNewClip() { if (mClips.empty()) { WaveClip *clip = CreateClip(); clip->SetOffset(mOffset); return clip; } else return mClips.back().get(); } /*! @excsafety{No-fail} */ WaveClip* WaveTrack::RightmostOrNewClip() { if (mClips.empty()) { WaveClip *clip = CreateClip(); clip->SetOffset(mOffset); return clip; } else { auto it = mClips.begin(); WaveClip *rightmost = (*it++).get(); double maxOffset = rightmost->GetOffset(); for (auto end = mClips.end(); it != end; ++it) { WaveClip *clip = it->get(); double offset = clip->GetOffset(); if (maxOffset < offset) maxOffset = offset, rightmost = clip; } return rightmost; } } int WaveTrack::GetClipIndex(const WaveClip* clip) const { int result; FindClip(mClips, clip, &result); return result; } WaveClip* WaveTrack::GetClipByIndex(int index) { if(index < (int)mClips.size()) return mClips[index].get(); else return nullptr; } const WaveClip* WaveTrack::GetClipByIndex(int index) const { return const_cast(*this).GetClipByIndex(index); } int WaveTrack::GetNumClips() const { return mClips.size(); } bool WaveTrack::CanOffsetClips( const std::vector &clips, double amount, double *allowedAmount /* = NULL */) { if (allowedAmount) *allowedAmount = amount; const auto &moving = [&](WaveClip *clip){ // linear search might be improved, but expecting few moving clips // compared with the fixed clips return clips.end() != std::find( clips.begin(), clips.end(), clip ); }; for (const auto &c: mClips) { if ( moving( c.get() ) ) continue; for (const auto clip : clips) { if (c->GetStartTime() < clip->GetEndTime() + amount && c->GetEndTime() > clip->GetStartTime() + amount) { if (!allowedAmount) return false; // clips overlap if (amount > 0) { if (c->GetStartTime()-clip->GetEndTime() < *allowedAmount) *allowedAmount = c->GetStartTime()-clip->GetEndTime(); if (*allowedAmount < 0) *allowedAmount = 0; } else { if (c->GetEndTime()-clip->GetStartTime() > *allowedAmount) *allowedAmount = c->GetEndTime()-clip->GetStartTime(); if (*allowedAmount > 0) *allowedAmount = 0; } } } } if (allowedAmount) { if (*allowedAmount == amount) return true; // Check if the NEW calculated amount would not violate // any other constraint if (!CanOffsetClips(clips, *allowedAmount, nullptr)) { *allowedAmount = 0; // play safe and don't allow anything return false; } else return true; } else return true; } bool WaveTrack::CanInsertClip( WaveClip* clip, double &slideBy, double &tolerance) const { for (const auto &c : mClips) { double d1 = c->GetStartTime() - (clip->GetEndTime()+slideBy); double d2 = (clip->GetStartTime()+slideBy) - c->GetEndTime(); if ( (d1<0) && (d2<0) ) { // clips overlap. // Try to rescue it. // The rescue logic is not perfect, and will typically // move the clip at most once. // We divide by 1000 rather than set to 0, to allow for // a second 'micro move' that is really about rounding error. if( -d1 < tolerance ){ // right edge of clip overlaps slightly. // slide clip left a small amount. slideBy +=d1; tolerance /=1000; } else if( -d2 < tolerance ){ // left edge of clip overlaps slightly. // slide clip right a small amount. slideBy -= d2; tolerance /=1000; } else return false; // clips overlap No tolerance left. } } return true; } /*! @excsafety{Weak} */ void WaveTrack::Split( double t0, double t1 ) { SplitAt( t0 ); if( t0 != t1 ) SplitAt( t1 ); } /*! @excsafety{Weak} */ void WaveTrack::SplitAt(double t) { for (const auto &c : mClips) { if (c->WithinClip(t)) { t = LongSamplesToTime(TimeToLongSamples(t)); // put t on a sample auto newClip = std::make_unique( *c, mpFactory, true ); c->Clear(t, c->GetEndTime()); newClip->Clear(c->GetStartTime(), t); //offset the NEW clip by the splitpoint (noting that it is already offset to c->GetStartTime()) sampleCount here = llrint(floor(((t - c->GetStartTime()) * mRate) + 0.5)); newClip->Offset(here.as_double()/(double)mRate); // This could invalidate the iterators for the loop! But we return // at once so it's okay mClips.push_back(std::move(newClip)); // transfer ownership return; } } } void WaveTrack::UpdateLocationsCache() const { auto clips = SortedClipArray(); mDisplayLocationsCache.clear(); // Count number of display locations int num = 0; { const WaveClip *prev = nullptr; for (const auto clip : clips) { num += clip->NumCutLines(); if (prev && fabs(prev->GetEndTime() - clip->GetStartTime()) < WAVETRACK_MERGE_POINT_TOLERANCE) ++num; prev = clip; } } if (num == 0) return; // Alloc necessary number of display locations mDisplayLocationsCache.reserve(num); // Add all display locations to cache int curpos = 0; const WaveClip *previousClip = nullptr; for (const auto clip: clips) { for (const auto &cc : clip->GetCutLines()) { // Add cut line expander point mDisplayLocationsCache.push_back(WaveTrackLocation{ clip->GetOffset() + cc->GetOffset(), WaveTrackLocation::locationCutLine }); curpos++; } if (previousClip) { if (fabs(previousClip->GetEndTime() - clip->GetStartTime()) < WAVETRACK_MERGE_POINT_TOLERANCE) { // Add merge point mDisplayLocationsCache.push_back(WaveTrackLocation{ previousClip->GetEndTime(), WaveTrackLocation::locationMergePoint, GetClipIndex(previousClip), GetClipIndex(clip) }); curpos++; } } previousClip = clip; } wxASSERT(curpos == num); } // Expand cut line (that is, re-insert audio, then DELETE audio saved in cut line) /*! @excsafety{Strong} */ void WaveTrack::ExpandCutLine(double cutLinePosition, double* cutlineStart, double* cutlineEnd) { bool editClipCanMove = GetEditClipsCanMove(); // Find clip which contains this cut line double start = 0, end = 0; auto pEnd = mClips.end(); auto pClip = std::find_if( mClips.begin(), pEnd, [&](const WaveClipHolder &clip) { return clip->FindCutLine(cutLinePosition, &start, &end); } ); if (pClip != pEnd) { auto &clip = *pClip; if (!editClipCanMove) { // We are not allowed to move the other clips, so see if there // is enough room to expand the cut line for (const auto &clip2: mClips) { if (clip2->GetStartTime() > clip->GetStartTime() && clip->GetEndTime() + end - start > clip2->GetStartTime()) // Strong-guarantee in case of this path throw SimpleMessageBoxException{ ExceptionType::BadUserAction, XO("There is not enough room available to expand the cut line"), XO("Warning"), "Error:_Insufficient_space_in_track" }; } } clip->ExpandCutLine(cutLinePosition); // Strong-guarantee provided that the following gives No-fail-guarantee if (cutlineStart) *cutlineStart = start; if (cutlineEnd) *cutlineEnd = end; // Move clips which are to the right of the cut line if (editClipCanMove) { for (const auto &clip2 : mClips) { if (clip2->GetStartTime() > clip->GetStartTime()) clip2->Offset(end - start); } } } } bool WaveTrack::RemoveCutLine(double cutLinePosition) { for (const auto &clip : mClips) if (clip->RemoveCutLine(cutLinePosition)) return true; return false; } /*! @excsafety{Strong} */ void WaveTrack::MergeClips(int clipidx1, int clipidx2) { WaveClip* clip1 = GetClipByIndex(clipidx1); WaveClip* clip2 = GetClipByIndex(clipidx2); if (!clip1 || !clip2) // Could happen if one track of a linked pair had a split and the other didn't. return; // Don't throw, just do nothing. // Append data from second clip to first clip // use Strong-guarantee clip1->Paste(clip1->GetEndTime(), clip2); // use No-fail-guarantee for the rest // Delete second clip auto it = FindClip(mClips, clip2); mClips.erase(it); } /*! @excsafety{Weak} -- Partial completion may leave clips at differing sample rates! */ void WaveTrack::Resample(int rate, ProgressDialog *progress) { for (const auto &clip : mClips) clip->Resample(rate, progress); mRate = rate; } namespace { template < typename Cont1, typename Cont2 > Cont1 FillSortedClipArray(const Cont2& mClips) { Cont1 clips; for (const auto &clip : mClips) clips.push_back(clip.get()); std::sort(clips.begin(), clips.end(), [](const WaveClip *a, const WaveClip *b) { return a->GetStartTime() < b->GetStartTime(); }); return clips; } } WaveClipPointers WaveTrack::SortedClipArray() { return FillSortedClipArray(mClips); } WaveClipConstPointers WaveTrack::SortedClipArray() const { return FillSortedClipArray(mClips); } ///Deletes all clips' wavecaches. Careful, This may not be threadsafe. void WaveTrack::ClearWaveCaches() { for (const auto &clip : mClips) clip->ClearWaveCache(); } WaveTrackCache::~WaveTrackCache() { } void WaveTrackCache::SetTrack(const std::shared_ptr &pTrack) { if (mPTrack != pTrack) { if (pTrack) { mBufferSize = pTrack->GetMaxBlockSize(); if (!mPTrack || mPTrack->GetMaxBlockSize() != mBufferSize) { Free(); mBuffers[0].data = Floats{ mBufferSize }; mBuffers[1].data = Floats{ mBufferSize }; } } else Free(); mPTrack = pTrack; mNValidBuffers = 0; } } const float *WaveTrackCache::GetFloats( sampleCount start, size_t len, bool mayThrow) { constexpr auto format = floatSample; if (format == floatSample && len > 0) { const auto end = start + len; bool fillFirst = (mNValidBuffers < 1); bool fillSecond = (mNValidBuffers < 2); // Discard cached results that we no longer need if (mNValidBuffers > 0 && (end <= mBuffers[0].start || start >= mBuffers[mNValidBuffers - 1].end())) { // Complete miss fillFirst = true; fillSecond = true; } else if (mNValidBuffers == 2 && start >= mBuffers[1].start && end > mBuffers[1].end()) { // Request starts in the second buffer and extends past it. // Discard the first buffer. // (But don't deallocate the buffer space.) mBuffers[0] .swap ( mBuffers[1] ); fillSecond = true; mNValidBuffers = 1; } else if (mNValidBuffers > 0 && start < mBuffers[0].start && 0 <= mPTrack->GetBlockStart(start)) { // Request is not a total miss but starts before the cache, // and there is a clip to fetch from. // Not the access pattern for drawing spectrogram or playback, // but maybe scrubbing causes this. // Move the first buffer into second place, and later // refill the first. // (This case might be useful when marching backwards through // the track, as with scrubbing.) mBuffers[0] .swap ( mBuffers[1] ); fillFirst = true; fillSecond = false; // Cache is not in a consistent state yet mNValidBuffers = 0; } // Refill buffers as needed if (fillFirst) { const auto start0 = mPTrack->GetBlockStart(start); if (start0 >= 0) { const auto len0 = mPTrack->GetBestBlockSize(start0); wxASSERT(len0 <= mBufferSize); if (!mPTrack->GetFloats( mBuffers[0].data.get(), start0, len0, fillZero, mayThrow)) return nullptr; mBuffers[0].start = start0; mBuffers[0].len = len0; if (!fillSecond && mBuffers[0].end() != mBuffers[1].start) fillSecond = true; // Keep the partially updated state consistent: mNValidBuffers = fillSecond ? 1 : 2; } else { // Request may fall between the clips of a track. // Invalidate all. WaveTrack::Get() will return zeroes. mNValidBuffers = 0; fillSecond = false; } } wxASSERT(!fillSecond || mNValidBuffers > 0); if (fillSecond) { mNValidBuffers = 1; const auto end0 = mBuffers[0].end(); if (end > end0) { const auto start1 = mPTrack->GetBlockStart(end0); if (start1 == end0) { const auto len1 = mPTrack->GetBestBlockSize(start1); wxASSERT(len1 <= mBufferSize); if (!mPTrack->GetFloats(mBuffers[1].data.get(), start1, len1, fillZero, mayThrow)) return nullptr; mBuffers[1].start = start1; mBuffers[1].len = len1; mNValidBuffers = 2; } } } wxASSERT(mNValidBuffers < 2 || mBuffers[0].end() == mBuffers[1].start); samplePtr buffer = nullptr; // will point into mOverlapBuffer auto remaining = len; // Possibly get an initial portion that is uncached // This may be negative const auto initLen = mNValidBuffers < 1 ? sampleCount( len ) : std::min(sampleCount( len ), mBuffers[0].start - start); if (initLen > 0) { // This might be fetching zeroes between clips mOverlapBuffer.Resize(len, format); // initLen is not more than len: auto sinitLen = initLen.as_size_t(); if (!mPTrack->GetFloats( // See comment below about casting reinterpret_cast(mOverlapBuffer.ptr()), start, sinitLen, fillZero, mayThrow)) return nullptr; wxASSERT( sinitLen <= remaining ); remaining -= sinitLen; start += initLen; buffer = mOverlapBuffer.ptr() + sinitLen * SAMPLE_SIZE(format); } // Now satisfy the request from the buffers for (int ii = 0; ii < mNValidBuffers && remaining > 0; ++ii) { const auto starti = start - mBuffers[ii].start; // Treatment of initLen above establishes this loop invariant, // and statements below preserve it: wxASSERT(starti >= 0); // This may be negative const auto leni = std::min( sampleCount( remaining ), mBuffers[ii].len - starti ); if (initLen <= 0 && leni == len) { // All is contiguous already. We can completely avoid copying // leni is nonnegative, therefore start falls within mBuffers[ii], // so starti is bounded between 0 and buffer length return mBuffers[ii].data.get() + starti.as_size_t() ; } else if (leni > 0) { // leni is nonnegative, therefore start falls within mBuffers[ii] // But we can't satisfy all from one buffer, so copy if (!buffer) { mOverlapBuffer.Resize(len, format); buffer = mOverlapBuffer.ptr(); } // leni is positive and not more than remaining const size_t size = sizeof(float) * leni.as_size_t(); // starti is less than mBuffers[ii].len and nonnegative memcpy(buffer, mBuffers[ii].data.get() + starti.as_size_t(), size); wxASSERT( leni <= remaining ); remaining -= leni.as_size_t(); start += leni; buffer += size; } } if (remaining > 0) { // Very big request! // Fall back to direct fetch if (!buffer) { mOverlapBuffer.Resize(len, format); buffer = mOverlapBuffer.ptr(); } // See comment below about casting if (!mPTrack->GetFloats( reinterpret_cast(buffer), start, remaining, fillZero, mayThrow)) return 0; } // Overlap buffer was meant for the more general support of sample formats // besides float, which explains the cast return reinterpret_cast(mOverlapBuffer.ptr()); } else { #if 0 // Cache works only for float format. mOverlapBuffer.Resize(len, format); if (mPTrack->Get(mOverlapBuffer.ptr(), format, start, len, fillZero, mayThrow)) return mOverlapBuffer.ptr(); #else // No longer handling other than float format. Therefore len is 0. #endif return nullptr; } } void WaveTrackCache::Free() { mBuffers[0].Free(); mBuffers[1].Free(); mOverlapBuffer.Free(); mNValidBuffers = 0; } auto WaveTrack::AllClipsIterator::operator ++ () -> AllClipsIterator & { // The unspecified sequence is a post-order, but there is no // promise whether sister nodes are ordered in time. if ( !mStack.empty() ) { auto &pair = mStack.back(); if ( ++pair.first == pair.second ) { mStack.pop_back(); } else push( (*pair.first)->GetCutLines() ); } return *this; } void WaveTrack::AllClipsIterator::push( WaveClipHolders &clips ) { auto pClips = &clips; while (!pClips->empty()) { auto first = pClips->begin(); mStack.push_back( Pair( first, pClips->end() ) ); pClips = &(*first)->GetCutLines(); } } #include "SampleBlock.h" void VisitBlocks(TrackList &tracks, BlockVisitor visitor, SampleBlockIDSet *pIDs) { for (auto wt : tracks.Any< const WaveTrack >()) { // Scan all clips within current track for(const auto &clip : wt->GetAllClips()) { // Scan all sample blocks within current clip auto blocks = clip->GetSequenceBlockArray(); for (const auto &block : *blocks) { auto &pBlock = block.sb; if ( pBlock ) { if ( pIDs && !pIDs->insert(pBlock->GetBlockID()).second ) continue; if ( visitor ) visitor( *pBlock ); } } } } } void InspectBlocks(const TrackList &tracks, BlockInspector inspector, SampleBlockIDSet *pIDs) { VisitBlocks( const_cast(tracks), std::move( inspector ), pIDs ); } #include "Project.h" #include "SampleBlock.h" static auto TrackFactoryFactory = []( AudacityProject &project ) { return std::make_shared< WaveTrackFactory >( ProjectSettings::Get( project ), SampleBlockFactory::New( project ) ); }; static const AudacityProject::AttachedObjects::RegisteredFactory key2{ TrackFactoryFactory }; WaveTrackFactory &WaveTrackFactory::Get( AudacityProject &project ) { return project.AttachedObjects::Get< WaveTrackFactory >( key2 ); } const WaveTrackFactory &WaveTrackFactory::Get( const AudacityProject &project ) { return Get( const_cast< AudacityProject & >( project ) ); } WaveTrackFactory &WaveTrackFactory::Reset( AudacityProject &project ) { auto result = TrackFactoryFactory( project ); project.AttachedObjects::Assign( key2, result ); return *result; } void WaveTrackFactory::Destroy( AudacityProject &project ) { project.AttachedObjects::Assign( key2, nullptr ); }