/********************************************************************** Audacity: A Digital Audio Editor TrackArtist.cpp Dominic Mazzoni *******************************************************************//*! \class TrackArtist \brief This class handles the actual rendering of WaveTracks (both waveforms and spectra), NoteTracks, LabelTracks and TimeTracks. It's actually a little harder than it looks, because for waveforms at least it needs to cache the samples that are currently on-screen. How Audacity Redisplay Works \n Roger Dannenberg \n Oct 2010 \n This is a brief guide to Audacity redisplay -- it may not be complete. It is my attempt to understand the complicated graphics strategy. One basic idea is that redrawing waveforms is rather slow, so Audacity saves waveform images in bitmaps to make redrawing faster. In particular, during audio playback (and recording), the vertical time indicator is drawn over the waveform about 20 times per second. To avoid unnecessary computation, the indicator is erased by copying a column of pixels from a bitmap image of the waveform. Notice that this implies a two-stage process: first, waveforms are drawn to the bitmp; then, the bitmap (or pieces of it) are copied to the screen, perhaps along with other graphics. The bitmap is for the entire track panel, i.e. multiple tracks, and includes things like the Gain and Pan slders to the left of the waveform images. The screen update uses a mixture of direct drawing and indirect paint events. The "normal" way to update a graphical display is to call the Refresh() method when something invalidates the screen. Later, the system calls OnPaint(), which the application overrides to (re)draw the screen. In wxWidgets, you can also draw directly to the screen without calling Refresh() and without waiting for OnPaint() to be called. I would expect there to be a 2-level invalidation scheme: Some changes invalidate the bitmap, forcing a bitmap redraw *and* a screen redraw. Other changes merely update the screen using pre-existing bitmaps. In Audacity, the "2-level" invalidation works like this: Anything that invalidates the bitmap calls TrackPanel::Refresh(), which has an eraseBackground parameter. This flag says to redraw the bitmap when OnPaint() is called. If eraseBackground is false, the existing bitmap can be used for waveform images. Audacity also draws directly to the screen to update the time indicator during playback. To move the indicator, one column of pixels is drawn to the screen to remove the indicator. Then the indicator is drawn at a new time location. The track panel consists of many components. The tree of calls that update the bitmap looks like this: \code TrackPanel::DrawTracks(), calls TrackArtist::DrawTracks(); TrackPanel::DrawEverythingElse(); for each track, TrackPanel::DrawOutside(); TrackPanel::DrawOutsideOfTrack(); TrackPanel::DrawBordersAroundTrack(); TrackPanel::DrawShadow(); TrackInfo::DrawCloseBox(); TrackInfo::DrawTitleBar(); TrackInfo::DrawMinimize(); TrackInfo::DrawBordersWithin(); various TrackInfo sliders and buttons TrackArtist::DrawVRuler(); TrackPanel::DrawZooming(); draws horizontal dashed lines during zoom-drag TrackPanel::HighlightFocusedTrack(); draws yellow highlight on selected track draw snap guidelines if any \endcode After drawing the bitmap and blitting the bitmap to the screen, the following calls are (sometimes) made. To keep track of what has been drawn on screen over the bitmap images, \li \c mLastCursor is the position of the vertical line representing sel0, the selected time position \li \c mLastIndicator is the position of the moving vertical line during playback \code TrackPanel::DoDrawIndicator(); copy pixel column from bitmap to screen to erase indicator line TrackPanel::DoDrawCursor(); [if mLastCursor == mLastIndicator] TrackPanel::DisplaySelection(); AdornedRulerPanel::DrawIndicator(); [not part of TrackPanel graphics] draw indicator on each track TrackPanel::DoDrawCursor(); draw cursor on each track [at mviewInfo->selectedRegion.t0()] AdornedRulerPanel::DrawCursor(); [not part of TrackPanel graphics] TrackPanel::DisplaySelection(); \endcode To move the indicator, TrackPanel::OnTimer() calls the following, using a drawing context (DC) for the screen. (Refresh is not called to create an OnPaint event. Instead, drawing is direct to the screen.) \code TrackPanel::DrawIndicator(); TrackPanel::DoDrawIndicator(); \endcode Notice that TrackPanel::DrawZooming(), TrackPanel::HighlightFocusedTrack(), and snap guidelines could be drawn directly to the screen rather than to the bitmap, generally eliminating redraw work. One problem is slider udpates. Sliders are in the left area of the track panel. They are not wxWindows like wxSliders, but instead are just drawn on the TrackPanel. When slider state changes, *all* tracks do a full refresh, including recomputing the backing store. It would make more sense to just invalidate the region containing the slider. However, doing that would require either incrementally updating the bitmap (not currently done), or maintaining the sliders and other track info on the screen and not in the bitmap. In my opinion, the bitmap should contain only the waveform, note, and label images along with gray selection highlights. The track info (sliders, buttons, title, etc.), track selection highlight, cursor, and indicator should be drawn in the normal way, and clipping regions should be used to avoid excessive copying of bitmaps (say, when sliders move), or excessive redrawing of track info widgets (say, when scrolling occurs). This is a fairly tricky code change since it requires careful specification of what and where redraw should take place when any state changes. One surprising finding is that NoteTrack display is slow compared to WaveTrack display. Each note takes some time to gather attributes and select colors, and while audio draws two amplitudes per horizontal pixels, large MIDI scores can have more notes than horizontal pixels. This can make slider changes very sluggish, but this can also be a problem with many audio tracks. *//*******************************************************************/ #include "Audacity.h" #include "TrackArtist.h" #include "AudacityApp.h" #include "float_cast.h" #include #include #include #include #include #include #include #include #include #include #include #include #ifdef USE_MIDI #include "NoteTrack.h" #endif // USE_MIDI #include "AColor.h" #include "BlockFile.h" #include "Envelope.h" #include "Track.h" #include "WaveTrack.h" #include "LabelTrack.h" #include "TimeTrack.h" #include "Prefs.h" #include "Sequence.h" #include "Spectrum.h" #include "ViewInfo.h" #include "widgets/Ruler.h" #include "Theme.h" #include "AllThemeResources.h" #undef PROFILE_WAVEFORM #ifdef PROFILE_WAVEFORM #ifdef __WXMSW__ #include #else #include #endif double gWaveformTimeTotal = 0; int gWaveformTimeCount = 0; namespace { struct Profiler { Profiler() { # ifdef __WXMSW__ _time64(&tv0); # else gettimeofday(&tv0, NULL); # endif } ~Profiler() { # ifdef __WXMSW__ _time64(&tv1); double elapsed = _difftime64(tv1, tv0); # else gettimeofday(&tv1, NULL); double elapsed = (tv1.tv_sec + tv1.tv_usec*0.000001) - (tv0.tv_sec + tv0.tv_usec*0.000001); # endif gWaveformTimeTotal += elapsed; gWaveformTimeCount++; wxPrintf(wxT("Avg waveform drawing time: %f\n"), gWaveformTimeTotal / gWaveformTimeCount); } # ifdef __WXMSW__ __time64_t tv0, tv1; #else struct timeval tv0, tv1; #endif }; } #endif #ifdef USE_MIDI /* const int octaveHeight = 62; const int blackPos[5] = { 6, 16, 32, 42, 52 }; const int whitePos[7] = { 0, 9, 17, 26, 35, 44, 53 }; const int notePos[12] = { 1, 6, 11, 16, 21, 27, 32, 37, 42, 47, 52, 57 }; // map pitch number to window coordinate of the *top* of the note // Note the "free" variable bottom, which is assumed to be a local // variable set to the offset of pitch 0 relative to the window #define IPITCH_TO_Y(t, p) (bottom - ((p) / 12) * octaveHeight - \ notePos[(p) % 12] - (t)->GetPitchHeight()) // GetBottom is called from a couple of places to compute the hypothetical // coordinate of the bottom of pitch 0 in window coordinates. See // IPITCH_TO_Y above, which computes coordinates relative to GetBottom() // Note the -NOTE_MARGIN, which leaves a little margin to draw notes that // are out of bounds. I'm not sure why the -2 is necessary. int TrackArtist::GetBottom(NoteTrack *t, const wxRect &r) { int bottomNote = t->GetBottomNote(); int bottom = r.y + r.height - 2 - t->GetNoteMargin() + ((bottomNote / 12) * octaveHeight + notePos[bottomNote % 12]); return bottom; } */ #endif // USE_MIDI TrackArtist::TrackArtist() { mInsetLeft = 0; mInsetTop = 0; mInsetRight = 0; mInsetBottom = 0; mdBrange = ENV_DB_RANGE; mShowClipping = false; UpdatePrefs(); SetColours(); vruler = new Ruler(); #ifdef EXPERIMENTAL_FFT_Y_GRID fftYGridOld=true; #endif //EXPERIMENTAL_FFT_Y_GRID #ifdef EXPERIMENTAL_FIND_NOTES fftFindNotesOld=false; #endif } TrackArtist::~TrackArtist() { delete vruler; } void TrackArtist::SetColours() { theTheme.SetBrushColour( blankBrush, clrBlank ); theTheme.SetBrushColour( unselectedBrush, clrUnselected); theTheme.SetBrushColour( selectedBrush, clrSelected); theTheme.SetBrushColour( sampleBrush, clrSample); theTheme.SetBrushColour( selsampleBrush, clrSelSample); theTheme.SetBrushColour( dragsampleBrush, clrDragSample); theTheme.SetBrushColour( blankSelectedBrush, clrBlankSelected); theTheme.SetPenColour( blankPen, clrBlank); theTheme.SetPenColour( unselectedPen, clrUnselected); theTheme.SetPenColour( selectedPen, clrSelected); theTheme.SetPenColour( samplePen, clrSample); theTheme.SetPenColour( selsamplePen, clrSelSample); theTheme.SetPenColour( muteSamplePen, clrMuteSample); theTheme.SetPenColour( odProgressDonePen, clrProgressDone); theTheme.SetPenColour( odProgressNotYetPen, clrProgressNotYet); theTheme.SetPenColour( rmsPen, clrRms); theTheme.SetPenColour( muteRmsPen, clrMuteRms); theTheme.SetPenColour( shadowPen, clrShadow); theTheme.SetPenColour( clippedPen, clrClipped); theTheme.SetPenColour( muteClippedPen, clrMuteClipped); theTheme.SetPenColour( blankSelectedPen,clrBlankSelected); } void TrackArtist::SetInset(int left, int top, int right, int bottom) { mInsetLeft = left; mInsetTop = top; mInsetRight = right; mInsetBottom = bottom; } void TrackArtist::DrawTracks(TrackList * tracks, Track * start, wxDC & dc, wxRegion & reg, wxRect & r, wxRect & clip, ViewInfo * viewInfo, bool drawEnvelope, bool drawSamples, bool drawSliders) { wxRect trackRect = r; wxRect stereoTrackRect; TrackListIterator iter(tracks); Track *t; bool hasSolo = false; for (t = iter.First(); t; t = iter.Next()) { if (t->GetSolo()) { hasSolo = true; break; } } #if defined(DEBUG_CLIENT_AREA) // Change the +0 to +1 or +2 to see the bounding box mInsetLeft = 1+0; mInsetTop = 5+0; mInsetRight = 6+0; mInsetBottom = 2+0; // This just show what the passed in rectanges enclose dc.SetPen(wxColour(*wxGREEN)); dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.DrawRectangle(r); dc.SetPen(wxColour(*wxBLUE)); dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.DrawRectangle(clip); #endif gPrefs->Read(wxT("/GUI/ShowTrackNameInWaveform"), &mbShowTrackNameInWaveform, false); t = iter.StartWith(start); while (t) { trackRect.y = t->GetY() - viewInfo->vpos; trackRect.height = t->GetHeight(); if (trackRect.y > clip.GetBottom() && !t->GetLinked()) { break; } #if defined(DEBUG_CLIENT_AREA) // Filled rectangle to show the interior of the client area wxRect zr = trackRect; zr.x+=1; zr.y+=5; zr.width-=7; zr.height-=7; dc.SetPen(*wxCYAN_PEN); dc.SetBrush(*wxRED_BRUSH); dc.DrawRectangle(zr); #endif stereoTrackRect = trackRect; // For various reasons, the code will break if we display one // of a stereo pair of tracks but not the other - for example, // if you try to edit the envelope of one track when its linked // pair is off the screen, then it won't be able to edit the // offscreen envelope. So we compute the rect of the track and // its linked partner, and see if any part of that rect is on-screen. // If so, we draw both. Otherwise, we can safely draw neither. Track *link = t->GetLink(); if (link) { if (t->GetLinked()) { // If we're the first track stereoTrackRect.height += link->GetHeight(); } else { // We're the second of two stereoTrackRect.y -= link->GetHeight(); stereoTrackRect.height += link->GetHeight(); } } #ifdef EXPERIMENTAL_OUTPUT_DISPLAY if(MONO_WAVE_PAN(t)){ stereoTrackRect.height += t->GetHeight(true); t->SetVirtualStereo(false); } #endif if (stereoTrackRect.Intersects(clip) && reg.Contains(stereoTrackRect)) { wxRect rr = trackRect; rr.x += mInsetLeft; rr.y += mInsetTop; rr.width -= (mInsetLeft + mInsetRight); rr.height -= (mInsetTop + mInsetBottom); DrawTrack(t, dc, rr, viewInfo, drawEnvelope, drawSamples, drawSliders, hasSolo); } #ifdef EXPERIMENTAL_OUTPUT_DISPLAY if(MONO_WAVE_PAN(t)){ trackRect.y = t->GetY(true) - viewInfo->vpos; trackRect.height = t->GetHeight(true); stereoTrackRect = trackRect; stereoTrackRect.y -= t->GetHeight(); stereoTrackRect.height += t->GetHeight(); t->SetVirtualStereo(true); if (stereoTrackRect.Intersects(clip) && reg.Contains(stereoTrackRect)) { wxRect rr = trackRect; rr.x += mInsetLeft; rr.y += mInsetTop; rr.width -= (mInsetLeft + mInsetRight); rr.height -= (mInsetTop + mInsetBottom); DrawTrack(t, dc, rr, viewInfo, drawEnvelope, drawSamples, drawSliders, hasSolo); } } #endif t = iter.Next(); } } void TrackArtist::DrawTrack(const Track * t, wxDC & dc, const wxRect & r, const ViewInfo * viewInfo, bool drawEnvelope, bool drawSamples, bool drawSliders, bool hasSolo) { switch (t->GetKind()) { case Track::Wave: { WaveTrack* wt = (WaveTrack*)t; for (WaveClipList::compatibility_iterator it=wt->GetClipIterator(); it; it=it->GetNext()) { it->GetData()->ClearDisplayRect(); } bool muted = (hasSolo || t->GetMute()) && !t->GetSolo(); switch (wt->GetDisplay()) { case WaveTrack::WaveformDisplay: DrawWaveform(wt, dc, r, viewInfo, drawEnvelope, drawSamples, drawSliders, false, muted); break; case WaveTrack::WaveformDBDisplay: DrawWaveform(wt, dc, r, viewInfo, drawEnvelope, drawSamples, drawSliders, true, muted); break; case WaveTrack::SpectrumDisplay: case WaveTrack::SpectrumLogDisplay: case WaveTrack::SpectralSelectionDisplay: case WaveTrack::SpectralSelectionLogDisplay: case WaveTrack::PitchDisplay: DrawSpectrum(wt, dc, r, viewInfo); break; } if (mbShowTrackNameInWaveform && // Exclude right channel of stereo track !(!wt->GetLinked() && wt->GetLink())) { wxFont labelFont(12, wxSWISS, wxNORMAL, wxNORMAL); dc.SetFont(labelFont); dc.SetTextForeground(wxColour(255, 255, 0)); dc.DrawText (wt->GetName(), r.x+10, r.y); // move right 10 pixels to avoid overwriting <- symbol } break; // case Wave } #ifdef USE_MIDI case Track::Note: { bool muted = (hasSolo || t->GetMute()) && !t->GetSolo(); DrawNoteTrack((NoteTrack *)t, dc, r, viewInfo, muted); break; } #endif // USE_MIDI case Track::Label: DrawLabelTrack((LabelTrack *)t, dc, r, viewInfo); break; case Track::Time: DrawTimeTrack((TimeTrack *)t, dc, r, viewInfo); break; } } void TrackArtist::DrawVRuler(Track *t, wxDC * dc, wxRect & r) { int kind = t->GetKind(); // Label and Time tracks do not have a vruler // But give it a beveled area if (kind == Track::Label) { wxRect bev = r; bev.Inflate(-1, -1); bev.width += 1; AColor::BevelTrackInfo(*dc, true, bev); return; } // Time tracks if (kind == Track::Time) { wxRect bev = r; bev.Inflate(-1, -1); bev.width += 1; AColor::BevelTrackInfo(*dc, true, bev); // Right align the ruler wxRect rr = r; rr.width--; if (t->vrulerSize.GetWidth() < r.GetWidth()) { int adj = rr.GetWidth() - t->vrulerSize.GetWidth(); rr.x += adj; rr.width -= adj; } UpdateVRuler(t, rr); vruler->Draw(*dc); return; } // All waves have a ruler in the info panel // The ruler needs a bevelled surround. if (kind == Track::Wave) { wxRect bev = r; bev.Inflate(-1, -1); bev.width += 1; AColor::BevelTrackInfo(*dc, true, bev); // Pitch doesn't have a ruler if (((WaveTrack *)t)->GetDisplay() == WaveTrack::PitchDisplay) { return; } // Right align the ruler wxRect rr = r; rr.width--; if (t->vrulerSize.GetWidth() < r.GetWidth()) { int adj = rr.GetWidth() - t->vrulerSize.GetWidth(); rr.x += adj; rr.width -= adj; } UpdateVRuler(t, rr); vruler->Draw(*dc); return; } #ifdef USE_MIDI // The note track draws a vertical keyboard to label pitches if (kind == Track::Note) { UpdateVRuler(t, r); dc->SetPen(*wxTRANSPARENT_PEN); dc->SetBrush(*wxWHITE_BRUSH); wxRect bev = r; bev.x++; bev.y++; bev.width--; bev.height--; dc->DrawRectangle(bev); r.y += 2; r.height -= 2; //int bottom = GetBottom((NoteTrack *) t, r); NoteTrack *track = (NoteTrack *) t; track->PrepareIPitchToY(r); wxPen hilitePen; hilitePen.SetColour(120, 120, 120); wxBrush blackKeyBrush; blackKeyBrush.SetColour(70, 70, 70); dc->SetBrush(blackKeyBrush); int fontSize = 10; #ifdef __WXMSW__ fontSize = 8; #endif wxFont labelFont(fontSize, wxSWISS, wxNORMAL, wxNORMAL); dc->SetFont(labelFont); int octave = 0; int obottom = track->GetOctaveBottom(octave); int marg = track->GetNoteMargin(); //IPITCH_TO_Y(octave * 12) + PITCH_HEIGHT + 1; while (obottom >= r.y) { dc->SetPen(*wxBLACK_PEN); for (int white = 0; white < 7; white++) { int pos = track->GetWhitePos(white); if (obottom - pos > r.y + marg + 1 && // don't draw too close to margin line -- it's annoying obottom - pos < r.y + r.height - marg - 3) AColor::Line(*dc, r.x, obottom - pos, r.x + r.width, obottom - pos); } wxRect br = r; br.height = track->GetPitchHeight(); br.x++; br.width = 17; for (int black = 0; black < 5; black++) { br.y = obottom - track->GetBlackPos(black); if (br.y > r.y + marg - 2 && br.y + br.height < r.y + r.height - marg) { dc->SetPen(hilitePen); dc->DrawRectangle(br); dc->SetPen(*wxBLACK_PEN); AColor::Line(*dc, br.x + 1, br.y + br.height - 1, br.x + br.width - 1, br.y + br.height - 1); AColor::Line(*dc, br.x + br.width - 1, br.y + 1, br.x + br.width - 1, br.y + br.height - 1); } } if (octave >= 1 && octave <= 10) { wxString s; // ISO standard: A440 is in the 4th octave, denoted // A4 <- the "4" should be a subscript. s.Printf(wxT("C%d"), octave - 1); wxCoord width, height; dc->GetTextExtent(s, &width, &height); if (obottom - height + 4 > r.y && obottom + 4 < r.y + r.height) { dc->SetTextForeground(wxColour(60, 60, 255)); dc->DrawText(s, r.x + r.width - width, obottom - height + 2); } } obottom = track->GetOctaveBottom(++octave); } // draw lines delineating the out-of-bounds margins dc->SetPen(*wxBLACK_PEN); // you would think the -1 offset here should be -2 to match the // adjustment to r.y (see above), but -1 produces correct output AColor::Line(*dc, r.x, r.y + marg - 1, r.x + r.width, r.y + marg - 1); // since the margin gives us the bottom of the line, // the extra -1 gets us to the top AColor::Line(*dc, r.x, r.y + r.height - marg - 1, r.x + r.width, r.y + r.height - marg - 1); } #endif // USE_MIDI } void TrackArtist::UpdateVRuler(Track *t, wxRect & r) { // Label tracks do not have a vruler if (t->GetKind() == Track::Label) { return; } // Time tracks if (t->GetKind() == Track::Time) { TimeTrack *tt = (TimeTrack *)t; float min, max; min = tt->GetRangeLower() * 100.0; max = tt->GetRangeUpper() * 100.0; vruler->SetBounds(r.x, r.y+1, r.x + r.width, r.y + r.height-1); vruler->SetOrientation(wxVERTICAL); vruler->SetRange(max, min); vruler->SetFormat((tt->GetDisplayLog()) ? Ruler::RealLogFormat : Ruler::RealFormat); vruler->SetUnits(wxT("")); vruler->SetLabelEdges(false); vruler->SetLog(tt->GetDisplayLog()); } // All waves have a ruler in the info panel // The ruler needs a bevelled surround. if (t->GetKind() == Track::Wave) { WaveTrack *wt = (WaveTrack *)t; int display = wt->GetDisplay(); if (display == WaveTrack::WaveformDisplay) { // Waveform float min, max; wt->GetDisplayBounds(&min, &max); if(wt->GetLastDisplay()==WaveTrack::WaveformDBDisplay) { // do a translation into the WaveTrack::WaveformDisplay space wt->SetDisplay(WaveTrack::WaveformDisplay); // this makes the last display not WaveformDBDisplay float sign = (min >= 0 ? 1 : -1); if (min != 0.) { min = pow(10., (fabs(min)*mdBrange - mdBrange)/20.0); if (min < 0.0) min = 0.0; min *= sign; } sign = (max >= 0 ? 1 : -1); if (max != 0.) { max = pow(10., (fabs(max)*mdBrange - mdBrange)/20.0); if (max < 0.0) max = 0.0; max *= sign; } wt->SetDisplayBounds(min, max); } vruler->SetBounds(r.x, r.y+1, r.x + r.width, r.y + r.height-1); vruler->SetOrientation(wxVERTICAL); vruler->SetRange(max, min); vruler->SetFormat(Ruler::RealFormat); vruler->SetUnits(wxT("")); vruler->SetLabelEdges(false); vruler->SetLog(false); } else if (display == WaveTrack::WaveformDBDisplay) { // Waveform (db) vruler->SetUnits(wxT("")); float min, max; wt->GetDisplayBounds(&min, &max); if(wt->GetLastDisplay()==WaveTrack::WaveformDisplay) { // do a translation into the WaveTrack::WaveformDBDisplay space wt->SetDisplay(WaveTrack::WaveformDBDisplay); // this makes the last display not WaveformDisplay float sign = (min >= 0 ? 1 : -1); if (min != 0.) { min = (20.0 * log10(fabs(min)) + mdBrange) / mdBrange; if (min < 0.0) min = 0.0; min *= sign; } sign = (max >= 0 ? 1 : -1); if (max != 0.) { max = (20.0 * log10(fabs(max)) + mdBrange) / mdBrange; if (max < 0.0) max = 0.0; max *= sign; } wt->SetDisplayBounds(min, max); } if (max > 0) { int top = 0; float topval = 0; int bot = r.height; float botval = -mdBrange; if (min < 0) { bot = top + (int)((max / (max-min))*(bot-top)); min = 0; } if (max > 1) { top += (int)((max-1)/(max-min) * (bot-top)); max = 1; } if (max < 1 && max > 0) topval = -((1-max)*mdBrange); if (min > 0) { botval = -((1-min)*mdBrange); } vruler->SetBounds(r.x, r.y+top+1, r.x + r.width, r.y + bot-1); vruler->SetOrientation(wxVERTICAL); vruler->SetRange(topval, botval); } else vruler->SetBounds(0.0, 0.0, 0.0, 0.0); // A.C.H I couldn't find a way to just disable it? vruler->SetFormat(Ruler::RealLogFormat); vruler->SetLabelEdges(true); vruler->SetLog(false); } else if ( (display == WaveTrack::SpectrumDisplay) || (display == WaveTrack::SpectralSelectionDisplay) ) { // Spectrum if (r.height < 60) return; double rate = wt->GetRate(); int freq = lrint(rate/2.); int maxFreq = GetSpectrumMaxFreq(freq); #ifdef EXPERIMENTAL_FFT_SKIP_POINTS maxFreq/=(mFftSkipPoints+1); #endif //EXPERIMENTAL_FFT_SKIP_POINTS if(maxFreq > freq) maxFreq = freq; int minFreq = GetSpectrumMinFreq(0); #ifdef EXPERIMENTAL_FFT_SKIP_POINTS minFreq/=(mFftSkipPoints+1); #endif //EXPERIMENTAL_FFT_SKIP_POINTS if(minFreq < 0) minFreq = 0; /* draw the ruler we will use Hz if maxFreq is < 2000, otherwise we represent kHz, and append to the numbers a "k" */ vruler->SetBounds(r.x, r.y+1, r.x + r.width, r.y + r.height-1); vruler->SetOrientation(wxVERTICAL); vruler->SetFormat(Ruler::RealFormat); vruler->SetLabelEdges(true); // use kHz in scale, if appropriate if (maxFreq>=2000) { vruler->SetRange((maxFreq/1000.), (minFreq/1000.)); vruler->SetUnits(wxT("k")); } else { // use Hz vruler->SetRange(int(maxFreq), int(minFreq)); vruler->SetUnits(wxT("")); } vruler->SetLog(false); } else if ( (display == WaveTrack::SpectrumLogDisplay) || (display == WaveTrack::SpectralSelectionLogDisplay) ) { // SpectrumLog if (r.height < 10) return; double rate = wt->GetRate(); int freq = lrint(rate/2.); int maxFreq = GetSpectrumLogMaxFreq(freq); #ifdef EXPERIMENTAL_FFT_SKIP_POINTS maxFreq/=(mFftSkipPoints+1); #endif //EXPERIMENTAL_FFT_SKIP_POINTS if(maxFreq > freq) maxFreq = freq; int minFreq = GetSpectrumLogMinFreq(freq/1000.0); #ifdef EXPERIMENTAL_FFT_SKIP_POINTS minFreq/=(mFftSkipPoints+1); #endif //EXPERIMENTAL_FFT_SKIP_POINTS if(minFreq < 1) minFreq = 1; /* draw the ruler we will use Hz if maxFreq is < 2000, otherwise we represent kHz, and append to the numbers a "k" */ vruler->SetBounds(r.x, r.y+1, r.x + r.width, r.y + r.height-1); vruler->SetOrientation(wxVERTICAL); vruler->SetFormat(Ruler::IntFormat); vruler->SetLabelEdges(true); vruler->SetRange(maxFreq, minFreq); vruler->SetUnits(wxT("")); vruler->SetLog(true); } else if (display == WaveTrack::PitchDisplay) { // Pitch } } #ifdef USE_MIDI // The note track isn't drawing a ruler at all! // But it needs to! else if (t->GetKind() == Track::Note) { vruler->SetBounds(r.x, r.y+1, r.x + 1, r.y + r.height-1); vruler->SetOrientation(wxVERTICAL); } #endif // USE_MIDI vruler->GetMaxSize(&t->vrulerSize.x, &t->vrulerSize.y); } /// Takes a value between min and max and returns a value between /// height and 0 /// \todo Should this function move int GuiWaveTrack where it can /// then use the zoomMin, zoomMax and height values without having /// to have them passed in to it?? int GetWaveYPos(float value, float min, float max, int height, bool dB, bool outer, float dBr, bool clip) { if (dB) { if (height == 0) { return 0; } float sign = (value >= 0 ? 1 : -1); if (value != 0.) { float db = 20.0 * log10(fabs(value)); value = (db + dBr) / dBr; if (!outer) { value -= 0.5; } if (value < 0.0) { value = 0.0; } value *= sign; } } else { if (!outer) { if (value >= 0.0) { value -= 0.5; } else { value += 0.5; } } } if (clip) { if (value < min) { value = min; } if (value > max) { value = max; } } value = (max - value) / (max - min); return (int) (value * (height - 1) + 0.5); } float FromDB(float value, double dBRange) { if (value == 0) return 0; double sign = (value >= 0 ? 1 : -1); return pow(10.0, ((fabs(value) * dBRange) - dBRange) / 20.0)*sign; } float ValueOfPixel(int y, int height, bool offset, bool dB, double dBRange, float zoomMin, float zoomMax) { wxASSERT(height > 0); float v = zoomMax - (y / (float)height) * (zoomMax - zoomMin); if (offset) { if (v > 0.0) v += .5; else v -= .5; } if (dB) v = FromDB(v, dBRange); return v; } void TrackArtist::DrawNegativeOffsetTrackArrows(wxDC &dc, const wxRect &r) { // Draws two black arrows on the left side of the track to // indicate the user that the track has been time-shifted // to the left beyond t=0.0. dc.SetPen(*wxBLACK_PEN); AColor::Line(dc, r.x + 2, r.y + 6, r.x + 8, r.y + 6); AColor::Line(dc, r.x + 2, r.y + 6, r.x + 6, r.y + 2); AColor::Line(dc, r.x + 2, r.y + 6, r.x + 6, r.y + 10); AColor::Line(dc, r.x + 2, r.y + r.height - 8, r.x + 8, r.y + r.height - 8); AColor::Line(dc, r.x + 2, r.y + r.height - 8, r.x + 6, r.y + r.height - 4); AColor::Line(dc, r.x + 2, r.y + r.height - 8, r.x + 6, r.y + r.height - 12); } void TrackArtist::DrawWaveformBackground(wxDC &dc, const wxRect &r, const double env[], float zoomMin, float zoomMax, bool dB, const sampleCount where[], sampleCount ssel0, sampleCount ssel1, bool drawEnvelope, bool bIsSyncLockSelected) { // Visually (one vertical slice of the waveform background, on its side; // the "*" is the actual waveform background we're drawing // //1.0 0.0 -1.0 // |--------------------------------|--------------------------------| // *************** *************** // | | | | // maxtop maxbot mintop minbot int h = r.height; int halfHeight = wxMax(h / 2, 1); int maxtop, lmaxtop = 0; int mintop, lmintop = 0; int maxbot, lmaxbot = 0; int minbot, lminbot = 0; bool sel, lsel = false; int x, lx = 0; int l, w; dc.SetPen(*wxTRANSPARENT_PEN); dc.SetBrush(blankBrush); dc.DrawRectangle(r); for (x = 0; x < r.width; x++) { // First we compute the truncated shape of the waveform background. // If drawEnvelope is true, then we compute the lower border of the // envelope. maxtop = GetWaveYPos(env[x], zoomMin, zoomMax, h, dB, true, mdBrange, true); maxbot = GetWaveYPos(env[x], zoomMin, zoomMax, h, dB, false, mdBrange, true); mintop = GetWaveYPos(-env[x], zoomMin, zoomMax, h, dB, false, mdBrange, true); minbot = GetWaveYPos(-env[x], zoomMin, zoomMax, h, dB, true, mdBrange, true); // Make sure it's odd so that a that max and min mirror each other mintop +=1; minbot +=1; if (!drawEnvelope || maxbot > mintop) { maxbot = halfHeight; mintop = halfHeight; } // We don't draw selection color for sync-lock selected tracks. sel = (ssel0 <= where[x] && where[x + 1] < ssel1) && !bIsSyncLockSelected; if (lmaxtop == maxtop && lmintop == mintop && lmaxbot == maxbot && lminbot == minbot && lsel == sel) { continue; } dc.SetBrush(lsel ? selectedBrush : unselectedBrush); l = r.x + lx; w = x - lx; if (lmaxbot < lmintop - 1) { dc.DrawRectangle(l, r.y + lmaxtop, w, lmaxbot - lmaxtop); dc.DrawRectangle(l, r.y + lmintop, w, lminbot - lmintop); } else { dc.DrawRectangle(l, r.y + lmaxtop, w, lminbot - lmaxtop); } lmaxtop = maxtop; lmintop = mintop; lmaxbot = maxbot; lminbot = minbot; lsel = sel; lx = x; } dc.SetBrush(lsel ? selectedBrush : unselectedBrush); l = r.x + lx; w = x - lx; if (lmaxbot < lmintop - 1) { dc.DrawRectangle(l, r.y + lmaxtop, w, lmaxbot - lmaxtop); dc.DrawRectangle(l, r.y + lmintop, w, lminbot - lmintop); } else { dc.DrawRectangle(l, r.y + lmaxtop, w, lminbot - lmaxtop); } // If sync-lock selected, draw in linked graphics. if (bIsSyncLockSelected && ssel0 < ssel1) { // Find the beginning/end of the selection int begin, end; for (x = 0; x < r.width && where[x] < ssel0; ++x); begin = x; for (; x < r.width && where[x] < ssel1; ++x); end = x; DrawSyncLockTiles(&dc, wxRect(r.x + begin, r.y, end - 1 - begin, r.height)); } //OK, the display bounds are between min and max, which //is spread across r.height. Draw the line at the proper place. if (zoomMin < 0 && zoomMax > 0) { int half = (int)((zoomMax / (zoomMax - zoomMin)) * h); dc.SetPen(*wxBLACK_PEN); AColor::Line(dc, r.x, r.y + half, r.x + r.width, r.y + half); } } void TrackArtist::DrawMinMaxRMS(wxDC &dc, const wxRect &r, const double env[], float zoomMin, float zoomMax, bool dB, const WaveDisplay &display, bool /* showProgress */, bool muted #ifdef EXPERIMENTAL_OUTPUT_DISPLAY , const float gain #endif ) { const float *const min = display.min; const float *const max = display.max; const float *const rms = display.rms; const int *const bl = display.bl; // Display a line representing the // min and max of the samples in this region int lasth1 = std::numeric_limits::max(); int lasth2 = std::numeric_limits::min(); int h1; int h2; int *r1 = new int[r.width]; int *r2 = new int[r.width]; int *clipped = NULL; int clipcnt = 0; int x; if (mShowClipping) { clipped = new int[r.width]; } long pixAnimOffset = (long)fabs((double)(wxDateTime::Now().GetTicks() * -10)) + wxDateTime::Now().GetMillisecond() / 100; //10 pixels a second bool drawStripes = true; bool drawWaveform = true; dc.SetPen(muted ? muteSamplePen : samplePen); for (x = 0; x < r.width; x++) { int xx = r.x + x; double v; #ifdef EXPERIMENTAL_OUTPUT_DISPLAY //JWA: "gain" variable passed to function includes the pan value and is used below 4/14/13 v = min[x] * env[x] * gain; #else v = min[x] * env[x]; #endif if (clipped && mShowClipping && (v <= -MAX_AUDIO)) { if (clipcnt == 0 || clipped[clipcnt - 1] != xx) { clipped[clipcnt++] = xx; } } h1 = GetWaveYPos(v, zoomMin, zoomMax, r.height, dB, true, mdBrange, true); #ifdef EXPERIMENTAL_OUTPUT_DISPLAY v = max[x] * env[x] * gain; #else v = max[x] * env[x]; #endif if (clipped && mShowClipping && (v >= MAX_AUDIO)) { if (clipcnt == 0 || clipped[clipcnt - 1] != xx) { clipped[clipcnt++] = xx; } } h2 = GetWaveYPos(v, zoomMin, zoomMax, r.height, dB, true, mdBrange, true); // JKC: This adjustment to h1 and h2 ensures that the drawn // waveform is continuous. if (x > 0) { if (h1 < lasth2) { h1 = lasth2 - 1; } if (h2 > lasth1) { h2 = lasth1 + 1; } } lasth1 = h1; lasth2 = h2; #ifdef EXPERIMENTAL_OUTPUT_DISPLAY r1[x] = GetWaveYPos(-rms[x] * env[x]*gain, zoomMin, zoomMax, r.height, dB, true, mdBrange, true); r2[x] = GetWaveYPos(rms[x] * env[x]*gain, zoomMin, zoomMax, r.height, dB, true, mdBrange, true); #else r1[x] = GetWaveYPos(-rms[x] * env[x], zoomMin, zoomMax, r.height, dB, true, mdBrange, true); r2[x] = GetWaveYPos(rms[x] * env[x], zoomMin, zoomMax, r.height, dB, true, mdBrange, true); #endif // Make sure the rms isn't larger than the waveform min/max if (r1[x] > h1 - 1) { r1[x] = h1 - 1; } if (r2[x] < h2 + 1) { r2[x] = h2 + 1; } if (r2[x] > r1[x]) { r2[x] = r1[x]; } if (bl[x] <= -1) { if (drawStripes) { // TODO:unify with buffer drawing. dc.SetPen((bl[x] % 2) ? muteSamplePen : samplePen); for (int y = 0; y < r.height / 25 + 1; y++) { // we are drawing over the buffer, but I think DrawLine takes care of this. AColor::Line(dc, xx, r.y + 25 * y + (x /*+pixAnimOffset*/) % 25, xx, r.y + 25 * y + (x /*+pixAnimOffset*/) % 25 + 6); //take the min so we don't draw past the edge } } // draw a dummy waveform - some kind of sinusoid. We want to animate it so the user knows it's a dummy. Use the second's unit of a get time function. // Lets use a triangle wave for now since it's easier - I don't want to use sin() or make a wavetable just for this. if (drawWaveform) { int triX; dc.SetPen(samplePen); triX = fabs((double)((x + pixAnimOffset) % (2 * r.height)) - r.height) + r.height; for (int y = 0; y < r.height; y++) { if ((y + triX) % r.height == 0) { dc.DrawPoint(xx, r.y + y); } } } // Restore the pen for remaining pixel columns! dc.SetPen(muted ? muteSamplePen : samplePen); } else { AColor::Line(dc, xx, r.y + h2, xx, r.y + h1); } } // Stroke rms over the min-max dc.SetPen(muted ? muteRmsPen : rmsPen); for (int x = 0; x < r.width; x++) { int xx = r.x + x; if (bl[x] <= -1) { } else if (r1[x] != r2[x]) { AColor::Line(dc, xx, r.y + r2[x], xx, r.y + r1[x]); } } // Draw the clipping lines if (clipcnt) { dc.SetPen(muted ? muteClippedPen : clippedPen); while (--clipcnt >= 0) { int xx = clipped[clipcnt]; AColor::Line(dc, xx, r.y, xx, r.y + r.height); } } if (mShowClipping) { delete[] clipped; } delete [] r1; delete [] r2; } void TrackArtist::DrawIndividualSamples(wxDC &dc, const wxRect &r, float zoomMin, float zoomMax, bool dB, WaveClip *clip, double t0, double pps, double WXUNUSED(h), bool drawSamples, bool showPoints, bool muted) { double rate = clip->GetRate(); sampleCount s0 = (sampleCount) (t0 * rate + 0.5); sampleCount slen = (sampleCount) (r.width * rate / pps + 0.5); sampleCount snSamples = clip->GetNumSamples(); slen += 4; if (s0 > snSamples) { return; } if (s0 + slen > snSamples) { slen = snSamples - s0; } float *buffer = new float[slen]; clip->GetSamples((samplePtr)buffer, floatSample, s0, slen); int *xpos = new int[slen]; int *ypos = new int[slen]; int *clipped = NULL; int clipcnt = 0; sampleCount s; if (mShowClipping) { clipped = new int[slen]; } dc.SetPen(muted ? muteSamplePen : samplePen); for (s = 0; s < slen; s++) { double tt = (s / rate); // MB: (s0/rate - t0) is the distance from the left edge of the screen // to the first sample. int xx = (int)rint((tt + s0 / rate - t0) * pps); if (xx < -10000) { xx = -10000; } if (xx > 10000) { xx = 10000; } xpos[s] = xx; // t0 + clip->GetOffset() is 'h' (the absolute time of the left edge) for 'r'. tt = buffer[s] * clip->GetEnvelope()->GetValueAtX(xx + r.x, r, t0 + clip->GetOffset(), pps); if (clipped && mShowClipping && ((tt <= -MAX_AUDIO) || (tt >= MAX_AUDIO))) clipped[clipcnt++] = xx; ypos[s] = GetWaveYPos(tt, zoomMin, zoomMax, r.height, dB, true, mdBrange, false); if (ypos[s] < -1) { ypos[s] = -1; } if (ypos[s] > r.height) { ypos[s] = r.height; } } // Draw lines for (s = 0; s < slen - 1; s++) { AColor::Line(dc, r.x + xpos[s], r.y + ypos[s], r.x + xpos[s + 1], r.y + ypos[s + 1]); } if (showPoints) { // Draw points int tickSize= drawSamples ? 4 : 3;// Bigger ellipses when draggable. wxRect pr; pr.width = tickSize; pr.height = tickSize; //different colour when draggable. dc.SetBrush( drawSamples ? dragsampleBrush : sampleBrush); for (s = 0; s < slen; s++) { if (ypos[s] >= 0 && ypos[s] < r.height) { pr.x = r.x + xpos[s] - tickSize/2; pr.y = r.y + ypos[s] - tickSize/2; dc.DrawEllipse(pr); } } } // Draw clipping if (clipcnt) { dc.SetPen(muted ? muteClippedPen : clippedPen); while (--clipcnt >= 0) { s = clipped[clipcnt]; AColor::Line(dc, r.x + s, r.y, r.x + s, r.y + r.height); } } if (mShowClipping) { delete [] clipped; } delete[]buffer; delete[]xpos; delete[]ypos; } void TrackArtist::DrawEnvelope(wxDC &dc, const wxRect &r, const double env[], float zoomMin, float zoomMax, bool dB) { int h = r.height; dc.SetPen(AColor::envelopePen); for (int x = 0; x < r.width; x++) { int cenvTop = GetWaveYPos(env[x], zoomMin, zoomMax, h, dB, true, mdBrange, true); int cenvBot = GetWaveYPos(-env[x], zoomMin, zoomMax, h, dB, true, mdBrange, true); int envTop = GetWaveYPos(env[x], zoomMin, zoomMax, h, dB, true, mdBrange, false); int envBot = GetWaveYPos(-env[x], zoomMin, zoomMax, h, dB, true, mdBrange, false); // Make the collision at zero actually look solid if (cenvBot - cenvTop < 9) { int value = (int)((zoomMax / (zoomMax - zoomMin)) * h); cenvTop = value - 4; cenvBot = value + 4; } DrawEnvLine(dc, r, x, envTop, cenvTop, true); DrawEnvLine(dc, r, x, envBot, cenvBot, false); } } void TrackArtist::DrawEnvLine(wxDC &dc, const wxRect &r, int x, int y, int cy, bool top) { int xx = r.x + x; int yy = r.y + cy; if (y < 0) { if (x % 4 != 3) { AColor::Line(dc, xx, yy, xx, yy + 3); } } else if (y > r.height) { if (x % 4 != 3) { AColor::Line(dc, xx, yy - 3, xx, yy); } } else { if (top) { AColor::Line(dc, xx, yy, xx, yy + 3); } else { AColor::Line(dc, xx, yy - 3, xx, yy); } } } void TrackArtist::DrawWaveform(WaveTrack *track, wxDC & dc, const wxRect & r, const ViewInfo *viewInfo, bool drawEnvelope, bool drawSamples, bool drawSliders, bool dB, bool muted) { DrawBackgroundWithSelection(&dc, r, track, blankSelectedBrush, blankBrush, viewInfo->selectedRegion.t0(), viewInfo->selectedRegion.t1(), viewInfo->h, viewInfo->zoom); for (WaveClipList::compatibility_iterator it = track->GetClipIterator(); it; it = it->GetNext()) DrawClipWaveform(track, it->GetData(), dc, r, viewInfo, drawEnvelope, drawSamples, dB, muted); // Update cache for locations, e.g. cutlines and merge points track->UpdateLocationsCache(); for (int i = 0; iGetNumCachedLocations(); i++) { WaveTrack::Location loc = track->GetCachedLocation(i); double x = (loc.pos - viewInfo->h) * viewInfo->zoom; if (x >= 0 && x < r.width) { dc.SetPen(*wxGREY_PEN); AColor::Line(dc, (int) (r.x + x - 1), r.y, (int) (r.x + x - 1), r.y + r.height); if (loc.typ == WaveTrack::locationCutLine) { dc.SetPen(*wxRED_PEN); } else { dc.SetPen(*wxBLACK_PEN); } AColor::Line(dc, (int) (r.x + x), r.y, (int) (r.x + x), r.y + r.height); dc.SetPen(*wxGREY_PEN); AColor::Line(dc, (int) (r.x + x + 1), r.y, (int) (r.x + x + 1), r.y + r.height); } } if (drawSliders) { DrawTimeSlider(dc, r, true); // directed right DrawTimeSlider(dc, r, false); // directed left } } namespace { struct ClipParameters { // Do a bunch of calculations common to waveform and spectrum drawing. ClipParameters (bool spectrum, const WaveTrack *track, const WaveClip *clip, const wxRect &r, const SelectedRegion &selectedRegion, const ViewInfo &viewInfo) { selectedRegion; tOffset = clip->GetOffset(); rate = clip->GetRate(); h = viewInfo.h; //The horizontal position in seconds pps = viewInfo.zoom; //points-per-second--the zoom level double sel0 = viewInfo.selectedRegion.t0(); //left selection bound double sel1 = viewInfo.selectedRegion.t1(); //right selection bound //If the track isn't selected, make the selection empty if (!track->GetSelected() && (spectrum || !track->IsSyncLockSelected())) { // PRL: why was there a difference for spectrum? sel0 = sel1 = 0.0; } const double trackLen = clip->GetEndTime() - clip->GetStartTime(); tstep = 1.0 / pps; // Seconds per point tpre = h - tOffset; // offset corrected time of // left edge of display tpost = tpre + (r.width * tstep); // offset corrected time of // right edge of display const double sps = 1. / rate; //seconds-per-sample // Determine whether we should show individual samples // or draw circular points as well showIndividualSamples = (pps / rate > 0.5); //zoomed in a lot showPoints = (pps / rate > 3.0); //zoomed in even more // Calculate actual selection bounds so that t0 > 0 and t1 < the // end of the track t0 = (tpre >= 0.0 ? tpre : 0.0); t1 = (tpost < trackLen - sps * .99 ? tpost : trackLen - sps * .99); if (showIndividualSamples) { // adjustment so that the last circular point doesn't appear // to be hanging off the end t1 += 2. / pps; } // Make sure t1 (the right bound) is greater than 0 if (t1 < 0.0) { t1 = 0.0; } // Make sure t1 is greater than t0 if (t0 > t1) { t0 = t1; } // Use the WaveTrack method to show what is selected and 'should' be copied, pasted etc. ssel0 = std::max(sampleCount(0), spectrum ? sampleCount((sel0 - tOffset) * rate + .99) // PRL: why? : track->TimeToLongSamples(sel0 - tOffset) ); ssel1 = std::max(sampleCount(0), spectrum ? sampleCount((sel1 - tOffset) * rate + .99) // PRL: why? : track->TimeToLongSamples(sel1 - tOffset) ); //trim selection so that it only contains the actual samples if (ssel0 != ssel1 && ssel1 > (sampleCount)(0.5 + trackLen * rate)) { ssel1 = (sampleCount)(0.5 + trackLen * rate); } // The variable "mid" will be the rectangle containing the // actual waveform, as opposed to any blank area before // or after the track. mid = r; // If the left edge of the track is to the right of the left // edge of the display, then there's some blank area to the // left of the track. Reduce the "mid" // rect by size of the blank area. if (tpre < 0) { // Fill in the area to the left of the track double delta = r.width; if (t0 < tpost) { delta = (int)((t0 - tpre) * pps); } // Offset the rectangle containing the waveform by the width // of the area we just erased. mid.x += (int)delta; mid.width -= (int)delta; } // If the right edge of the track is to the left of the the right // edge of the display, then there's some blank area to the right // of the track. Reduce the "mid" rect by the // size of the blank area. if (tpost > t1) { wxRect post = r; if (t1 > tpre) { post.x += (int)((t1 - tpre) * pps); } post.width = r.width - (post.x - r.x); // Reduce the rectangle containing the waveform by the width // of the area we just erased. mid.width -= post.width; } } double tOffset; double rate; double h; // absolute time of left edge of display double tstep; double tpre; // offset corrected time of left edge of display // double h1; double tpost; // offset corrected time of right edge of display // Calculate actual selection bounds so that t0 > 0 and t1 < the // end of the track double t0; double t1; double pps; bool showIndividualSamples, showPoints; sampleCount ssel0; sampleCount ssel1; wxRect mid; }; } void TrackArtist::DrawClipWaveform(WaveTrack *track, WaveClip *clip, wxDC & dc, const wxRect & r, const ViewInfo *viewInfo, bool drawEnvelope, bool drawSamples, bool dB, bool muted) { #ifdef PROFILE_WAVEFORM Profiler profiler; #endif const ClipParameters params(false, track, clip, r, viewInfo->selectedRegion, *viewInfo); const wxRect &mid = params.mid; // The "mid" rect contains the part of the display actually // containing the waveform. If it's empty, we're done. if (mid.width <= 0) { return; } const double &t0 = params.t0; const double &pps = params.pps; const double &tOffset = params.tOffset; const double &tstep = params.tstep; const double &ssel0 = params.ssel0; const double &ssel1 = params.ssel1; const bool &showIndividualSamples = params.showIndividualSamples; const bool &showPoints = params.showPoints; const double &h = params.h; const double &tpre = params.tpre; const double &tpost = params.tpost; const double &t1 = params.t1; // Calculate sample-based offset-corrected selection dc.SetPen(*wxTRANSPARENT_PEN); // If we get to this point, the clip is actually visible on the // screen, so remember the display rectangle. clip->SetDisplayRect(mid); // The bounds (controlled by vertical zooming; -1.0...1.0 // by default) float zoomMin, zoomMax; track->GetDisplayBounds(&zoomMin, &zoomMax); WaveDisplay display; bool isLoadingOD = false;//true if loading on demand block in sequence. // The WaveClip class handles the details of computing the shape // of the waveform. The only way GetWaveDisplay will fail is if // there's a serious error, like some of the waveform data can't // be loaded. So if the function returns false, we can just exit. display.Allocate(mid.width); if (!clip->GetWaveDisplay(display, t0, pps, isLoadingOD)) { return; } // Get the values of the envelope corresponding to each pixel // in the display, and use these to compute the height of the // track at each pixel double *envValues = new double[mid.width]; clip->GetEnvelope()->GetValues(envValues, mid.width, t0 + tOffset, tstep); // Draw the background of the track, outlining the shape of // the envelope and using a colored pen for the selected // part of the waveform DrawWaveformBackground(dc, mid, envValues, zoomMin, zoomMax, dB, display.where, ssel0, ssel1, drawEnvelope, !track->GetSelected()); if (!showIndividualSamples) { #ifdef EXPERIMENTAL_OUTPUT_DISPLAY DrawMinMaxRMS(dc, mid, envValues, zoomMin, zoomMax, dB, min, max, rms, bl, isLoadingOD, muted, track->GetChannelGain(track->GetChannel())); #else DrawMinMaxRMS(dc, mid, envValues, zoomMin, zoomMax, dB, display, isLoadingOD, muted); #endif } else { DrawIndividualSamples(dc, mid, zoomMin, zoomMax, dB, clip, t0, pps, h, drawSamples, showPoints, muted); } if (drawEnvelope) { DrawEnvelope(dc, mid, envValues, zoomMin, zoomMax, dB); clip->GetEnvelope()->DrawPoints(dc, r, h, pps, dB, zoomMin, zoomMax); } delete[] envValues; // Draw arrows on the left side if the track extends to the left of the // beginning of time. :) if (h == 0.0 && tOffset < 0.0) { DrawNegativeOffsetTrackArrows(dc, r); } // Draw clip edges dc.SetPen(*wxGREY_PEN); if (tpre < 0) { AColor::Line(dc, mid.x - 1, mid.y, mid.x - 1, mid.y + r.height); } if (tpost > t1) { AColor::Line(dc, mid.x + mid.width, mid.y, mid.x + mid.width, mid.y + r.height); } } void TrackArtist::DrawTimeSlider(wxDC & dc, const wxRect & r, bool rightwards) { const int border = 3; // 3 pixels all round. const int width = 6; // width of the drag box. const int taper = 6; // how much the box tapers by. const int barSpacing = 4; // how far apart the bars are. const int barWidth = 3; const int xFlat = 3; //Enough space to draw in? if (r.height <= ((taper+border + barSpacing) * 2)) { return; } if (r.width <= (width * 2 + border * 3)) { return; } // The draggable box is tapered towards the direction you drag it. int leftTaper = rightwards ? 0 : 6; int rightTaper = rightwards ? 6 : 0; int xLeft = rightwards ? (r.x + border - 2) : (r.x + r.width + 1 - (border + width)); int yTop = r.y + border; int yBot = r.y + r.height - border - 1; AColor::Light(&dc, false); AColor::Line(dc, xLeft, yBot - leftTaper, xLeft, yTop + leftTaper); AColor::Line(dc, xLeft, yTop + leftTaper, xLeft + xFlat, yTop); AColor::Line(dc, xLeft + xFlat, yTop, xLeft + width, yTop + rightTaper); AColor::Dark(&dc, false); AColor::Line(dc, xLeft + width, yTop + rightTaper, xLeft + width, yBot - rightTaper); AColor::Line(dc, xLeft + width, yBot - rightTaper, xLeft + width-xFlat, yBot); AColor::Line(dc, xLeft + width - xFlat, yBot, xLeft, yBot - leftTaper); int firstBar = yTop + taper + taper / 2; int nBars = (yBot - yTop - taper * 3) / barSpacing + 1; xLeft += (width - barWidth + 1) / 2; int y; int i; AColor::Light(&dc, false); for (i = 0;i < nBars; i++) { y = firstBar + barSpacing * i; AColor::Line(dc, xLeft, y, xLeft + barWidth, y); } AColor::Dark(&dc, false); for(i = 0;i < nBars; i++){ y = firstBar + barSpacing * i + 1; AColor::Line(dc, xLeft, y, xLeft + barWidth, y); } } void TrackArtist::DrawSpectrum(WaveTrack *track, wxDC & dc, const wxRect & r, const ViewInfo *viewInfo) { DrawBackgroundWithSelection(&dc, r, track, blankSelectedBrush, blankBrush, viewInfo->selectedRegion.t0(), viewInfo->selectedRegion.t1(), viewInfo->h, viewInfo->zoom); WaveTrackCache cache(track); for (WaveClipList::compatibility_iterator it = track->GetClipIterator(); it; it = it->GetNext()) { DrawClipSpectrum(cache, it->GetData(), dc, r, viewInfo); } } static inline float findValue (float *spectrum, float bin0, float bin1, int half, bool autocorrelation, int gain, int range) { float value; #if 0 // Averaging method if (int(bin1) == int(bin0)) { value = spectrum[int(bin0)]; } else { float binwidth= bin1 - bin0; value = spectrum[int(bin0)] * (1.f - bin0 + (int)bin0); bin0 = 1 + int (bin0); while (bin0 < int(bin1)) { value += spectrum[int(bin0)]; bin0 += 1.0; } // Do not reference past end of freq array. if (int(bin1) >= half) { bin1 -= 1.0; } value += spectrum[int(bin1)] * (bin1 - int(bin1)); value /= binwidth; } #else half; // Maximum method, and no apportionment of any single bins over multiple pixel rows // See Bug971 int bin = std::min(half - 1, int(floor(0.5 + bin0))); const int limitBin = std::min(half, int(floor(0.5 + bin1))); value = spectrum[bin]; while (++bin < limitBin) value = std::max(value, spectrum[bin]); #endif if (!autocorrelation) { // Last step converts dB to a 0.0-1.0 range value = (value + range + gain) / (double)range; } value = std::min(1.0f, std::max(0.0f, value)); return value; } // Helper function to decide on which color set to use. // dashCount counts both dashes and the spaces between them. inline AColor::ColorGradientChoice ChooseColorSet( float bin0, float bin1, float selBinLo, float selBinCenter, float selBinHi, int dashCount, bool isSpectral ) { if (!isSpectral) return AColor::ColorGradientTimeSelected; if ((selBinCenter >= 0) && (bin0 <= selBinCenter) && (selBinCenter < bin1)) return AColor::ColorGradientEdge; if ((0 == dashCount % 2) && (((selBinLo >= 0) && (bin0 <= selBinLo) && ( selBinLo < bin1)) || ((selBinHi >= 0) && (bin0 <= selBinHi) && ( selBinHi < bin1)))) return AColor::ColorGradientEdge; if ((selBinLo < 0 || selBinLo < bin1) && (selBinHi < 0 || selBinHi > bin0)) return AColor::ColorGradientTimeAndFrequencySelected; return AColor::ColorGradientTimeSelected; } void TrackArtist::DrawClipSpectrum(WaveTrackCache &cache, WaveClip *clip, wxDC & dc, const wxRect & r, const ViewInfo *viewInfo) { #ifdef PROFILE_WAVEFORM Profiler profiler; #endif const WaveTrack *const track = cache.GetTrack(); const int display = track->GetDisplay(); const bool autocorrelation = (WaveTrack::PitchDisplay == display); const bool logF = (WaveTrack::SpectrumLogDisplay == display || WaveTrack::SpectralSelectionLogDisplay == display); enum { MONOCHROME_LINE = 230, COLORED_LINE = 0 }; enum { DASH_LENGTH = 10 /* pixels */ }; const ClipParameters params(true, track, clip, r, viewInfo->selectedRegion, *viewInfo); const wxRect &mid = params.mid; // The "mid" rect contains the part of the display actually // containing the waveform. If it's empty, we're done. if (mid.width <= 0) { return; } const double &t0 = params.t0; const double &pps = params.pps; const double &tstep = params.tstep; const double &ssel0 = params.ssel0; const double &ssel1 = params.ssel1; const double &rate = params.rate; double freqLo = SelectedRegion::UndefinedFrequency; double freqHi = SelectedRegion::UndefinedFrequency; #ifdef EXPERIMENTAL_SPECTRAL_EDITING if (!autocorrelation) { freqLo = viewInfo->selectedRegion.f0(); freqHi = viewInfo->selectedRegion.f1(); } #endif int range = gPrefs->Read(wxT("/Spectrum/Range"), 80L); int gain = gPrefs->Read(wxT("/Spectrum/Gain"), 20L); dc.SetPen(*wxTRANSPARENT_PEN); // We draw directly to a bit image in memory, // and then paint this directly to our offscreen // bitmap. Note that this could be optimized even // more, but for now this is not bad. -dmazzoni wxImage *image = new wxImage((int) mid.width, (int) mid.height); if (!image)return; unsigned char *data = image->GetData(); int windowSize = GetSpectrumWindowSize(!autocorrelation); const int half = windowSize / 2; float *freq = new float[mid.width * half]; sampleCount *where = new sampleCount[mid.width+1]; bool updated = clip->GetSpectrogram(cache, freq, where, mid.width, t0, pps, autocorrelation); #ifdef EXPERIMENTAL_FFT_SKIP_POINTS int fftSkipPoints = gPrefs->Read(wxT("/Spectrum/FFTSkipPoints"), 0L); int fftSkipPoints1 = fftSkipPoints + 1; #endif //EXPERIMENTAL_FFT_SKIP_POINTS int ifreq = lrint(rate/2); int maxFreq; if (!logF) maxFreq = GetSpectrumMaxFreq(ifreq); else maxFreq = GetSpectrumLogMaxFreq(ifreq); if(maxFreq > ifreq) maxFreq = ifreq; int minFreq; if (!logF) { minFreq = GetSpectrumMinFreq(0); if(minFreq < 0) minFreq = 0; } else { minFreq = GetSpectrumLogMinFreq(ifreq/1000.0); if(minFreq < 1) // Paul L: I suspect this line is now unreachable minFreq = 1.0; } // PRL: Must the following two be integers? int minSamples = int((double)minFreq * (double)windowSize / rate + 0.5); // units are fft bins int maxSamples = int((double)maxFreq * (double)windowSize / rate + 0.5); float binPerPx = float(maxSamples - minSamples) / float(mid.height); const float // e=exp(1.0f), f = rate / 2.0f / half, lmin = logf(float(minFreq)), lmax = logf(float(maxFreq)), scale = lmax - lmin; #ifdef EXPERIMENTAL_FFT_Y_GRID const float log2 = logf(2.0f), scale2 = (lmax - lmin) / log2, lmin2 = lmin / log2; bool *yGrid; yGrid = new bool[mid.height]; for (int y = 0; y < mid.height; y++) { float n = (float(y) / mid.height*scale2 - lmin2) * 12; float n2 = (float(y + 1) / mid.height*scale2 - lmin2) * 12; float f = float(minFreq) / (mFftSkipPoints + 1)*powf(2.0f, n / 12.0f + lmin2); float f2 = float(minFreq) / (mFftSkipPoints + 1)*powf(2.0f, n2 / 12.0f + lmin2); n = logf(f / 440) / log2 * 12; n2 = logf(f2 / 440) / log2 * 12; if (floor(n) < floor(n2)) yGrid[y] = true; else yGrid[y] = false; } #endif //EXPERIMENTAL_FFT_Y_GRID if (!updated && clip->mSpecPxCache->valid && (clip->mSpecPxCache->len == mid.height * mid.width) #ifdef EXPERIMENTAL_FFT_Y_GRID && mFftYGrid==fftYGridOld #endif //EXPERIMENTAL_FFT_Y_GRID #ifdef EXPERIMENTAL_FIND_NOTES && mFftFindNotes==fftFindNotesOld && mFindNotesMinA==findNotesMinAOld && mNumberOfMaxima==findNotesNOld && mFindNotesQuantize==findNotesQuantizeOld #endif ) { // cache is up to date } else { delete clip->mSpecPxCache; clip->mSpecPxCache = new SpecPxCache(mid.width * mid.height); clip->mSpecPxCache->valid = true; #ifdef EXPERIMENTAL_FIND_NOTES fftFindNotesOld=mFftFindNotes; findNotesMinAOld=mFindNotesMinA; findNotesNOld=mNumberOfMaxima; findNotesQuantizeOld=mFindNotesQuantize; #endif #ifdef EXPERIMENTAL_FIND_NOTES const float #ifdef EXPERIMENTAL_FFT_SKIP_POINTS lmins = logf(float(minFreq) / (mFftSkipPoints + 1)), lmaxs = logf(float(maxFreq) / (mFftSkipPoints + 1)), #else //!EXPERIMENTAL_FFT_SKIP_POINTS lmins = lmin, lmaxs = lmax, #endif //EXPERIMENTAL_FFT_SKIP_POINTS ; #endif //EXPERIMENTAL_FIND_NOTES #ifdef EXPERIMENTAL_FIND_NOTES int maxima[128]; float maxima0[128], maxima1[128]; const float #ifdef EXPERIMENTAL_FFT_SKIP_POINTS f2bin = half / (rate / 2.0f / (mFftSkipPoints + 1)), #else //!EXPERIMENTAL_FFT_SKIP_POINTS f2bin = half / (rate / 2.0f), #endif //EXPERIMENTAL_FFT_SKIP_POINTS bin2f = 1.0f / f2bin, minDistance = powf(2.0f, 2.0f / 12.0f), i0 = expf(lmin) / f, i1 = expf(scale + lmin) / f, minColor = 0.0f; const int maxTableSize = 1024; int *indexes = new int[maxTableSize]; #endif //EXPERIMENTAL_FIND_NOTES for (int x = 0; x < mid.width; ++x) { if (!logF) { for (int yy = 0; yy < mid.height; yy++) { float bin0 = float(yy) * binPerPx + minSamples; float bin1 = float(yy + 1) * binPerPx + minSamples; const float value = findValue (freq + half * x, bin0, bin1, half, autocorrelation, gain, range); clip->mSpecPxCache->values[x * mid.height + yy] = value; } } else { #ifdef EXPERIMENTAL_FIND_NOTES int maximas=0; if (!usePxCache && mFftFindNotes) { for (int i = maxTableSize-1; i >= 0; i--) indexes[i]=-1; // Build a table of (most) values, put the index in it. for (int i = int(i0); i < int(i1); i++) { float freqi=freq[x0+int(i)]; int value=int((freqi+gain+range)/range*(maxTableSize-1)); if (value < 0) value=0; if (value >= maxTableSize) value=maxTableSize-1; indexes[value]=i; } // Build from the indices an array of maxima. for (int i = maxTableSize-1; i >= 0; i--) { int index=indexes[i]; if (index >= 0) { float freqi=freq[x0+index]; if (freqi < mFindNotesMinA) break; bool ok=true; for (int m=0; m < maximas; m++) { // Avoid to store very close maxima. float maxm = maxima[m]; if (maxm/index < minDistance && index/maxm < minDistance) { ok=false; break; } } if (ok) { maxima[maximas++] = index; if (maximas >= mNumberOfMaxima) break; } } } // The f2pix helper macro converts a frequency into a pixel coordinate. #define f2pix(f) (logf(f)-lmins)/(lmaxs-lmins)*mid.height // Possibly quantize the maxima frequencies and create the pixel block limits. for (int i=0; i < maximas; i++) { int index=maxima[i]; float f = float(index)*bin2f; if (mFindNotesQuantize) { f = expf(int(log(f/440)/log2*12-0.5)/12.0f*log2)*440; maxima[i] = f*f2bin; } float f0 = expf((log(f/440)/log2*24-1)/24.0f*log2)*440; maxima0[i] = f2pix(f0); float f1 = expf((log(f/440)/log2*24+1)/24.0f*log2)*440; maxima1[i] = f2pix(f1); } } int it=0; int oldBin0=-1; bool inMaximum = false; #endif //EXPERIMENTAL_FIND_NOTES double yy2_base = exp(lmin) / f; float yy2 = yy2_base; double exp_scale_per_height = exp(scale / mid.height); for (int yy = 0; yy < mid.height; yy++) { if (int(yy2) >= half) yy2=half-1; if (yy2<0) yy2=0; float bin0 = float(yy2); yy2_base *= exp_scale_per_height; float yy3 = yy2_base; if (int(yy3)>=half) yy3=half-1; if (yy3<0) yy3=0; float bin1 = float(yy3); float value; #ifdef EXPERIMENTAL_FIND_NOTES if (mFftFindNotes) { if (it < maximas) { float i0=maxima0[it]; if (yy >= i0) inMaximum = true; if (inMaximum) { float i1=maxima1[it]; if (yy+1 <= i1) { value=sumFreqValues(freq, x0, bin0, bin1); if (value < mFindNotesMinA) value = minColor; else value = (value + gain + range) / (double)range; } else { it++; inMaximum = false; value = minColor; } } else { value = minColor; } } else value = minColor; } else #endif //EXPERIMENTAL_FIND_NOTES { value = findValue (freq + half * x, bin0, bin1, half, autocorrelation, gain, range); } clip->mSpecPxCache->values[x * mid.height + yy] = value; yy2 = yy2_base; } // each y } // is logF } // each x } // updating cache float selBinLo = freqLo * (double)windowSize / rate; float selBinHi = freqHi * (double)windowSize / rate; float selBinCenter = ((freqLo < 0 || freqHi < 0) ? -1 : sqrt(freqLo * freqHi)) * (double)windowSize / rate; sampleCount w1 = sampleCount(0.5 + rate * t0 ); for (int x = 0; x < mid.width; ++x) { sampleCount w0 = w1; w1 = sampleCount(0.5 + rate * (t0 + (x+1) * tstep) ); // TODO: The logF and non-logF case are very similar. // They should be merged and simplified. if (!logF) { for (int yy = 0; yy < mid.height; yy++) { float bin0 = float (yy) * binPerPx + minSamples; float bin1 = float (yy + 1) * binPerPx + minSamples; // For spectral selection, determine what colour // set to use. We use a darker selection if // in both spectral range and time range. AColor::ColorGradientChoice selected = AColor::ColorGradientUnselected; // If we are in the time selected range, then we may use a different color set. if (ssel0 <= w0 && w1 < ssel1) { bool isSpectral = ((track->GetDisplay() == WaveTrack::SpectralSelectionDisplay) || (track->GetDisplay() == WaveTrack::SpectralSelectionLogDisplay)); selected = ChooseColorSet( bin0, bin1, selBinLo, selBinCenter, selBinHi, x/DASH_LENGTH, isSpectral ); } unsigned char rv, gv, bv; const float value = clip->mSpecPxCache->values[x * mid.height + yy]; GetColorGradient(value, selected, mIsGrayscale, &rv, &gv, &bv); int px = ((mid.height - 1 - yy) * mid.width + x) * 3; data[px++] = rv; data[px++] = gv; data[px] = bv; } } else //logF { double yy2_base=exp(lmin)/f; float yy2 = yy2_base; double exp_scale_per_height = exp(scale/mid.height); for (int yy = 0; yy < mid.height; yy++) { if (int(yy2)>=half) yy2=half-1; if (yy2<0) yy2=0; float bin0 = float(yy2); yy2_base *= exp_scale_per_height; float yy3 = yy2_base; if (int(yy3)>=half) yy3=half-1; if (yy3<0) yy3=0; float bin1 = float(yy3); AColor::ColorGradientChoice selected = AColor::ColorGradientUnselected; // If we are in the time selected range, then we may use a different color set. if (ssel0 <= w0 && w1 < ssel1) { bool isSpectral = ((track->GetDisplay() == WaveTrack::SpectralSelectionDisplay) || (track->GetDisplay() == WaveTrack::SpectralSelectionLogDisplay)); selected = ChooseColorSet( bin0, bin1, selBinLo, selBinCenter, selBinHi, x/DASH_LENGTH, isSpectral ); } const float value = clip->mSpecPxCache->values[x * mid.height + yy]; yy2 = yy2_base; unsigned char rv, gv, bv; GetColorGradient(value, selected, mIsGrayscale, &rv, &gv, &bv); #ifdef EXPERIMENTAL_FFT_Y_GRID if (mFftYGrid && yGrid[yy]) { rv /= 1.1f; gv /= 1.1f; bv /= 1.1f; } #endif //EXPERIMENTAL_FFT_Y_GRID int px = ((mid.height - 1 - yy) * mid.width + x) * 3; data[px++] = rv; data[px++] = gv; data[px] = bv; } // each y } // logF } // each x // If we get to this point, the clip is actually visible on the // screen, so remember the display rectangle. clip->SetDisplayRect(mid); wxBitmap converted = wxBitmap(*image); wxMemoryDC memDC; memDC.SelectObject(converted); dc.Blit(mid.x, mid.y, mid.width, mid.height, &memDC, 0, 0, wxCOPY, FALSE); delete image; delete[] where; delete[] freq; #ifdef EXPERIMENTAL_FFT_Y_GRID delete[] yGrid; #endif //EXPERIMENTAL_FFT_Y_GRID #ifdef EXPERIMENTAL_FIND_NOTES delete[] indexes; #endif //EXPERIMENTAL_FIND_NOTES } void TrackArtist::InvalidateSpectrumCache(TrackList *tracks) { TrackListOfKindIterator iter(Track::Wave, tracks); for (Track *t = iter.First(); t; t = iter.Next()) { InvalidateSpectrumCache((WaveTrack *)t); } } void TrackArtist::InvalidateSpectrumCache(WaveTrack *track) { WaveClipList::compatibility_iterator it; for (it = track->GetClipIterator(); it; it = it->GetNext()) { it->GetData()->mSpecPxCache->valid = false; } } #ifdef USE_MIDI /* Note: recall that Allegro attributes end in a type identifying letter. In addition to standard notes, an Allegro_Note can denote a graphic. A graphic is a note with a loud of zero (for quick testing) and an attribute named "shapea" set to one of the following atoms: line from (time, pitch) to (time+dur, y1r), where y1r is an attribute rectangle from (time, pitch) to (time+dur, y1r), where y1r is an attribute triangle coordinates are (time, pitch), (x1r, y1r), (x2r, y2r) dur must be the max of x1r-time, x2r-time polygon coordinates are (time, pitch), (x1r, y1r), (x2r, y2r), (x3r, y3r), ... are coordinates (since we cannot represent arrays as attribute values, we just generate as many attribute names as we need) dur must be the max of xNr-time for all N oval similar to rectangle Note: this oval has horizontal and vertical axes only text drawn at (time, pitch) duration should be zero (text is clipped based on time and duration, NOT based on actual coordinates) and optional attributes as follows: linecolori is 0x00rrggbb format color for line or text foreground fillcolori is 0x00rrggbb format color for fill or text background linethicki is line thickness in pixels, 0 for no line filll is true to fill rectangle or draw text background (default is false) fonta is one of ['roman', 'swiss', 'modern'] (font, otherwise use default) weighta may be 'bold' (font) (default is normal) sizei is font size (default is 8) justifys is a string containing two letters, a horizontal code and a vertical code. The horizontal code is as follows: l: the coordinate is to the left of the string (default) c: the coordinate is at the center of the string r: the coordinate is at the right of the string The vertical code is as follows: t: the coordinate is at the top of the string c: the coordinate is at the center of the string b: the coordinate is at the bottom of the string d: the coordinate is at the baseline of the string (default) Thus, -justifys:"lt" places the left top of the string at the point given by (pitch, time). The default value is "ld". */ /* Declare Static functions */ static const char *IsShape(Alg_note_ptr note); static double LookupRealAttribute(Alg_note_ptr note, Alg_attribute attr, double def); static long LookupIntAttribute(Alg_note_ptr note, Alg_attribute attr, long def); static bool LookupLogicalAttribute(Alg_note_ptr note, Alg_attribute attr, bool def); static const char *LookupStringAttribute(Alg_note_ptr note, Alg_attribute attr, const char *def); static const char *LookupAtomAttribute(Alg_note_ptr note, Alg_attribute attr, char *def); //static int PITCH_TO_Y(double p, int bottom); // returns NULL if note is not a shape, // returns atom (string) value of note if note is a shape const char *IsShape(Alg_note_ptr note) { Alg_parameters_ptr parameters = note->parameters; while (parameters) { if (strcmp(parameters->parm.attr_name(), "shapea") == 0) { return parameters->parm.a; } parameters = parameters->next; } return NULL; } // returns value of attr, or default if not found double LookupRealAttribute(Alg_note_ptr note, Alg_attribute attr, double def) { Alg_parameters_ptr parameters = note->parameters; while (parameters) { if (parameters->parm.attr_name() == attr + 1 && parameters->parm.attr_type() == 'r') { return parameters->parm.r; } parameters = parameters->next; } return def; } // returns value of attr, or default if not found long LookupIntAttribute(Alg_note_ptr note, Alg_attribute attr, long def) { Alg_parameters_ptr parameters = note->parameters; while (parameters) { if (parameters->parm.attr_name() == attr + 1 && parameters->parm.attr_type() == 'i') { return parameters->parm.i; } parameters = parameters->next; } return def; } // returns value of attr, or default if not found bool LookupLogicalAttribute(Alg_note_ptr note, Alg_attribute attr, bool def) { Alg_parameters_ptr parameters = note->parameters; while (parameters) { if (parameters->parm.attr_name() == attr + 1 && parameters->parm.attr_type() == 'l') { return parameters->parm.l; } parameters = parameters->next; } return def; } // returns value of attr, or default if not found const char *LookupStringAttribute(Alg_note_ptr note, Alg_attribute attr, const char *def) { Alg_parameters_ptr parameters = note->parameters; while (parameters) { if (parameters->parm.attr_name() == attr + 1 && parameters->parm.attr_type() == 's') { return parameters->parm.s; } parameters = parameters->next; } return def; } // returns value of attr, or default if not found const char *LookupAtomAttribute(Alg_note_ptr note, Alg_attribute attr, char *def) { Alg_parameters_ptr parameters = note->parameters; while (parameters) { if (parameters->parm.attr_name() == attr + 1 && parameters->parm.attr_type() == 'a') { return parameters->parm.s; } parameters = parameters->next; } return def; } #define TIME_TO_X(t) (r.x + (int) (((t) - h) * pps)) #define X_TO_TIME(xx) (((xx) - r.x) / pps + h) // CLIP(x) changes x to lie between +/- CLIP_MAX due to graphics display problems // with very large coordinate values (this happens when you zoom in very far) // This will cause incorrect things to be displayed, but at these levels of zoom // you will only see a small fraction of the overall shape. Note that rectangles // and lines are clipped in a way that preserves correct graphics, so in // particular, line plots will be correct at any zoom (limited by floating point // precision). #define CLIP_MAX 16000 #define CLIP(x) { long c = (x); if (c < -CLIP_MAX) c = -CLIP_MAX; \ if (c > CLIP_MAX) c = CLIP_MAX; (x) = c; } #define RED(i) ( unsigned char )( (((i) >> 16) & 0xff) ) #define GREEN(i) ( unsigned char )( (((i) >> 8) & 0xff) ) #define BLUE(i) ( unsigned char )( ((i) & 0xff) ) //#define PITCH_TO_Y(p) (r.y + r.height - int(pitchht * ((p) + 0.5 - pitch0) + 0.5)) /* int PitchToY(double p, int bottom) { int octave = (((int) (p + 0.5)) / 12); int n = ((int) (p + 0.5)) % 12; return IPITCH_TO_Y((int) (p + 0.5)); // was: bottom - octave * octaveHeight - notePos[n] - 4; } */ /* DrawNoteBackground is called by DrawNoteTrack twice: once to draw the unselected background, and once to draw the selected background. The selected background is the same except for the horizontal range and the colors. The background rectangle region is given by r; the selected region is given by sel. The first time this is called, sel is equal to r, and the entire region is drawn with unselected background colors. */ void TrackArtist::DrawNoteBackground(NoteTrack *track, wxDC &dc, const wxRect &r, const wxRect &sel, const ViewInfo *viewInfo, const wxBrush &wb, const wxPen &wp, const wxBrush &bb, const wxPen &bp, const wxPen &mp) { dc.SetBrush(wb); dc.SetPen(wp); dc.DrawRectangle(sel); // fill rectangle with white keys background double h = viewInfo->h; double pps = viewInfo->zoom; int left = TIME_TO_X(track->GetOffset()); if (left < sel.x) left = sel.x; // clip on left int right = TIME_TO_X(track->GetOffset() + track->mSeq->get_real_dur()); if (right > sel.x + sel.width) right = sel.x + sel.width; // clip on right // need overlap between MIDI data and the background region if (left >= right) return; dc.SetBrush(bb); int octave = 0; // obottom is the window coordinate of octave divider line int obottom = track->GetOctaveBottom(octave); // eOffset is for the line between E and F; there's another line // between B and C, hence the offset of 2 for two line thicknesses int eOffset = track->GetPitchHeight() * 5 + 2; while (obottom > r.y + track->GetNoteMargin() + 3) { // draw a black line separating octaves if this octave botton is visible if (obottom < r.y + r.height - track->GetNoteMargin()) { dc.SetPen(*wxBLACK_PEN); // obottom - 1 because obottom is at the bottom of the line AColor::Line(dc, left, obottom - 1, right, obottom - 1); } dc.SetPen(bp); // draw a black-key stripe colored line separating E and F if visible if (obottom - eOffset > r.y && obottom - eOffset < r.y + r.height) { AColor::Line(dc, left, obottom - eOffset, right, obottom - eOffset); } // draw visible black key lines wxRect br; br.x = left; br.width = right - left; br.height = track->GetPitchHeight(); for (int black = 0; black < 5; black++) { br.y = obottom - track->GetBlackPos(black); if (br.y > r.y && br.y + br.height < r.y + r.height) { dc.DrawRectangle(br); // draw each black key background stripe } } obottom = track->GetOctaveBottom(++octave); } // draw bar lines Alg_seq_ptr seq = track->mSeq; // We assume that sliding a NoteTrack around slides the barlines // along with the notes. This means that when we write out a track // as Allegro or MIDI without the offset, we'll need to insert an // integer number of measures of silence, using tempo change to // match the duration to the offset. // Iterate over all time signatures to generate beat positions of // bar lines, map the beats to times, map the times to position, // and draw the bar lines that fall within the region of interest (sel) // seq->convert_to_beats(); dc.SetPen(mp); Alg_time_sigs &sigs = seq->time_sig; int i = 0; // index into ts[] double next_bar_beat = 0.0; double beats_per_measure = 4.0; while (true) { if (i < sigs.length() && sigs[i].beat < next_bar_beat + ALG_EPS) { // new time signature takes effect Alg_time_sig &sig = sigs[i++]; next_bar_beat = sig.beat; beats_per_measure = (sig.num * 4.0) / sig.den; } // map beat to time double t = seq->get_time_map()->beat_to_time(next_bar_beat); // map time to position int x = TIME_TO_X(t + track->GetOffset()); if (x > right) break; AColor::Line(dc, x, sel.y, x, sel.y + sel.height); next_bar_beat += beats_per_measure; } } /* DrawNoteTrack: Draws a piano-roll style display of sequence data with added graphics. Since there may be notes outside of the display region, reserve a half-note-height margin at the top and bottom of the window and draw out-of-bounds notes here instead. */ void TrackArtist::DrawNoteTrack(NoteTrack *track, wxDC & dc, const wxRect & r, const ViewInfo *viewInfo, bool muted) { SonifyBeginNoteBackground(); double h = viewInfo->h; double pps = viewInfo->zoom; double sel0 = viewInfo->selectedRegion.t0(); double sel1 = viewInfo->selectedRegion.t1(); double h1 = X_TO_TIME(r.x + r.width); Alg_seq_ptr seq = track->mSeq; if (!seq) { assert(track->mSerializationBuffer); // JKC: Previously this indirected via seq->, a NULL pointer. // This was actually OK, since unserialize is a static function. // Alg_seq:: is clearer. Alg_track_ptr alg_track = Alg_seq::unserialize(track->mSerializationBuffer, track->mSerializationLength); assert(alg_track->get_type() == 's'); track->mSeq = seq = (Alg_seq_ptr) alg_track; free(track->mSerializationBuffer); track->mSerializationBuffer = NULL; } assert(seq); int visibleChannels = track->mVisibleChannels; if (!track->GetSelected()) sel0 = sel1 = 0.0; // reserve 1/2 note height at top and bottom of track for // out-of-bounds notes int numPitches = (r.height) / track->GetPitchHeight(); if (numPitches < 0) numPitches = 0; // cannot be negative // bottom is the hypothetical location of the bottom of pitch 0 relative to // the top of the clipping region r: r.height - PITCH_HEIGHT/2 is where the // bottomNote is displayed, and to that // we add the height of bottomNote from the position of pitch 0 track->PrepareIPitchToY(r); // Background comes in 6 colors: // 214, 214,214 -- unselected white keys // 192,192,192 -- unselected black keys // 170,170,170 -- unselected bar lines // 165,165,190 -- selected white keys // 148,148,170 -- selected black keys // 131,131,150 -- selected bar lines wxPen blackStripePen; blackStripePen.SetColour(192, 192, 192); wxBrush blackStripeBrush; blackStripeBrush.SetColour(192, 192, 192); wxPen barLinePen; barLinePen.SetColour(170, 170, 170); DrawNoteBackground(track, dc, r, r, viewInfo, blankBrush, blankPen, blackStripeBrush, blackStripePen, barLinePen); dc.SetClippingRegion(r); // Draw the selection background // First, the white keys, as a single rectangle // In other words fill the selection area with selectedWhiteKeyPen wxRect selBG; selBG.y = r.y; selBG.height = r.height; selBG.x = TIME_TO_X(sel0); selBG.width = TIME_TO_X(sel1) - TIME_TO_X(sel0); wxPen selectedWhiteKeyPen; selectedWhiteKeyPen.SetColour(165, 165, 190); dc.SetPen(selectedWhiteKeyPen); wxBrush selectedWhiteKeyBrush; selectedWhiteKeyBrush.SetColour(165, 165, 190); // Then, the black keys and octave stripes, as smaller rectangles wxPen selectedBlackKeyPen; selectedBlackKeyPen.SetColour(148, 148, 170); wxBrush selectedBlackKeyBrush; selectedBlackKeyBrush.SetColour(148, 148, 170); wxPen selectedBarLinePen; selectedBarLinePen.SetColour(131, 131, 150); DrawNoteBackground(track, dc, r, selBG, viewInfo, selectedWhiteKeyBrush, selectedWhiteKeyPen, selectedBlackKeyBrush, selectedBlackKeyPen, selectedBarLinePen); SonifyEndNoteBackground(); SonifyBeginNoteForeground(); int marg = track->GetNoteMargin(); // NOTE: it would be better to put this in some global initialization // function rather than do lookups every time. Alg_attribute line = symbol_table.insert_string("line"); Alg_attribute rectangle = symbol_table.insert_string("rectangle"); Alg_attribute triangle = symbol_table.insert_string("triangle"); Alg_attribute polygon = symbol_table.insert_string("polygon"); Alg_attribute oval = symbol_table.insert_string("oval"); Alg_attribute text = symbol_table.insert_string("text"); Alg_attribute texts = symbol_table.insert_string("texts"); Alg_attribute x1r = symbol_table.insert_string("x1r"); Alg_attribute x2r = symbol_table.insert_string("x2r"); Alg_attribute y1r = symbol_table.insert_string("y1r"); Alg_attribute y2r = symbol_table.insert_string("y2r"); Alg_attribute linecolori = symbol_table.insert_string("linecolori"); Alg_attribute fillcolori = symbol_table.insert_string("fillcolori"); Alg_attribute linethicki = symbol_table.insert_string("linethicki"); Alg_attribute filll = symbol_table.insert_string("filll"); Alg_attribute fonta = symbol_table.insert_string("fonta"); Alg_attribute roman = symbol_table.insert_string("roman"); Alg_attribute swiss = symbol_table.insert_string("swiss"); Alg_attribute modern = symbol_table.insert_string("modern"); Alg_attribute weighta = symbol_table.insert_string("weighta"); Alg_attribute bold = symbol_table.insert_string("bold"); Alg_attribute sizei = symbol_table.insert_string("sizei"); Alg_attribute justifys = symbol_table.insert_string("justifys"); // We want to draw in seconds, so we need to convert to seconds seq->convert_to_seconds(); Alg_iterator iterator(seq, false); iterator.begin(); //for every event Alg_event_ptr evt; while (0 != (evt = iterator.next())) { if (evt->get_type() == 'n') { // 'n' means a note Alg_note_ptr note = (Alg_note_ptr) evt; // if the note's channel is visible if (visibleChannels & (1 << (evt->chan & 15))) { double x = note->time + track->GetOffset(); double x1 = x + note->dur; if (x < h1 && x1 > h) { // omit if outside box const char *shape = NULL; if (note->loud > 0.0 || 0 == (shape = IsShape(note))) { wxRect nr; // "note rectangle" nr.y = track->PitchToY(note->pitch); nr.height = track->GetPitchHeight(); nr.x = r.x + (int) ((x - h) * pps); nr.width = (int) ((note->dur * pps) + 0.5); if (nr.x + nr.width >= r.x && nr.x < r.x + r.width) { if (nr.x < r.x) { nr.width -= (r.x - nr.x); nr.x = r.x; } if (nr.x + nr.width > r.x + r.width) // clip on right nr.width = r.x + r.width - nr.x; if (nr.y + nr.height < r.y + marg + 3) { // too high for window nr.y = r.y; nr.height = marg; dc.SetBrush(*wxBLACK_BRUSH); dc.SetPen(*wxBLACK_PEN); dc.DrawRectangle(nr); } else if (nr.y >= r.y + r.height - marg - 1) { // too low for window nr.y = r.y + r.height - marg; nr.height = marg; dc.SetBrush(*wxBLACK_BRUSH); dc.SetPen(*wxBLACK_PEN); dc.DrawRectangle(nr); } else { if (nr.y + nr.height > r.y + r.height - marg) nr.height = r.y + r.height - nr.y; if (nr.y < r.y + marg) { int offset = r.y + marg - nr.y; nr.height -= offset; nr.y += offset; } // nr.y += r.y; if (muted) AColor::LightMIDIChannel(&dc, note->chan + 1); else AColor::MIDIChannel(&dc, note->chan + 1); dc.DrawRectangle(nr); if (track->GetPitchHeight() > 2) { AColor::LightMIDIChannel(&dc, note->chan + 1); AColor::Line(dc, nr.x, nr.y, nr.x + nr.width-2, nr.y); AColor::Line(dc, nr.x, nr.y, nr.x, nr.y + nr.height-2); AColor::DarkMIDIChannel(&dc, note->chan + 1); AColor::Line(dc, nr.x+nr.width-1, nr.y, nr.x+nr.width-1, nr.y+nr.height-1); AColor::Line(dc, nr.x, nr.y+nr.height-1, nr.x+nr.width-1, nr.y+nr.height-1); } // } } } } else if (shape) { // draw a shape according to attributes // add 0.5 to pitch because pitches are plotted with // height = PITCH_HEIGHT; thus, the center is raised // by PITCH_HEIGHT * 0.5 int y = track->PitchToY(note->pitch); long linecolor = LookupIntAttribute(note, linecolori, -1); long linethick = LookupIntAttribute(note, linethicki, 1); long fillcolor = -1; long fillflag = 0; // set default color to be that of channel AColor::MIDIChannel(&dc, note->chan+1); if (shape != text) { if (linecolor != -1) dc.SetPen(wxPen(wxColour(RED(linecolor), GREEN(linecolor), BLUE(linecolor)), linethick, wxSOLID)); } if (shape != line) { fillcolor = LookupIntAttribute(note, fillcolori, -1); fillflag = LookupLogicalAttribute(note, filll, false); if (fillcolor != -1) dc.SetBrush(wxBrush(wxColour(RED(fillcolor), GREEN(fillcolor), BLUE(fillcolor)), wxSOLID)); if (!fillflag) dc.SetBrush(*wxTRANSPARENT_BRUSH); } int y1 = track->PitchToY(LookupRealAttribute(note, y1r, note->pitch)); if (shape == line) { // extreme zooms caues problems under windows, so we have to do some // clipping before calling display routine if (x < h) { // clip line on left y = int((y + (y1 - y) * (h - x) / (x1 - x)) + 0.5); x = h; } if (x1 > h1) { // clip line on right y1 = int((y + (y1 - y) * (h1 - x) / (x1 - x)) + 0.5); x1 = h1; } AColor::Line(dc, TIME_TO_X(x), y, TIME_TO_X(x1), y1); } else if (shape == rectangle) { if (x < h) { // clip on left, leave 10 pixels to spare x = h - (linethick + 10) / pps; } if (x1 > h1) { // clip on right, leave 10 pixels to spare x1 = h1 + (linethick + 10) / pps; } dc.DrawRectangle(TIME_TO_X(x), y, int((x1 - x) * pps + 0.5), y1 - y + 1); } else if (shape == triangle) { wxPoint points[3]; points[0].x = TIME_TO_X(x); CLIP(points[0].x); points[0].y = y; points[1].x = TIME_TO_X(LookupRealAttribute(note, x1r, note->pitch)); CLIP(points[1].x); points[1].y = y1; points[2].x = TIME_TO_X(LookupRealAttribute(note, x2r, x)); CLIP(points[2].x); points[2].y = track->PitchToY(LookupRealAttribute(note, y2r, note->pitch)); dc.DrawPolygon(3, points); } else if (shape == polygon) { wxPoint points[20]; // upper bound of 20 sides points[0].x = TIME_TO_X(x); CLIP(points[0].x); points[0].y = y; points[1].x = TIME_TO_X(LookupRealAttribute(note, x1r, x)); CLIP(points[1].x); points[1].y = y1; points[2].x = TIME_TO_X(LookupRealAttribute(note, x2r, x)); CLIP(points[2].x); points[2].y = track->PitchToY(LookupRealAttribute(note, y2r, note->pitch)); int n = 3; while (n < 20) { char name[8]; sprintf(name, "x%dr", n); Alg_attribute attr = symbol_table.insert_string(name); double xn = LookupRealAttribute(note, attr, -1000000.0); if (xn == -1000000.0) break; points[n].x = TIME_TO_X(xn); CLIP(points[n].x); sprintf(name, "y%dr", n - 1); attr = symbol_table.insert_string(name); double yn = LookupRealAttribute(note, attr, -1000000.0); if (yn == -1000000.0) break; points[n].y = track->PitchToY(yn); n++; } dc.DrawPolygon(n, points); } else if (shape == oval) { int ix = TIME_TO_X(x); CLIP(ix); int ix1 = int((x1 - x) * pps + 0.5); if (ix1 > CLIP_MAX * 2) ix1 = CLIP_MAX * 2; // CLIP a width dc.DrawEllipse(ix, y, ix1, y1 - y + 1); } else if (shape == text) { if (linecolor != -1) dc.SetTextForeground(wxColour(RED(linecolor), GREEN(linecolor), BLUE(linecolor))); // if no color specified, copy color from brush else dc.SetTextForeground(dc.GetBrush().GetColour()); // This seems to have no effect, so I commented it out. -RBD //if (fillcolor != -1) // dc.SetTextBackground(wxColour(RED(fillcolor), // GREEN(fillcolor), // BLUE(fillcolor))); //// if no color specified, copy color from brush //else dc.SetTextBackground(dc.GetPen().GetColour()); const char *font = LookupAtomAttribute(note, fonta, NULL); const char *weight = LookupAtomAttribute(note, weighta, NULL); int size = LookupIntAttribute(note, sizei, 8); const char *justify = LookupStringAttribute(note, justifys, "ld"); wxFont wxfont; wxfont.SetFamily(font == roman ? wxROMAN : (font == swiss ? wxSWISS : (font == modern ? wxMODERN : wxDEFAULT))); wxfont.SetStyle(wxNORMAL); wxfont.SetWeight(weight == bold ? wxBOLD : wxNORMAL); wxfont.SetPointSize(size); dc.SetFont(wxfont); // now do justification const char *s = LookupStringAttribute(note, texts, ""); #ifdef __WXMAC__ long textWidth, textHeight; #else int textWidth, textHeight; #endif dc.GetTextExtent(LAT1CTOWX(s), &textWidth, &textHeight); long hoffset = 0; long voffset = -textHeight; // default should be baseline of text if (strlen(justify) != 2) justify = "ld"; if (justify[0] == 'c') hoffset = -(textWidth/2); else if (justify[0] == 'r') hoffset = -textWidth; if (justify[1] == 't') voffset = 0; else if (justify[1] == 'c') voffset = -(textHeight/2); else if (justify[1] == 'b') voffset = -textHeight; if (fillflag) { // It should be possible to do this with background color, // but maybe because of the transfer mode, no background is // drawn. To fix this, just draw a rectangle: dc.SetPen(wxPen(wxColour(RED(fillcolor), GREEN(fillcolor), BLUE(fillcolor)), 1, wxSOLID)); dc.DrawRectangle(TIME_TO_X(x) + hoffset, y + voffset, textWidth, textHeight); } dc.DrawText(LAT1CTOWX(s), TIME_TO_X(x) + hoffset, y + voffset); } } } } } } iterator.end(); // draw black line between top/bottom margins and the track dc.SetPen(*wxBLACK_PEN); AColor::Line(dc, r.x, r.y + marg, r.x + r.width, r.y + marg); AColor::Line(dc, r.x, r.y + r.height - marg - 1, // subtract 1 to get r.x + r.width, r.y + r.height - marg - 1); // top of line if (h == 0.0 && track->GetOffset() < 0.0) { DrawNegativeOffsetTrackArrows(dc, r); } dc.DestroyClippingRegion(); SonifyEndNoteForeground(); } #endif // USE_MIDI void TrackArtist::DrawLabelTrack(LabelTrack *track, wxDC & dc, const wxRect & r, const ViewInfo *viewInfo) { double sel0 = viewInfo->selectedRegion.t0(); double sel1 = viewInfo->selectedRegion.t1(); if (!track->GetSelected() && !track->IsSyncLockSelected()) sel0 = sel1 = 0.0; track->Draw(dc, r, viewInfo->h, viewInfo->zoom, sel0, sel1); } void TrackArtist::DrawTimeTrack(TimeTrack *track, wxDC & dc, const wxRect & r, const ViewInfo *viewInfo) { track->Draw(dc, r, viewInfo->h, viewInfo->zoom); wxRect envRect = r; envRect.height -= 2; double lower = track->GetRangeLower(), upper = track->GetRangeUpper(); if(track->GetDisplayLog()) { // MB: silly way to undo the work of GetWaveYPos while still getting a logarithmic scale double dBRange = gPrefs->Read(wxT("/GUI/EnvdBRange"), ENV_DB_RANGE); lower = 20.0 * log10(std::max(1.0e-7, lower)) / dBRange + 1.0; upper = 20.0 * log10(std::max(1.0e-7, upper)) / dBRange + 1.0; } track->GetEnvelope()->DrawPoints(dc, envRect, viewInfo->h, viewInfo->zoom, track->GetDisplayLog(), lower, upper); } void TrackArtist::UpdatePrefs() { mdBrange = gPrefs->Read(wxT("/GUI/EnvdBRange"), mdBrange); mShowClipping = gPrefs->Read(wxT("/GUI/ShowClipping"), mShowClipping); // mMaxFreq should have the same default as in SpectrumPrefs. mMaxFreq = gPrefs->Read(wxT("/Spectrum/MaxFreq"), 8000L); mMinFreq = gPrefs->Read(wxT("/Spectrum/MinFreq"), -1); mLogMaxFreq = gPrefs->Read(wxT("/SpectrumLog/MaxFreq"), -1); if( mLogMaxFreq < 0 ) mLogMaxFreq = mMaxFreq; mLogMinFreq = gPrefs->Read(wxT("/SpectrumLog/MinFreq"), -1); if( mLogMinFreq < 0 ) mLogMinFreq = mMinFreq; if (mLogMinFreq < 1) mLogMinFreq = 1; mWindowSize = gPrefs->Read(wxT("/Spectrum/FFTSize"), 256); #ifdef EXPERIMENTAL_ZERO_PADDED_SPECTROGRAMS mZeroPaddingFactor = gPrefs->Read(wxT("/Spectrum/ZeroPaddingFactor"), 1); #endif mIsGrayscale = (gPrefs->Read(wxT("/Spectrum/Grayscale"), 0L) != 0); #ifdef EXPERIMENTAL_FFT_Y_GRID mFftYGrid = (gPrefs->Read(wxT("/Spectrum/FFTYGrid"), 0L) != 0); #endif //EXPERIMENTAL_FFT_Y_GRID #ifdef EXPERIMENTAL_FIND_NOTES mFftFindNotes = (gPrefs->Read(wxT("/Spectrum/FFTFindNotes"), 0L) != 0); mFindNotesMinA = gPrefs->Read(wxT("/Spectrum/FindNotesMinA"), -30.0); mNumberOfMaxima = gPrefs->Read(wxT("/Spectrum/FindNotesN"), 5L); mFindNotesQuantize = (gPrefs->Read(wxT("/Spectrum/FindNotesQuantize"), 0L) != 0); #endif //EXPERIMENTAL_FIND_NOTES #ifdef EXPERIMENTAL_FFT_SKIP_POINTS mFftSkipPoints = gPrefs->Read(wxT("/Spectrum/FFTSkipPoints"), 0L); #endif //EXPERIMENTAL_FFT_SKIP_POINTS gPrefs->Flush(); } // Get various preference values int TrackArtist::GetSpectrumMinFreq(int deffreq) { return mMinFreq < 0 ? deffreq : mMinFreq; } int TrackArtist::GetSpectrumMaxFreq(int deffreq) { return mMaxFreq < 0 ? deffreq : mMaxFreq; } int TrackArtist::GetSpectrumLogMinFreq(int deffreq) { return mLogMinFreq < 0 ? deffreq : mLogMinFreq; } int TrackArtist::GetSpectrumLogMaxFreq(int deffreq) { return mLogMaxFreq < 0 ? deffreq : mLogMaxFreq; } int TrackArtist::GetSpectrumWindowSize(bool includeZeroPadding) { #ifdef EXPERIMENTAL_ZERO_PADDED_SPECTROGRAMS if (includeZeroPadding) return mWindowSize * mZeroPaddingFactor; else #endif return mWindowSize; } #ifdef EXPERIMENTAL_FFT_SKIP_POINTS int TrackArtist::GetSpectrumFftSkipPoints() { return mFftSkipPoints; } #endif // Set various preference values void TrackArtist::SetSpectrumMinFreq(int freq) { mMinFreq = freq; } void TrackArtist::SetSpectrumMaxFreq(int freq) { mMaxFreq = freq; } void TrackArtist::SetSpectrumLogMinFreq(int freq) { mLogMinFreq = freq; } void TrackArtist::SetSpectrumLogMaxFreq(int freq) { mLogMaxFreq = freq; } // Draws the sync-lock bitmap, tiled; always draws stationary relative to the DC // // AWD: now that the tiles don't link together, we're drawing a tilted grid, at // two steps down for every one across. This creates a pattern that repeats in // 5-step by 5-step boxes. Because we're only drawing in 5/25 possible positions // we have a grid spacing somewhat smaller than the image dimensions. Thus we // acheive lower density than with a square grid and eliminate edge cases where // no tiles are displayed. // // The pattern draws in tiles at (0,0), (2,1), (4,2), (1,3), and (3,4) in each // 5x5 box. // // There may be a better way to do this, or a more appealing pattern. void TrackArtist::DrawSyncLockTiles(wxDC *dc, wxRect r) { wxBitmap syncLockBitmap(theTheme.Image(bmpSyncLockSelTile)); // Grid spacing is a bit smaller than actual image size int gridW = syncLockBitmap.GetWidth() - 6; int gridH = syncLockBitmap.GetHeight() - 8; // Horizontal position within the grid, modulo its period int blockX = (r.x / gridW) % 5; // Amount to offset drawing of first column int xOffset = r.x % gridW; if (xOffset < 0) xOffset += gridW; // Check if we're missing an extra column to the left (this can happen // because the tiles are bigger than the grid spacing) bool extraCol = false; if (syncLockBitmap.GetWidth() - gridW > xOffset) { extraCol = true; xOffset += gridW; blockX = (blockX - 1) % 5; } // Make sure blockX is non-negative if (blockX < 0) blockX += 5; int x = 0; while (x < r.width) { int width = syncLockBitmap.GetWidth() - xOffset; if (x + width > r.width) width = r.width - x; // // Draw each row in this column // // Vertical position in the grid, modulo its period int blockY = (r.y / gridH) % 5; // Amount to offset drawing of first row int yOffset = r.y % gridH; if (yOffset < 0) yOffset += gridH; // Check if we're missing an extra row on top (this can happen because // the tiles are bigger than the grid spacing) bool extraRow = false; if (syncLockBitmap.GetHeight() - gridH > yOffset) { extraRow = true; yOffset += gridH; blockY = (blockY - 1) % 5; } // Make sure blockY is non-negative if (blockY < 0) blockY += 5; int y = 0; while (y < r.height) { int height = syncLockBitmap.GetHeight() - yOffset; if (y + height > r.height) height = r.height - y; // AWD: draw blocks according to our pattern if ((blockX == 0 && blockY == 0) || (blockX == 2 && blockY == 1) || (blockX == 4 && blockY == 2) || (blockX == 1 && blockY == 3) || (blockX == 3 && blockY == 4)) { // Do we need to get a sub-bitmap? if (width != syncLockBitmap.GetWidth() || height != syncLockBitmap.GetHeight()) { wxBitmap subSyncLockBitmap = syncLockBitmap.GetSubBitmap(wxRect(xOffset, yOffset, width, height)); dc->DrawBitmap(subSyncLockBitmap, r.x + x, r.y + y, true); } else { dc->DrawBitmap(syncLockBitmap, r.x + x, r.y + y, true); } } // Updates for next row if (extraRow) { // Second offset row, still at y = 0; no more extra rows yOffset -= gridH; extraRow = false; } else { // Move on in y, no more offset rows y += gridH - yOffset; yOffset = 0; } blockY = (blockY + 1) % 5; } // Updates for next column if (extraCol) { // Second offset column, still at x = 0; no more extra columns xOffset -= gridW; extraCol = false; } else { // Move on in x, no more offset rows x += gridW - xOffset; xOffset = 0; } blockX = (blockX + 1) % 5; } } void TrackArtist::DrawBackgroundWithSelection(wxDC *dc, const wxRect &r, Track *track, wxBrush &selBrush, wxBrush &unselBrush, double sel0, double sel1, double h, double pps) { //MM: Draw background. We should optimize that a bit more. //AWD: "+ 1.5" and "+ 2.5" throughout match code in //AdornedRulerPanel::DoDrawSelection() and make selection line up with ruler. //I don't know if/why this is correct. dc->SetPen(*wxTRANSPARENT_PEN); if (track->GetSelected() || track->IsSyncLockSelected()) { // Rectangles before, within, after the selction wxRect before = r; wxRect within = r; wxRect after = r; before.width = int ((sel0 - h) * pps + 2.5); if (before.GetRight() > r.GetRight()) { before.width = r.width; } if (before.width > 0) { dc->SetBrush(unselBrush); dc->DrawRectangle(before); within.x = before.GetRight(); } within.width = r.x + int ((sel1 - h) * pps + 2.5) - within.x; if (within.GetRight() > r.GetRight()) { within.width = r.GetRight() - within.x; } if (within.width > 0) { if (track->GetSelected()) { dc->SetBrush(selBrush); dc->DrawRectangle(within); } else { // Per condition above, track must be sync-lock selected dc->SetBrush(unselBrush); dc->DrawRectangle(within); DrawSyncLockTiles(dc, within); } after.x = within.GetRight(); } else { // `within` not drawn; start where it would have gone after.x = within.x; } after.width = r.GetRight() - after.x; if (after.width > 0) { dc->SetBrush(unselBrush); dc->DrawRectangle(after); } } else { // Track not selected; just draw background dc->SetBrush(unselBrush); dc->DrawRectangle(r); } }