/********************************************************************** Audacity: A Digital Audio Editor DeviceToolBar.cpp Dominic Mazzoni *******************************************************************//*! \class DeviceToolBar \brief A toobar to allow easier changing of input and output devices . *//*******************************************************************/ #include "../Audacity.h" // For compilers that support precompilation, includes "wx/wx.h". #include #ifndef WX_PRECOMP #include #include #include #include #include #include #include #endif #include "../AudacityApp.h" #include "DeviceToolBar.h" #include "ToolDock.h" #include "../AColor.h" #include "../AllThemeResources.h" #include "../AudioIO.h" #include "../ImageManipulation.h" #include "../Prefs.h" #include "../Project.h" #include "../Theme.h" #include "../widgets/Grabber.h" #include "../DeviceManager.h" IMPLEMENT_CLASS(DeviceToolBar, ToolBar); //////////////////////////////////////////////////////////// /// Methods for DeviceToolBar //////////////////////////////////////////////////////////// BEGIN_EVENT_TABLE(DeviceToolBar, ToolBar) EVT_CHOICE(wxID_ANY, DeviceToolBar::OnChoice) EVT_COMMAND(wxID_ANY, EVT_CAPTURE_KEY, DeviceToolBar::OnCaptureKey) END_EVENT_TABLE() //Standard contructor DeviceToolBar::DeviceToolBar() : ToolBar(DeviceBarID, _("Device"), wxT("Device"), true) { } DeviceToolBar::~DeviceToolBar() { delete mPlayBitmap; delete mRecordBitmap; } void DeviceToolBar::Create(wxWindow *parent) { ToolBar::Create(parent); // Simulate a size event to set initial meter placement/size wxSizeEvent dummy; OnSize(dummy); } void DeviceToolBar::RecreateTipWindows() { } static wxString MakeDeviceSourceString(DeviceSourceMap *map) { wxString ret; ret = map->deviceString; if (map->totalSources > 1) ret += wxString(": ", wxConvLocal) + map->sourceString; return ret; } void DeviceToolBar::DeinitChildren() { mPlayBitmap = NULL; mRecordBitmap = NULL; mInput = NULL; mOutput = NULL; mInputChannels = NULL; mHost = NULL; } void DeviceToolBar::Populate() { DeinitChildren(); // Hosts mHost = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); mHost->SetName(_("Audio Host")); Add(mHost, 0, wxALIGN_CENTER); // Output device mPlayBitmap = new wxBitmap(theTheme.Bitmap(bmpSpeaker)); Add(new wxStaticBitmap(this, wxID_ANY, *mPlayBitmap), 0, wxALIGN_CENTER); mOutput = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); mOutput->SetName(_("Output Device")); Add(mOutput, 0, wxALIGN_CENTER); // Input device mRecordBitmap = new wxBitmap(theTheme.Bitmap(bmpMic)); Add(new wxStaticBitmap(this, wxID_ANY, *mRecordBitmap), 0, wxALIGN_CENTER); mInput = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); mInput->SetName(_("Input Device")); Add(mInput, 0, wxALIGN_CENTER); mInputChannels = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); mInputChannels->SetName(_("Input Channels")); Add(mInputChannels, 0, wxALIGN_CENTER); mHost->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(DeviceToolBar::OnFocus), NULL, this); mHost->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(DeviceToolBar::OnFocus), NULL, this); mOutput->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(DeviceToolBar::OnFocus), NULL, this); mOutput->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(DeviceToolBar::OnFocus), NULL, this); mInput->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(DeviceToolBar::OnFocus), NULL, this); mInput->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(DeviceToolBar::OnFocus), NULL, this); mInputChannels->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(DeviceToolBar::OnFocus), NULL, this); mInputChannels->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(DeviceToolBar::OnFocus), NULL, this); RefillCombos(); } void DeviceToolBar::RefillCombos() { FillHosts(); FillHostDevices(); FillInputChannels(); // make the device display selection reflect the prefs if they exist UpdatePrefs(); } void DeviceToolBar::OnFocus(wxFocusEvent &event) { wxCommandEvent e(EVT_CAPTURE_KEYBOARD); if (event.GetEventType() == wxEVT_KILL_FOCUS) { e.SetEventType(EVT_RELEASE_KEYBOARD); } e.SetEventObject(this); GetParent()->GetEventHandler()->ProcessEvent(e); Refresh(false); event.Skip(); } void DeviceToolBar::OnCaptureKey(wxCommandEvent &event) { wxKeyEvent *kevent = (wxKeyEvent *)event.GetEventObject(); int keyCode = kevent->GetKeyCode(); // Pass UP/DOWN/LEFT/RIGHT through for input/output choice if (FindFocus() == mOutput && (keyCode == WXK_LEFT || keyCode == WXK_RIGHT || keyCode == WXK_UP || keyCode == WXK_DOWN)) { return; } if (FindFocus() == mInput && (keyCode == WXK_LEFT || keyCode == WXK_RIGHT || keyCode == WXK_UP || keyCode == WXK_DOWN)) { return; } event.Skip(); return; } void DeviceToolBar::UpdatePrefs() { wxString hostName; wxString devName; wxString sourceName; wxString desc; std::vector &inMaps = DeviceManager::Instance()->GetInputDeviceMaps(); std::vector &outMaps = DeviceManager::Instance()->GetOutputDeviceMaps(); int hostSelectionIndex = mHost->GetSelection(); wxString oldHost = hostSelectionIndex >= 0 ? mHost->GetString(hostSelectionIndex) : wxT(""); hostName = gPrefs->Read(wxT("/AudioIO/Host"), wxT("")); // if the prefs host name doesn't match the one displayed, it changed // in another project's DeviceToolBar, so we need to repopulate everything. if (oldHost != hostName) FillHostDevices(); devName = gPrefs->Read(wxT("/AudioIO/RecordingDevice"), wxT("")); sourceName = gPrefs->Read(wxT("/AudioIO/RecordingSource"), wxT("")); if (sourceName == wxT("")) desc = devName; else desc = devName + wxString(": ", wxConvLocal) + sourceName; if (mInput->GetStringSelection() != desc && mInput->FindString(desc) != wxNOT_FOUND) { mInput->SetStringSelection(desc); FillInputChannels(); } else if (mInput->GetStringSelection() != desc && mInput->GetCount()) { //use the 0th index if we have no familiar devices mInput->SetSelection(0); for (size_t i = 0; i < inMaps.size(); i++) { if (inMaps[i].hostString == hostName && MakeDeviceSourceString(&inMaps[i]) == mInput->GetString(0)) { SetDevices(&inMaps[i], NULL); break; } } } devName = gPrefs->Read(wxT("/AudioIO/PlaybackDevice"), wxT("")); sourceName = gPrefs->Read(wxT("/AudioIO/PlaybackSource"), wxT("")); if (sourceName == wxT("")) desc = devName; else desc = devName + wxString(": ", wxConvLocal) + sourceName; mOutput->SetStringSelection(desc); if (mOutput->GetStringSelection() != desc && mOutput->FindString(desc) != wxNOT_FOUND) { mOutput->SetStringSelection(desc); } else if (mOutput->GetStringSelection() != desc && mOutput->GetCount()) { //use the 0th index if we have no familiar devices mOutput->SetSelection(0); for (size_t i = 0; i < outMaps.size(); i++) { if (outMaps[i].hostString == hostName && MakeDeviceSourceString(&outMaps[i]) == mOutput->GetString(0)) { SetDevices(NULL, &outMaps[i]); break; } } } long oldChannels = 1, newChannels; oldChannels = mInputChannels->GetSelection() + 1; gPrefs->Read(wxT("/AudioIO/RecordChannels"), &newChannels, 0); if (newChannels > 0 && oldChannels != newChannels) mInputChannels->SetSelection(newChannels - 1); mHost->SetStringSelection(hostName); RegenerateTooltips(); // Set label to pull in language change SetLabel(_("Device")); // Give base class a chance ToolBar::UpdatePrefs(); Layout(); Refresh(); } void DeviceToolBar::EnableDisableButtons() { if (gAudioIO) { // we allow changes when monitoring, but not when recording bool audioStreamActive = gAudioIO->IsStreamActive() && !gAudioIO->IsMonitoring(); mHost->Enable(!audioStreamActive); mInput->Enable(!audioStreamActive); mOutput->Enable(!audioStreamActive); mInputChannels->Enable(!audioStreamActive); } } void DeviceToolBar::RegenerateTooltips() { #if wxUSE_TOOLTIPS mOutput->SetToolTip(_("Output Device")); mInput->SetToolTip(_("Input Device")); mHost->SetToolTip(_("Audio Host")); mInputChannels->SetToolTip(_("Input Channels")); #endif } bool DeviceToolBar::Layout() { bool ret; RepositionCombos(); ret = ToolBar::Layout(); return ret; } // returns true if the combo is constrained and false otherwise // @param toolbarWidth the width of the toolbar in pixels // @param ratio an in/out for the desired and resultant width ratio. // @param flex the amount of extra space allowed to have past the available. // the amount used is subtracted. static bool RepositionCombo(wxWindow *combo, int toolbarWidth, wxSize desiredSize, float &ratio, float &flex, int marginPixels, bool changesRatio) { float ratioChange; bool constrained = false; // push margin pixels desiredSize.x += marginPixels; // truncate the window size if necessary if (desiredSize.x > toolbarWidth * (flex + ratio)) { constrained = true; desiredSize.SetWidth(toolbarWidth * (flex + ratio)); if (desiredSize.GetWidth() - marginPixels < 0) desiredSize.SetWidth(marginPixels); } // keep track of how much space gained or lost so it can be used by other combos. if (changesRatio) { ratioChange = (desiredSize.x / ((float) toolbarWidth)) - ratio; ratio += ratioChange; flex -= ratioChange; } // pop the margin pixels desiredSize.x -= marginPixels; combo->SetMinSize(desiredSize); combo->SetMaxSize(desiredSize); return constrained; } //These don't add up to 1 because there is a bit of margin that we allow //the layout sizer to handle. #define kHostWidthRatio 0.13f #define kInputWidthRatio 0.32f #define kOutputWidthRatio 0.32f #define kChannelsWidthRatio 0.18f void DeviceToolBar::RepositionCombos() { int w, h, dockw, dockh; float ratioUnused; bool constrained = true; wxWindow *window; wxSize desiredInput, desiredOutput, desiredHost, desiredChannels; float hostRatio, outputRatio, inputRatio, channelsRatio; // if the toolbar is docked then the width we should use is the project width. // as the toolbar's with can extend past this. GetClientSize(&w, &h); if (IsDocked()) { // If the toolbar is docked its width can be larger than what is actually viewable // So take the min. We don't need to worry about having another toolbar to the left off us // because if we are larger than the dock size we always get our own row. // and if smaller then we don't use the dock size (because we take the min). window = GetDock(); window->GetClientSize(&dockw, &dockh); if (dockw < w) w = dockw; } // subtract the main grabber on the left and the resizer as well w -= grabberWidth + GetResizeGrabberWidth(); if (w <= 0) return; // set up initial sizes and ratios hostRatio = kHostWidthRatio; inputRatio = kInputWidthRatio; outputRatio = kOutputWidthRatio; channelsRatio = kChannelsWidthRatio; desiredHost = mHost->GetBestSize(); desiredInput = mInput->GetBestSize(); desiredOutput = mOutput->GetBestSize(); desiredChannels = mInputChannels->GetBestSize(); // wxGtk has larger comboboxes than the other platforms. For DeviceToolBar this will cause // the height to be double because of the discrete grid layout. So we shrink it to prevent this. #ifdef __WXGTK__ desiredHost.SetHeight(desiredHost.GetHeight() -4); desiredInput.SetHeight(desiredHost.GetHeight()); desiredOutput.SetHeight(desiredHost.GetHeight()); desiredChannels.SetHeight(desiredHost.GetHeight()); #endif ratioUnused = 0.995f - (kHostWidthRatio + kInputWidthRatio + kOutputWidthRatio + kChannelsWidthRatio); int i = 0; // limit the amount of times we solve contraints to 5 while (constrained && ratioUnused > 0.01f && i < 5) { i++; constrained = false; constrained = RepositionCombo(mHost, w, desiredHost, hostRatio, ratioUnused, 0, true) || constrained; constrained = RepositionCombo(mInput, w, desiredInput, inputRatio, ratioUnused, mRecordBitmap->GetWidth(), true) || constrained; constrained = RepositionCombo(mOutput, w, desiredOutput, outputRatio, ratioUnused, mPlayBitmap->GetWidth(), true) || constrained; constrained = RepositionCombo(mInputChannels, w, desiredChannels, channelsRatio, ratioUnused, 0, true) || constrained; } Update(); } void DeviceToolBar::FillHosts() { wxArrayString hosts; size_t i; std::vector &inMaps = DeviceManager::Instance()->GetInputDeviceMaps(); std::vector &outMaps = DeviceManager::Instance()->GetOutputDeviceMaps(); // go over our lists add the host to the list if it isn't there yet for (i = 0; i < inMaps.size(); i++) if (hosts.Index(inMaps[i].hostString) == wxNOT_FOUND) hosts.Add(inMaps[i].hostString); for (i = 0; i < outMaps.size(); i++) if (hosts.Index(outMaps[i].hostString) == wxNOT_FOUND) hosts.Add(outMaps[i].hostString); mHost->Clear(); mHost->Append(hosts); if (hosts.GetCount() == 0) mHost->Enable(false); } void DeviceToolBar::FillHostDevices() { std::vector &inMaps = DeviceManager::Instance()->GetInputDeviceMaps(); std::vector &outMaps = DeviceManager::Instance()->GetOutputDeviceMaps(); //read what is in the prefs wxString host = gPrefs->Read(wxT("/AudioIO/Host"), wxT("")); size_t i; int foundHostIndex = -1; // if the host is not in the hosts combo then we rescanned. // set it to blank so we search for another host. if (mHost->FindString(host) == wxNOT_FOUND) host = wxT(""); for (i = 0; i < outMaps.size(); i++) { if (outMaps[i].hostString == host) { foundHostIndex = outMaps[i].hostIndex; break; } } if (foundHostIndex == -1) { for (i = 0; i < inMaps.size(); i++) { if (inMaps[i].hostString == host) { foundHostIndex = inMaps[i].hostIndex; break; } } } // If no host was found based on the prefs device host, load the first available one if (foundHostIndex == -1) { if (outMaps.size()) foundHostIndex = outMaps[0].hostIndex; else if (inMaps.size()) foundHostIndex = inMaps[0].hostIndex; } // If we still have no host it means no devices, in which case do nothing. if (foundHostIndex == -1) return; // Repopulate the Input/Output device list available to the user mInput->Clear(); for (i = 0; i < inMaps.size(); i++) { if (foundHostIndex == inMaps[i].hostIndex) { mInput->Append(MakeDeviceSourceString(&inMaps[i])); if (host == wxT("")) { host = inMaps[i].hostString; gPrefs->Write(wxT("/AudioIO/Host"), host); mHost->SetStringSelection(host); } } } mInput->Enable(mInput->GetCount() ? true : false); mOutput->Clear(); for (i = 0; i < outMaps.size(); i++) { if (foundHostIndex == outMaps[i].hostIndex) { mOutput->Append(MakeDeviceSourceString(&outMaps[i])); if (host == wxT("")) { host = outMaps[i].hostString; gPrefs->Write(wxT("/AudioIO/Host"), host); mHost->SetStringSelection(host); } } } mOutput->Enable(mOutput->GetCount() ? true : false); // The setting of the Device is left up to OnChoice } //return 1 if host changed, 0 otherwise. int DeviceToolBar::ChangeHost() { int hostSelectionIndex; hostSelectionIndex = mHost->GetSelection(); wxString oldHost = gPrefs->Read(wxT("/AudioIO/Host"), wxT("")); wxString newHost = hostSelectionIndex >= 0 ? mHost->GetString(hostSelectionIndex) : oldHost; if (oldHost == newHost) return 0; //change the host and switch to correct devices. gPrefs->Write(wxT("/AudioIO/Host"), newHost); // populate the devices FillHostDevices(); return 1; } void DeviceToolBar::FillInputChannels() { std::vector &inMaps = DeviceManager::Instance()->GetInputDeviceMaps(); wxString host = gPrefs->Read(wxT("/AudioIO/Host"), wxT("")); wxString device = gPrefs->Read(wxT("/AudioIO/RecordingDevice"), wxT("")); wxString source = gPrefs->Read(wxT("/AudioIO/RecordingSource"), wxT("")); long oldChannels = 1, newChannels; gPrefs->Read(wxT("/AudioIO/RecordChannels"), &oldChannels); int index = -1; size_t i, j; mInputChannels->Clear(); for (i = 0; i < inMaps.size(); i++) { if (source == inMaps[i].sourceString && device == inMaps[i].deviceString && host == inMaps[i].hostString) { // add one selection for each channel of this source for (j = 0; j < (unsigned int) inMaps[i].numChannels; j++) { wxString name; if (j == 0) { name = _("1 (Mono) Input Channel"); } else if (j == 1) { name = _("2 (Stereo) Input Channels"); } else { name = wxString::Format(wxT("%d"), j + 1); } mInputChannels->Append(name); } newChannels = inMaps[i].numChannels; if (oldChannels < newChannels && oldChannels >= 1) newChannels = oldChannels; if (newChannels >= 1) mInputChannels->SetSelection(newChannels - 1); gPrefs->Write(wxT("/AudioIO/RecordChannels"), newChannels); mInputChannels->Enable(mInputChannels->GetCount() ? true : false); index = i; break; } } if (index == -1) mInputChannels->Enable(false); } void DeviceToolBar::SetDevices(DeviceSourceMap *in, DeviceSourceMap *out) { if (in) { gPrefs->Write(wxT("/AudioIO/RecordingDevice"), in->deviceString); gPrefs->Write(wxT("/AudioIO/RecordingSourceIndex"), in->sourceIndex); if (in->totalSources >= 1) { gPrefs->Write(wxT("/AudioIO/RecordingSource"), in->sourceString); } else { gPrefs->Write(wxT("/AudioIO/RecordingSource"), wxT("")); } FillInputChannels(); } if (out) { gPrefs->Write(wxT("/AudioIO/PlaybackDevice"), out->deviceString); if (out->totalSources >= 1) { gPrefs->Write(wxT("/AudioIO/PlaybackSource"), out->sourceString); } else { gPrefs->Write(wxT("/AudioIO/PlaybackSource"), wxT("")); } } } void DeviceToolBar::ChangeDevice(bool isInput) { int newIndex = -1; wxChoice *combo = isInput ? mInput :mOutput; size_t i; int selectionIndex = mInput->GetSelection(); std::vector &maps = isInput ? DeviceManager::Instance()->GetInputDeviceMaps() : DeviceManager::Instance()->GetOutputDeviceMaps(); wxString host = gPrefs->Read(wxT("/AudioIO/Host"), wxT("")); // Find device indices for input and output if (selectionIndex >= 0 ) { wxString newDevice = combo->GetStringSelection(); for (i = 0; i < maps.size(); ++i) { wxString name; name = MakeDeviceSourceString(&maps[i]); if (name == newDevice && maps[i].hostString == host) { newIndex = i; } } } if (newIndex < 0) { wxLogDebug(wxT("DeviceToolBar::OnChoice(): couldn't find device indices")); return; } SetDevices(isInput ? &maps[newIndex] : NULL, isInput ? NULL : &maps[newIndex]); } void DeviceToolBar::OnChoice(wxCommandEvent &event) { wxObject *eventObject = event.GetEventObject(); //if we've changed hosts, we've handled the device switching already. if (eventObject == mHost) { ChangeHost(); } else if (eventObject == mInputChannels) { int channelsSelectionIndex = mInputChannels->GetSelection(); if (channelsSelectionIndex >= 0) gPrefs->Write(wxT("/AudioIO/RecordChannels"),channelsSelectionIndex + 1); } else if (eventObject == mInput) { ChangeDevice(true); } else if (eventObject == mOutput) { ChangeDevice(false); } if (gAudioIO) { // We cannot have gotten here if gAudioIO->IsAudioTokenActive(), // per the setting of AudioIONotBusyFlag and AudioIOBusyFlag in // AudacityProject::GetUpdateFlags(). // However, we can have an invalid audio token (so IsAudioTokenActive() // is false), but be monitoring. // If monitoring, have to stop the stream, so HandleDeviceChange() can work. // We could disable the Preferences command while monitoring, i.e., // set AudioIONotBusyFlag/AudioIOBusyFlag according to monitoring, as well. // Instead allow it because unlike recording, for example, monitoring // is not clearly something that should prohibit changing device. // TO-DO: We *could* be smarter in this method and call HandleDeviceChange() // only when the device choices actually changed. True of lots of prefs! // As is, we always stop monitoring before handling the device change. if (gAudioIO->IsMonitoring()) { gAudioIO->StopStream(); while (gAudioIO->IsBusy()) wxMilliSleep(100); } gAudioIO->HandleDeviceChange(); } // Update all projects' DeviceToolBar. for (size_t i = 0; i < gAudacityProjects.GetCount(); i++) { gAudacityProjects[i]->GetDeviceToolBar()->UpdatePrefs(); } } void DeviceToolBar::ShowInputDialog() { ShowComboDialog(mInput, wxString(_("Select Input Device"))); } void DeviceToolBar::ShowOutputDialog() { ShowComboDialog(mOutput, wxString(_("Select Output Device"))); } void DeviceToolBar::ShowHostDialog() { ShowComboDialog(mHost, wxString(_("Select Audio Host"))); } void DeviceToolBar::ShowChannelsDialog() { ShowComboDialog(mInputChannels, wxString(_("Select Input Channels"))); } void DeviceToolBar::ShowComboDialog(wxChoice *combo, const wxString &title) { if (!combo || combo->GetCount() == 0) { wxMessageBox(_("Device information is not available.")); return; } #if USE_PORTMIXER wxArrayString inputSources = combo->GetStrings(); wxDialog dlg(NULL, wxID_ANY, title); ShuttleGui S(&dlg, eIsCreating); wxChoice *c; S.StartVerticalLay(true); { S.StartHorizontalLay(wxCENTER, false); { c = S.AddChoice(combo->GetName(), combo->GetStringSelection(), &inputSources); } S.EndHorizontalLay(); S.AddStandardButtons(); } S.EndVerticalLay(); dlg.SetSize(dlg.GetSizer()->GetMinSize()); dlg.Center(); if (dlg.ShowModal() == wxID_OK) { wxCommandEvent dummyEvent; dummyEvent.SetEventObject(combo); // SetSelection() doesn't send an event, so we call OnChoice explicitly combo->SetSelection(c->GetSelection()); OnChoice(dummyEvent); } #endif }