/********************************************************************** Audacity: A Digital Audio Editor MixerBoard.cpp Vaughan Johnson, January 2007 Dominic Mazzoni **********************************************************************/ #include "MixerBoard.h" #include #include #include // for wxUSE_* macros #include #include #include #include // for wxSystemSettings::GetColour and wxSystemSettings::GetMetric #include "AColor.h" #include "AllThemeResources.h" #include "AudioIO.h" #ifdef USE_MIDI #include "NoteTrack.h" #endif #include "CommonCommandFlags.h" #include "KeyboardCapture.h" #include "prefs/GUISettings.h" // for RTL_WORKAROUND #include "Project.h" #include "ProjectAudioIO.h" #include "ProjectAudioManager.h" #include "ProjectHistory.h" #include "ProjectFileIO.h" #include "ProjectSettings.h" #include "ProjectWindow.h" #include "SelectUtilities.h" #include "Theme.h" #include "TrackPanel.h" // for EVT_TRACK_PANEL_TIMER #include "TrackUtilities.h" #include "UndoManager.h" #include "WaveTrack.h" #include "widgets/AButton.h" #include "widgets/Meter.h" #include "../images/MusicalInstruments.h" #ifdef __WXMSW__ #include "../images/AudacityLogo.xpm" #else #include "../images/AudacityLogo48x48.xpm" #endif #include "commands/CommandManager.h" #define AudacityMixerBoardTitle XO("Audacity Mixer Board%s") // class MixerTrackSlider BEGIN_EVENT_TABLE(MixerTrackSlider, ASlider) EVT_MOUSE_EVENTS(MixerTrackSlider::OnMouseEvent) EVT_SET_FOCUS(MixerTrackSlider::OnFocus) EVT_KILL_FOCUS(MixerTrackSlider::OnFocus) EVT_COMMAND(wxID_ANY, EVT_CAPTURE_KEY, MixerTrackSlider::OnCaptureKey) END_EVENT_TABLE() MixerTrackSlider::MixerTrackSlider(wxWindow * parent, wxWindowID id, const TranslatableString &name, const wxPoint & pos, const wxSize & size, const ASlider::Options &options) : ASlider(parent, id, name, pos, size, options) { } void MixerTrackSlider::OnMouseEvent(wxMouseEvent &event) { ASlider::OnMouseEvent(event); if (event.ButtonUp()) { MixerTrackCluster* pMixerTrackCluster = (MixerTrackCluster*)(this->GetParent()); switch (mStyle) { case DB_SLIDER: pMixerTrackCluster->HandleSliderGain(true); break; case PAN_SLIDER: pMixerTrackCluster->HandleSliderPan(true); break; default: break; // no-op } } } void MixerTrackSlider::OnFocus(wxFocusEvent &event) { KeyboardCapture::OnFocus( *this, event ); } void MixerTrackSlider::OnCaptureKey(wxCommandEvent &event) { wxKeyEvent *kevent = (wxKeyEvent *)event.GetEventObject(); int keyCode = kevent->GetKeyCode(); // Pass LEFT/RIGHT/UP/DOWN/PAGEUP/PAGEDOWN through for input/output sliders if (keyCode == WXK_LEFT || keyCode == WXK_RIGHT || keyCode == WXK_UP || keyCode == WXK_DOWN || keyCode == WXK_PAGEUP || keyCode == WXK_PAGEDOWN) { return; } event.Skip(); return; } // class MixerTrackCluster const int kInset = 4; const int kDoubleInset = (2 * kInset); const int kTripleInset = (3 * kInset); const int kQuadrupleInset = (4 * kInset); const int TRACK_NAME_HEIGHT = 18; const int MUSICAL_INSTRUMENT_HEIGHT_AND_WIDTH = 48; const int MUTE_SOLO_HEIGHT = 19; const int PAN_HEIGHT = 24; const int kLeftSideStackWidth = MUSICAL_INSTRUMENT_HEIGHT_AND_WIDTH - kDoubleInset; //vvv Change when numbers shown on slider scale. const int kRightSideStackWidth = MUSICAL_INSTRUMENT_HEIGHT_AND_WIDTH + kDoubleInset; const int kMixerTrackClusterWidth = kLeftSideStackWidth + kRightSideStackWidth + kQuadrupleInset; // kDoubleInset margin on both sides enum { ID_BITMAPBUTTON_MUSICAL_INSTRUMENT = 13000, ID_SLIDER_PAN, ID_SLIDER_GAIN, #ifdef EXPERIMENTAL_MIDI_OUT ID_SLIDER_VELOCITY, #endif ID_TOGGLEBUTTON_MUTE, ID_TOGGLEBUTTON_SOLO, }; BEGIN_EVENT_TABLE(MixerTrackCluster, wxPanelWrapper) EVT_MOUSE_EVENTS(MixerTrackCluster::OnMouseEvent) EVT_PAINT(MixerTrackCluster::OnPaint) EVT_BUTTON(ID_BITMAPBUTTON_MUSICAL_INSTRUMENT, MixerTrackCluster::OnButton_MusicalInstrument) EVT_SLIDER(ID_SLIDER_PAN, MixerTrackCluster::OnSlider_Pan) EVT_SLIDER(ID_SLIDER_GAIN, MixerTrackCluster::OnSlider_Gain) #ifdef EXPERIMENTAL_MIDI_OUT EVT_SLIDER(ID_SLIDER_VELOCITY, MixerTrackCluster::OnSlider_Velocity) #endif //v EVT_COMMAND_SCROLL(ID_SLIDER_GAIN, MixerTrackCluster::OnSliderScroll_Gain) EVT_COMMAND(ID_TOGGLEBUTTON_MUTE, wxEVT_COMMAND_BUTTON_CLICKED, MixerTrackCluster::OnButton_Mute) EVT_COMMAND(ID_TOGGLEBUTTON_SOLO, wxEVT_COMMAND_BUTTON_CLICKED, MixerTrackCluster::OnButton_Solo) END_EVENT_TABLE() MixerTrackCluster::MixerTrackCluster(wxWindow* parent, MixerBoard* grandParent, AudacityProject* project, const std::shared_ptr &pTrack, const wxPoint& pos /*= wxDefaultPosition*/, const wxSize& size /*= wxDefaultSize*/) : wxPanelWrapper(parent, -1, pos, size) , mTrack{ pTrack } { mMixerBoard = grandParent; mProject = project; wxASSERT( pTrack ); SetName( Verbatim( mTrack->GetName() ) ); //this->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_3DFACE)); this->SetBackgroundColour( theTheme.Colour( clrMedium ) ); // Not sure why, but sizers weren't getting offset vertically, // probably because not using wxDefaultPosition, // so positions are calculated explicitly below, and sizers code was removed. // (Still available in Audacity_UmixIt branch off 1.2.6.) // track name wxPoint ctrlPos(kDoubleInset, kDoubleInset); wxSize ctrlSize(size.GetWidth() - kQuadrupleInset, TRACK_NAME_HEIGHT); mStaticText_TrackName = safenew auStaticText(this, mTrack->GetName()); //v Useful when different tracks are different colors, but not now. // mStaticText_TrackName->SetBackgroundColour(this->GetTrackColor()); mStaticText_TrackName->SetForegroundColour(theTheme.Colour(clrMedium)); mStaticText_TrackName->SetForegroundColour(theTheme.Colour(clrTrackPanelText)); mStaticText_TrackName->SetSize( ctrlSize ); mStaticText_TrackName->SetPosition( ctrlPos ); // gain and velocity sliders at left (both in same place) ctrlPos.x = kDoubleInset; ctrlPos.y += TRACK_NAME_HEIGHT + kDoubleInset; const int nGainSliderHeight = size.GetHeight() - ctrlPos.y - kQuadrupleInset; ctrlSize.Set(kLeftSideStackWidth - kQuadrupleInset, nGainSliderHeight); mSlider_Gain = safenew MixerTrackSlider( this, ID_SLIDER_GAIN, /* i18n-hint: title of the Gain slider, used to adjust the volume */ XO("Gain"), ctrlPos, ctrlSize, ASlider::Options{} .Style( DB_SLIDER ) .Orientation( wxVERTICAL )); mSlider_Gain->SetName(_("Gain")); #ifdef EXPERIMENTAL_MIDI_OUT mSlider_Velocity = safenew MixerTrackSlider( this, ID_SLIDER_VELOCITY, /* i18n-hint: title of the MIDI Velocity slider */ XO("Velocity"), ctrlPos, ctrlSize, ASlider::Options{} .Style( VEL_SLIDER ) .Orientation( wxVERTICAL )); mSlider_Velocity->SetName(_("Velocity")); #endif // other controls and meter at right // musical instrument image ctrlPos.x += kLeftSideStackWidth + kInset; // + kInset to center it in right side stack ctrlSize.Set(MUSICAL_INSTRUMENT_HEIGHT_AND_WIDTH, MUSICAL_INSTRUMENT_HEIGHT_AND_WIDTH); wxBitmap* bitmap = mMixerBoard->GetMusicalInstrumentBitmap(mTrack.get()); wxASSERT(bitmap); mBitmapButton_MusicalInstrument = safenew wxBitmapButton(this, ID_BITMAPBUTTON_MUSICAL_INSTRUMENT, *bitmap, ctrlPos, ctrlSize, wxBU_AUTODRAW, wxDefaultValidator, _("Musical Instrument")); mBitmapButton_MusicalInstrument->SetName(_("Musical Instrument")); // pan slider ctrlPos.x -= kInset; // Remove inset for instrument, so Pan is at leftmost of left side stack. ctrlPos.y += MUSICAL_INSTRUMENT_HEIGHT_AND_WIDTH + kDoubleInset; ctrlSize.Set(kRightSideStackWidth, PAN_HEIGHT); // The width of the pan slider must be odd (don't ask). if (!(ctrlSize.x & 1)) ctrlSize.x--; mSlider_Pan = safenew MixerTrackSlider( this, ID_SLIDER_PAN, /* i18n-hint: Title of the Pan slider, used to move the sound left or right */ XO("Pan"), ctrlPos, ctrlSize, ASlider::Options{}.Style( PAN_SLIDER )); mSlider_Pan->SetName(_("Pan")); // mute/solo buttons stacked below Pan slider ctrlPos.y += PAN_HEIGHT + kDoubleInset; ctrlSize.Set(mMixerBoard->mMuteSoloWidth, MUTE_SOLO_HEIGHT); mToggleButton_Mute = safenew AButton(this, ID_TOGGLEBUTTON_MUTE, ctrlPos, ctrlSize, *(mMixerBoard->mImageMuteUp), *(mMixerBoard->mImageMuteOver), *(mMixerBoard->mImageMuteDown), *(mMixerBoard->mImageMuteDown), *(mMixerBoard->mImageMuteDisabled), true); // toggle button mToggleButton_Mute->SetName(_("Mute")); mToggleButton_Mute->SetAlternateImages( 1, *(mMixerBoard->mImageMuteUp), *(mMixerBoard->mImageMuteOver), *(mMixerBoard->mImageMuteDown), *(mMixerBoard->mImageMuteDown), *(mMixerBoard->mImageMuteDisabled)); ctrlPos.y += MUTE_SOLO_HEIGHT; mToggleButton_Solo = safenew AButton(this, ID_TOGGLEBUTTON_SOLO, ctrlPos, ctrlSize, *(mMixerBoard->mImageSoloUp), *(mMixerBoard->mImageSoloOver), *(mMixerBoard->mImageSoloDown), *(mMixerBoard->mImageSoloDown), *(mMixerBoard->mImageSoloDisabled), true); // toggle button mToggleButton_Solo->SetName(_("Solo")); bool bSoloNone = ProjectSettings::Get( *mProject ).IsSoloNone(); mToggleButton_Solo->Show(!bSoloNone); // meter ctrlPos.y += (bSoloNone ? 0 : MUTE_SOLO_HEIGHT) + kDoubleInset; const int nMeterHeight = nGainSliderHeight - (MUSICAL_INSTRUMENT_HEIGHT_AND_WIDTH + kDoubleInset) - (PAN_HEIGHT + kDoubleInset) - (MUTE_SOLO_HEIGHT + (bSoloNone ? 0 : MUTE_SOLO_HEIGHT) + kDoubleInset); ctrlSize.Set(kRightSideStackWidth, nMeterHeight); mMeter.Release(); if (GetWave()) { mMeter = safenew MeterPanel(mProject, // AudacityProject* project, this, -1, // wxWindow* parent, wxWindowID id, false, // bool isInput ctrlPos, ctrlSize, // const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, MeterPanel::MixerTrackCluster); // Style style = HorizontalStereo, mMeter->SetName(XO("Signal Level Meter")); } #if wxUSE_TOOLTIPS mStaticText_TrackName->SetToolTip(mTrack->GetName()); mToggleButton_Mute->SetToolTip(XO("Mute")); mToggleButton_Solo->SetToolTip(XO("Solo")); if (GetWave()) mMeter->SetToolTip(XO("Signal Level Meter")); #endif // wxUSE_TOOLTIPS UpdateForStateChange(); #ifdef __WXMAC__ wxSizeEvent event(GetSize(), GetId()); event.SetEventObject(this); GetEventHandler()->ProcessEvent(event); #endif } WaveTrack *MixerTrackCluster::GetWave() const { return dynamic_cast< WaveTrack * >( mTrack.get() ); } WaveTrack *MixerTrackCluster::GetRight() const { // TODO: more-than-two-channels auto left = GetWave(); if (left) { auto channels = TrackList::Channels(left); if ( channels.size() > 1 ) return * ++ channels.first; } return nullptr; } #ifdef EXPERIMENTAL_MIDI_OUT NoteTrack *MixerTrackCluster::GetNote() const { return dynamic_cast< NoteTrack * >( mTrack.get() ); } #endif // Old approach modified things in situ. // However with a theme change there is so much to modify, it is easier // to recreate. #if 0 void MixerTrackCluster::UpdatePrefs() { this->SetBackgroundColour( theTheme.Colour( clrMedium ) ); mStaticText_TrackName->SetForegroundColour(theTheme.Colour(clrTrackPanelText)); HandleResize(); // in case TracksBehaviorsSolo changed } #endif void MixerTrackCluster::HandleResize() // For wxSizeEvents, update gain slider and meter. { wxSize scrolledWindowClientSize = this->GetParent()->GetClientSize(); const int newClusterHeight = scrolledWindowClientSize.GetHeight() - kDoubleInset - // nClusterHeight from MixerBoard::UpdateTrackClusters wxSystemSettings::GetMetric(wxSYS_HSCROLL_Y) + // wxScrolledWindow::GetClientSize doesn't account for its scrollbar size. kDoubleInset; this->SetSize(-1, newClusterHeight); // Change only the heights of mSlider_Gain and mMeter. // But update shown status of mToggleButton_Solo, which affects top of mMeter. const int nGainSliderHeight = newClusterHeight - (kInset + // margin above mStaticText_TrackName TRACK_NAME_HEIGHT + kDoubleInset) - // mStaticText_TrackName + margin kQuadrupleInset; // margin below gain slider mSlider_Gain->SetSize(-1, nGainSliderHeight); #ifdef EXPERIMENTAL_MIDI_OUT mSlider_Velocity->SetSize(-1, nGainSliderHeight); #endif bool bSoloNone = ProjectSettings::Get( *mProject ).IsSoloNone(); mToggleButton_Solo->Show(!bSoloNone); const int nRequiredHeightAboveMeter = MUSICAL_INSTRUMENT_HEIGHT_AND_WIDTH + kDoubleInset + PAN_HEIGHT + kDoubleInset + MUTE_SOLO_HEIGHT + (bSoloNone ? 0 : MUTE_SOLO_HEIGHT) + kDoubleInset; const int nMeterY = kDoubleInset + // margin at top TRACK_NAME_HEIGHT + kDoubleInset + nRequiredHeightAboveMeter; const int nMeterHeight = nGainSliderHeight - nRequiredHeightAboveMeter; if (mMeter) mMeter->SetSize(-1, nMeterY, -1, nMeterHeight); } void MixerTrackCluster::HandleSliderGain(const bool bWantPushState /*= false*/) { float fValue = mSlider_Gain->Get(); if (GetWave()) GetWave()->SetGain(fValue); if (GetRight()) GetRight()->SetGain(fValue); // Update the TrackPanel correspondingly. TrackPanel::Get( *mProject ).RefreshTrack(mTrack.get()); if (bWantPushState) ProjectHistory::Get( *mProject ) .PushState(XO("Moved gain slider"), XO("Gain"), UndoPush::CONSOLIDATE ); } #ifdef EXPERIMENTAL_MIDI_OUT void MixerTrackCluster::HandleSliderVelocity(const bool bWantPushState /*= false*/) { float fValue = mSlider_Velocity->Get(); if (GetNote()) GetNote()->SetVelocity(fValue); // Update the TrackPanel correspondingly. TrackPanel::Get( *mProject ).RefreshTrack(mTrack.get()); if (bWantPushState) ProjectHistory::Get( *mProject ) .PushState(XO("Moved velocity slider"), XO("Velocity"), UndoPush::CONSOLIDATE); } #endif void MixerTrackCluster::HandleSliderPan(const bool bWantPushState /*= false*/) { float fValue = mSlider_Pan->Get(); if (GetWave()) // test in case track is a NoteTrack GetWave()->SetPan(fValue); if (GetRight()) GetRight()->SetPan(fValue); // Update the TrackPanel correspondingly. TrackPanel::Get( *mProject ).RefreshTrack(mTrack.get()); if (bWantPushState) ProjectHistory::Get( *mProject ) .PushState(XO("Moved pan slider"), XO("Pan"), UndoPush::CONSOLIDATE ); } void MixerTrackCluster::ResetMeter(const bool bResetClipping) { if (mMeter) mMeter->Reset(GetWave()->GetRate(), bResetClipping); } // Update appearance to match the state of the track void MixerTrackCluster::UpdateForStateChange() { const wxString newName = mTrack->GetName(); if (newName != GetName()) { SetName( Verbatim( newName ) ); mStaticText_TrackName->SetLabel(newName); mStaticText_TrackName->SetName(newName); #if wxUSE_TOOLTIPS mStaticText_TrackName->SetToolTip(newName); #endif mBitmapButton_MusicalInstrument->SetBitmapLabel( *(mMixerBoard->GetMusicalInstrumentBitmap(mTrack.get()))); } mToggleButton_Mute->SetAlternateIdx(mTrack->GetSolo() ? 1 : 0); if (mTrack->GetMute()) mToggleButton_Mute->PushDown(); else mToggleButton_Mute->PopUp(); bool bIsSolo = mTrack->GetSolo(); if (bIsSolo) mToggleButton_Solo->PushDown(); else mToggleButton_Solo->PopUp(); mToggleButton_Mute->SetAlternateIdx(bIsSolo ? 1 : 0); if (!GetWave()) mSlider_Pan->Hide(); else mSlider_Pan->Set(GetWave()->GetPan()); if (!GetWave()) mSlider_Gain->Hide(); else mSlider_Gain->Set(GetWave()->GetGain()); #ifdef EXPERIMENTAL_MIDI_OUT if (!GetNote()) mSlider_Velocity->Hide(); else mSlider_Velocity->Set(GetNote()->GetVelocity()); #endif } void MixerTrackCluster::UpdateMeter(const double t0, const double t1) { // NoteTracks do not (currently) register on meters. It would probably be // a good idea to display 16 channel "active" lights rather than a meter if (!GetWave()) return; if ((t0 < 0.0) || (t1 < 0.0) || (t0 >= t1) || // bad time value or nothing to show ((mMixerBoard->HasSolo() || mTrack->GetMute()) && !mTrack->GetSolo()) ) { //v Vaughan, 2011-02-25: Moved the update back to TrackPanel::OnTimer() as it helps with // playback issues reported by Bill and noted on Bug 258, so no assert. // Vaughan, 2011-02-04: Now that we're updating all meters from audacityAudioCallback, // this causes an assert if you click Mute while playing, because ResetMeter() resets // the timer, and wxTimerbase says that can only be done from main thread -- // but it seems to work fine. this->ResetMeter(false); return; } // Vaughan, 2010-11-27: // This commented out code is flawed. Mistaken understanding of "frame" vs "window". // Caused me to override MeterPanel::UpdateDisplay(). // But I think it's got a good idea, of calling WaveTracks' GetMinMax and GetRMS // instead of passing in all the data and asking the meter to derive peak and rms. // May be worth revisiting as I think it should perform better, because it uses the min/max/rms // stored in sample blocks, rather than calculating them, but for now, changing it to use the // original MeterPanel::UpdateDisplay(). New code is below the previous (now commented out). // //const size_t kFramesPerBuffer = 4; //float min; // dummy, since it's not shown in meters //Floats maxLeft{kFramesPerBuffer}; //Floats rmsLeft{kFramesPerBuffer}; //Floats maxRight{kFramesPerBuffer}; //Floats rmsRight{kFramesPerBuffer}; // //#ifdef EXPERIMENTAL_MIDI_OUT // bool bSuccess = (GetWave() != nullptr); //#else // bool bSuccess = true; //#endif //const double dFrameInterval = (t1 - t0) / (double)kFramesPerBuffer; //double dFrameT0 = t0; //double dFrameT1 = t0 + dFrameInterval; //int i = 0; //while (bSuccess && (i < kFramesPerBuffer)) //{ // bSuccess &= // mTrack->GetMinMax(&min, &(maxLeft[i]), dFrameT0, dFrameT1) && // mTrack->GetRMS(&(rmsLeft[i]), dFrameT0, dFrameT1); // if (bSuccess && mRightTrack) // bSuccess &= // mRightTrack->GetMinMax(&min, &(maxRight[i]), dFrameT0, dFrameT1) && // mRightTrack->GetRMS(&(rmsRight[i]), dFrameT0, dFrameT1); // else // { // // Mono: Start with raw values same as left. // // To be modified by bWantPostFadeValues and channel pan/gain. // maxRight[i] = maxLeft[i]; // rmsRight[i] = rmsLeft[i]; // } // dFrameT0 += dFrameInterval; // dFrameT1 += dFrameInterval; // i++; //} // //const bool bWantPostFadeValues = true; //v Turn this into a checkbox on MixerBoard? For now, always true. //if (bSuccess && bWantPostFadeValues) //if (bSuccess) //{ // for (i = 0; i < kFramesPerBuffer; i++) // { // float gain = mTrack->GetChannelGain(0); // maxLeft[i] *= gain; // rmsLeft[i] *= gain; // if (mRightTrack) // gain = mRightTrack->GetChannelGain(1); // maxRight[i] *= gain; // rmsRight[i] *= gain; // } // if ( mMeter ) mMeter->UpdateDisplay( // 2, // If mono, show left track values in both meters, as in MeterToolBar, rather than nChannels. // kFramesPerBuffer, // maxLeft, rmsLeft, // maxRight, rmsRight, // mTrack->TimeToLongSamples(t1 - t0)); //} // const auto pTrack = GetWave(); auto startSample = (sampleCount)((pTrack->GetRate() * t0) + 0.5); auto scnFrames = (sampleCount)((pTrack->GetRate() * (t1 - t0)) + 0.5); // Expect that the difference of t1 and t0 is the part of a track played // in about 1/20 second (ticks of TrackPanel timer), so this won't overflow auto nFrames = scnFrames.as_size_t(); Floats tempFloatsArray{ nFrames }; decltype(tempFloatsArray) meterFloatsArray; // Don't throw on read error in this drawing update routine bool bSuccess = pTrack->GetFloats(tempFloatsArray.get(), startSample, nFrames, fillZero, false); if (bSuccess) { // We always pass a stereo sample array to the meter, as it shows 2 channels. // Mono shows same in both meters. // Since we're not mixing, need to duplicate same signal for "right" channel in mono case. meterFloatsArray = Floats{ 2 * nFrames }; // Interleave for stereo. Left/mono first. for (unsigned int index = 0; index < nFrames; index++) meterFloatsArray[2 * index] = tempFloatsArray[index]; if (GetRight()) // Again, don't throw bSuccess = GetRight()->GetFloats(tempFloatsArray.get(), startSample, nFrames, fillZero, false); if (bSuccess) // Interleave right channel, or duplicate same signal for "right" channel in mono case. for (unsigned int index = 0; index < nFrames; index++) meterFloatsArray[(2 * index) + 1] = tempFloatsArray[index]; } //const bool bWantPostFadeValues = true; //v Turn this into a checkbox on MixerBoard? For now, always true. //if (bSuccess && bWantPostFadeValues) if (bSuccess) { //vvv Need to apply envelope, too? See Mixer::MixSameRate. float gain = pTrack->GetChannelGain(0); for (unsigned int index = 0; index < nFrames; index++) meterFloatsArray[2 * index] *= gain; if (GetRight()) gain = GetRight()->GetChannelGain(1); else gain = pTrack->GetChannelGain(1); for (unsigned int index = 0; index < nFrames; index++) meterFloatsArray[(2 * index) + 1] *= gain; // Clip to [-1.0, 1.0] range. for (unsigned int index = 0; index < 2 * nFrames; index++) if (meterFloatsArray[index] < -1.0) meterFloatsArray[index] = -1.0; else if (meterFloatsArray[index] > 1.0) meterFloatsArray[index] = 1.0; if (mMeter) mMeter->UpdateDisplay(2, nFrames, meterFloatsArray.get()); } else this->ResetMeter(false); } // private wxColour MixerTrackCluster::GetTrackColor() { return wxColour(102, 255, 102); // same as Meter playback color } // event handlers void MixerTrackCluster::HandleSelect(bool bShiftDown, bool bControlDown) { SelectUtilities::DoListSelection(*mProject, mTrack.get(), bShiftDown, bControlDown, true); } void MixerTrackCluster::OnMouseEvent(wxMouseEvent& event) { if (event.ButtonUp()) this->HandleSelect(event.ShiftDown(), event.ControlDown()); else event.Skip(); } void MixerTrackCluster::OnPaint(wxPaintEvent & WXUNUSED(event)) { UpdateForStateChange(); auto selected = mTrack->GetSelected(); wxColour col = theTheme.Colour(selected ? clrTrackInfoSelected : clrTrackInfo) ; SetBackgroundColour( col ); if (mMeter) mMeter->SetBackgroundColour( col ); mStaticText_TrackName->SetBackgroundColour( col ); mSlider_Gain->SetBackgroundColour( col ); mSlider_Pan->SetBackgroundColour( col ); wxPaintDC dc(this); AColor::MediumTrackInfo(&dc, selected); dc.DrawRectangle(this->GetClientRect()); wxSize clusterSize = this->GetSize(); wxRect bev(0, 0, clusterSize.GetWidth() - 1, clusterSize.GetHeight() - 1); //bev.Inflate(-1, -1); AColor::Bevel(dc, true, bev);// same bevel whether selected or not. } void MixerTrackCluster::OnButton_MusicalInstrument(wxCommandEvent& WXUNUSED(event)) { const auto &state = ::wxGetMouseState(); this->HandleSelect(state.ShiftDown(), state.ControlDown()); } void MixerTrackCluster::OnSlider_Gain(wxCommandEvent& WXUNUSED(event)) { this->HandleSliderGain(); } #ifdef EXPERIMENTAL_MIDI_OUT void MixerTrackCluster::OnSlider_Velocity(wxCommandEvent& WXUNUSED(event)) { this->HandleSliderVelocity(); } #endif //v void MixerTrackCluster::OnSliderScroll_Gain(wxScrollEvent& WXUNUSED(event)) //{ //int sliderValue = (int)(mSlider_Gain->Get()); //v mSlider_Gain->GetValue(); //#ifdef __WXMSW__ // // Negate because wxSlider on Windows has min at top, max at bottom. // // mSlider_Gain->GetValue() is in [-6,36]. wxSlider has min at top, so this is [-36dB,6dB]. // sliderValue = -sliderValue; //#endif //wxString str = _("Gain: "); //if (sliderValue > 0) // str += "+"; //str += wxString::Format("%d dB", sliderValue); //mSlider_Gain->SetToolTip(str); //} void MixerTrackCluster::OnSlider_Pan(wxCommandEvent& WXUNUSED(event)) { this->HandleSliderPan(); } void MixerTrackCluster::OnButton_Mute(wxCommandEvent& WXUNUSED(event)) { TrackUtilities::DoTrackMute( *mProject, mTrack.get(), mToggleButton_Mute->WasShiftDown()); mToggleButton_Mute->SetAlternateIdx(mTrack->GetSolo() ? 1 : 0); // Update the TrackPanel correspondingly. if (ProjectSettings::Get(*mProject).IsSoloSimple()) ProjectWindow::Get( *mProject ).RedrawProject(); else // Update only the changed track. TrackPanel::Get( *mProject ).RefreshTrack(mTrack.get()); } void MixerTrackCluster::OnButton_Solo(wxCommandEvent& WXUNUSED(event)) { TrackUtilities::DoTrackSolo( *mProject, mTrack.get(), mToggleButton_Solo->WasShiftDown()); bool bIsSolo = mTrack->GetSolo(); mToggleButton_Mute->SetAlternateIdx(bIsSolo ? 1 : 0); // Update the TrackPanel correspondingly. // Bug 509: Must repaint all, as many tracks can change with one Solo change. ProjectWindow::Get( *mProject ).RedrawProject(); } // class MusicalInstrument MusicalInstrument::MusicalInstrument(std::unique_ptr &&pBitmap, const wxString & strXPMfilename) { mBitmap = std::move(pBitmap); int nUnderscoreIndex; wxString strFilename = strXPMfilename; strFilename.MakeLower(); // Make sure, so we don't have to do case insensitive comparison. wxString strKeyword; while ((nUnderscoreIndex = strFilename.Find(wxT('_'))) != -1) { strKeyword = strFilename.Left(nUnderscoreIndex); mKeywords.push_back(strKeyword); strFilename = strFilename.Mid(nUnderscoreIndex + 1); } if (!strFilename.empty()) // Skip trailing underscores. mKeywords.push_back(strFilename); // Add the last one. } MusicalInstrument::~MusicalInstrument() { mKeywords.clear(); } // class MixerBoardScrolledWindow // wxScrolledWindow ignores mouse clicks in client area, // but they don't get passed to Mixerboard. // We need to catch them to deselect all track clusters. BEGIN_EVENT_TABLE(MixerBoardScrolledWindow, wxScrolledWindow) EVT_MOUSE_EVENTS(MixerBoardScrolledWindow::OnMouseEvent) END_EVENT_TABLE() MixerBoardScrolledWindow::MixerBoardScrolledWindow(AudacityProject* project, MixerBoard* parent, wxWindowID id /*= -1*/, const wxPoint& pos /*= wxDefaultPosition*/, const wxSize& size /*= wxDefaultSize*/, long style /*= wxHSCROLL | wxVSCROLL*/) : wxScrolledWindow(parent, id, pos, size, style) { mMixerBoard = parent; mProject = project; } MixerBoardScrolledWindow::~MixerBoardScrolledWindow() { } void MixerBoardScrolledWindow::OnMouseEvent(wxMouseEvent& event) { if (event.ButtonUp()) { //v Even when I implement MixerBoard::OnMouseEvent and call event.Skip() // here, MixerBoard::OnMouseEvent never gets called. // So, added mProject to MixerBoardScrolledWindow and just directly do what's needed here. SelectUtilities::SelectNone( *mProject ); } else event.Skip(); } // class MixerBoard #define MIXER_BOARD_MIN_HEIGHT 460 // Min width is one cluster wide, plus margins. #define MIXER_BOARD_MIN_WIDTH kTripleInset + kMixerTrackClusterWidth*2 + kTripleInset BEGIN_EVENT_TABLE(MixerBoard, wxWindow) EVT_PAINT(MixerBoard::OnPaint) EVT_SIZE(MixerBoard::OnSize) END_EVENT_TABLE() MixerBoard::MixerBoard(AudacityProject* pProject, wxFrame* parent, const wxPoint& pos /*= wxDefaultPosition*/, const wxSize& size /*= wxDefaultSize*/) : wxWindow(parent, -1, pos, size) { // public data members // mute & solo button images // Create once and store on MixerBoard for use in all MixerTrackClusters. mImageMuteUp = NULL; mImageMuteOver = NULL; mImageMuteDown = NULL; mImageMuteDownWhileSolo = NULL; mImageMuteDisabled = NULL; mImageSoloUp = NULL; mImageSoloOver = NULL; mImageSoloDown = NULL; mImageSoloDisabled = NULL; mMuteSoloWidth = kRightSideStackWidth - kInset; // correct for max width, but really set in MixerBoard::CreateMuteSoloImages // private data members this->LoadMusicalInstruments(); // Set up mMusicalInstruments. mProject = pProject; wxASSERT(pProject); // to justify safenew mScrolledWindow = safenew MixerBoardScrolledWindow( pProject, // AudacityProject* project, this, -1, // wxWindow* parent, wxWindowID id = -1, this->GetClientAreaOrigin(), // const wxPoint& pos = wxDefaultPosition, size, // const wxSize& size = wxDefaultSize, wxHSCROLL); // long style = wxHSCROLL | wxVSCROLL, const wxString& name = "scrolledWindow") // Set background color to same as TrackPanel background. // #ifdef EXPERIMENTAL_THEMING // mScrolledWindow->SetBackgroundColour(this->GetParent()->GetBackgroundColour()); // #else // mScrolledWindow->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW)); // #endif mScrolledWindow->SetBackgroundColour( theTheme.Colour( clrMedium ) ); RTL_WORKAROUND(mScrolledWindow); mScrolledWindow->SetScrollRate(10, 0); // no vertical scroll mScrolledWindow->SetVirtualSize(size); /* This doesn't work to make the mScrolledWindow automatically resize, so do it explicitly in OnSize. auto pBoxSizer = std::make_unique(wxVERTICAL); pBoxSizer->push_back(mScrolledWindow, 0, wxExpand, 0); this->SetAutoLayout(true); this->SetSizer(pBoxSizer); pBoxSizer->Fit(this); pBoxSizer->SetSizeHints(this); */ mPrevT1 = 0.0; mTracks = &TrackList::Get( *mProject ); // Events from the project don't propagate directly to this other frame, so... mProject->Bind(EVT_TRACK_PANEL_TIMER, &MixerBoard::OnTimer, this); mTracks->Bind(EVT_TRACKLIST_SELECTION_CHANGE, &MixerBoard::OnTrackChanged, this); mTracks->Bind(EVT_TRACKLIST_PERMUTED, &MixerBoard::OnTrackSetChanged, this); mTracks->Bind(EVT_TRACKLIST_ADDITION, &MixerBoard::OnTrackSetChanged, this); mTracks->Bind(EVT_TRACKLIST_DELETION, &MixerBoard::OnTrackSetChanged, this); mTracks->Bind(EVT_TRACKLIST_TRACK_DATA_CHANGE, &MixerBoard::OnTrackChanged, this); wxTheApp->Connect(EVT_AUDIOIO_PLAYBACK, wxCommandEventHandler(MixerBoard::OnStartStop), NULL, this); } void MixerBoard::UpdatePrefs() { // Destroys this: static_cast(GetParent())->Recreate( mProject ); // Old approach modified things in situ. // However with a theme change there is so much to modify, it is easier // to recreate. #if 0 mScrolledWindow->SetBackgroundColour( theTheme.Colour( clrMedium ) ); if( mImageMuteUp ){ mImageMuteUp.reset(); mImageMuteOver.reset(); mImageMuteDown.reset(); mImageMuteDownWhileSolo.reset(); mImageMuteDisabled.reset(); mImageSoloUp.reset(); mImageSoloOver.reset(); mImageSoloDown.reset(); mImageSoloDisabled.reset(); } for (unsigned int nClusterIndex = 0; nClusterIndex < mMixerTrackClusters.size(); nClusterIndex++) mMixerTrackClusters[nClusterIndex]->UpdatePrefs(); Refresh(); #endif } // Reassign mixer input strips (MixerTrackClusters) to Track Clusters // both have the same order. // If EXPERIMENTAL_MIDI_OUT, then Note Tracks appear in the // mixer, and we must be able to convert and reuse a MixerTrackCluster // from audio to midi or midi to audio. This task is handled by // UpdateForStateChange(). // void MixerBoard::UpdateTrackClusters() { if (!mImageMuteUp) this->CreateMuteSoloImages(); const int nClusterHeight = mScrolledWindow->GetClientSize().GetHeight() - kDoubleInset; size_t nClusterCount = mMixerTrackClusters.size(); unsigned int nClusterIndex = 0; MixerTrackCluster* pMixerTrackCluster = NULL; for (auto pPlayableTrack: mTracks->Leaders()) { // TODO: more-than-two-channels auto spTrack = pPlayableTrack->SharedPointer(); if (nClusterIndex < nClusterCount) { // Already showing it. // Track clusters are maintained in the same order as the WaveTracks. // Track pointers can change for the "same" track for different states // on the undo stack, so update the pointers and display name. mMixerTrackClusters[nClusterIndex]->mTrack = spTrack; // Assume linked track is wave or null mMixerTrackClusters[nClusterIndex]->UpdateForStateChange(); } else { // Not already showing it. Add a NEW MixerTrackCluster. wxPoint clusterPos( kInset + nClusterIndex * kMixerTrackClusterWidth, kInset); wxSize clusterSize(kMixerTrackClusterWidth, nClusterHeight); pMixerTrackCluster = safenew MixerTrackCluster(mScrolledWindow, this, mProject, spTrack, clusterPos, clusterSize); if (pMixerTrackCluster) mMixerTrackClusters.push_back(pMixerTrackCluster); } nClusterIndex++; } if (pMixerTrackCluster) { // Added at least one MixerTrackCluster. this->UpdateWidth(); this->ResizeTrackClusters(); } else while (nClusterIndex < nClusterCount) { // We've got too many clusters. // This can happen only on things like Undo New Audio Track or Undo Import // that don't call RemoveTrackCluster explicitly. // We've already updated the track pointers for the clusters to the left, so just remove all the rest. // Successively DELETE from right to left. RemoveTrackCluster(--nClusterCount); } } int MixerBoard::GetTrackClustersWidth() { return kInset + // extra margin at left for first one (mMixerTrackClusters.size() * // number of tracks times (kInset + kMixerTrackClusterWidth)) + // left margin and width for each kDoubleInset; // plus final right margin } void MixerBoard::RemoveTrackCluster(size_t nIndex) { auto pMixerTrackCluster = mMixerTrackClusters[nIndex]; mMixerTrackClusters.erase(mMixerTrackClusters.begin() + nIndex); pMixerTrackCluster->Destroy(); // DELETE is unsafe on wxWindow. // Close the gap, if any. wxPoint pos; int targetX; for (unsigned int i = nIndex; i < mMixerTrackClusters.size(); i++) { pos = mMixerTrackClusters[i]->GetPosition(); targetX = kInset + // extra inset to left for first one, so it's double (i * (kInset + kMixerTrackClusterWidth)) + // left margin and width for each kInset; // plus left margin for this cluster if (pos.x != targetX) mMixerTrackClusters[i]->Move(targetX, pos.y); } this->UpdateWidth(); } wxBitmap* MixerBoard::GetMusicalInstrumentBitmap(const Track* pTrack) { if (mMusicalInstruments.empty()) return NULL; // random choice: return mMusicalInstruments[(int)pTrack % mMusicalInstruments.size()].mBitmap; const wxString strTrackName(pTrack->GetName().MakeLower()); size_t nBestItemIndex = 0; unsigned int nBestScore = 0; unsigned int nInstrIndex = 0; unsigned int nKeywordIndex; unsigned int nNumKeywords; unsigned int nPointsPerMatch; unsigned int nScore; for (nInstrIndex = 0; nInstrIndex < mMusicalInstruments.size(); nInstrIndex++) { nScore = 0; nNumKeywords = mMusicalInstruments[nInstrIndex]->mKeywords.size(); if (nNumKeywords > 0) { nPointsPerMatch = 10 / nNumKeywords; for (nKeywordIndex = 0; nKeywordIndex < nNumKeywords; nKeywordIndex++) if (strTrackName.Contains(mMusicalInstruments[nInstrIndex]->mKeywords[nKeywordIndex])) { nScore += nPointsPerMatch + // Longer keywords get more points. (2 * mMusicalInstruments[nInstrIndex]->mKeywords[nKeywordIndex].length()); } } // Choose later one if just matching nBestScore, for better variety, // and so default works as last element. if (nScore >= nBestScore) { nBestScore = nScore; nBestItemIndex = nInstrIndex; } } return mMusicalInstruments[nBestItemIndex]->mBitmap.get(); } bool MixerBoard::HasSolo() { return !(( mTracks->Any() + &PlayableTrack::GetSolo ).empty()); } void MixerBoard::RefreshTrackClusters(bool bEraseBackground /*= true*/) { for (unsigned int i = 0; i < mMixerTrackClusters.size(); i++) mMixerTrackClusters[i]->Refresh(bEraseBackground); } void MixerBoard::ResizeTrackClusters() { for (unsigned int nClusterIndex = 0; nClusterIndex < mMixerTrackClusters.size(); nClusterIndex++) mMixerTrackClusters[nClusterIndex]->HandleResize(); } void MixerBoard::ResetMeters(const bool bResetClipping) { mPrevT1 = BAD_STREAM_TIME; if (!this->IsShown()) return; for (unsigned int i = 0; i < mMixerTrackClusters.size(); i++) mMixerTrackClusters[i]->ResetMeter(bResetClipping); } void MixerBoard::UpdateMeters(const double t1, const bool bLoopedPlay) { if (!this->IsShown() || (t1 == BAD_STREAM_TIME)) return; if (mPrevT1 == BAD_STREAM_TIME) { mPrevT1 = t1; return; } // In loopedPlay mode, at the end of the loop, mPrevT1 is set to // selection end, so the next t1 will be less, but we do want to // keep updating the meters. if (t1 <= mPrevT1) { if (bLoopedPlay) mPrevT1 = t1; return; } for (unsigned int i = 0; i < mMixerTrackClusters.size(); i++) mMixerTrackClusters[i]->UpdateMeter(mPrevT1, t1); mPrevT1 = t1; } void MixerBoard::UpdateWidth() { int newWidth = this->GetTrackClustersWidth(); // Min width is one cluster wide, plus margins. if (newWidth < MIXER_BOARD_MIN_WIDTH) newWidth = MIXER_BOARD_MIN_WIDTH; mScrolledWindow->SetVirtualSize(newWidth, -1); this->GetParent()->SetSize(newWidth + kDoubleInset, -1); } // // private methods // void MixerBoard::MakeButtonBitmap( wxMemoryDC & dc, wxBitmap & WXUNUSED(bitmap), wxRect & bev, const TranslatableString & str, bool up ) { const auto translation = str.Translation(); int textWidth, textHeight; int fontSize = 10; #ifdef __WXMSW__ fontSize = 8; #endif wxFont font(fontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL); GetTextExtent(translation, &textWidth, &textHeight, NULL, NULL, &font); AColor::UseThemeColour( &dc, clrMedium ); dc.DrawRectangle(bev); AColor::Bevel2( dc, up, bev, false ); wxCoord x = bev.x + (bev.width - textWidth) / 2; wxCoord y = bev.y + (bev.height - textHeight) / 2; dc.SetFont(font); dc.SetTextForeground(theTheme.Colour(clrTrackPanelText)); dc.SetBackgroundMode(wxTRANSPARENT); dc.DrawText(translation, x, y); // dc.DrawText(translation, 0, 0); } void MixerBoard::CreateMuteSoloImages() { // Much of this is similar to TrackInfo::MuteOrSoloDrawFunction. wxMemoryDC dc; auto str = XO("Mute"); //mMuteSoloWidth = textWidth + kQuadrupleInset; //if (mMuteSoloWidth < kRightSideStackWidth - kInset) mMuteSoloWidth = kRightSideStackWidth - kInset; wxBitmap bitmap(mMuteSoloWidth, MUTE_SOLO_HEIGHT,24); dc.SelectObject(bitmap); wxRect bev(0, 0, mMuteSoloWidth, MUTE_SOLO_HEIGHT); const bool up=true; const bool down=false; MakeButtonBitmap( dc, bitmap, bev, str, up ); mImageMuteUp = std::make_unique(bitmap.ConvertToImage()); mImageMuteOver = std::make_unique(bitmap.ConvertToImage()); // Same as up, for now. MakeButtonBitmap( dc, bitmap, bev, str, down ); //AColor::Bevel(dc, false, bev); mImageMuteDown = std::make_unique(bitmap.ConvertToImage()); MakeButtonBitmap( dc, bitmap, bev, str, down ); mImageMuteDownWhileSolo = std::make_unique(bitmap.ConvertToImage()); mImageMuteDisabled = std::make_unique(mMuteSoloWidth, MUTE_SOLO_HEIGHT); // Leave empty because unused. str = XO("Solo"); MakeButtonBitmap( dc, bitmap, bev, str, up ); mImageSoloUp = std::make_unique(bitmap.ConvertToImage()); mImageSoloOver = std::make_unique(bitmap.ConvertToImage()); // Same as up, for now. MakeButtonBitmap( dc, bitmap, bev, str, down ); mImageSoloDown = std::make_unique(bitmap.ConvertToImage()); mImageSoloDisabled = std::make_unique(mMuteSoloWidth, MUTE_SOLO_HEIGHT); // Leave empty because unused. } int MixerBoard::FindMixerTrackCluster(const PlayableTrack* pTrack, MixerTrackCluster** hMixerTrackCluster) const { *hMixerTrackCluster = NULL; for (unsigned int i = 0; i < mMixerTrackClusters.size(); i++) { if (mMixerTrackClusters[i]->mTrack.get() == pTrack) { *hMixerTrackCluster = mMixerTrackClusters[i]; return i; } } return -1; } void MixerBoard::LoadMusicalInstruments() { const struct Data { const char * const *bitmap; wxString name; } table[] = { {acoustic_guitar_gtr_xpm, wxT("acoustic_guitar_gtr")}, {acoustic_piano_pno_xpm, wxT("acoustic_piano_pno")}, {back_vocal_bg_vox_xpm, wxT("back_vocal_bg_vox")}, {clap_xpm, wxT("clap")}, {drums_dr_xpm, wxT("drums_dr")}, {electric_bass_guitar_bs_gtr_xpm, wxT("electric_bass_guitar_bs_gtr")}, {electric_guitar_gtr_xpm, wxT("electric_guitar_gtr")}, {electric_piano_pno_key_xpm, wxT("electric_piano_pno_key")}, {kick_xpm, wxT("kick")}, {loop_xpm, wxT("loop")}, {organ_org_xpm, wxT("organ_org")}, {perc_xpm, wxT("perc")}, {sax_xpm, wxT("sax")}, {snare_xpm, wxT("snare")}, {string_violin_cello_xpm, wxT("string_violin_cello")}, {synth_xpm, wxT("synth")}, {tambo_xpm, wxT("tambo")}, {trumpet_horn_xpm, wxT("trumpet_horn")}, {turntable_xpm, wxT("turntable")}, {vibraphone_vibes_xpm, wxT("vibraphone_vibes")}, {vocal_vox_xpm, wxT("vocal_vox")}, // This one must be last, so it wins when best score is 0. {_default_instrument_xpm, wxEmptyString}, }; wxRect bev(1, 1, MUSICAL_INSTRUMENT_HEIGHT_AND_WIDTH - 2, MUSICAL_INSTRUMENT_HEIGHT_AND_WIDTH - 2); wxMemoryDC dc; for (const auto &data : table) { auto bmp = std::make_unique(data.bitmap,24); dc.SelectObject(*bmp); AColor::Bevel(dc, false, bev); mMusicalInstruments.push_back(std::make_unique( std::move(bmp), data.name )); }; } // event handlers void MixerBoard::OnPaint(wxPaintEvent& evt) { if (!mUpToDate) { mUpToDate = true; UpdateTrackClusters(); Refresh(); } // Does the base class do anything for repainting? evt.Skip(); } void MixerBoard::OnSize(wxSizeEvent &evt) { // this->FitInside() doesn't work, and it doesn't happen automatically. Is wxScrolledWindow wrong? mScrolledWindow->SetSize(evt.GetSize()); this->ResizeTrackClusters(); this->RefreshTrackClusters(true); } void MixerBoard::OnTimer(wxCommandEvent &event) { // PRL 12 Jul 2015: Moved the below (with comments) out of TrackPanel::OnTimer. // Vaughan, 2011-01-28: No longer doing this on timer. // Now it's in AudioIO::SetMeters() and AudioIO::StopStream(), as with Meter Toolbar meters. //if (pMixerBoard) // pMixerBoard->ResetMeters(false); //v Vaughan, 2011-02-25: Moved this update back here from audacityAudioCallback. // See note there. // Vaughan, 2010-01-30: // Since all we're doing here is updating the meters, I moved it to // audacityAudioCallback where it calls gAudioIO->mOutputMeter->UpdateDisplay(). if (ProjectAudioIO::Get( *mProject ).IsAudioActive()) { auto gAudioIO = AudioIO::Get(); UpdateMeters( gAudioIO->GetStreamTime(), (ProjectAudioManager::Get( *mProject ).GetLastPlayMode() == PlayMode::loopedPlay) ); } // Let other listeners get the notification event.Skip(); } void MixerBoard::OnTrackChanged(TrackListEvent &evt) { evt.Skip(); auto pTrack = evt.mpTrack.lock(); auto pPlayable = dynamic_cast( pTrack.get() ); if ( pPlayable ) { MixerTrackCluster *pMixerTrackCluster; FindMixerTrackCluster( pPlayable, &pMixerTrackCluster ); if ( pMixerTrackCluster ) pMixerTrackCluster->Refresh(); } } void MixerBoard::OnTrackSetChanged(wxEvent &evt) { evt.Skip(); mUpToDate = false; UpdateTrackClusters(); Refresh(); } void MixerBoard::OnStartStop(wxCommandEvent &evt) { evt.Skip(); bool start = evt.GetInt(); ResetMeters( start ); } // class MixerBoardFrame BEGIN_EVENT_TABLE(MixerBoardFrame, wxFrame) EVT_KEY_DOWN(MixerBoardFrame::OnKeyEvent) EVT_CLOSE(MixerBoardFrame::OnCloseWindow) EVT_MAXIMIZE(MixerBoardFrame::OnMaximize) EVT_SIZE(MixerBoardFrame::OnSize) END_EVENT_TABLE() // Default to fitting one track. const wxSize kDefaultSize = wxSize(MIXER_BOARD_MIN_WIDTH, MIXER_BOARD_MIN_HEIGHT); MixerBoardFrame::MixerBoardFrame(AudacityProject* parent) : wxFrame( &GetProjectFrame( *parent ), -1, wxString{}, wxDefaultPosition, kDefaultSize, wxDEFAULT_FRAME_STYLE | wxFRAME_FLOAT_ON_PARENT) , mProject(parent) { SetWindowTitle(); auto titleChanged = [&](wxCommandEvent &evt) { SetWindowTitle(); evt.Skip(); }; wxTheApp->Bind( EVT_PROJECT_TITLE_CHANGE, titleChanged ); mMixerBoard = safenew MixerBoard(parent, this, wxDefaultPosition, kDefaultSize); this->SetSizeHints(MIXER_BOARD_MIN_WIDTH, MIXER_BOARD_MIN_HEIGHT); mMixerBoard->UpdateTrackClusters(); // loads either the XPM or the windows resource, depending on the platform #if !defined(__WXMAC__) && !defined(__WXX11__) { #ifdef __WXMSW__ wxIcon ic{ wxICON(AudacityLogo) }; #else wxIcon ic{wxICON(AudacityLogo48x48)}; #endif SetIcon(ic); } #endif Center(); } MixerBoardFrame::~MixerBoardFrame() { } // event handlers void MixerBoardFrame::OnCloseWindow(wxCloseEvent &WXUNUSED(event)) { // Fix for bug #2175. // // If the mixerboard enters fullscreen, the main project will be // "lowered", so ensure it's visible after the mixerboard closes. #if defined(__WXMAC__) dynamic_cast(GetParent())->Raise(); #endif this->Hide(); } void MixerBoardFrame::OnMaximize(wxMaximizeEvent &event) { // Update the size hints to show all tracks before skipping to let default handling happen. mMixerBoard->UpdateWidth(); event.Skip(); } void MixerBoardFrame::OnSize(wxSizeEvent & WXUNUSED(event)) { mMixerBoard->SetSize(this->GetClientSize()); } void MixerBoardFrame::OnKeyEvent(wxKeyEvent & event) { AudacityProject *project = mMixerBoard->mProject; auto &commandManager = CommandManager::Get( *project ); commandManager.FilterKeyEvent(project, event, true); } void MixerBoardFrame::Recreate( AudacityProject *pProject ) { wxPoint pos = mMixerBoard->GetPosition(); wxSize siz = mMixerBoard->GetSize(); wxSize siz2 = this->GetSize(); //wxLogDebug("Got rid of board %p", mMixerBoard ); mMixerBoard->Destroy(); mMixerBoard = NULL; mMixerBoard = safenew MixerBoard(pProject, this, pos, siz); //wxLogDebug("Created NEW board %p", mMixerBoard ); mMixerBoard->UpdateTrackClusters(); mMixerBoard->SetSize( siz ); this->SetSize( siz2 ); SetWindowTitle(); } void MixerBoardFrame::SetWindowTitle() { wxString name = mProject->GetProjectName(); if (!name.empty()) { name.Prepend(wxT(" - ")); } SetTitle(AudacityMixerBoardTitle.Format(name).Translation()); } // Remaining code hooks this add-on into the application #include "commands/CommandContext.h" namespace { // Mixer board window attached to each project is built on demand by: AudacityProject::AttachedWindows::RegisteredFactory sMixerBoardKey{ []( AudacityProject &parent ) -> wxWeakRef< wxWindow > { return safenew MixerBoardFrame( &parent ); } }; // Define our extra menu item that invokes that factory struct Handler : CommandHandlerObject { void OnMixerBoard(const CommandContext &context) { auto &project = context.project; auto mixerBoardFrame = &project.AttachedWindows::Get( sMixerBoardKey ); mixerBoardFrame->Show(); mixerBoardFrame->Raise(); mixerBoardFrame->SetFocus(); } }; CommandHandlerObject &findCommandHandler(AudacityProject &) { // Handler is not stateful. Doesn't need a factory registered with // AudacityProject. static Handler instance; return instance; } // Register that menu item using namespace MenuTable; AttachedItem sAttachment{ wxT("View/Windows"), ( FinderScope{ findCommandHandler }, Command( wxT("MixerBoard"), XXO("&Mixer Board..."), &Handler::OnMixerBoard, PlayableTracksExistFlag()) ) }; }