audacia/src/toolbars/ControlToolBar.cpp

1503 lines
44 KiB
C++
Raw Normal View History

/**********************************************************************
Audacity: A Digital Audio Editor
ControlToolBar.cpp
Dominic Mazzoni
Shane T. Mueller
James Crook
Leland Lucius
2014-06-03 20:30:19 +00:00
*******************************************************************//**
\class ControlToolBar
\brief A ToolBar that has the main Transport buttons.
2014-06-03 20:30:19 +00:00
In the GUI, this is referred to as "Transport Toolbar", as
it corresponds to commands in the Transport menu.
"Control Toolbar" is historic.
This class, which is a child of Toolbar, creates the
window containing the Transport (rewind/play/stop/record/ff)
buttons. The window can be embedded within a
normal project window, or within a ToolbarFrame that is
managed by a global ToolBarStub called
gControlToolBarStub.
All of the controls in this window were custom-written for
Audacity - they are not native controls on any platform -
however, it is intended that the images could be easily
replaced to allow "skinning" or just customization to
2014-06-03 20:30:19 +00:00
match the look and feel of each platform.
*//*******************************************************************/
#include <algorithm>
#include <cfloat>
#include "../Audacity.h"
#include "../Experimental.h"
2015-07-03 04:20:21 +00:00
#include "ControlToolBar.h"
// For compilers that support precompilation, includes "wx/wx.h".
#include <wx/wxprec.h>
#ifndef WX_PRECOMP
#include <wx/app.h>
#include <wx/dc.h>
#include <wx/event.h>
#include <wx/image.h>
#include <wx/intl.h>
#include <wx/statusbr.h>
#include <wx/timer.h>
#endif
#include <wx/tooltip.h>
#include <wx/datetime.h>
#include "TranscriptionToolBar.h"
#include "MeterToolBar.h"
#include "../AColor.h"
#include "../AdornedRulerPanel.h"
#include "../AllThemeResources.h"
#include "../AudioIO.h"
#include "../ImageManipulation.h"
#include "../Menus.h"
#include "../Prefs.h"
#include "../Project.h"
#include "../Theme.h"
2015-07-03 04:20:21 +00:00
#include "../WaveTrack.h"
#include "../widgets/AButton.h"
#include "../widgets/Meter.h"
#include "../widgets/LinkingHtmlWindow.h"
#include "../widgets/ErrorDialog.h"
#include "../FileNames.h"
#include "../tracks/ui/Scrubbing.h"
#include "../prefs/TracksPrefs.h"
#include "../toolbars/ToolManager.h"
#include "../TrackPanel.h"
IMPLEMENT_CLASS(ControlToolBar, ToolBar);
//static
AudacityProject *ControlToolBar::mBusyProject = NULL;
////////////////////////////////////////////////////////////
/// Methods for ControlToolBar
////////////////////////////////////////////////////////////
BEGIN_EVENT_TABLE(ControlToolBar, ToolBar)
EVT_CHAR(ControlToolBar::OnKeyEvent)
EVT_BUTTON(ID_PLAY_BUTTON, ControlToolBar::OnPlay)
EVT_BUTTON(ID_STOP_BUTTON, ControlToolBar::OnStop)
EVT_BUTTON(ID_RECORD_BUTTON, ControlToolBar::OnRecord)
EVT_BUTTON(ID_REW_BUTTON, ControlToolBar::OnRewind)
EVT_BUTTON(ID_FF_BUTTON, ControlToolBar::OnFF)
EVT_BUTTON(ID_PAUSE_BUTTON, ControlToolBar::OnPause)
END_EVENT_TABLE()
//Standard constructor
// This was called "Control" toolbar in the GUI before - now it is "Transport".
// Note that we use the legacy "Control" string as the section because this
// gets written to prefs and cannot be changed in prefs to maintain backwards
// compatibility
ControlToolBar::ControlToolBar()
: ToolBar(TransportBarID, _("Transport"), wxT("Control"))
{
mPaused = false;
gPrefs->Read(wxT("/GUI/ErgonomicTransportButtons"), &mErgonomicTransportButtons, true);
mStrLocale = gPrefs->Read(wxT("/Locale/Language"), wxT(""));
mSizer = NULL;
/* i18n-hint: These are strings for the status bar, and indicate whether Audacity
is playing or recording or stopped, and whether it is paused. */
mStatePlay = XO("Playing");
mStateStop = XO("Stopped");
mStateRecord = XO("Recording");
mStatePause = XO("Paused");
}
ControlToolBar::~ControlToolBar()
{
}
void ControlToolBar::Create(wxWindow * parent)
{
ToolBar::Create(parent);
}
// This is a convenience function that allows for button creation in
// MakeButtons() with fewer arguments
AButton *ControlToolBar::MakeButton(ControlToolBar *pBar,
teBmps eEnabledUp, teBmps eEnabledDown, teBmps eDisabled,
int id,
bool processdownevents,
const wxChar *label)
{
AButton *r = ToolBar::MakeButton(pBar,
bmpRecoloredUpLarge, bmpRecoloredDownLarge, bmpRecoloredUpHiliteLarge, bmpRecoloredHiliteLarge,
eEnabledUp, eEnabledDown, eDisabled,
wxWindowID(id),
wxDefaultPosition, processdownevents,
theTheme.ImageSize( bmpRecoloredUpLarge ));
r->SetLabel(label);
enum { deflation =
#ifdef __WXMAC__
6
#else
12
#endif
};
r->SetFocusRect( r->GetClientRect().Deflate( deflation, deflation ) );
return r;
}
// static
void ControlToolBar::MakeAlternateImages(AButton &button, int idx,
teBmps eEnabledUp,
teBmps eEnabledDown,
teBmps eDisabled)
{
ToolBar::MakeAlternateImages(button, idx,
bmpRecoloredUpLarge, bmpRecoloredDownLarge, bmpRecoloredUpHiliteLarge, bmpRecoloredHiliteLarge,
eEnabledUp, eEnabledDown, eDisabled,
theTheme.ImageSize( bmpRecoloredUpLarge ));
}
void ControlToolBar::Populate()
{
SetBackgroundColour( theTheme.Colour( clrMedium ) );
MakeButtonBackgroundsLarge();
mPause = MakeButton(this, bmpPause, bmpPause, bmpPauseDisabled,
ID_PAUSE_BUTTON, true, _("Pause"));
mPlay = MakeButton(this, bmpPlay, bmpPlay, bmpPlayDisabled,
ID_PLAY_BUTTON, true, _("Play"));
MakeAlternateImages(*mPlay, 1, bmpLoop, bmpLoop, bmpLoopDisabled);
MakeAlternateImages(*mPlay, 2,
bmpCutPreview, bmpCutPreview, bmpCutPreviewDisabled);
MakeAlternateImages(*mPlay, 3,
bmpScrub, bmpScrub, bmpScrubDisabled);
MakeAlternateImages(*mPlay, 4,
bmpSeek, bmpSeek, bmpSeekDisabled);
mPlay->FollowModifierKeys();
mStop = MakeButton(this, bmpStop, bmpStop, bmpStopDisabled ,
ID_STOP_BUTTON, false, _("Stop"));
mRewind = MakeButton(this, bmpRewind, bmpRewind, bmpRewindDisabled,
ID_REW_BUTTON, false, _("Skip to Start"));
mFF = MakeButton(this, bmpFFwd, bmpFFwd, bmpFFwdDisabled,
ID_FF_BUTTON, false, _("Skip to End"));
mRecord = MakeButton(this, bmpRecord, bmpRecord, bmpRecordDisabled,
ID_RECORD_BUTTON, false, _("Record"));
bool bPreferNewTrack;
gPrefs->Read("/GUI/PreferNewTrackRecord",&bPreferNewTrack, false);
if( !bPreferNewTrack )
MakeAlternateImages(*mRecord, 1, bmpRecordBelow, bmpRecordBelow,
bmpRecordBelowDisabled);
else
MakeAlternateImages(*mRecord, 1, bmpRecordBeside, bmpRecordBeside,
bmpRecordBesideDisabled);
mRecord->FollowModifierKeys();
#if wxUSE_TOOLTIPS
RegenerateTooltips();
2014-06-03 20:30:19 +00:00
wxToolTip::Enable(true);
wxToolTip::SetDelay(1000);
#endif
// Set default order and mode
ArrangeButtons();
}
void ControlToolBar::RegenerateTooltips()
{
#if wxUSE_TOOLTIPS
for (long iWinID = ID_PLAY_BUTTON; iWinID < BUTTON_COUNT; iWinID++)
{
auto pCtrl = static_cast<AButton*>(this->FindWindow(iWinID));
const wxChar *name = nullptr;
switch (iWinID)
{
case ID_PLAY_BUTTON:
2017-04-02 22:07:13 +00:00
// Without shift
name = wxT("PlayStop");
break;
case ID_RECORD_BUTTON:
2017-04-02 22:07:13 +00:00
// Without shift
//name = wxT("Record");
name = wxT("Record1stChoice");
break;
case ID_PAUSE_BUTTON:
name = wxT("Pause");
break;
case ID_STOP_BUTTON:
name = wxT("Stop");
break;
case ID_FF_BUTTON:
name = wxT("CursProjectEnd");
break;
case ID_REW_BUTTON:
name = wxT("CursProjectStart");
break;
}
std::vector<TranslatedInternalString> commands(
1u, { name, pCtrl->GetLabel() } );
// Some have a second
switch (iWinID)
{
case ID_PLAY_BUTTON:
// With shift
commands.push_back( { wxT("PlayLooped"), _("Loop Play") } );
break;
case ID_RECORD_BUTTON:
// With shift
{ bool bPreferNewTrack;
gPrefs->Read("/GUI/PreferNewTrackRecord",&bPreferNewTrack, false);
// For the shortcut tooltip.
commands.push_back( {
wxT("Record2ndChoice"),
!bPreferNewTrack
? _("Record New Track")
: _("Append Record")
} );
}
break;
case ID_PAUSE_BUTTON:
break;
case ID_STOP_BUTTON:
break;
case ID_FF_BUTTON:
// With shift
commands.push_back( {
wxT("SelEnd"), _("Select to End") } );
break;
case ID_REW_BUTTON:
// With shift
commands.push_back( {
wxT("SelStart"), _("Select to Start") } );
break;
}
ToolBar::SetButtonToolTip(*pCtrl, commands.data(), commands.size());
}
#endif
}
void ControlToolBar::UpdatePrefs()
{
bool updated = false;
bool active;
gPrefs->Read( wxT("/GUI/ErgonomicTransportButtons"), &active, true );
if( mErgonomicTransportButtons != active )
{
mErgonomicTransportButtons = active;
updated = true;
}
wxString strLocale = gPrefs->Read(wxT("/Locale/Language"), wxT(""));
if (mStrLocale != strLocale)
{
mStrLocale = strLocale;
updated = true;
}
if( updated )
{
ReCreateButtons(); // side effect: calls RegenerateTooltips()
Updated();
}
else
2014-06-03 20:30:19 +00:00
// The other reason to regenerate tooltips is if keyboard shortcuts for
// transport buttons changed, but that's too much work to check for, so just
// always do it. (Much cheaper than calling ReCreateButtons() in all cases.
RegenerateTooltips();
// Set label to pull in language change
SetLabel(_("Transport"));
// Give base class a chance
ToolBar::UpdatePrefs();
}
void ControlToolBar::ArrangeButtons()
{
int flags = wxALIGN_CENTER | wxRIGHT;
// (Re)allocate the button sizer
if( mSizer )
{
Detach( mSizer );
std::unique_ptr < wxSizer > {mSizer}; // DELETE it
}
Add((mSizer = safenew wxBoxSizer(wxHORIZONTAL)), 1, wxEXPAND);
// Start with a little extra space
mSizer->Add( 5, 55 );
// Add the buttons in order based on ergonomic setting
if( mErgonomicTransportButtons )
{
mPause->MoveBeforeInTabOrder( mRecord );
mPlay->MoveBeforeInTabOrder( mRecord );
mStop->MoveBeforeInTabOrder( mRecord );
mRewind->MoveBeforeInTabOrder( mRecord );
mFF->MoveBeforeInTabOrder( mRecord );
mSizer->Add( mPause, 0, flags, 2 );
mSizer->Add( mPlay, 0, flags, 2 );
mSizer->Add( mStop, 0, flags, 2 );
mSizer->Add( mRewind, 0, flags, 2 );
mSizer->Add( mFF, 0, flags, 10 );
mSizer->Add( mRecord, 0, flags, 5 );
}
else
{
mRewind->MoveBeforeInTabOrder( mFF );
mPlay->MoveBeforeInTabOrder( mFF );
mRecord->MoveBeforeInTabOrder( mFF );
mPause->MoveBeforeInTabOrder( mFF );
mStop->MoveBeforeInTabOrder( mFF );
mSizer->Add( mRewind, 0, flags, 2 );
mSizer->Add( mPlay, 0, flags, 2 );
mSizer->Add( mRecord, 0, flags, 2 );
mSizer->Add( mPause, 0, flags, 2 );
mSizer->Add( mStop, 0, flags, 2 );
mSizer->Add( mFF, 0, flags, 5 );
}
// Layout the sizer
mSizer->Layout();
// Layout the toolbar
Layout();
// (Re)Establish the minimum size
SetMinSize( GetSizer()->GetMinSize() );
}
void ControlToolBar::ReCreateButtons()
{
bool playDown = false;
bool playShift = false;
bool pauseDown = false;
bool recordDown = false;
bool recordShift = false;
// ToolBar::ReCreateButtons() will get rid of its sizer and
// since we've attached our sizer to it, ours will get deleted too
// so clean ours up first.
if( mSizer )
{
playDown = mPlay->IsDown();
playShift = mPlay->WasShiftDown();
pauseDown = mPause->IsDown();
recordDown = mRecord->IsDown();
recordShift = mRecord->WasShiftDown();
Detach( mSizer );
std::unique_ptr < wxSizer > {mSizer}; // DELETE it
mSizer = NULL;
}
ToolBar::ReCreateButtons();
if (playDown)
{
ControlToolBar::PlayAppearance appearance =
playShift ? ControlToolBar::PlayAppearance::Looped
: ControlToolBar::PlayAppearance::Straight;
SetPlay(playDown, appearance);
}
if (pauseDown)
{
mPause->PushDown();
}
if (recordDown)
{
SetRecord(recordDown, recordShift);
}
EnableDisableButtons();
RegenerateTooltips();
}
void ControlToolBar::Repaint( wxDC *dc )
{
#ifndef USE_AQUA_THEME
wxSize s = mSizer->GetSize();
wxPoint p = mSizer->GetPosition();
wxRect bevelRect( p.x, p.y, s.GetWidth() - 1, s.GetHeight() - 1 );
AColor::Bevel( *dc, true, bevelRect );
#endif
}
void ControlToolBar::EnableDisableButtons()
{
AudacityProject *p = GetActiveProject();
2016-03-28 20:04:49 +00:00
bool paused = mPause->IsDown();
bool playing = mPlay->IsDown();
bool recording = mRecord->IsDown();
bool busy = gAudioIO->IsBusy();
// Only interested in audio type tracks
bool tracks = p && p->GetTracks()->Any<AudioTrack>(); // PRL: PlayableTrack ?
2016-03-28 20:04:49 +00:00
if (p) {
TranscriptionToolBar *const playAtSpeedTB = p->GetTranscriptionToolBar();
if (playAtSpeedTB)
playAtSpeedTB->SetEnabled(CanStopAudioStream() && tracks && !recording);
}
2016-03-28 20:04:49 +00:00
mPlay->SetEnabled(CanStopAudioStream() && tracks && !recording);
mRecord->SetEnabled(
CanStopAudioStream() &&
!(busy && !recording && !paused) &&
!(playing && !paused)
);
2016-03-28 20:04:49 +00:00
mStop->SetEnabled(CanStopAudioStream() && (playing || recording));
mRewind->SetEnabled(IsPauseDown() || (!playing && !recording));
mFF->SetEnabled(tracks && (IsPauseDown() || (!playing && !recording)));
//auto pProject = GetActiveProject();
mPause->SetEnabled(CanStopAudioStream());
}
void ControlToolBar::SetPlay(bool down, PlayAppearance appearance)
{
if (down) {
mPlay->SetShift(appearance == PlayAppearance::Looped);
mPlay->SetControl(appearance == PlayAppearance::CutPreview);
mPlay->SetAlternateIdx(static_cast<int>(appearance));
mPlay->PushDown();
}
else {
mPlay->PopUp();
mPlay->SetAlternateIdx(0);
}
EnableDisableButtons();
UpdateStatusBar(GetActiveProject());
}
void ControlToolBar::SetStop(bool down)
{
if (down)
mStop->PushDown();
else {
if(FindFocus() == mStop)
mPlay->SetFocus();
mStop->PopUp();
}
EnableDisableButtons();
}
2018-06-09 19:09:51 +00:00
void ControlToolBar::SetRecord(bool down, bool altAppearance)
{
if (down)
{
2018-06-09 19:09:51 +00:00
mRecord->SetAlternateIdx(altAppearance ? 1 : 0);
mRecord->PushDown();
}
else
{
mRecord->SetAlternateIdx(0);
mRecord->PopUp();
}
EnableDisableButtons();
}
bool ControlToolBar::IsPauseDown() const
{
return mPause->IsDown();
}
bool ControlToolBar::IsRecordDown() const
{
return mRecord->IsDown();
}
int ControlToolBar::PlayPlayRegion(const SelectedRegion &selectedRegion,
const AudioIOStartStreamOptions &options,
PlayMode mode,
PlayAppearance appearance, /* = PlayOption::Straight */
bool backwards, /* = false */
bool playWhiteSpace /* = false */)
// STRONG-GUARANTEE (for state of mCutPreviewTracks)
{
2016-03-28 20:04:49 +00:00
if (!CanStopAudioStream())
return -1;
bool useMidi = true;
// Remove these lines to experiment with scrubbing/seeking of note tracks
if (options.pScrubbingOptions)
useMidi = false;
// Uncomment this for laughs!
// backwards = true;
double t0 = selectedRegion.t0();
double t1 = selectedRegion.t1();
// SelectedRegion guarantees t0 <= t1, so we need another boolean argument
// to indicate backwards play.
const bool looped = options.playLooped;
if (backwards)
std::swap(t0, t1);
SetPlay(true, appearance);
bool success = false;
auto cleanup = finally( [&] {
if (!success) {
SetPlay(false);
SetStop(false);
SetRecord(false);
}
} );
if (gAudioIO->IsBusy())
return -1;
2014-06-03 20:30:19 +00:00
const bool cutpreview = appearance == PlayAppearance::CutPreview;
if (cutpreview && t0==t1)
return -1; /* msmeyer: makes no sense */
AudacityProject *p = GetActiveProject();
if (!p)
return -1; // Should never happen, but...
TrackList *t = p->GetTracks();
if (!t)
return -1; // Should never happen, but...
p->mLastPlayMode = mode;
bool hasaudio;
if (useMidi)
hasaudio = ! p->GetTracks()->Any<PlayableTrack>().empty();
else
hasaudio = ! p->GetTracks()->Any<WaveTrack>().empty();
double latestEnd = (playWhiteSpace)? t1 : t->GetEndTime();
if (!hasaudio)
return -1; // No need to continue without audio tracks
#if defined(EXPERIMENTAL_SEEK_BEHIND_CURSOR)
double init_seek = 0.0;
2014-06-03 20:30:19 +00:00
#endif
if (t1 == t0) {
if (looped) {
// play selection if there is one, otherwise
// set start of play region to project start,
// and loop the project from current play position.
if ((t0 > p->GetSel0()) && (t0 < p->GetSel1())) {
t0 = p->GetSel0();
t1 = p->GetSel1();
}
else {
// loop the entire project
t0 = t->GetStartTime();
t1 = t->GetEndTime();
}
} else {
// move t0 to valid range
if (t0 < 0) {
t0 = t->GetStartTime();
}
else if (t0 > t->GetEndTime()) {
t0 = t->GetEndTime();
}
#if defined(EXPERIMENTAL_SEEK_BEHIND_CURSOR)
else {
init_seek = t0; //AC: init_seek is where playback will 'start'
t0 = t->GetStartTime();
}
#endif
}
t1 = t->GetEndTime();
}
else {
// maybe t1 < t0, with backwards scrubbing for instance
if (backwards)
std::swap(t0, t1);
t0 = std::max(0.0, std::min(t0, latestEnd));
t1 = std::max(0.0, std::min(t1, latestEnd));
if (backwards)
std::swap(t0, t1);
}
int token = -1;
if (t1 != t0) {
if (cutpreview) {
const double tless = std::min(t0, t1);
const double tgreater = std::max(t0, t1);
double beforeLen, afterLen;
gPrefs->Read(wxT("/AudioIO/CutPreviewBeforeLen"), &beforeLen, 2.0);
gPrefs->Read(wxT("/AudioIO/CutPreviewAfterLen"), &afterLen, 1.0);
double tcp0 = tless-beforeLen;
double diff = tgreater - tless;
double tcp1 = (tgreater+afterLen) - diff;
SetupCutPreviewTracks(tcp0, tless, tgreater, tcp1);
if (backwards)
std::swap(tcp0, tcp1);
if (mCutPreviewTracks)
{
AudioIOStartStreamOptions myOptions = options;
myOptions.cutPreviewGapStart = t0;
myOptions.cutPreviewGapLen = t1 - t0;
token = gAudioIO->StartStream(
GetAllPlaybackTracks(*mCutPreviewTracks, false, useMidi),
tcp0, tcp1, myOptions);
}
else
// Cannot create cut preview tracks, clean up and exit
return -1;
}
else {
// Lifted the following into AudacityProject::GetDefaultPlayOptions()
/*
if (!timetrack) {
timetrack = t->GetTimeTrack();
}
*/
token = gAudioIO->StartStream(
GetAllPlaybackTracks(*t, false, useMidi),
t0, t1, options);
}
if (token != 0) {
success = true;
p->SetAudioIOToken(token);
mBusyProject = p;
#if defined(EXPERIMENTAL_SEEK_BEHIND_CURSOR)
//AC: If init_seek was set, now's the time to make it happen.
gAudioIO->SeekStream(init_seek);
#endif
}
else {
// Bug1627 (part of it):
// infinite error spew when trying to start scrub:
// Problem was that the error dialog yields to events,
// causing recursion to this function in the scrub timer
// handler! Easy fix, just delay the user alert instead.
CallAfter( [=]{
// Show error message if stream could not be opened
ShowErrorDialog(this, _("Error"),
_("Error opening sound device.\nTry changing the audio host, playback device and the project sample rate."),
wxT("Error_opening_sound_device"));
});
}
}
if (!success)
return -1;
StartScrollingIfPreferred();
// Let other UI update appearance
if (p)
p->GetRulerPanel()->DrawBothOverlays();
return token;
}
void ControlToolBar::PlayCurrentRegion(bool looped /* = false */,
bool cutpreview /* = false */)
{
2016-03-28 20:04:49 +00:00
if (!CanStopAudioStream())
return;
AudacityProject *p = GetActiveProject();
if (p)
{
double playRegionStart, playRegionEnd;
p->GetPlayRegion(&playRegionStart, &playRegionEnd);
AudioIOStartStreamOptions options(p->GetDefaultPlayOptions());
options.playLooped = looped;
if (cutpreview)
options.timeTrack = NULL;
ControlToolBar::PlayAppearance appearance =
cutpreview ? ControlToolBar::PlayAppearance::CutPreview
: looped ? ControlToolBar::PlayAppearance::Looped
: ControlToolBar::PlayAppearance::Straight;
PlayPlayRegion(SelectedRegion(playRegionStart, playRegionEnd),
options,
(looped ? PlayMode::loopedPlay : PlayMode::normalPlay),
appearance);
}
}
void ControlToolBar::OnKeyEvent(wxKeyEvent & event)
{
if (event.ControlDown() || event.AltDown()) {
event.Skip();
return;
}
2016-03-28 20:04:49 +00:00
// Does not appear to be needed on Linux. Perhaps on some other platform?
// If so, "!CanStopAudioStream()" should probably apply.
if (event.GetKeyCode() == WXK_SPACE) {
if (gAudioIO->IsStreamActive(GetActiveProject()->GetAudioIOToken())) {
SetPlay(false);
SetStop(true);
StopPlaying();
}
else if (!gAudioIO->IsBusy()) {
//SetPlay(true);// Not needed as done in PlayPlayRegion
SetStop(false);
PlayCurrentRegion();
}
return;
}
event.Skip();
}
void ControlToolBar::OnPlay(wxCommandEvent & WXUNUSED(evt))
{
auto p = GetActiveProject();
2016-03-28 20:04:49 +00:00
if (!CanStopAudioStream())
return;
StopPlaying();
if (p) p->TP_DisplaySelection();
auto cleanup = finally( [&]{ UpdateStatusBar(p); } );
PlayDefault();
}
void ControlToolBar::OnStop(wxCommandEvent & WXUNUSED(evt))
{
2016-03-28 20:04:49 +00:00
if (CanStopAudioStream()) {
StopPlaying();
UpdateStatusBar(GetActiveProject());
2016-03-28 20:04:49 +00:00
}
}
bool ControlToolBar::CanStopAudioStream()
{
return (!gAudioIO->IsStreamActive() ||
gAudioIO->IsMonitoring() ||
gAudioIO->GetOwningProject() == GetActiveProject());
}
void ControlToolBar::PlayDefault()
{
// Let control have precedence over shift
const bool cutPreview = mPlay->WasControlDown();
const bool looped = !cutPreview &&
mPlay->WasShiftDown();
PlayCurrentRegion(looped, cutPreview);
}
void ControlToolBar::StopPlaying(bool stopStream /* = true*/)
{
StopScrolling();
AudacityProject *project = GetActiveProject();
if(project) {
// Let scrubbing code do some appearance change
project->GetScrubber().StopScrubbing();
}
2016-03-28 20:04:49 +00:00
if (!CanStopAudioStream())
return;
mStop->PushDown();
SetStop(false);
if(stopStream)
gAudioIO->StopStream();
SetPlay(false);
SetRecord(false);
#ifdef EXPERIMENTAL_AUTOMATED_INPUT_LEVEL_ADJUSTMENT
gAudioIO->AILADisable();
#endif
mPause->PopUp();
mPaused=false;
//Make sure you tell gAudioIO to unpause
gAudioIO->SetPaused(mPaused);
2014-06-03 20:30:19 +00:00
ClearCutPreviewTracks();
mBusyProject = NULL;
// So that we continue monitoring after playing or recording.
// also clean the MeterQueues
if( project ) {
project->MayStartMonitoring();
MeterPanel *meter = project->GetPlaybackMeter();
if( meter ) {
meter->Clear();
}
2014-12-18 05:51:38 +00:00
meter = project->GetCaptureMeter();
if( meter ) {
meter->Clear();
}
}
const auto toolbar = project->GetToolManager()->GetToolBar(ScrubbingBarID);
toolbar->EnableDisableButtons();
}
2015-08-26 05:13:14 +00:00
void ControlToolBar::Pause()
{
2016-03-28 20:04:49 +00:00
if (!CanStopAudioStream())
gAudioIO->SetPaused(!gAudioIO->IsPaused());
else {
wxCommandEvent dummy;
OnPause(dummy);
}
2015-08-26 05:13:14 +00:00
}
WaveTrackArray ControlToolBar::ChooseExistingRecordingTracks(
AudacityProject &proj, bool selectedOnly)
{
auto p = &proj;
size_t recordingChannels =
std::max(0L, gPrefs->Read(wxT("/AudioIO/RecordChannels"), 2));
bool strictRules = (recordingChannels <= 2);
// Iterate over all wave tracks, or over selected wave tracks only.
//
// In the usual cases of one or two recording channels, seek a first-fit
// unbroken sub-sequence for which the total number of channels matches the
// required number exactly. Never drop inputs or fill only some channels
// of a track.
//
// In case of more than two recording channels, choose tracks only among the
// selected. Simply take the earliest wave tracks, until the number of
// channels is enough. If there are fewer channels than inputs, but at least
// one channel, then some of the input channels will be dropped.
//
// Resulting tracks may be non-consecutive within the list of all tracks
// (there may be non-wave tracks between, or non-selected tracks when
// considering selected tracks only.)
if (!strictRules && !selectedOnly)
return {};
auto trackList = p->GetTracks();
std::vector<unsigned> channelCounts;
WaveTrackArray candidates;
const auto range = trackList->Leaders<WaveTrack>();
for ( auto candidate : selectedOnly ? range + &Track::IsSelected : range ) {
// count channels in this track
const auto channels = TrackList::Channels( candidate );
unsigned nChannels = channels.size();
if (strictRules && nChannels > recordingChannels) {
// The recording would under-fill this track's channels
// Can't use any partial accumulated results
// either. Keep looking.
candidates.clear();
channelCounts.clear();
continue;
}
else {
// Might use this but may have to discard some of the accumulated
while(strictRules &&
nChannels + candidates.size() > recordingChannels) {
auto nOldChannels = channelCounts[0];
wxASSERT(nOldChannels > 0);
channelCounts.erase(channelCounts.begin());
candidates.erase(candidates.begin(),
candidates.begin() + nOldChannels);
}
channelCounts.push_back(nChannels);
for ( auto channel : channels ) {
candidates.push_back(Track::Pointer<WaveTrack>(channel));
if(candidates.size() == recordingChannels)
// Done!
return candidates;
}
}
}
if (!strictRules && !candidates.empty())
// good enough
return candidates;
// If the loop didn't exit early, we could not find enough channels
return {};
}
void ControlToolBar::OnRecord(wxCommandEvent &evt)
// STRONG-GUARANTEE (for state of current project's tracks)
{
// TODO: It would be neater if Menu items and Toolbar buttons used the same code for
// enabling/disabling, and all fell into the same action routines.
// Here instead we reduplicate some logic (from CommandHandler) because it isn't
// normally used for buttons.
// Code from CommandHandler start...
2018-05-18 11:13:09 +00:00
AudacityProject * p = GetActiveProject();
wxASSERT(p);
if (!p)
return;
bool altAppearance = mRecord->WasShiftDown();
if (evt.GetInt() == 1) // used when called by keyboard shortcut. Default (0) ignored.
altAppearance = true;
if (evt.GetInt() == 2)
altAppearance = false;
bool bPreferNewTrack;
gPrefs->Read("/GUI/PreferNewTrackRecord", &bPreferNewTrack, false);
const bool appendRecord = (altAppearance == bPreferNewTrack);
if (p) {
double t0 = p->GetSel0();
double t1 = p->GetSel1();
2018-05-18 12:01:21 +00:00
// When no time selection, recording duration is 'unlimited'.
if (t1 == t0)
2018-05-18 12:01:21 +00:00
t1 = DBL_MAX;
WaveTrackArray existingTracks;
if (appendRecord) {
const auto trackRange = p->GetTracks()->Any< const WaveTrack >();
// Try to find wave tracks to record into. (If any are selected,
// try to choose only from them; else if wave tracks exist, may record into any.)
existingTracks = ChooseExistingRecordingTracks(*p, true);
if ( !existingTracks.empty() )
t0 = std::max( t0,
( trackRange + &Track::IsSelected ).max( &Track::GetEndTime ) );
else {
existingTracks = ChooseExistingRecordingTracks(*p, false);
t0 = std::max( t0, trackRange.max( &Track::GetEndTime ) );
// If suitable tracks still not found, will record into NEW ones,
// but the choice of t0 does not depend on that.
}
// Whether we decided on NEW tracks or not:
if (t1 <= p->GetSel0() && p->GetSel1() > p->GetSel0()) {
t1 = p->GetSel1(); // record within the selection
}
else {
t1 = DBL_MAX; // record for a long, long time
}
}
TransportTracks transportTracks;
if (UseDuplex()) {
// Remove recording tracks from the list of tracks for duplex ("overdub")
// playback.
/* TODO: set up stereo tracks if that is how the user has set up
* their preferences, and choose sample format based on prefs */
transportTracks = GetAllPlaybackTracks(*p->GetTracks(), false, true);
for (const auto &wt : existingTracks) {
auto end = transportTracks.playbackTracks.end();
auto it = std::find(transportTracks.playbackTracks.begin(), end, wt);
if (it != end)
transportTracks.playbackTracks.erase(it);
}
}
transportTracks.captureTracks = existingTracks;
AudioIOStartStreamOptions options(p->GetDefaultPlayOptions());
DoRecord(*p, transportTracks, t0, t1, altAppearance, options);
}
}
bool ControlToolBar::UseDuplex()
{
bool duplex;
gPrefs->Read(wxT("/AudioIO/Duplex"), &duplex,
#ifdef EXPERIMENTAL_DA
false
#else
true
#endif
);
return duplex;
}
bool ControlToolBar::DoRecord(AudacityProject &project,
const TransportTracks &tracks,
double t0, double t1,
bool altAppearance,
const AudioIOStartStreamOptions &options)
{
CommandFlag flags = AlwaysEnabledFlag; // 0 means recalc flags.
// NB: The call may have the side effect of changing flags.
bool allowed = GetMenuManager(project).TryToMakeActionAllowed(
project,
flags,
AudioIONotBusyFlag | CanStopAudioStreamFlag,
AudioIONotBusyFlag | CanStopAudioStreamFlag);
if (!allowed)
return false;
// ...end of code from CommandHandler.
if (gAudioIO->IsBusy()) {
if (!CanStopAudioStream() || 0 == gAudioIO->GetNumCaptureChannels())
mRecord->PopUp();
else
mRecord->PushDown();
return false;
}
SetRecord(true, altAppearance);
bool success = false;
auto cleanup = finally([&] {
if (!success) {
SetPlay(false);
SetStop(false);
SetRecord(false);
}
// Success or not:
UpdateStatusBar(GetActiveProject());
});
auto transportTracks = tracks;
// Will replace any given capture tracks with temporaries
transportTracks.captureTracks.clear();
const auto p = &project;
bool appendRecord = !tracks.captureTracks.empty();
{
if (appendRecord) {
// Append recording:
// Pad selected/all wave tracks to make them all the same length
for (const auto &wt : tracks.captureTracks)
{
auto endTime = wt->GetEndTime();
// If the track was chosen for recording and playback both,
// remember the original in preroll tracks, before making the
// pending replacement.
bool prerollTrack = make_iterator_range(transportTracks.playbackTracks).contains(wt);
if (prerollTrack)
transportTracks.prerollTracks.push_back(wt);
// A function that copies all the non-sample data between
// wave tracks; in case the track recorded to changes scale
// type (for instance), during the recording.
auto updater = [](Track &d, const Track &s){
auto &dst = static_cast<WaveTrack&>(d);
auto &src = static_cast<const WaveTrack&>(s);
dst.Reinit(src);
};
// Get a copy of the track to be appended, to be pushed into
// undo history only later.
auto pending = std::static_pointer_cast<WaveTrack>(
p->GetTracks()->RegisterPendingChangedTrack(
updater, wt.get() ) );
// End of current track is before or at recording start time.
// Less than or equal, not just less than, to ensure a clip boundary.
// when append recording.
if (endTime <= t0) {
// Pad the recording track with silence, up to the
// maximum time.
auto newTrack = p->GetTrackFactory()->NewWaveTrack();
newTrack->SetWaveColorIndex( wt->GetWaveColorIndex() );
newTrack->InsertSilence(0.0, t0 - endTime);
newTrack->Flush();
pending->Clear(endTime, t0);
pending->Paste(endTime, newTrack.get());
}
transportTracks.captureTracks.push_back(pending);
}
p->GetTracks()->UpdatePendingTracks();
}
if( transportTracks.captureTracks.empty() )
{ // recording to NEW track(s).
bool recordingNameCustom, useTrackNumber, useDateStamp, useTimeStamp;
wxString defaultTrackName, defaultRecordingTrackName;
// Count the tracks.
const auto trackList = p->GetTracks();
auto numTracks = trackList->Leaders<const WaveTrack>().size();
auto recordingChannels = std::max(1L, gPrefs->Read(wxT("/AudioIO/RecordChannels"), 2));
gPrefs->Read(wxT("/GUI/TrackNames/RecordingNameCustom"), &recordingNameCustom, false);
gPrefs->Read(wxT("/GUI/TrackNames/TrackNumber"), &useTrackNumber, false);
gPrefs->Read(wxT("/GUI/TrackNames/DateStamp"), &useDateStamp, false);
gPrefs->Read(wxT("/GUI/TrackNames/TimeStamp"), &useTimeStamp, false);
defaultTrackName = TracksPrefs::GetDefaultAudioTrackNamePreference();
gPrefs->Read(wxT("/GUI/TrackNames/RecodingTrackName"), &defaultRecordingTrackName, defaultTrackName);
wxString baseTrackName = recordingNameCustom? defaultRecordingTrackName : defaultTrackName;
Track *first {};
for (int c = 0; c < recordingChannels; c++) {
std::shared_ptr<WaveTrack> newTrack{
p->GetTrackFactory()->NewWaveTrack().release()
};
if (!first)
first = newTrack.get();
// Quantize bounds to the rate of the new track.
if (c == 0) {
if (t0 < DBL_MAX)
t0 = newTrack->LongSamplesToTime(newTrack->TimeToLongSamples(t0));
if (t1 < DBL_MAX)
t1 = newTrack->LongSamplesToTime(newTrack->TimeToLongSamples(t1));
}
newTrack->SetOffset(t0);
wxString nameSuffix = wxString(wxT(""));
if (useTrackNumber) {
nameSuffix += wxString::Format(wxT("%d"), 1 + numTracks + c);
}
if (useDateStamp) {
if (!nameSuffix.empty()) {
nameSuffix += wxT("_");
}
nameSuffix += wxDateTime::Now().FormatISODate();
}
if (useTimeStamp) {
if (!nameSuffix.empty()) {
nameSuffix += wxT("_");
}
nameSuffix += wxDateTime::Now().FormatISOTime();
}
// ISO standard would be nice, but ":" is unsafe for file name.
nameSuffix.Replace(wxT(":"), wxT("-"));
if (baseTrackName.empty()) {
newTrack->SetName(nameSuffix);
}
else if (nameSuffix.empty()) {
newTrack->SetName(baseTrackName);
}
else {
newTrack->SetName(baseTrackName + wxT("_") + nameSuffix);
}
if ((recordingChannels > 2) && !(p->GetTracksFitVerticallyZoomed())) {
newTrack->SetMinimized(true);
}
p->GetTracks()->RegisterPendingNewTrack( newTrack );
transportTracks.captureTracks.push_back(newTrack);
// Bug 1548. New track needs the focus.
p->GetTrackPanel()->SetFocusedTrack( newTrack.get() );
}
p->GetTracks()->GroupChannels(*first, recordingChannels);
}
//Automated Input Level Adjustment Initialization
#ifdef EXPERIMENTAL_AUTOMATED_INPUT_LEVEL_ADJUSTMENT
gAudioIO->AILAInitialize();
#endif
2016-03-28 20:04:49 +00:00
int token = gAudioIO->StartStream(transportTracks, t0, t1, options);
success = (token != 0);
2014-06-03 20:30:19 +00:00
if (success) {
p->SetAudioIOToken(token);
mBusyProject = p;
StartScrollingIfPreferred();
}
else {
CancelRecording();
// Show error message if stream could not be opened
wxString msg = wxString::Format(_("Error opening recording device.\nError code: %s"), gAudioIO->LastPaErrorString());
ShowErrorDialog(this, _("Error"), msg, wxT("Error_opening_sound_device"));
}
}
return success;
}
void ControlToolBar::OnPause(wxCommandEvent & WXUNUSED(evt))
2014-06-03 20:30:19 +00:00
{
2016-03-28 20:04:49 +00:00
if (!CanStopAudioStream()) {
return;
}
if(mPaused)
{
mPause->PopUp();
mPaused=false;
}
else
2014-06-03 20:30:19 +00:00
{
mPause->PushDown();
mPaused=true;
}
2014-06-03 20:30:19 +00:00
#ifdef EXPERIMENTAL_SCRUBBING_SUPPORT
// Bug 1494 - Pausing a seek or scrub should just STOP as
// it is confusing to be in a paused scrub state.
bool bStopInstead = mPaused &&
gAudioIO->IsScrubbing() &&
!GetActiveProject()->GetScrubber().IsSpeedPlaying();
if (bStopInstead) {
wxCommandEvent dummy;
OnStop(dummy);
return;
}
if (gAudioIO->IsScrubbing())
GetActiveProject()->GetScrubber().Pause(mPaused);
else
#endif
{
gAudioIO->SetPaused(mPaused);
}
UpdateStatusBar(GetActiveProject());
}
void ControlToolBar::OnRewind(wxCommandEvent & WXUNUSED(evt))
{
mRewind->PushDown();
mRewind->PopUp();
AudacityProject *p = GetActiveProject();
if (p) {
p->StopIfPaused();
p->Rewind(mRewind->WasShiftDown());
}
}
void ControlToolBar::OnFF(wxCommandEvent & WXUNUSED(evt))
{
mFF->PushDown();
mFF->PopUp();
AudacityProject *p = GetActiveProject();
if (p) {
p->StopIfPaused();
p->SkipEnd(mFF->WasShiftDown());
}
}
void ControlToolBar::SetupCutPreviewTracks(double WXUNUSED(playStart), double cutStart,
double cutEnd, double WXUNUSED(playEnd))
// STRONG-GUARANTEE (for state of mCutPreviewTracks)
{
ClearCutPreviewTracks();
AudacityProject *p = GetActiveProject();
if (p) {
auto trackRange = p->GetTracks()->Selected<const PlayableTrack>();
if( !trackRange.empty() ) {
auto cutPreviewTracks = TrackList::Create();
for (const auto track1 : trackRange) {
// Duplicate and change tracks
// Clear has a very small chance of throwing
2014-06-03 20:30:19 +00:00
auto newTrack = track1->Duplicate();
newTrack->Clear(cutStart, cutEnd);
cutPreviewTracks->Add(std::move(newTrack));
}
// use NOTHROW-GUARANTEE:
mCutPreviewTracks = cutPreviewTracks;
}
}
}
void ControlToolBar::ClearCutPreviewTracks()
{
if (mCutPreviewTracks)
mCutPreviewTracks->Clear();
mCutPreviewTracks.reset();
}
// works out the width of the field in the status bar needed for the state (eg play, record pause)
int ControlToolBar::WidthForStatusBar(wxStatusBar* const sb)
{
int xMax = 0;
const auto pauseString = wxT(" ") + wxGetTranslation(mStatePause);
auto update = [&] (const wxString &state) {
int x, y;
sb->GetTextExtent(
wxGetTranslation(state) + pauseString + wxT("."),
&x, &y
);
xMax = std::max(x, xMax);
};
update(mStatePlay);
update(mStateStop);
update(mStateRecord);
// Note that Scrubbing + Paused is not allowed.
for(const auto &state : Scrubber::GetAllUntranslatedStatusStrings())
update(state);
return xMax + 30; // added constant needed because xMax isn't large enough for some reason, plus some space.
}
wxString ControlToolBar::StateForStatusBar()
{
wxString state;
auto pProject = GetActiveProject();
auto scrubState =
pProject ? pProject->GetScrubber().GetUntranslatedStateString() : wxString();
if (!scrubState.empty())
state = wxGetTranslation(scrubState);
else if (mPlay->IsDown())
state = wxGetTranslation(mStatePlay);
else if (mRecord->IsDown())
state = wxGetTranslation(mStateRecord);
else
state = wxGetTranslation(mStateStop);
if (mPause->IsDown())
{
state.Append(wxT(" "));
state.Append(wxGetTranslation(mStatePause));
}
state.Append(wxT("."));
return state;
}
void ControlToolBar::UpdateStatusBar(AudacityProject *pProject)
{
pProject->GetStatusBar()->SetStatusText(StateForStatusBar(), stateStatusBarField);
}
bool ControlToolBar::IsTransportingPinned()
{
if (!TracksPrefs::GetPinnedHeadPreference())
return false;
const auto &scrubber = ::GetActiveProject()->GetScrubber();
return
!(scrubber.HasMark() &&
!scrubber.WasSpeedPlaying() &&
!Scrubber::ShouldScrubPinned());
}
void ControlToolBar::StartScrollingIfPreferred()
{
if (IsTransportingPinned())
StartScrolling();
#ifdef __WXMAC__
else if (::GetActiveProject()->GetScrubber().HasMark()) {
// PRL: cause many "unnecessary" refreshes. For reasons I don't understand,
// doing this causes wheel rotation events (mapped from the double finger vertical
// swipe) to be delivered more uniformly to the application, so that speed control
// works better.
::GetActiveProject()->GetPlaybackScroller().Activate
(AudacityProject::PlaybackScroller::Mode::Refresh);
}
#endif
else
StopScrolling();
}
void ControlToolBar::StartScrolling()
{
using Mode = AudacityProject::PlaybackScroller::Mode;
const auto project = GetActiveProject();
if (project) {
2018-08-01 01:32:48 +00:00
auto mode = Mode::Pinned;
#if 0
// Enable these lines to pin the playhead right instead of center,
// when recording but not overdubbing.
if (gAudioIO->GetNumCaptureChannels() > 0) {
// recording
// Display a fixed recording head while scrolling the waves continuously.
// If you overdub, you may want to anticipate some context in existing tracks,
// so center the head. If not, put it rightmost to display as much wave as we can.
bool duplex;
2017-06-26 18:44:39 +00:00
#ifdef EXPERIMENTAL_DA
gPrefs->Read(wxT("/AudioIO/Duplex"), &duplex, false);
2017-06-26 18:44:39 +00:00
#else
gPrefs->Read(wxT("/AudioIO/Duplex"), &duplex, true);
#endif
if (duplex) {
// See if there is really anything being overdubbed
if (gAudioIO->GetNumPlaybackChannels() == 0)
// No.
duplex = false;
}
if (!duplex)
mode = Mode::Right;
}
#endif
project->GetPlaybackScroller().Activate(mode);
}
}
void ControlToolBar::StopScrolling()
{
const auto project = GetActiveProject();
if(project)
project->GetPlaybackScroller().Activate
(AudacityProject::PlaybackScroller::Mode::Off);
}
void ControlToolBar::CommitRecording()
{
const auto project = GetActiveProject();
project->GetTracks()->ApplyPendingTracks();
}
void ControlToolBar::CancelRecording()
{
const auto project = GetActiveProject();
project->GetTracks()->ClearPendingTracks();
}