mirror of
https://github.com/cookiengineer/audacity
synced 2025-06-27 09:38:39 +02:00
We can't go to 3.0.1 yet as there are still build issues on Linux and OSX. You can get Windows to build, but there's still some display issues. These changes should work with wxWidgets 2.8.12 as well, so we can take our time to get things working properly before switching over.
570 lines
16 KiB
C++
570 lines
16 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
Lyrics.cpp
|
|
|
|
Dominic Mazzoni
|
|
Vaughan Johnson
|
|
|
|
**********************************************************************/
|
|
|
|
#include <math.h>
|
|
|
|
#include <wx/defs.h>
|
|
#include <wx/dcmemory.h>
|
|
#include <wx/mimetype.h>
|
|
|
|
#include "Lyrics.h"
|
|
#include "Internat.h"
|
|
#include "Project.h" // for GetActiveProject
|
|
|
|
#include <wx/arrimpl.cpp>
|
|
WX_DEFINE_OBJARRAY(SyllableArray);
|
|
|
|
|
|
BEGIN_EVENT_TABLE(HighlightTextCtrl, wxTextCtrl)
|
|
EVT_MOUSE_EVENTS(HighlightTextCtrl::OnMouseEvent)
|
|
END_EVENT_TABLE()
|
|
|
|
HighlightTextCtrl::HighlightTextCtrl(Lyrics* 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)
|
|
mLyrics(parent)
|
|
{
|
|
}
|
|
|
|
void HighlightTextCtrl::OnMouseEvent(wxMouseEvent& event)
|
|
{
|
|
if (event.ButtonUp())
|
|
{
|
|
long from, to;
|
|
this->GetSelection(&from, &to);
|
|
|
|
int nCurSyl = mLyrics->GetCurrentSyllableIndex();
|
|
int nNewSyl = mLyrics->FindSyllable(from);
|
|
if (nNewSyl != nCurSyl)
|
|
{
|
|
Syllable* pCurSyl = mLyrics->GetSyllable(nNewSyl);
|
|
AudacityProject* pProj = GetActiveProject();
|
|
pProj->SetSel0(pCurSyl->t);
|
|
|
|
//v Should probably select to end as in AudacityProject::OnSelectCursorEnd,
|
|
// but better to generalize that in AudacityProject methods.
|
|
pProj->mViewInfo.selectedRegion.setT1(pCurSyl->t);
|
|
}
|
|
}
|
|
|
|
event.Skip();
|
|
}
|
|
|
|
|
|
//v static const kHighlightTextCtrlID = 7654;
|
|
|
|
BEGIN_EVENT_TABLE(Lyrics, wxPanel)
|
|
EVT_CHAR(Lyrics::OnKeyEvent)
|
|
EVT_PAINT(Lyrics::OnPaint)
|
|
EVT_SIZE(Lyrics::OnSize)
|
|
|
|
//v Doesn't seem to be a way to capture a selection event in a read-only wxTextCtrl.
|
|
// EVT_COMMAND_LEFT_CLICK(kHighlightTextCtrlID, Lyrics::OnHighlightTextCtrl)
|
|
END_EVENT_TABLE()
|
|
|
|
IMPLEMENT_CLASS(Lyrics, wxPanel)
|
|
|
|
Lyrics::Lyrics(wxWindow* parent, wxWindowID id,
|
|
const wxPoint& pos /*= wxDefaultPosition*/,
|
|
const wxSize& size /*= wxDefaultSize*/):
|
|
wxPanel(parent, id, pos, size),
|
|
mWidth(size.x), mHeight(size.y)
|
|
{
|
|
mKaraokeHeight = mHeight;
|
|
mLyricsStyle = kBouncingBallLyrics; // default
|
|
mKaraokeFontSize = this->GetDefaultFontSize(); // Call only after mLyricsStyle is set.
|
|
|
|
this->SetBackgroundColour(*wxWHITE);
|
|
|
|
mHighlightTextCtrl =
|
|
new HighlightTextCtrl(this, -1, // wxWindow* parent, wxWindowID id,
|
|
wxT(""), // const wxString& value = wxT(""),
|
|
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
|
|
}
|
|
|
|
Lyrics::~Lyrics()
|
|
{
|
|
}
|
|
|
|
#define I_FIRST_REAL_SYLLABLE 2
|
|
|
|
void Lyrics::Clear()
|
|
{
|
|
mSyllables.Clear();
|
|
mText = wxT("");
|
|
|
|
// Add two dummy syllables at the beginning
|
|
mSyllables.Add(Syllable());
|
|
mSyllables[0].t = -2.0;
|
|
mSyllables.Add(Syllable());
|
|
mSyllables[1].t = -1.0;
|
|
|
|
mHighlightTextCtrl->Clear();
|
|
}
|
|
|
|
void Lyrics::Add(double t, wxString syllable)
|
|
{
|
|
int i = mSyllables.GetCount();
|
|
|
|
if (mSyllables[i-1].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.
|
|
mSyllables[i-1].text += syllable;
|
|
mSyllables[i-1].textWithSpace += syllable;
|
|
mSyllables[i-1].char1 += syllable.Length();
|
|
return;
|
|
}
|
|
|
|
mSyllables.Add(Syllable());
|
|
mSyllables[i].t = t;
|
|
mSyllables[i].text = syllable;
|
|
|
|
mSyllables[i].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("-"))
|
|
mSyllables[i].textWithSpace = wxT(" ") + syllable;
|
|
else
|
|
mSyllables[i].textWithSpace = syllable;
|
|
|
|
mText += mSyllables[i].textWithSpace;
|
|
mSyllables[i].char1 = mText.Length();
|
|
|
|
int nTextLen = mSyllables[i].textWithSpace.Length();
|
|
if ((nTextLen > 0) && (mSyllables[i].textWithSpace.Right(1) == wxT("_")))
|
|
mHighlightTextCtrl->AppendText(mSyllables[i].textWithSpace.Left(nTextLen - 1) + wxT("\n"));
|
|
else
|
|
mHighlightTextCtrl->AppendText(mSyllables[i].textWithSpace);
|
|
}
|
|
|
|
void Lyrics::Finish(double finalT)
|
|
{
|
|
// Add 3 dummy syllables at the end
|
|
int i = mSyllables.GetCount();
|
|
mSyllables.Add(Syllable());
|
|
mSyllables[i].t = finalT + 1.0;
|
|
mSyllables.Add(Syllable());
|
|
mSyllables[i+1].t = finalT + 2.0;
|
|
mSyllables.Add(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 syllable whose char0 <= startChar <= char1.
|
|
int Lyrics::FindSyllable(long startChar)
|
|
{
|
|
int i1, i2;
|
|
|
|
i1 = 0;
|
|
i2 = mSyllables.GetCount();
|
|
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.GetCount()) - 3)
|
|
i1 = mSyllables.GetCount() - 3;
|
|
|
|
return i1;
|
|
}
|
|
|
|
void Lyrics::SetLyricsStyle(const LyricsStyle newLyricsStyle)
|
|
{
|
|
if (mLyricsStyle == newLyricsStyle)
|
|
return;
|
|
|
|
mLyricsStyle = newLyricsStyle;
|
|
mHighlightTextCtrl->Show(mLyricsStyle == kHighlightLyrics);
|
|
|
|
wxSizeEvent ignore;
|
|
this->OnSize(ignore);
|
|
}
|
|
|
|
|
|
unsigned int Lyrics::GetDefaultFontSize() const
|
|
{
|
|
return (mLyricsStyle == kBouncingBallLyrics) ? 48 : 10;
|
|
}
|
|
|
|
void Lyrics::SetDrawnFont(wxDC *dc)
|
|
{
|
|
dc->SetFont(wxFont(mKaraokeFontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
|
|
}
|
|
|
|
void Lyrics::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 Lyrics::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.GetCount(); i++) {
|
|
if ((i < I_FIRST_REAL_SYLLABLE) || // Clear() starts the list with I_FIRST_REAL_SYLLABLE dummies.
|
|
(i >= mSyllables.GetCount()-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.GetCount()-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 Lyrics::FindSyllable(double t)
|
|
{
|
|
int i1, i2;
|
|
|
|
i1 = 0;
|
|
i2 = mSyllables.GetCount();
|
|
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.GetCount()) - 3)
|
|
i1 = mSyllables.GetCount() - 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 Lyrics::GetKaraokePosition(double t,
|
|
int *outX, double *outY)
|
|
{
|
|
*outX = 0;
|
|
*outY = 0;
|
|
|
|
if (t < mSyllables[I_FIRST_REAL_SYLLABLE].t || t > mSyllables[mSyllables.GetCount()-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 Lyrics::Update(double t)
|
|
{
|
|
if (t < 0.0)
|
|
{
|
|
// TrackPanel::OnTimer passes gAudioIO->GetStreamTime(), which is -1000000000 if !IsStreamActive().
|
|
// In that case, use the selection start time.
|
|
AudacityProject* pProj = GetActiveProject();
|
|
mT = pProj->GetSel0();
|
|
}
|
|
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 Lyrics::OnKeyEvent(wxKeyEvent & event)
|
|
{
|
|
GetActiveProject()->HandleKeyDown(event);
|
|
}
|
|
|
|
void Lyrics::OnPaint(wxPaintEvent & WXUNUSED(event))
|
|
{
|
|
if (!this->GetParent()->IsShown())
|
|
return;
|
|
|
|
if (mLyricsStyle == kBouncingBallLyrics)
|
|
{
|
|
wxPaintDC dc(this);
|
|
|
|
if (!mMeasurementsDone)
|
|
Measure(&dc);
|
|
|
|
#ifdef __WXMAC__
|
|
// Mac OS X automatically double-buffers the screen for you,
|
|
// so our bitmap is unneccessary
|
|
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 Lyrics::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;
|
|
wxPaintEvent ignore;
|
|
this->OnPaint(ignore);
|
|
}
|
|
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 Lyrics::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 Lyrics::HandlePaint(wxDC &dc)
|
|
{
|
|
wxASSERT(mLyricsStyle == kBouncingBallLyrics);
|
|
dc.SetBrush(*wxWHITE_BRUSH);
|
|
dc.DrawRectangle(0, 0, mWidth, mKaraokeHeight);
|
|
|
|
this->HandlePaint_BouncingBall(dc);
|
|
}
|
|
|
|
void Lyrics::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.GetCount(); 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), wxSOLID));
|
|
dc.DrawEllipse(ball);
|
|
}
|
|
}
|
|
|