audacia/src/Lyrics.cpp

665 lines
19 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
Lyrics.cpp
Dominic Mazzoni
Vaughan Johnson
**********************************************************************/
#include "Lyrics.h"
#include <math.h>
#include <wx/dcclient.h>
#include <wx/defs.h>
#include <wx/dcmemory.h>
#include <wx/frame.h>
#include <wx/mimetype.h>
#include "AudioIO.h"
#include "Project.h"
#include "ProjectWindowBase.h"
#include "LabelTrack.h"
#include "commands/CommandManager.h"
#include "UndoManager.h"
#include "ViewInfo.h"
BEGIN_EVENT_TABLE(HighlightTextCtrl, wxTextCtrl)
EVT_MOUSE_EVENTS(HighlightTextCtrl::OnMouseEvent)
END_EVENT_TABLE()
HighlightTextCtrl::HighlightTextCtrl(LyricsPanel* parent,
wxWindowID id,
const wxString& value /* = {} */,
const wxPoint& pos /*= wxDefaultPosition*/,
const wxSize& size /*= wxDefaultSize*/)
: wxTextCtrl(parent, id, // wxWindow* parent, wxWindowID id,
value, // const wxString& value = {},
pos, // const wxPoint& pos = wxDefaultPosition,
size, // const wxSize& size = wxDefaultSize,
wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH | wxTE_RICH2 | wxTE_AUTO_URL | wxTE_NOHIDESEL), //v | wxHSCROLL)
mLyricsPanel(parent)
{
}
void HighlightTextCtrl::OnMouseEvent(wxMouseEvent& event)
{
if (event.ButtonUp())
{
long from, to;
this->GetSelection(&from, &to);
int nCurSyl = mLyricsPanel->GetCurrentSyllableIndex();
int nNewSyl = mLyricsPanel->FindSyllable(from);
if (nNewSyl != nCurSyl)
{
Syllable* pCurSyl = mLyricsPanel->GetSyllable(nNewSyl);
auto pProj = FindProjectFromWindow( this );
auto &selectedRegion = ViewInfo::Get( *pProj ).selectedRegion;
selectedRegion.setT0( pCurSyl->t );
//v Should probably select to end as in
// SelectUtilities::Handler::OnSelectCursorEnd,
// but better to generalize that in AudacityProject methods.
selectedRegion.setT1( pCurSyl->t );
}
}
event.Skip();
}
//v static const kHighlightTextCtrlID = 7654;
BEGIN_EVENT_TABLE(LyricsPanel, wxPanelWrapper)
EVT_KEY_DOWN(LyricsPanel::OnKeyEvent)
EVT_PAINT(LyricsPanel::OnPaint)
EVT_SIZE(LyricsPanel::OnSize)
//v Doesn't seem to be a way to capture a selection event in a read-only wxTextCtrl.
// EVT_COMMAND_LEFT_CLICK(kHighlightTextCtrlID, LyricsPanel::OnHighlightTextCtrl)
END_EVENT_TABLE()
IMPLEMENT_CLASS(LyricsPanel, wxPanel)
LyricsPanel::LyricsPanel(wxWindow* parent, wxWindowID id,
AudacityProject *project,
const wxPoint& pos /*= wxDefaultPosition*/,
const wxSize& size /*= wxDefaultSize*/) :
wxPanelWrapper(parent, id, pos, size, wxWANTS_CHARS),
mWidth(size.x), mHeight(size.y)
, mProject(project)
{
mKaraokeHeight = mHeight;
mLyricsStyle = kBouncingBallLyrics; // default
mKaraokeFontSize = this->GetDefaultFontSize(); // Call only after mLyricsPanelStyle is set.
this->SetBackgroundColour(*wxWHITE);
mHighlightTextCtrl =
safenew HighlightTextCtrl(this, -1, // wxWindow* parent, wxWindowID id,
wxT(""), // const wxString& value = {},
wxPoint(0, 0), // const wxPoint& pos = wxDefaultPosition,
size); // const wxSize& size = wxDefaultSize
this->SetHighlightFont();
mHighlightTextCtrl->Show(mLyricsStyle == kHighlightLyrics); // test, in case we conditionalize the default, above
mT = 0.0;
Clear();
Finish(0.0);
#ifdef __WXMAC__
wxSizeEvent dummyEvent;
OnSize(dummyEvent);
#endif
parent->Bind(wxEVT_SHOW, &LyricsPanel::OnShow, this);
project->Bind(EVT_UNDO_PUSHED, &LyricsPanel::UpdateLyrics, this);
project->Bind(EVT_UNDO_MODIFIED, &LyricsPanel::UpdateLyrics, this);
project->Bind(EVT_UNDO_OR_REDO, &LyricsPanel::UpdateLyrics, this);
project->Bind(EVT_UNDO_RESET, &LyricsPanel::UpdateLyrics, this);
wxTheApp->Bind(EVT_AUDIOIO_PLAYBACK, &LyricsPanel::OnStartStop, this);
wxTheApp->Bind(EVT_AUDIOIO_CAPTURE, &LyricsPanel::OnStartStop, this);
}
LyricsPanel::~LyricsPanel()
{
}
#define I_FIRST_REAL_SYLLABLE 2
void LyricsPanel::Clear()
{
mSyllables.clear();
mText = wxT("");
// Add two dummy syllables at the beginning
mSyllables.push_back(Syllable());
mSyllables[0].t = -2.0;
mSyllables.push_back(Syllable());
mSyllables[1].t = -1.0;
mHighlightTextCtrl->Clear();
}
void LyricsPanel::AddLabels(const LabelTrack *pLT)
{
const size_t numLabels = pLT->GetNumLabels();
wxString highlightText;
for (size_t ii = 0; ii < numLabels; ++ii) {
const LabelStruct *const pLabel = pLT->GetLabel(ii);
Add(pLabel->getT0(), pLabel->title, highlightText);
}
mHighlightTextCtrl->AppendText(highlightText);
}
void LyricsPanel::Add(double t, const wxString &syllable, wxString &highlightText)
{
int i = mSyllables.size();
{
Syllable &prevSyllable = mSyllables[i - 1];
if (prevSyllable.t == t) {
// We can't have two syllables with the same time, so append
// this to the end of the previous one if they're at the
// same time.
prevSyllable.text += syllable;
prevSyllable.textWithSpace += syllable;
prevSyllable.char1 += syllable.length();
return;
}
}
mSyllables.push_back(Syllable());
Syllable &thisSyllable = mSyllables[i];
thisSyllable.t = t;
thisSyllable.text = syllable;
thisSyllable.char0 = mText.length();
// Put a space between syllables unless the previous one
// ended in a hyphen
if (i > 0 &&
// mSyllables[i-1].text.length() > 0 &&
mSyllables[i - 1].text.Right(1) != wxT("-"))
thisSyllable.textWithSpace = wxT(" ") + syllable;
else
thisSyllable.textWithSpace = syllable;
mText += thisSyllable.textWithSpace;
thisSyllable.char1 = mText.length();
int nTextLen = thisSyllable.textWithSpace.length();
if ((nTextLen > 0) && (thisSyllable.textWithSpace.Right(1) == wxT("_")))
highlightText += (thisSyllable.textWithSpace.Left(nTextLen - 1) + wxT("\n"));
else
highlightText += thisSyllable.textWithSpace;
}
void LyricsPanel::Finish(double finalT)
{
// Add 3 dummy syllables at the end
int i = mSyllables.size();
mSyllables.push_back(Syllable());
mSyllables[i].t = finalT + 1.0;
mSyllables.push_back(Syllable());
mSyllables[i+1].t = finalT + 2.0;
mSyllables.push_back(Syllable());
mSyllables[i+2].t = finalT + 3.0;
// Mark measurements as invalid
mMeasurementsDone = false; // only for drawn text
mCurrentSyllable = 0;
mHighlightTextCtrl->ShowPosition(0);
}
// Binary-search for the syllable whose char0 <= startChar <= char1.
int LyricsPanel::FindSyllable(long startChar)
{
int i1, i2;
i1 = 0;
i2 = mSyllables.size();
while (i2 > i1+1) {
int pmid = (i1+i2)/2;
if (mSyllables[pmid].char0 > startChar)
i2 = pmid;
else
i1 = pmid;
}
if (i1 < 2)
i1 = 2;
if (i1 > (int)(mSyllables.size()) - 3)
i1 = mSyllables.size() - 3;
return i1;
}
void LyricsPanel::SetLyricsStyle(const LyricsStyle newLyricsStyle)
{
if (mLyricsStyle == newLyricsStyle)
return;
mLyricsStyle = newLyricsStyle;
mHighlightTextCtrl->Show(mLyricsStyle == kHighlightLyrics);
wxSizeEvent ignore;
this->OnSize(ignore);
}
unsigned int LyricsPanel::GetDefaultFontSize() const
{
return (mLyricsStyle == kBouncingBallLyrics) ? 48 : 10;
}
void LyricsPanel::SetDrawnFont(wxDC *dc)
{
dc->SetFont(wxFont(mKaraokeFontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
}
void LyricsPanel::SetHighlightFont() // for kHighlightLyrics
{
wxFont newFont(mKaraokeFontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
mHighlightTextCtrl->SetDefaultStyle(wxTextAttr(wxNullColour, wxNullColour, newFont));
mHighlightTextCtrl->SetStyle(0, mHighlightTextCtrl->GetLastPosition(),
wxTextAttr(wxNullColour, wxNullColour, newFont));
}
void LyricsPanel::Measure(wxDC *dc) // only for drawn text
{
this->SetDrawnFont(dc);
int width = 0, height = 0;
const int kIndent = 4;
int x = 2*kIndent;
unsigned int i;
for(i = 0; i < mSyllables.size(); i++) {
if ((i < I_FIRST_REAL_SYLLABLE) || // Clear() starts the list with I_FIRST_REAL_SYLLABLE dummies.
(i >= mSyllables.size() - 3)) // Finish() ends with 3 dummies.
{
dc->GetTextExtent(wxT("DUMMY"), &width, &height); // Get the correct height even if we're at i=0.
width = 0;
}
else {
dc->GetTextExtent(mSyllables[i].textWithSpace, &width, &height);
}
// Add some space between words; the space is normally small but
// when there's a long pause relative to the previous word, insert
// extra space.
int extraWidth;
if (i >= I_FIRST_REAL_SYLLABLE && i < mSyllables.size() - 2)
{
double deltaThis = mSyllables[i+1].t - mSyllables[i].t;
double deltaPrev = mSyllables[i].t - mSyllables[i-1].t;
double ratio;
if (deltaPrev > 0.0)
ratio = deltaThis / deltaPrev;
else
ratio = deltaThis;
if (ratio > 2.0)
extraWidth = 15 + (int)(15.0 * ratio);
else
extraWidth = 15;
}
else
extraWidth = 20;
mSyllables[i].width = width + extraWidth;
mSyllables[i].leftX = x;
mSyllables[i].x = x + width/2;
x += mSyllables[i].width;
}
mTextHeight = height;
mMeasurementsDone = true;
}
// Binary-search for the syllable with the largest time not greater than t
int LyricsPanel::FindSyllable(double t)
{
int i1, i2;
i1 = 0;
i2 = mSyllables.size();
while (i2 > i1+1) {
int pmid = (i1+i2)/2;
if (mSyllables[pmid].t > t)
i2 = pmid;
else
i1 = pmid;
}
if (i1 < 2)
i1 = 2;
if (i1 > (int)(mSyllables.size()) - 3)
i1 = mSyllables.size() - 3;
return i1;
}
// Bouncing Ball:
// Given the current time t, returns the x/y position of the scrolling
// karaoke display. For some syllable i, when t==mSyllables[i].t,
// it will return mSyllables[i].x for outX and 0 for outY.
// In-between words, outX is interpolated using smooth acceleration
// between the two neighboring words, and y is a positive number indicating
// the bouncing ball height
void LyricsPanel::GetKaraokePosition(double t,
int *outX, double *outY)
{
*outX = 0;
*outY = 0;
if (t < mSyllables[I_FIRST_REAL_SYLLABLE].t || t > mSyllables[mSyllables.size() - 3].t)
return;
int i0, i1, i2, i3;
int x0, x1, x2, x3;
double t0, t1, t2, t3;
i1 = FindSyllable(t);
i2 = i1 + 1;
// Because we've padded the syllables with two dummies at the beginning
// and end, we know that i0...i3 will always exist. Also, we've made
// sure that we don't ever have two of the same time, so t2>t1 strictly.
//
// t
// \/
// time: t0 t1 t2 t3
// pos: x0 x1 x2 x3
// index: i0 i1 i2 i3
// vel: vel1 vel2
i0 = i1 - 1;
i3 = i2 + 1;
x0 = mSyllables[i0].x;
x1 = mSyllables[i1].x;
x2 = mSyllables[i2].x;
x3 = mSyllables[i3].x;
t0 = mSyllables[i0].t;
t1 = mSyllables[i1].t;
t2 = mSyllables[i2].t;
t3 = mSyllables[i3].t;
double linear_vel0 = (x1 - x0) / (t1 - t0);
double linear_vel1 = (x2 - x1) / (t2 - t1);
double linear_vel2 = (x3 - x2) / (t3 - t2);
// average velocities
double v1 = (linear_vel0 + linear_vel1) / 2;
double v2 = (linear_vel1 + linear_vel2) / 2;
// Solve a cubic equation f(t) = at^3 + bt^2 + ct + d
// which gives the position x as a function of
// (t - t1), by constraining f(0), f'(0), f(t2-t1), f'(t2-t1)
double delta_t = t2 - t1;
double delta_x = x2 - x1;
v1 *= delta_t;
v2 *= delta_t;
double a = v1 + v2 - 2*delta_x;
double b = 3*delta_x - 2*v1 - v2;
double c = v1;
double d = x1;
t = (t - t1) / (t2 - t1);
double xx = a*t*t*t + b*t*t + c*t + d;
// Unfortunately sometimes our cubic goes backwards. This is a quick
// hack to stop that from happening.
if (xx < x1)
xx = x1;
*outX = (int)xx;
// The y position is a simple cosine curve; the max height is a
// function of the time.
double height = t2 - t1 > 4.0? 1.0: sqrt((t2-t1)/4.0);
*outY = height * sin(M_PI * t);
}
void LyricsPanel::Update(double t)
{
if (t < 0.0)
{
// TrackPanel::OnTimer passes gAudioIO->GetStreamTime(), which is -DBL_MAX if !IsStreamActive().
// In that case, use the selection start time.
auto pProj = FindProjectFromWindow( this );
const auto &selectedRegion = ViewInfo::Get( *pProj ).selectedRegion;
mT = selectedRegion.t0();
}
else
mT = t;
if (mLyricsStyle == kBouncingBallLyrics)
{
wxRect karaokeRect(0, 0, mWidth, mKaraokeHeight);
this->Refresh(false, &karaokeRect);
}
int i = FindSyllable(mT);
if (i == mCurrentSyllable)
return;
mCurrentSyllable = i;
if (mLyricsStyle == kHighlightLyrics)
{
mHighlightTextCtrl->SetSelection(mSyllables[i].char0, mSyllables[i].char1);
//v No trail for now.
//// Leave a trail behind the selection, by highlighting.
//if (i == I_FIRST_REAL_SYLLABLE)
// // Reset the trail to zero.
// mHighlightTextCtrl->SetStyle(0, mHighlightTextCtrl->GetLastPosition(), wxTextAttr(wxNullColour, *wxWHITE));
//// Mark the trail for mSyllables[i].
//mHighlightTextCtrl->SetStyle(mSyllables[i].char0, mSyllables[i].char1, wxTextAttr(wxNullColour, *wxLIGHT_GREY));
//v Too much flicker: mHighlightTextCtrl->ShowPosition(mSyllables[i].char0);
}
}
void LyricsPanel::UpdateLyrics(wxEvent &e)
{
e.Skip();
// It's crucial to not do that repopulating during playback.
auto gAudioIO = AudioIOBase::Get();
if (gAudioIO->IsStreamActive()) {
mDelayedUpdate = true;
return;
}
Clear();
if (!mProject)
return;
// Lyrics come from only the first label track.
auto pLabelTrack =
*TrackList::Get( *mProject ).Any< const LabelTrack >().begin();
if (!pLabelTrack)
return;
// The code that updates the lyrics is rather expensive when there
// are a lot of labels.
// So - bail out early if the lyrics window is not visible.
// We will later force an update when the lyrics window is made visible.
auto parent = dynamic_cast<wxFrame*>(GetParent());
if( !(parent && parent->IsVisible()) )
return;
AddLabels(pLabelTrack);
Finish(pLabelTrack->GetEndTime());
const auto &selectedRegion = ViewInfo::Get( *mProject ).selectedRegion;
Update(selectedRegion.t0());
}
void LyricsPanel::OnStartStop(wxCommandEvent &e)
{
e.Skip();
if ( !e.GetInt() && mDelayedUpdate ) {
mDelayedUpdate = false;
UpdateLyrics( e );
}
}
void LyricsPanel::OnShow(wxShowEvent &e)
{
e.Skip();
if (e.IsShown())
UpdateLyrics(e);
}
void LyricsPanel::OnKeyEvent(wxKeyEvent & event)
{
auto project = FindProjectFromWindow( this );
auto &commandManager = CommandManager::Get( *project );
commandManager.FilterKeyEvent(project, event, true);
event.Skip();
}
void LyricsPanel::OnPaint(wxPaintEvent & WXUNUSED(event))
{
wxPaintDC dc(this);
DoPaint(dc);
}
void LyricsPanel::DoPaint(wxDC &dc)
{
if (!this->GetParent()->IsShown())
return;
if (mLyricsStyle == kBouncingBallLyrics)
{
if (!mMeasurementsDone)
Measure(&dc);
#ifdef __WXMAC__
// Mac OS X automatically double-buffers the screen for you,
// so our bitmap is unnecessary
HandlePaint(dc);
#else
wxBitmap bitmap(mWidth, mKaraokeHeight);
wxMemoryDC memDC;
memDC.SelectObject(bitmap);
HandlePaint(memDC);
dc.Blit(0, 0, mWidth, mKaraokeHeight, &memDC, 0, 0, wxCOPY, FALSE);
#endif
}
else // (mLyricsStyle == kHighlightLyrics)
{
//v causes flicker in ported version
// this->SetHighlightFont();
}
}
void LyricsPanel::OnSize(wxSizeEvent & WXUNUSED(event))
{
GetClientSize(&mWidth, &mHeight);
mKaraokeHeight = mHeight;
mKaraokeFontSize =
(int)((float)(this->GetDefaultFontSize() * mHeight) / (float)LYRICS_DEFAULT_HEIGHT);
// Usually don't get the size window we want, usually less than
// LYRICS_DEFAULT_HEIGHT, so bump it a little.
mKaraokeFontSize += 2;
if (mLyricsStyle == kBouncingBallLyrics)
{
mMeasurementsDone = false;
wxClientDC dc(this);
this->DoPaint(dc);
}
else // (mLyricsStyle == kHighlightLyrics)
{
mHighlightTextCtrl->SetSize(mWidth, mKaraokeHeight);
this->SetHighlightFont();
}
this->Refresh(false);
}
//v Doesn't seem to be a way to capture a selection event in a read-only wxTextCtrl.
//void LyricsPanel::OnHighlightTextCtrl(wxCommandEvent & event)
//{
// long from, to;
//
// mHighlightTextCtrl->GetSelection(&from, &to);
// // TODO: Find the start time of the corresponding syllable and set playback to start there.
//}
void LyricsPanel::HandlePaint(wxDC &dc)
{
wxASSERT(mLyricsStyle == kBouncingBallLyrics);
dc.SetBrush(*wxWHITE_BRUSH);
dc.DrawRectangle(0, 0, mWidth, mKaraokeHeight);
this->HandlePaint_BouncingBall(dc);
}
void LyricsPanel::HandlePaint_BouncingBall(wxDC &dc)
{
int ctr = mWidth / 2;
int x;
double y;
GetKaraokePosition(mT, &x, &y);
dc.SetTextForeground(wxColour(238, 0, 102));
bool changedColor = false;
SetDrawnFont(&dc);
unsigned int i;
wxCoord yTextTop = mKaraokeHeight - mTextHeight - 4;
for(i = 0; i < mSyllables.size(); i++) {
if (mSyllables[i].x + mSyllables[i].width < (x - ctr))
continue;
if (mSyllables[i].x > x + ctr)
continue;
if (!changedColor && mSyllables[i].x >= x) {
dc.SetTextForeground(*wxBLACK);
changedColor = true;
}
wxString text = mSyllables[i].text;
if (text.length() > 0 && text.Right(1) == wxT("_")) {
text = text.Left(text.length() - 1);
}
dc.DrawText(text,
mSyllables[i].leftX + ctr - x,
yTextTop);
}
int ballRadius = (int)(mTextHeight / 8.0);
int bounceTop = ballRadius * 2;
int bounceHeight = yTextTop - bounceTop;
int yi = (int)(yTextTop - 4 - (y * bounceHeight));
if (mT >= 0.0) {
wxRect ball(ctr - ballRadius, yi - ballRadius, 2 * ballRadius, 2 * ballRadius);
dc.SetBrush(wxBrush(wxColour(238, 0, 102), wxBRUSHSTYLE_SOLID));
dc.DrawEllipse(ball);
}
}