// Copyright (c) 2005, Rodrigo Braz Monteiro
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//   * Neither the name of the Aegisub Group nor the names of its contributors
//     may be used to endorse or promote products derived from this software
//     without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// -----------------------------------------------------------------------------
//
// AEGISUB
//
// Website: http://aegisub.cellosoft.com
// Contact: mailto:zeratul@cellosoft.com
//


///////////
// Headers
#include <wx/tglbtn.h>
#include <wx/filename.h>
#include <math.h>
#include <vector>
#include "audio_display.h"
#include "audio_provider_stream.h"
#include "main.h"
#include "ass_dialogue.h"
#include "subs_grid.h"
#include "ass_file.h"
#include "subs_edit_box.h"
#include "options.h"
#include "audio_karaoke.h"
#include "audio_box.h"
#include "fft.h"
#include "video_context.h"
#include "vfr.h"
#include "colorspace.h"
#include "hotkeys.h"
#include "utils.h"
#include "timeedit_ctrl.h"
#include "standard_paths.h"
#ifdef _DEBUG
#include "audio_provider_dummy.h"
#endif


#ifdef __WXMAC__
# define AudioDisplayWindowStyle wxWANTS_CHARS
#else
# define AudioDisplayWindowStyle wxSUNKEN_BORDER | wxWANTS_CHARS
#endif

///////////////
// Constructor
AudioDisplay::AudioDisplay(wxWindow *parent)
: wxWindow (parent, -1, wxDefaultPosition, wxSize(200,Options.AsInt(_T("Audio Display Height"))), AudioDisplayWindowStyle , _T("Audio Display"))
{
	// Set variables
	origImage = NULL;
	spectrumDisplay = NULL;
	spectrumDisplaySelected = NULL;
	spectrumRenderer = NULL;
	ScrollBar = NULL;
	dialogue = NULL;
	karaoke = NULL;
	peak = NULL;
	min = NULL;
	hasSel = false;
	diagUpdated = false;
	NeedCommit = false;
	loaded = false;
	temporary = false;
	blockUpdate = false;
	dontReadTimes = false;
	holding = false;
	draggingScale = false;
	scrubbing = false;
	Position = 0;
	PositionSample = 0;
	oldCurPos = 0;
	scale = 1.0f;
	provider = NULL;
	player = NULL;
	hold = 0;
	samples = 0;
	samplesPercent = 100;
	hasFocus = (wxWindow::FindFocus() == this);
	needImageUpdate = false;
	needImageUpdateWeak = true;

	// Init
	UpdateTimer.SetOwner(this,Audio_Update_Timer);
	GetClientSize(&w,&h);
	h -= Options.AsBool(_T("Audio Draw Timeline")) ? 20 : 0;
	SetSamplesPercent(50,false);

	// Set cursor
	//wxCursor cursor(wxCURSOR_BLANK);
	//SetCursor(cursor);

	//wxLog::SetActiveTarget(new wxLogWindow(NULL,_T("Log"),true,false));
}


//////////////
// Destructor
AudioDisplay::~AudioDisplay() {
	if (player) player->CloseStream();
	delete provider;
	delete player;
	delete origImage;
	delete spectrumRenderer;
	delete spectrumDisplay;
	delete spectrumDisplaySelected;
	delete peak;
	delete min;
	provider = NULL;
	player = NULL;
	origImage = NULL;
	spectrumRenderer = NULL;
	spectrumDisplay = NULL;
	spectrumDisplaySelected = NULL;
	peak = NULL;
	min = NULL;
}


/////////
// Reset
void AudioDisplay::Reset() {
	wxLogDebug(_T("AudioDisplay::Reset"));
	hasSel = false;
	diagUpdated = false;
	NeedCommit = false;
	karaoke->enabled = false;
	karaoke->syllables.clear();
	box->karaokeMode = false;
	box->KaraokeButton->SetValue(false);
	dialogue = NULL;
}


////////////////
// Update image
void AudioDisplay::UpdateImage(bool weak) {
	// Update samples
	UpdateSamples();

	// Set image as needing to be redrawn
	needImageUpdate = true;
	if (weak == false && needImageUpdateWeak == true) {
		needImageUpdateWeak = false;
	}
	Refresh(false);
}

void AudioDisplay::DoUpdateImage() {
	// Loaded?
	if (!loaded || !provider) return;

	// Needs updating?
	if (!needImageUpdate) return;
	bool weak = needImageUpdateWeak;

	// Prepare bitmap
	int timelineHeight = Options.AsBool(_T("Audio Draw Timeline")) ? 20 : 0;
	int displayH = h+timelineHeight;
	if (origImage) {
		if (origImage->GetWidth() != w || origImage->GetHeight() != displayH) {
			delete origImage;
			origImage = NULL;
		}
	}

	// Options
	bool draw_boundary_lines = Options.AsBool(_T("Audio Draw Secondary Lines"));
	bool draw_selection_background = Options.AsBool(_T("Audio Draw Selection Background"));
	bool drawKeyframes = Options.AsBool(_T("Audio Draw Keyframes"));

	// Invalid dimensions
	if (w == 0 || displayH == 0) return;

	// New bitmap
	if (!origImage) origImage = new wxBitmap(w,displayH,-1);

	// Is spectrum?
	bool spectrum = false;
	if (provider && Options.AsBool(_T("Audio Spectrum"))) {
		spectrum = true;
	}

	// Draw image to be displayed
	wxMemoryDC dc;
	dc.SelectObject(*origImage);

	// Black background
	dc.SetPen(*wxTRANSPARENT_PEN);
	dc.SetBrush(wxBrush(Options.AsColour(_T("Audio Background"))));
	dc.DrawRectangle(0,0,w,h);

	// Selection position
	hasSel = false;
	hasKaraoke = karaoke->enabled;
	selStart = 0;
	selEnd = 0;
	lineStart = 0;
	lineEnd = 0;
	selStartCap = 0;
	selEndCap = 0;
	int64_t drawSelStart = 0;
	int64_t drawSelEnd = 0;
	if (dialogue) {
		GetDialoguePos(lineStart,lineEnd,false);
		hasSel = true;
		if (hasKaraoke) {
			GetKaraokePos(selStartCap,selEndCap,true);
			GetKaraokePos(drawSelStart,drawSelEnd,false);
			selStart = lineStart;
			selEnd = lineEnd;
		}
		else {
			GetDialoguePos(selStartCap,selEndCap,true);
			selStart = lineStart;
			selEnd = lineEnd;
			drawSelStart = lineStart;
			drawSelEnd = lineEnd;
		}
	}

	// Draw selection bg
	if (hasSel && drawSelStart < drawSelEnd && draw_selection_background) {
		if (NeedCommit && !karaoke->enabled) dc.SetBrush(wxBrush(Options.AsColour(_T("Audio Selection Background Modified"))));
		else dc.SetBrush(wxBrush(Options.AsColour(_T("Audio Selection Background"))));
		dc.DrawRectangle(drawSelStart,0,drawSelEnd-drawSelStart,h);
	}

	// Draw spectrum
	if (spectrum) {
		DrawSpectrum(dc,weak);
	}

	// Waveform
	else if (provider) {
		DrawWaveform(dc,weak);
	}

	// Nothing
	else {
		dc.DrawLine(0,h/2,w,h/2);
	}

	// Draw seconds boundaries
	if (draw_boundary_lines) {
		int64_t start = Position*samples;
		int rate = provider->GetSampleRate();
		int pixBounds = rate / samples;
		dc.SetPen(wxPen(Options.AsColour(_T("Audio Seconds Boundaries")),1,wxDOT));
		if (pixBounds >= 8) {
			for (int x=0;x<w;x++) {
				if (((x*samples)+start) % rate < samples) {
					dc.DrawLine(x,0,x,h);
				}
			}
		}
	}

	// Draw current frame
	if (Options.AsBool(_T("Audio Draw Video Position"))) {
		if (VideoContext::Get()->IsLoaded()) {
			dc.SetPen(wxPen(Options.AsColour(_T("Audio Play Cursor")),2,wxLONG_DASH));
			int x = GetXAtMS(VFR_Output.GetTimeAtFrame(VideoContext::Get()->GetFrameN()));
			dc.DrawLine(x,0,x,h);
		}
	}

	// Draw keyframes
	if (drawKeyframes && VideoContext::Get()->KeyFramesLoaded()) {
		DrawKeyframes(dc);
	}

	// Draw previous line
	DrawInactiveLines(dc);

	if (hasSel) {
		// Draw boundaries
		if (true) {
			// Draw start boundary
			int selWidth = Options.AsInt(_T("Audio Line boundaries Thickness"));
			dc.SetPen(wxPen(Options.AsColour(_T("Audio Line boundary start"))));
			dc.SetBrush(wxBrush(Options.AsColour(_T("Audio Line boundary start"))));
			dc.DrawRectangle(lineStart-selWidth/2+1,0,selWidth,h);
			wxPoint points1[3] = { wxPoint(lineStart,0), wxPoint(lineStart+10,0), wxPoint(lineStart,10) };
			wxPoint points2[3] = { wxPoint(lineStart,h-1), wxPoint(lineStart+10,h-1), wxPoint(lineStart,h-11) };
			dc.DrawPolygon(3,points1);
			dc.DrawPolygon(3,points2);

			// Draw end boundary
			dc.SetPen(wxPen(Options.AsColour(_T("Audio Line boundary end"))));
			dc.SetBrush(wxBrush(Options.AsColour(_T("Audio Line boundary end"))));
			dc.DrawRectangle(lineEnd-selWidth/2+1,0,selWidth,h);
			wxPoint points3[3] = { wxPoint(lineEnd,0), wxPoint(lineEnd-10,0), wxPoint(lineEnd,10) };
			wxPoint points4[3] = { wxPoint(lineEnd,h-1), wxPoint(lineEnd-10,h-1), wxPoint(lineEnd,h-11) };
			dc.DrawPolygon(3,points3);
			dc.DrawPolygon(3,points4);
		}

		// Draw karaoke
		if (hasKaraoke) {
			try {
				// Prepare
				wxPen curPen(Options.AsColour(_T("Audio Syllable boundaries")),1,wxDOT);
				dc.SetPen(curPen);
				wxFont curFont(9,wxFONTFAMILY_DEFAULT,wxFONTSTYLE_NORMAL,wxFONTWEIGHT_BOLD,false,_T("Verdana"),wxFONTENCODING_SYSTEM);
				dc.SetFont(curFont);
				if (!spectrum) dc.SetTextForeground(Options.AsColour(_T("Audio Syllable text")));
				else dc.SetTextForeground(wxColour(255,255,255));
				size_t karn = karaoke->syllables.size();
				int64_t pos1,pos2;
				int len,curpos;
				wxCoord tw=0,th=0;
				AudioKaraokeSyllable *curSyl;
				wxString temptext;

				// Draw syllables
				for (size_t i=0;i<karn;i++) {
					curSyl = &karaoke->syllables.at(i);
					len = curSyl->duration*10;
					curpos = curSyl->start_time*10;
					if (len != -1) {
						pos1 = GetXAtMS(curStartMS+curpos);
						pos2 = GetXAtMS(curStartMS+len+curpos);
						dc.DrawLine(pos2,0,pos2,h);
						temptext = curSyl->text;
						temptext.Trim(true);
						temptext.Trim(false);
						GetTextExtent(temptext,&tw,&th,NULL,NULL,&curFont);
						dc.DrawText(temptext,(pos1+pos2-tw)/2,4);
					}
				}
			}
			catch (...) {
				// FIXME?
			}
		}
	}

	// Modified text
	if (NeedCommit) {
		dc.SetFont(wxFont(9,wxDEFAULT,wxFONTSTYLE_NORMAL,wxFONTWEIGHT_BOLD,false,_T("Verdana"))); // FIXME: hardcoded font name
		dc.SetTextForeground(wxColour(255,0,0));
		if (selStart <= selEnd) {
			dc.DrawText(_T("Modified"),4,4);
		}
		else {
			dc.DrawText(_T("Negative time"),4,4);
		}
	}

	// Draw timescale
	if (timelineHeight) {
		DrawTimescale(dc);
	}

	// Draw selection border
	if (hasFocus) {
		dc.SetPen(*wxGREEN_PEN);
		dc.SetBrush(*wxTRANSPARENT_BRUSH);
		dc.DrawRectangle(0,0,w,h);
	}

	// Done
	needImageUpdate = false;
	needImageUpdateWeak = true;
}


///////////////////////
// Draw Inactive Lines
void AudioDisplay::DrawInactiveLines(wxDC &dc) {
	// Check if there is anything to do
	int shadeType = Options.AsInt(_T("Audio Inactive Lines Display Mode"));
	if (shadeType == 0) return;

	// Spectrum?
	bool spectrum = false;
	if (provider && Options.AsBool(_T("Audio Spectrum"))) {
		spectrum = true;
	}

	// Set options
	dc.SetBrush(wxBrush(Options.AsColour(_T("Audio Line boundary inactive line"))));
	int selWidth = Options.AsInt(_T("Audio Line boundaries Thickness"));
	AssDialogue *shade;
	int shadeX1,shadeX2;
	int shadeFrom,shadeTo;

	// Only previous
	if (shadeType == 1) {
		shadeFrom = this->line_n-1;
		shadeTo = shadeFrom+1;
	}

	// All
	else {
		shadeFrom = 0;
		shadeTo = grid->GetRows();
	}
	
	for (int j=shadeFrom;j<shadeTo;j++) {
		if (j == line_n) continue;
		if (j < 0) continue;
		shade = grid->GetDialogue(j);

		if (shade) {
			// Get coordinates
			shadeX1 = GetXAtMS(shade->Start.GetMS());
			shadeX2 = GetXAtMS(shade->End.GetMS());
			if (shadeX2 < 0 || shadeX1 > w) continue;

			// Draw over waveform
			if (!spectrum) {
				// Selection
				int selX1 = MAX(0,GetXAtMS(curStartMS));
				int selX2 = MIN(w,GetXAtMS(curEndMS));

				// Get ranges (x1->x2, x3->x4).
				int x1 = MAX(0,shadeX1);
				int x2 = MIN(w,shadeX2);
				int x3 = MAX(x1,selX2);
				int x4 = MAX(x2,selX2);

				// Clip first range
				x1 = MIN(x1,selX1);
				x2 = MIN(x2,selX1);

				// Set pen and draw
				dc.SetPen(wxPen(Options.AsColour(_T("Audio Waveform Inactive"))));
				for (int i=x1;i<x2;i++) dc.DrawLine(i,peak[i],i,min[i]-1);
				for (int i=x3;i<x4;i++) dc.DrawLine(i,peak[i],i,min[i]-1);
			}

			// Draw boundaries
			dc.SetPen(wxPen(Options.AsColour(_T("Audio Line boundary inactive line"))));
			dc.DrawRectangle(shadeX1-selWidth/2+1,0,selWidth,h);
			dc.DrawRectangle(shadeX2-selWidth/2+1,0,selWidth,h);
		}
	}
}


//////////////////
// Draw keyframes
void AudioDisplay::DrawKeyframes(wxDC &dc) {
	wxArrayInt KeyFrames = VideoContext::Get()->GetKeyFrames();
	int nKeys = (int)KeyFrames.Count();
	dc.SetPen(wxPen(wxColour(255,0,255),1));

	// Get min and max frames to care about
	int minFrame = VFR_Output.GetFrameAtTime(GetMSAtX(0),true);
	int maxFrame = VFR_Output.GetFrameAtTime(GetMSAtX(w),true);

	// Scan list
	for (int i=0;i<nKeys;i++) {
		int cur = KeyFrames[i];
		if (cur >= minFrame && cur <= maxFrame) {
			int x = GetXAtMS(VFR_Output.GetTimeAtFrame(cur,true));
			dc.DrawLine(x,0,x,h);
		}
		else if (cur > maxFrame) break;
	}
}


//////////////////
// Draw timescale
void AudioDisplay::DrawTimescale(wxDC &dc) {
	// Set size
	int timelineHeight = Options.AsBool(_T("Audio Draw Timeline")) ? 20 : 0;

	// Set colours
	dc.SetBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
	dc.SetPen(*wxTRANSPARENT_PEN);
	dc.DrawRectangle(0,h,w,timelineHeight);
	dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT));
	dc.DrawLine(0,h,w,h);
	dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_3DHIGHLIGHT));
	dc.DrawLine(0,h+1,w,h+1);
	dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT));
	dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT));
	wxFont scaleFont;
	scaleFont.SetFaceName(_T("Tahoma")); // FIXME: hardcoded font name
	scaleFont.SetPointSize(8);
	dc.SetFont(scaleFont);

	// Timescale ticks
	int64_t start = Position*samples;
	int rate = provider->GetSampleRate();
	for (int i=1;i<32;i*=2) {
		int pixBounds = rate / (samples * 4 / i);
		if (pixBounds >= 8) {
			for (int x=0;x<w;x++) {
				int64_t pos = (x*samples)+start;
				// Second boundary
				if (pos % rate < samples) {
					dc.DrawLine(x,h+2,x,h+8);

					// Draw text
					wxCoord textW,textH;
					int hr = 0;
					int m = 0;
					int s = pos/rate;
					while (s >= 3600) {
						s -= 3600;
						hr++;
					}
					while (s >= 60) {
						s -= 60;
						m++;
					}
					wxString text;
					if (hr) text = wxString::Format(_T("%i:%02i:%02i"),hr,m,s);
					else if (m) text = wxString::Format(_T("%i:%02i"),m,s);
					else text = wxString::Format(_T("%i"),s);
					dc.GetTextExtent(text,&textW,&textH,NULL,NULL,&scaleFont);
					dc.DrawText(text,MAX(0,x-textW/2)+1,h+8);
				}

				// Other
				else if (pos % (rate / 4 * i) < samples) {
					dc.DrawLine(x,h+2,x,h+5);
				}
			}
			break;
		}
	}
}


////////////
// Waveform
void AudioDisplay::DrawWaveform(wxDC &dc,bool weak) {
	// Prepare Waveform
	if (!weak || peak == NULL || min == NULL) {
		if (peak) delete peak;
		if (min) delete min;
		peak = new int[w];
		min = new int[w];
	}

	// Get waveform
	if (!weak) {
		provider->GetWaveForm(min,peak,Position*samples,w,h,samples,scale);
	}

	// Draw pre-selection
	if (!hasSel) selStartCap = w;
	dc.SetPen(wxPen(Options.AsColour(_T("Audio Waveform"))));
	for (int64_t i=0;i<selStartCap;i++) {
		dc.DrawLine(i,peak[i],i,min[i]-1);
	}

	if (hasSel) {
		// Draw selection
		if (Options.AsBool(_T("Audio Draw Selection Background"))) {
			if (NeedCommit && !karaoke->enabled) dc.SetPen(wxPen(Options.AsColour(_T("Audio Waveform Modified"))));
			else dc.SetPen(wxPen(Options.AsColour(_T("Audio Waveform Selected"))));
		}
		for (int64_t i=selStartCap;i<selEndCap;i++) {
			dc.DrawLine(i,peak[i],i,min[i]-1);
		}

		// Draw post-selection
		dc.SetPen(wxPen(Options.AsColour(_T("Audio Waveform"))));
		for (int64_t i=selEndCap;i<w;i++) {
			dc.DrawLine(i,peak[i],i,min[i]-1);
		}
	}
}


//////////////////////////
// Draw spectrum analyzer
void AudioDisplay::DrawSpectrum(wxDC &finaldc,bool weak) {
	if (!weak || !spectrumDisplay || spectrumDisplay->GetWidth() != w || spectrumDisplay->GetHeight() != h) {
		if (spectrumDisplay) {
			delete spectrumDisplay;
			delete spectrumDisplaySelected;
			spectrumDisplay = 0;
			spectrumDisplaySelected = 0;
		}
		weak = false;
	}

	if (!weak) {
		if (!spectrumRenderer)
			spectrumRenderer = new AudioSpectrum(provider);

		spectrumRenderer->SetScaling(scale);

		unsigned char *img = (unsigned char *)malloc(h*w*3); // wxImage requires using malloc

		// Use a slightly slower, but simple way
		// Always draw the spectrum for the entire width
		// Hack: without those divs by 2 the display is horizontally compressed
		spectrumRenderer->RenderRange(Position*samples, (Position+w)*samples, false, img, 0, w, w, h);

		// The spectrum bitmap will have been deleted above already, so just make a new one
		wxImage imgobj(w, h, img, false);
		spectrumDisplay = new wxBitmap(imgobj);
	}

	if (hasSel && selStartCap < selEndCap && !spectrumDisplaySelected) {
		// There is a visible selection and we don't have a rendered one
		// This should be done regardless whether we're "weak" or not
		// Assume a few things were already set up when things were first rendered though
		unsigned char *img = (unsigned char *)malloc(h*w*3);
		spectrumRenderer->RenderRange(Position*samples, (Position+w)*samples, true, img, 0, w, w, h);
		wxImage imgobj(w, h, img, false);
		spectrumDisplaySelected = new wxBitmap(imgobj);
	}

	// Draw
	wxMemoryDC dc;
	dc.SelectObject(*spectrumDisplay);
	finaldc.Blit(0,0,w,h,&dc,0,0);

	if (hasSel && spectrumDisplaySelected && selStartCap < selEndCap) {
		dc.SelectObject(*spectrumDisplaySelected);
		finaldc.Blit(selStartCap, 0, selEndCap-selStartCap, h, &dc, selStartCap, 0);
	}
}

//////////////////////////
// Get selection position
void AudioDisplay::GetDialoguePos(int64_t &selStart,int64_t &selEnd, bool cap) {
	selStart = GetXAtMS(curStartMS);
	selEnd = GetXAtMS(curEndMS);

	if (cap) {
		if (selStart < 0) selStart = 0;
		if (selEnd < 0) selEnd = 0;
		if (selStart >= w) selStart = w-1;
		if (selEnd >= w) selEnd = w-1;
	}
}


////////////////////////
// Get karaoke position
void AudioDisplay::GetKaraokePos(int64_t &karStart,int64_t &karEnd, bool cap) {
	try {
		// Wrap around
		int nsyls = (int)karaoke->syllables.size();
		if (karaoke->curSyllable == -1) {
			karaoke->SetSyllable(nsyls-1);
		}
		if (karaoke->curSyllable >= nsyls) karaoke->curSyllable = nsyls-1;

		// Get positions
		int pos = karaoke->syllables.at(karaoke->curSyllable).start_time;
		int len = karaoke->syllables.at(karaoke->curSyllable).duration;
		karStart = GetXAtMS(curStartMS+pos*10);
		karEnd = GetXAtMS(curStartMS+pos*10+len*10);

		// Cap
		if (cap) {
			if (karStart < 0) karStart = 0;
			if (karEnd < 0) karEnd = 0;
			if (karStart >= w) karStart = w-1;
			if (karEnd >= w) karEnd = w-1;
		}
	}
	catch (...) {
	}
}


//////////
// Update
void AudioDisplay::Update() {
	if (blockUpdate) return;
	if (loaded) {
		if (Options.AsBool(_T("Audio Autoscroll")))
			MakeDialogueVisible();
		else
			UpdateImage(true);
	}
}


//////////////////////
// Recreate the image
void AudioDisplay::RecreateImage() {
	GetClientSize(&w,&h);
	h -= Options.AsBool(_T("Audio Draw Timeline")) ? 20 : 0;
	delete origImage;
	origImage = NULL;
	UpdateImage(false);
}


/////////////////////////
// Make dialogue visible
void AudioDisplay::MakeDialogueVisible(bool force) {
	wxLogDebug(_T("AudioDisplay::MakeDialogueVisible(force=%d)"), force?1:0);
	// Variables
	int temp1=0,temp2=0;
	if (karaoke->enabled) {
		// In karaoke mode the entire dialogue line should be visible instead of just the selected syllable
		GetTimesDialogue(temp1, temp2);
	} else {
		GetTimesSelection(temp1,temp2);
	}
	int startPos = GetSampleAtMS(temp1);
	int endPos = GetSampleAtMS(temp2);
	int startX = GetXAtMS(temp1);
	int endX = GetXAtMS(temp2);

	if (force || startX < 50 || endX > w-50) {
		if (startX < 50) {
			// Make sure the left edge of the selection is at least 50 pixels from the edge of the display
			UpdatePosition(startPos - 50*samples, true);
		} else {
			// Otherwise center the selection in display
			UpdatePosition((startPos+endPos-w*samples)/2,true);
		}
	}

	// Update
	UpdateImage();
}


////////////////
// Set position
void AudioDisplay::SetPosition(int pos) {
	wxLogDebug(_T("AudioDisplay::SetPosition(pos=%d)"), pos);
	Position = pos;
	PositionSample = pos * samples;
	UpdateImage();
}


///////////////////
// Update position
void AudioDisplay::UpdatePosition (int pos,bool IsSample) {
	// Safeguards
	if (!provider) return;
	if (IsSample) pos /= samples;
	int len = provider->GetNumSamples() / samples;
	if (pos < 0) pos = 0;
	if (pos >= len) pos = len-1;

	// Set
	Position = pos;
	PositionSample = pos*samples;
	UpdateScrollbar();
}


/////////////////////////////
// Set samples in percentage
// Note: aka Horizontal Zoom
void AudioDisplay::SetSamplesPercent(int percent,bool update,float pivot) {
	// Calculate
	if (percent < 1) percent = 1;
	if (percent > 100) percent = 100;
	if (samplesPercent == percent) return;
	samplesPercent = percent;

	// Update
	if (update) {
		// Center scroll
		int oldSamples = samples;
		UpdateSamples();
		PositionSample += int64_t((oldSamples-samples)*w*pivot);
		if (PositionSample < 0) PositionSample = 0;

		// Update
		UpdateSamples();
		UpdateScrollbar();
		UpdateImage();
		Refresh(false);
	}
}


//////////////////
// Update samples
void AudioDisplay::UpdateSamples() {
	// Set samples
	if (!provider) return;
	if (w) {
		int64_t totalSamples = provider->GetNumSamples();
		int total = totalSamples / w;
		int max = 5760000 / w;	// 2 minutes at 48 kHz maximum
		if (total > max) total = max;
		int min = 8;
		if (total < min) total = min;
		int range = total-min;
		samples = int(range*pow(samplesPercent/100.0,3)+min);

		// Set position
		int length = w * samples;
		if (PositionSample + length > totalSamples) {
			PositionSample = totalSamples - length;
			if (PositionSample < 0) PositionSample = 0;
			if (samples) Position = PositionSample / samples;
		}
	}
}


/////////////
// Set scale
void AudioDisplay::SetScale(float _scale) {
	if (scale == _scale) return;
	scale = _scale;
	UpdateImage();
}


//////////////////
// Load from file
void AudioDisplay::SetFile(wxString file) {
	wxLogDebug(_T("AudioDisplay::SetFile(file=%s)"), file.c_str());
	// Unload
	if (file.IsEmpty()) try {
		wxLogDebug(_T("AudioDisplay::SetFile: file is empty, just closing audio"));
		try {
			if (player) player->CloseStream();
		}
		catch (const wxChar *e) {
			wxLogError(e);
		}
		delete provider;
		delete player;
		delete spectrumRenderer;
		provider = NULL;
		player = NULL;
		spectrumRenderer = NULL;
		try {
			Reset();
		}
		catch (const wxChar *e) {
			wxLogError(e);
		}

		loaded = false;
		temporary = false;
		StandardPaths::SetPathValue(_T("?audio"),_T(""));
	}
	catch (wxString e) {
		wxLogError(e);
	}
	catch (const wxChar *e) {
		wxLogError(e);
	}
	catch (...) {
		wxLogError(_T("Unknown error unloading audio"));
	}

	// Load
	else {
		wxLogDebug(_T("AudioDisplay::SetFile: unloading old file"));
		SetFile(_T(""));
		try {
			// Get provider
			wxLogDebug(_T("AudioDisplay::SetFile: get audio provider"));
			bool is_dummy = false;
#ifdef _DEBUG
			if (file == _T("?dummy")) {
				is_dummy = true;
				provider = new DummyAudioProvider(150*60*1000, false); // 150 minutes non-noise
			} else if (file == _T("?noise")) {
				is_dummy = true;
				provider = new DummyAudioProvider(150*60*1000, true); // 150 minutes noise
			} else {
				provider = AudioProviderFactoryManager::GetAudioProvider(file);
			}
#else
			provider = AudioProviderFactoryManager::GetAudioProvider(file);
#endif

			// Get player
			wxLogDebug(_T("AudioDisplay::SetFile: get audio player"));
			player = AudioPlayerFactoryManager::GetAudioPlayer();
			player->SetDisplayTimer(&UpdateTimer);
			player->SetProvider(provider);
			player->OpenStream();
			loaded = true;

			// Add to recent
			if (!is_dummy) {
				wxLogDebug(_T("AudioDisplay::SetFile: add to recent"));
				Options.AddToRecentList(file,_T("Recent aud"));
				wxFileName fn(file);
				StandardPaths::SetPathValue(_T("?audio"),fn.GetPath());
			}

			// Update
			UpdateImage();
		}
		catch (const wxChar *e) {
			if (player) { delete player; player = 0; }
			if (provider) { delete provider; provider = 0; }
			wxLogError(e);
		}
		catch (wxString &err) {
			if (player) { delete player; player = 0; }
			if (provider) { delete provider; provider = 0; }
			wxLogDebug(_T("AudioDisplay::SetFile: gotcha!"));
			wxMessageBox(err,_T("Error loading audio"),wxICON_ERROR | wxOK);
		}
		catch (...) {
			if (player) { delete player; player = 0; }
			if (provider) { delete provider; provider = 0; }
			wxLogError(_T("Unknown error loading audio"));
		}
	}
	
	if (!loaded) return;

	assert(loaded == (provider != NULL));

	// Set default selection
	wxLogDebug(_T("AudioDisplay::SetFile: set default selection"));
	int n = grid->editBox->linen;
	SetDialogue(grid,grid->GetDialogue(n),n);
	wxLogDebug(_T("AudioDisplay::SetFile: returning"));
}


///////////////////
// Load from video
void AudioDisplay::SetFromVideo() {
	wxLogDebug(_T("AudioDisplay::SetFromVideo"));
	if (VideoContext::Get()->IsLoaded()) {
		wxString extension = VideoContext::Get()->videoName.Right(4);
		extension.LowerCase();

		if (extension != _T(".d2v")) SetFile(VideoContext::Get()->videoName);
	}
}


////////////////
// Reload audio
void AudioDisplay::Reload() {
	wxLogDebug(_T("AudioDisplay::Reload"));
	if (provider) SetFile(provider->GetFilename());
}


////////////////////
// Update scrollbar
void AudioDisplay::UpdateScrollbar() {
	if (!provider) return;
	int page = w/12;
	int len = provider->GetNumSamples() / samples / 12;
	Position = PositionSample / samples;
	ScrollBar->SetScrollbar(Position/12,page,len,int(page*0.7),true);
}


//////////////////////////////////////////////
// Gets the sample number at the x coordinate
int64_t AudioDisplay::GetSampleAtX(int x) {
	return (x+Position)*samples;
}


/////////////////////////////////////////////////
// Gets the x coordinate corresponding to sample
int AudioDisplay::GetXAtSample(int64_t n) {
	return samples ? (n/samples)-Position : 0;
}


/////////////////
// Get MS from X
int AudioDisplay::GetMSAtX(int64_t x) {
	return (PositionSample+(x*samples)) * 1000 / provider->GetSampleRate();
}


/////////////////
// Get X from MS
int AudioDisplay::GetXAtMS(int64_t ms) {
	return ((ms * provider->GetSampleRate() / 1000)-PositionSample)/samples;
}


////////////////////
// Get MS At sample
int AudioDisplay::GetMSAtSample(int64_t x) {
	return x * 1000 / provider->GetSampleRate();
}


////////////////////
// Get Sample at MS
int64_t AudioDisplay::GetSampleAtMS(int64_t ms) {
	return ms * provider->GetSampleRate() / 1000;
}


////////
// Play
void AudioDisplay::Play(int start,int end) {
	wxLogDebug(_T("AudioDisplay::Play"));
	Stop();

	// Check provider
	if (!provider) {
		wxLogDebug(_T("AudioDisplay::Play: no audio provider"));
		// Load temporary provider from video
		if (VideoContext::Get()->IsLoaded()) {
			wxLogDebug(_T("AudioDisplay::Play: has video provider"));
			try {
				// Get provider
				if (!VideoContext::Get()->videoName.StartsWith(_T("?dummy")))
					provider = AudioProviderFactoryManager::GetAudioProvider(VideoContext::Get()->videoName, 0);
				else
					return;

				// Get player
				player = AudioPlayerFactoryManager::GetAudioPlayer();
				player->SetDisplayTimer(&UpdateTimer);
				player->SetProvider(provider);
				player->OpenStream();
				temporary = true;
				wxLogDebug(_T("AudioDisplay::Play: got temp audio provider from video provider"));
			}
			catch (...) {
				wxLogDebug(_T("AudioDisplay::Play: exception getting audio provider from video, returning"));
				return;
			}
		}
		if (!provider) {
			wxLogDebug(_T("AudioDisplay::Play: has no provider, returning"));
			return;
		}
	}

	// Set defaults
	wxLogDebug(_T("AudioDisplay::Play: initialising playback"));
	int64_t num_samples = provider->GetNumSamples();
	start = GetSampleAtMS(start);
	if (end != -1) end = GetSampleAtMS(end);
	else end = num_samples-1;

	// Sanity checking
	if (start < 0) start = 0;
	if (start >= num_samples) start = num_samples-1;
	if (end < 0) end = 0;
	if (end >= num_samples) end = num_samples-1;
	if (end < start) end = start;

	// Call play
	player->Play(start,end-start);
	wxLogDebug(_T("AudioDisplay::Play: playback started, returning"));
}


////////
// Stop
void AudioDisplay::Stop() {
	wxLogDebug(_T("AudioDisplay::Stop"));
	if (VideoContext::Get()->IsPlaying()) VideoContext::Get()->Stop();
	if (player) player->Stop();
}


///////////////////////////
// Get samples of dialogue
void AudioDisplay::GetTimesDialogue(int &start,int &end) {
	wxLogDebug(_T("AudioDisplay::GetTimesDialogue"));
	if (!dialogue) {
		start = 0;
		end = 0;
		return;
	}

	start = dialogue->Start.GetMS();
	end = dialogue->End.GetMS();
}


////////////////////////////
// Get samples of selection
void AudioDisplay::GetTimesSelection(int &start,int &end) {
	wxLogDebug(_T("AudioDisplay::GetTimesSelection"));
	start = 0;
	end = 0;
	if (!dialogue) return;

	try {
		if (karaoke->enabled) {
			int pos = karaoke->syllables.at(karaoke->curSyllable).start_time;
			int len = karaoke->syllables.at(karaoke->curSyllable).duration;
			start = curStartMS+pos*10;
			end = curStartMS+pos*10+len*10;
		}
		else {
			start = curStartMS;
			end = curEndMS;
		}
	}
	catch (...) {}
}


/////////////////////////////
// Set the current selection
void AudioDisplay::SetSelection(int start, int end) {
	wxLogDebug(_T("AudioDisplay::SetSelection(start=%d, end=%d)"), start, end);
	curStartMS = start;
	curEndMS = end;
	Update();
}


////////////////
// Set dialogue
void AudioDisplay::SetDialogue(SubtitlesGrid *_grid,AssDialogue *diag,int n) {
	wxLogDebug(_T("AudioDisplay::SetDialogue"));
	// Actual parameters
	if (_grid) {
		wxLogDebug(_T("AudioDisplay::SetDialogue: has grid"));
		// Set variables
		grid = _grid;
		line_n = n;
		dialogue = diag;

		// Set flags
		diagUpdated = false;
		NeedCommit = false;

		// Set times
		if (dialogue && !dontReadTimes && Options.AsBool(_T("Audio grab times on select"))) {
			wxLogDebug(_T("AudioDisplay::SetDialogue: grabbing times"));
			int s = dialogue->Start.GetMS();
			int e = dialogue->End.GetMS();

			// Never do it for 0:00:00.00->0:00:00.00 lines
			if (s != 0 || e != 0) {
				curStartMS = s;
				curEndMS = e;
			}
		}
	}

	// Read karaoke data
	if (dialogue && karaoke->enabled) {
		wxLogDebug(_T("AudioDisplay::SetDialogue: in karaoke mode, loading new line into karaoke control"));
		NeedCommit = karaoke->LoadFromDialogue(dialogue);

		// Reset karaoke pos
		wxLogDebug(_T("AudioDisplay::SetDialogue: resetting karaoke position"));
		if (karaoke->curSyllable == -1) karaoke->SetSyllable((int)karaoke->syllables.size()-1);
		else karaoke->SetSyllable(0);
	}

	// Update
	Update();
	wxLogDebug(_T("AudioDisplay::SetDialogue: returning"));
}


//////////////////
// Commit changes
void AudioDisplay::CommitChanges (bool nextLine) {
	wxLogDebug(_T("AudioDisplay::CommitChanges(nextLine=%d)"), nextLine?1:0);
	// Loaded?
	if (!loaded) return;

	// Check validity
	bool wasKaraSplitting = false;
	bool validCommit = true;
	if (!karaoke->enabled && !karaoke->splitting) {
		if (!NeedCommit || curEndMS < curStartMS) validCommit = false;
	}

	// Update karaoke
	int karaSelStart = 0, karaSelEnd = -1;
	if (karaoke->enabled) {
		wxLogDebug(_T("AudioDisplay::CommitChanges: karaoke enabled, committing it"));
		wasKaraSplitting = karaoke->splitting;
		karaoke->Commit();
		// Get karaoke selection
		karaSelStart = karaoke->syllables.size();
		for (size_t k = 0; k < karaoke->syllables.size(); ++k) {
			if (karaoke->syllables[k].selected) {
				if ((signed)k < karaSelStart) karaSelStart = k;
				if ((signed)k > karaSelEnd) karaSelEnd = k;
			}
		}
		wxLogDebug(_T("AudioDisplay::CommitChanges: karaSelStart=%d karaSelEnd=%d"), karaSelStart, karaSelEnd);
	}
	
	// Commit ok?
	if (validCommit) {
		wxLogDebug(_T("AudioDisplay::CommitChanges: valid commit"));
		// Reset flags
		diagUpdated = false;
		NeedCommit = false;

		// Update dialogues
		blockUpdate = true;
		wxArrayInt sel = grid->GetSelection();
		int sels = (int)sel.Count();
		bool textNeedsCommit = grid->GetDialogue(sel[0])->Text != grid->editBox->TextEdit->GetText();
		AssDialogue *curDiag;
		for (int i=-1;i<sels;i++) {
			if (i == -1) curDiag = dialogue;
			else {
				curDiag = grid->GetDialogue(sel[i]);
				if (curDiag == dialogue) continue;
			}

			curDiag->Start.SetMS(curStartMS);
			curDiag->End.SetMS(curEndMS);
			if (!karaoke->enabled && textNeedsCommit) {
				// If user was editing karaoke stuff, that should take precedence of manual changes in the editbox,
				// so only update from editbox when not in kara mode
				curDiag->Text = grid->editBox->TextEdit->GetText();
			}
			curDiag->UpdateData();
		}

		// Update edit box
		wxLogDebug(_T("AudioDisplay::CommitChanges: updating time edit boxes"));
		grid->editBox->StartTime->Update();
		grid->editBox->EndTime->Update();
		grid->editBox->Duration->Update();

		// Update grid
		wxLogDebug(_T("AudioDisplay::CommitChanges: update grid"));
		grid->editBox->Update(!karaoke->enabled);
		grid->ass->FlagAsModified(_T(""));
		grid->CommitChanges();
		karaoke->SetSelection(karaSelStart, karaSelEnd);
		blockUpdate = false;
	}

	// Next line (ugh what a condition, can this be simplified?)
	if (nextLine && !karaoke->enabled && Options.AsBool(_T("Audio Next Line on Commit")) && !wasKaraSplitting) {
		wxLogDebug(_T("AudioDisplay::CommitChanges: going to next line"));
		// Insert a line if it doesn't exist
		int nrows = grid->GetRows();
		if (nrows == line_n + 1) {
			wxLogDebug(_T("AudioDisplay::CommitChanges: was on last line, inserting new"));
			AssDialogue *def = new AssDialogue;
			def->Start = grid->GetDialogue(line_n)->End;
			def->End = grid->GetDialogue(line_n)->End;
			def->End.SetMS(def->End.GetMS()+Options.AsInt(_T("Timing Default Duration")));
			def->Style = grid->GetDialogue(line_n)->Style;
			grid->InsertLine(def,line_n,true);
		}

		// Go to next
		dontReadTimes = true;
		Next();
		dontReadTimes = false;
		curStartMS = curEndMS;
		curEndMS = curStartMS + Options.AsInt(_T("Timing Default Duration"));
		NeedCommit = true;
	}

	Update();
	wxLogDebug(_T("AudioDisplay::CommitChanges: returning"));
}


////////////
// Add lead
void AudioDisplay::AddLead(bool in,bool out) {
	// Lead in
	if (in) {
		curStartMS -= Options.AsInt(_T("Audio Lead in"));
		if (curStartMS < 0) curStartMS = 0;
	}

	// Lead out
	if (out) {
		curEndMS += Options.AsInt(_T("Audio Lead out"));
	}

	// Set changes
	NeedCommit = true;
	if (Options.AsBool(_T("Audio Autocommit"))) CommitChanges();
	Update();
}


///////////////
// Event table
BEGIN_EVENT_TABLE(AudioDisplay, wxWindow)
    EVT_MOUSE_EVENTS(AudioDisplay::OnMouseEvent)
    EVT_PAINT(AudioDisplay::OnPaint)
	EVT_SIZE(AudioDisplay::OnSize)
	EVT_TIMER(Audio_Update_Timer,AudioDisplay::OnUpdateTimer)
	EVT_KEY_DOWN(AudioDisplay::OnKeyDown)
	EVT_SET_FOCUS(AudioDisplay::OnGetFocus)
	EVT_KILL_FOCUS(AudioDisplay::OnLoseFocus)
END_EVENT_TABLE()


/////////
// Paint
void AudioDisplay::OnPaint(wxPaintEvent& event) {
	if (w == 0 || h == 0) return;
	DoUpdateImage();

	wxPaintDC dc(this);
	dc.DrawBitmap(*origImage,0,0);
}


///////////////
// Mouse event
void AudioDisplay::OnMouseEvent(wxMouseEvent& event) {
	// Get x,y
	int64_t x = event.GetX();
	int64_t y = event.GetY();
	bool karMode = karaoke->enabled;
	bool shiftDown = event.m_shiftDown;
	int timelineHeight = Options.AsBool(_T("Audio Draw Timeline")) ? 20 : 0;

	// Leaving event
	if (event.Leaving()) {
		event.Skip();
		return;
	}

	// Is inside?
	bool inside = false;
	bool onScale = false;
	if (x >= 0 && y >= 0 && x < w) {
		if (y < h) {
			inside = true;

			// Get focus
			if (wxWindow::FindFocus() != this && Options.AsBool(_T("Audio Autofocus"))) SetFocus();
		}
		else if (y < h+timelineHeight) onScale = true;
	}

	// Buttons
	bool leftIsDown = event.ButtonIsDown(wxMOUSE_BTN_LEFT);
	bool rightIsDown = event.ButtonIsDown(wxMOUSE_BTN_RIGHT);
	bool buttonIsDown = leftIsDown || rightIsDown;
	bool leftClick = event.ButtonDown(wxMOUSE_BTN_LEFT);
	bool rightClick = event.ButtonDown(wxMOUSE_BTN_RIGHT);
	bool middleClick = event.Button(wxMOUSE_BTN_MIDDLE);
	bool buttonClick = leftClick || rightClick;
	bool defCursor = true;

	// Click type
	if (buttonClick && !holding) {
		holding = true;
		CaptureMouse();
	}
	if (!buttonIsDown && holding) {
		holding = false;
		if (HasCapture()) ReleaseMouse();
	}

	// Mouse wheel
	if (event.GetWheelRotation() != 0) {
		// Zoom or scroll?
		bool zoom = shiftDown;
		if (Options.AsBool(_T("Audio Wheel Default To Zoom"))) zoom = !zoom;

		// Zoom
		if (zoom) {
#ifdef __APPLE__
			// Reverse scroll directions on Apple... ugly hack
			// Otherwise left=right and right=left on systems that support four-way scrolling.
			int step = -event.GetWheelRotation() / event.GetWheelDelta();
#else
			int step = event.GetWheelRotation() / event.GetWheelDelta();
#endif
			int value = box->HorizontalZoom->GetValue()+step;
			box->HorizontalZoom->SetValue(value);
			SetSamplesPercent(value,true,float(x)/float(w));
		}

		// Scroll
		else {
			int step = -event.GetWheelRotation() * w / 360;
			UpdatePosition(Position+step,false);
			UpdateImage();
		}
	}

	// Cursor drawing
	if (!player->IsPlaying() && origImage) {
		// Draw bg
		wxClientDC dc(this);
		dc.DrawBitmap(*origImage,0,0);

		if (inside) {
			// Draw cursor
			dc.SetLogicalFunction(wxINVERT);
			dc.DrawLine(x,0,x,h);

			// Time
			if (Options.AsBool(_T("Audio Draw Cursor Time"))) {
				// Time string
				AssTime time;
				time.SetMS(GetMSAtX(x));
				wxString text = time.GetASSFormated();

				// Calculate metrics
				wxFont font(10,wxFONTFAMILY_DEFAULT,wxFONTSTYLE_NORMAL,wxFONTWEIGHT_BOLD,false,_T("Verdana"));
				dc.SetFont(font);
				int tw,th;
				GetTextExtent(text,&tw,&th,NULL,NULL,&font);

				// Set inversion
				bool left = true;
				if (x > w/2) left = false;

				// Text coordinates
				int dx;
				dx = x - tw/2;
				if (dx < 4) dx = 4;
				int max = w - tw - 4;
				if (dx > max) dx = max;
				int dy = 4;
				if (karMode) dy += th;

				// Draw text
				dc.SetTextForeground(wxColour(64,64,64));
				dc.DrawText(text,dx+1,dy-1);
				dc.DrawText(text,dx+1,dy+1);
				dc.DrawText(text,dx-1,dy-1);
				dc.DrawText(text,dx-1,dy+1);
				dc.SetTextForeground(wxColour(255,255,255));
				dc.DrawText(text,dx,dy);
			}
		}
	}

	// Scale dragging
	if ((hold == 0 && onScale) || draggingScale) {
		if (event.ButtonDown(wxMOUSE_BTN_LEFT)) {
			lastDragX = x;
			draggingScale = true;
		}
		else if (holding) {
			int delta = lastDragX - x;
			lastDragX = x;
			UpdatePosition(Position + delta);
			UpdateImage();
			Refresh(false);
			SetCursor(wxNullCursor);
			return;
		}
		else draggingScale = false;
	}

	// Outside
	if (!inside && hold == 0) return;

	// Left click
	if (leftClick) {
		SetFocus();
	}

	// Right click
	if (rightClick) {
		SetFocus();
		if (karaoke->enabled) {
			int syl = GetSyllableAtX(x);
			if (syl != -1) {
				int start = karaoke->syllables.at(syl).start_time * 10 + dialogue->Start.GetMS();
				int count = karaoke->syllables.at(syl).duration * 10;
				player->Play(GetSampleAtMS(start),GetSampleAtMS(count));
				//return;
			}
		}
	}

	// Middle click
	if (middleClick) {
		SetFocus();
		if (VideoContext::Get()->IsLoaded()) {
			VideoContext::Get()->JumpToTime(GetMSAtX(x),true);
		}
	}

	// Timing
	if (hasSel) {
		bool updated = false;
							
		// Grab start/end
		if (hold == 0) {
			bool gotGrab = false;
			bool karTime = karMode && !
#ifdef __APPLE__
				event.CmdDown();
#else
				event.ControlDown();
#endif

			// Line timing mode
			if (!karTime) {
				// Grab start
				if (abs64 (x - selStart) < 6 && Options.AsBool(_T("Disable Dragging Times"))==false) {
					wxCursor cursor(wxCURSOR_SIZEWE);
					SetCursor(cursor);
					defCursor = false;
					if (buttonClick) {
						hold = 1;
						gotGrab = true;
					}
				}

				// Grab end
				else if (abs64 (x - selEnd) < 6 && Options.AsBool(_T("Disable Dragging Times"))==false) {
					wxCursor cursor(wxCURSOR_SIZEWE);
					SetCursor(cursor);
					defCursor = false;
					if (buttonClick) {
						hold = 2;
						gotGrab = true;
					}
				}

				// Dragging nothing, time from scratch
				else {
					if (buttonClick) {
						if (leftClick) hold = 3;
						else hold = 2;
						lastX = x;
						gotGrab = true;
					}
				}
			}

			// Karaoke mode
			else {
				// Look for a syllable
				int64_t pos,len,curpos;
				AudioKaraokeSyllable *curSyl;
				size_t karn = karaoke->syllables.size();
				for (size_t i=0;i<karn;i++) {
					curSyl = &karaoke->syllables.at(i);
					len = curSyl->duration*10;
					curpos = curSyl->start_time*10;
					if (len != -1) {
						pos = GetXAtMS(curStartMS+len+curpos);

						// Grabbing syllable boundary
						if (abs64 (x - pos) < 4) {
							wxCursor cursor(wxCURSOR_SIZEWE);
							SetCursor(cursor);
							defCursor = false;
							if (event.LeftIsDown()) {
								hold = 4;
								holdSyl = (int)i;
								gotGrab = true;
							}
							break;
						}
					}
				}

				// No syllable found, select if possible
				if (hold == 0 && leftClick) {
					int syl = GetSyllableAtX(x);
					if (syl != -1) {
						karaoke->SetSyllable(syl);
						updated = true;
					}
				}
			}
		}

		// Drag start/end
		if (hold != 0) {
			// Dragging
			if (buttonIsDown) {
				// Drag from nothing or straight timing
				if (hold == 3 && buttonIsDown) {
					if (!karMode) {
						if (leftIsDown) curStartMS = GetMSAtX(x);
						else curEndMS = GetMSAtX(x);
						updated = true;
						diagUpdated = true;

						if (leftIsDown && abs((long)(x-lastX)) > Options.AsInt(_T("Audio Start Drag Sensitivity"))) {
							selStart = lastX;
							selEnd = x;
							curStartMS = GetBoundarySnap(GetMSAtX(lastX),10,event.ShiftDown(),true);
							curEndMS = GetMSAtX(x);
							hold = 2;
						}
					}
				}

				// Drag start
				if (hold == 1 && buttonIsDown) {
					// Set new value
					if (x != selStart) {
						int snapped = GetBoundarySnap(GetMSAtX(x),10,event.ShiftDown(),true);
						selStart = GetXAtMS(snapped);
						if (selStart > selEnd) {
							int temp = selStart;
							selStart = selEnd;
							selEnd = temp;
							hold = 2;
							curEndMS = snapped;
							snapped = GetMSAtX(selStart);
						}
						curStartMS = snapped;
						updated = true;
						diagUpdated = true;
					}
				}

				// Drag end
				if (hold == 2 && buttonIsDown) {
					// Set new value
					if (x != selEnd) {
						int snapped = GetBoundarySnap(GetMSAtX(x),10,event.ShiftDown(),false);
						selEnd = GetXAtMS(snapped);
						//selEnd = GetBoundarySnap(x,event.ShiftDown()?0:10,false);
						if (selStart > selEnd) {
							int temp = selStart;
							selStart = selEnd;
							selEnd = temp;
							hold = 1;
							curStartMS = snapped;
							snapped = GetMSAtX(selEnd);
						}
						curEndMS = snapped;
						updated = true;
						diagUpdated = true;
					}
				}

				// Drag karaoke
				if (hold == 4 && leftIsDown) {
					// Set new value
					int curpos,len,pos,nkar;
					AudioKaraokeSyllable *curSyl=NULL,*nextSyl=NULL;
					curSyl = &karaoke->syllables.at(holdSyl);
					nkar = (int)karaoke->syllables.size();
					if (holdSyl < nkar-1) {
						nextSyl = &karaoke->syllables.at(holdSyl+1);
					}
					curpos = curSyl->start_time;
					len = curSyl->duration;
					pos = GetXAtMS(curStartMS+(len+curpos)*10);
					if (x != pos) {
						// Calculate delta in centiseconds
						int delta = ((int64_t)(x-pos)*samples*100)/provider->GetSampleRate();

						// Apply delta
						int deltaMode = 0;
						if (shiftDown) deltaMode = 1;
						// else if (ctrlDown) deltaMode = 2;
						bool result = karaoke->SyllableDelta(holdSyl,delta,deltaMode);
						if (result) {
							updated = true;
							diagUpdated = true;
						}
					}
				}
			}

			// Release
			else {
				// Commit changes
				if (diagUpdated) {
					diagUpdated = false;
					NeedCommit = true;
					if (curStartMS <= curEndMS) {
						UpdateTimeEditCtrls();
						if (Options.AsBool(_T("Audio Autocommit"))) CommitChanges();
					}

					else UpdateImage(true);
				}

				// Update stuff
				SetCursor(wxNullCursor);
				hold = 0;
			}
		}

		// Update stuff
		if (updated) {
			if (diagUpdated) NeedCommit = true;
			if (karaoke->enabled) {
				AudioKaraokeSyllable &syl = karaoke->syllables[karaoke->curSyllable];
				player->SetEndPosition(GetSampleAtMS(curStartMS + (syl.start_time+syl.duration)*10));
			} else {
				player->SetEndPosition(GetSampleAtX(selEnd));
			}
			if (hold != 0) {
				wxCursor cursor(wxCURSOR_SIZEWE);
				SetCursor(cursor);
			}
			UpdateImage(true);
		}
	}

	// Not holding
	else {
		hold = 0;
	}

	// Restore cursor
	if (defCursor) SetCursor(wxNullCursor);
}


////////////////////////
// Get snap to boundary
int AudioDisplay::GetBoundarySnap(int ms,int rangeX,bool shiftHeld,bool start) {
	// Range?
	if (rangeX <= 0) return ms;

	// Convert range into miliseconds
	int rangeMS = rangeX*samples*1000 / provider->GetSampleRate();

	// Keyframe boundaries
	wxArrayInt boundaries;
	bool snapKey = Options.AsBool(_T("Audio snap to keyframes"));
	if (shiftHeld) snapKey = !snapKey;
	if (snapKey && VideoContext::Get()->KeyFramesLoaded() && Options.AsBool(_T("Audio Draw Keyframes"))) {
		int64_t keyMS;
		wxArrayInt keyFrames = VideoContext::Get()->GetKeyFrames();
		int frame;
		for (unsigned int i=0;i<keyFrames.Count();i++) {
			frame = keyFrames[i];
			if (!start) frame--;
			if (frame < 0) frame = 0;
			keyMS = VFR_Output.GetTimeAtFrame(frame,start);
			//if (start) keyX++;
			if (GetXAtMS(keyMS) >= 0 && GetXAtMS(keyMS) < w) boundaries.Add(keyMS);
		}
	}

	// Other subtitles' boundaries
	int inactiveType = Options.AsInt(_T("Audio Inactive Lines Display Mode"));
	bool snapLines = Options.AsBool(_T("Audio snap to other lines"));
	if (shiftHeld) snapLines = !snapLines;
	if (snapLines && (inactiveType == 1 || inactiveType == 2)) {
		AssDialogue *shade;
		int shadeX1,shadeX2;
		int shadeFrom,shadeTo;

		// Get range
		if (inactiveType == 1) {
			shadeFrom = this->line_n-1;
			shadeTo = shadeFrom+1;
		}
		else {
			shadeFrom = 0;
			shadeTo = grid->GetRows();
		}

		for (int j=shadeFrom;j<shadeTo;j++) {
			if (j == line_n) continue;
			shade = grid->GetDialogue(j);

			if (shade) {
				// Get coordinates
				shadeX1 = GetXAtMS(shade->Start.GetMS());
				shadeX2 = GetXAtMS(shade->End.GetMS());
				if (shadeX1 >= 0 && shadeX1 < w) boundaries.Add(shade->Start.GetMS());
				if (shadeX2 >= 0 && shadeX2 < w) boundaries.Add(shade->End.GetMS());
			}
		}
	}

	// See if ms falls within range of any of them
	int minDist = rangeMS+1;
	int bestMS = ms;
	for (unsigned int i=0;i<boundaries.Count();i++) {
		if (abs(ms-boundaries[i]) < minDist) {
			bestMS = boundaries[i];
			minDist = abs(ms-boundaries[i]);
		}
	}

	// Return best match
	return bestMS;
}


//
// SCRUBBING CODE, REMOVED FROM THE FUNCTION ABOVE
/*
	// Stop scrubbing
	bool scrubButton = false && event.ButtonIsDown(wxMOUSE_BTN_MIDDLE);
	if (scrubbing && !scrubButton) {
		// Release mouse
		scrubbing = false;
		if (HasCapture()) ReleaseMouse();

		// Stop player
		player->Stop();
		player->SetProvider(provider);
		delete scrubProvider;
	}

	// Start scrubbing
	if (!scrubbing && scrubButton && provider->GetChannels() == 1) {
		// Get mouse
		CaptureMouse();
		scrubbing = true;

		// Initialize provider
		player->Stop();
		scrubProvider = new StreamAudioProvider();
		scrubProvider->SetParams(provider->GetChannels(),provider->GetSampleRate(),provider->GetBytesPerSample());
		player->SetProvider(scrubProvider);

		// Set variables
		scrubLastPos = GetSampleAtX(x);
		scrubTime = clock();
		scrubLastRate = provider->GetSampleRate();
	}

	// Scrub
	if (scrubbing && scrubButton) {
		// Get current data
		int64_t exactPos = MAX(0,GetSampleAtX(x));
		int curScrubTime = clock();
		int scrubDeltaTime = curScrubTime - scrubTime;
		bool invert = exactPos < scrubLastPos;
		int64_t curScrubPos = exactPos;

		if (scrubDeltaTime > 0) {
			// Get derived data
			int rateChange = provider->GetSampleRate()/20;
			int curRate = MID(int(scrubLastRate-rateChange),abs(int(exactPos - scrubLastPos)) * CLOCKS_PER_SEC / scrubDeltaTime,int(scrubLastRate+rateChange));
			if (abs(curRate-scrubLastRate) < rateChange) curRate = scrubLastRate;
			curScrubPos = scrubLastPos + (curRate * scrubDeltaTime / CLOCKS_PER_SEC * (invert ? -1 : 1));
			int64_t scrubDelta = curScrubPos - scrubLastPos;
			scrubLastRate = curRate;

			// Copy data to buffer
			if (scrubDelta != 0) {
				// Create buffer
				int bufSize = scrubDeltaTime * scrubProvider->GetSampleRate() / CLOCKS_PER_SEC;
				short *buf = new short[bufSize];

				// Flag as inverted, if necessary
				if (invert) scrubDelta = -scrubDelta;

				// Copy data from original provider to temp buffer
				short *temp = new short[scrubDelta];
				provider->GetAudio(temp,MIN(curScrubPos,scrubLastPos),scrubDelta);

				// Scale
				float scale = float(double(scrubDelta) / double(bufSize));
				float start,end;
				int istart,iend;
				float tempfinal;
				for (int i=0;i<bufSize;i++) {
					start = i*scale;
					end = (i+1)*scale;
					istart = (int) start;
					iend = MIN((int) end,scrubDelta-1);
					if (istart == iend) tempfinal = temp[istart] * (end - start);
					else {
						tempfinal = temp[istart] * (1 + istart - start) + temp[iend] * (end - iend);
						for (int j=istart+1;j<iend;j++) tempfinal += temp[i];
					}
					buf[i] = tempfinal / scale;
				}
				//int len = MIN(bufSize,scrubDelta);
				//for (int i=0;i<len;i++) buf[i] = temp[i];
				//for (int i=len;i<bufSize;i++) buf[i] = 0;
				delete temp;

				// Invert
				if (invert) {
					short aux;
					for (int i=0;i<bufSize/2;i++) {
						aux = buf[i];
						buf[i] = buf[bufSize-i-1];
						buf[bufSize-i-1] = aux;
					}
				}

				// Send data to provider
				scrubProvider->Append(buf,bufSize);
				if (!player->IsPlaying()) player->Play(0,~0ULL);
				delete buf;
			}
		}

		// Update last pos and time
		scrubLastPos = curScrubPos;
		scrubTime = curScrubTime;

		// Return
		return;
	}

*/


//////////////
// Size event
void AudioDisplay::OnSize(wxSizeEvent &event) {
	// Set size
	GetClientSize(&w,&h);
	h -= Options.AsBool(_T("Audio Draw Timeline")) ? 20 : 0;

	// Update image
	UpdateSamples();
	if (samples) {
		UpdatePosition(PositionSample / samples);
	}
	UpdateImage();
	
	// Update scrollbar
	UpdateScrollbar();
}


///////////////
// Timer event
void AudioDisplay::OnUpdateTimer(wxTimerEvent &event) {
	if (!origImage)
		return;

	// Get lock and check if it's OK
	if (player->GetMutex()) {
		wxMutexLocker locker(*player->GetMutex());
		if (!locker.IsOk()) return;
	}
		
	if (!player->IsPlaying()) return;

	// Get DCs
	//wxMutexGuiEnter();
	wxClientDC dc(this);

	// Draw cursor
	int curpos = -1;
	if (player->IsPlaying()) {
		int64_t curPos = player->GetCurrentPosition();
		if (curPos > player->GetStartPosition() && curPos < player->GetEndPosition()) {
			// Scroll if needed
			int posX = GetXAtSample(curPos);
			bool fullDraw = false;
			bool centerLock = false;
			bool scrollToCursor = Options.AsBool(_T("Audio lock scroll on cursor"));
			if (centerLock) {
				int goTo = MAX(0,curPos - w*samples/2);
				if (goTo >= 0) {
					UpdatePosition(goTo,true);
					UpdateImage();
					fullDraw = true;
				}
			}
			else {
				if (scrollToCursor) {
					if (posX < 80 || posX > w-80) {
						int goTo = MAX(0,curPos - 80*samples);
						if (goTo >= 0) {
							UpdatePosition(goTo,true);
							UpdateImage();
							fullDraw = true;
						}
					}
				}
			}

			// Draw cursor
			wxMemoryDC src;
			curpos = GetXAtSample(curPos);
			if (curpos >= 0 && curpos < GetClientSize().GetWidth()) {
				dc.SetPen(wxPen(Options.AsColour(_T("Audio Play cursor"))));
				src.SelectObject(*origImage);
				if (fullDraw) {
					//dc.Blit(0,0,w,h,&src,0,0);
					dc.DrawLine(curpos,0,curpos,h);
					//dc.Blit(0,0,curpos-10,h,&src,0,0);
					//dc.Blit(curpos+10,0,w-curpos-10,h,&src,curpos+10,0);
				}
				else {
					dc.Blit(oldCurPos,0,1,h,&src,oldCurPos,0);
					dc.DrawLine(curpos,0,curpos,h);
				}
			}
		}
		else {
			if (curPos > player->GetEndPosition() + 8192) {
				player->Stop();
			}
			wxMemoryDC src;
			src.SelectObject(*origImage);
			dc.Blit(oldCurPos,0,1,h,&src,oldCurPos,0);
		}
	}

	// Restore background
	else {
		wxMemoryDC src;
		src.SelectObject(*origImage);
		dc.Blit(oldCurPos,0,1,h,&src,oldCurPos,0);
	}
	oldCurPos = curpos;
}


////////////
// Key down
void AudioDisplay::OnKeyDown(wxKeyEvent &event) {
	int key = event.GetKeyCode();
#ifdef __APPLE__
	Hotkeys.SetPressed(key,event.m_metaDown,event.m_altDown,event.m_shiftDown);
#else
	Hotkeys.SetPressed(key,event.m_controlDown,event.m_altDown,event.m_shiftDown);
#endif

	// Accept
	if (Hotkeys.IsPressed(_T("Audio Commit"))) {
		CommitChanges(true);
		//ChangeLine(1);
	}

	// Accept (SSA's "Grab times")
	if (Hotkeys.IsPressed(_T("Audio Commit Alt"))) {
		CommitChanges(true);
	}

	// Accept (stay)
	if (Hotkeys.IsPressed(_T("Audio Commit (Stay)"))) {
		CommitChanges();
	}

	// Previous
	if (Hotkeys.IsPressed(_T("Audio Prev Line")) || Hotkeys.IsPressed(_T("Audio Prev Line Alt"))) {
		Prev();
	}

	// Next
	if (Hotkeys.IsPressed(_T("Audio Next Line")) || Hotkeys.IsPressed(_T("Audio Next Line Alt"))) {
		Next();
	}

	// Play
	if (Hotkeys.IsPressed(_T("Audio Play")) || Hotkeys.IsPressed(_T("Audio Play Alt"))) {
		int start=0,end=0;
		GetTimesSelection(start,end);
		Play(start,end);
	}

	// Play/Stop
	if (Hotkeys.IsPressed(_T("Audio Play or Stop"))) {
		if (player->IsPlaying()) Stop();
		else {
			int start=0,end=0;
			GetTimesSelection(start,end);
			Play(start,end);
		}
	}

	// Stop
	if (Hotkeys.IsPressed(_T("Audio Stop"))) {
		Stop();
	}

	// Increase length
	if (Hotkeys.IsPressed(_T("Audio Karaoke Increase Len"))) {
		if (karaoke->enabled) {
			bool result = karaoke->SyllableDelta(karaoke->curSyllable,1,0);
			if (result) diagUpdated = true;
		}
	}

	// Increase length (shift)
	if (Hotkeys.IsPressed(_T("Audio Karaoke Increase Len Shift"))) {
		if (karaoke->enabled) {
			bool result = karaoke->SyllableDelta(karaoke->curSyllable,1,1);
			if (result) diagUpdated = true;
		}
	}

	// Decrease length
	if (Hotkeys.IsPressed(_T("Audio Karaoke Decrease Len"))) {
		if (karaoke->enabled) {
			bool result = karaoke->SyllableDelta(karaoke->curSyllable,-1,0);
			if (result) diagUpdated = true;
		}
	}

	// Decrease length (shift)
	if (Hotkeys.IsPressed(_T("Audio Karaoke Decrease Len Shift"))) {
		if (karaoke->enabled) {
			bool result = karaoke->SyllableDelta(karaoke->curSyllable,-1,1);
			if (result) diagUpdated = true;
		}
	}

	// Move backwards
	if (Hotkeys.IsPressed(_T("Audio Scroll Left"))) {
		UpdatePosition(Position-128,false);
		UpdateImage();
	}

	// Move forward
	if (Hotkeys.IsPressed(_T("Audio Scroll Right"))) {
		UpdatePosition(Position+128,false);
		UpdateImage();
	}

	// Play first 500 ms
	if (Hotkeys.IsPressed(_T("Audio Play First 500ms"))) {
		int start=0,end=0;
		GetTimesSelection(start,end);
		int e = start+500;
		if (e > end) e = end;
		Play(start,e);
	}

	// Play last 500 ms
	if (Hotkeys.IsPressed(_T("Audio Play Last 500ms"))) {
		int start=0,end=0;
		GetTimesSelection(start,end);
		int s = end-500;
		if (s < start) s = start;
		Play(s,end);
	}

	// Play 500 ms before
	if (Hotkeys.IsPressed(_T("Audio Play 500ms Before"))) {
		int start=0,end=0;
		GetTimesSelection(start,end);
		Play(start-500,start);
	}

	// Play 500 ms after
	if (Hotkeys.IsPressed(_T("Audio Play 500ms After"))) {
		int start=0,end=0;
		GetTimesSelection(start,end);
		Play(end,end+500);
	}

	// Play to end of file
	if (Hotkeys.IsPressed(_T("Audio Play To End"))) {
		int start=0,end=0;
		GetTimesSelection(start,end);
		Play(start,-1);
	}

	// Play original line
	if (Hotkeys.IsPressed(_T("Audio Play Original Line"))) {
		int start=0,end=0;
		GetTimesDialogue(start,end);
		SetSelection(start, end);
		Play(start,end);
	}

	// Lead in
	if (Hotkeys.IsPressed(_T("Audio Add Lead In"))) {
		AddLead(true,false);
	}

	// Lead out
	if (Hotkeys.IsPressed(_T("Audio Add Lead Out"))) {
		AddLead(false,true);
	}

	// Update
	if (diagUpdated) {
		diagUpdated = false;
		NeedCommit = true;
		if (Options.AsBool(_T("Audio Autocommit")) && curStartMS <= curEndMS) CommitChanges();
		else UpdateImage(true);
	}
}


///////////////
// Change line
void AudioDisplay::ChangeLine(int delta) {
	wxLogDebug(_T("AudioDisplay::ChangeLine(delta=%d)"), delta);
	if (dialogue) {
		wxLogDebug(_T("AudioDisplay::ChangeLine: has dialogue"));
		// Get next line number and make sure it's within bounds
		int next = line_n+delta;
		if (next == -1) next = 0;
		if (next == grid->GetRows()) next = grid->GetRows() - 1;
		wxLogDebug(_T("AudioDisplay::ChangeLine: next=%i"), next);

		// Set stuff
		NeedCommit = false;
		dialogue = NULL;
		grid->editBox->SetToLine(next);
		grid->SelectRow(next);
		grid->MakeCellVisible(next,0,true);
		if (!dialogue) UpdateImage(true);
		else UpdateImage(false);
		line_n = next;
	}
	wxLogDebug(_T("AudioDisplay::ChangeLine: returning"));
}


////////
// Next
void AudioDisplay::Next(bool play) {
	wxLogDebug(_T("AudioDisplay::Next"));
	// Karaoke
	if (karaoke->enabled) {
		wxLogDebug(_T("AudioDisplay::Next: karaoke enables, going to next syllable"));
		int nextSyl = karaoke->curSyllable+1;
		bool needsUpdate = true;

		// Last syllable; jump to next
		if (nextSyl >= (signed int)karaoke->syllables.size()) {
			wxLogDebug(_T("AudioDisplay::Next: last syllable on line"));
			// Already last?
			if (line_n == grid->GetRows()-1) return;

			if (NeedCommit) {
				wxLogDebug(_T("AudioDisplay::Next: uncommitted karaoke changes"));
				int result = wxMessageBox(_("Do you want to commit your changes? If you choose No, they will be discarded."),_("Commit?"),wxYES_NO | wxCANCEL | wxICON_QUESTION);
				//int result = wxNO;
				if (result == wxYES) {
					CommitChanges();
				}
				else if (result == wxCANCEL) {
					wxLogDebug(_T("AudioDisplay::Next: cancelled, returning"));
					karaoke->curSyllable = (int)karaoke->syllables.size()-1;
					return;
				}
			}
			wxLogDebug(_T("AudioDisplay::Next: going to next line"));
			nextSyl = 0;
			karaoke->curSyllable = 0;
			ChangeLine(1);
			needsUpdate = false;
		}

		// Set syllable
		wxLogDebug(_T("AudioDisplay::Next: set syllable"));
		karaoke->SetSyllable(nextSyl);
		if (needsUpdate) Update();
		int start=0,end=0;
		GetTimesSelection(start,end);
		if (play) Play(start,end);
	}

	// Plain mode
	else {
		wxLogDebug(_T("AudioDisplay::Next: going to next line"));
		ChangeLine(1);
	}

	wxLogDebug(_T("AudioDisplay::Next: returning"));
}


////////////
// Previous
void AudioDisplay::Prev(bool play) {
	wxLogDebug(_T("AudioDisplay::Prev"));
	// Karaoke
	if (karaoke->enabled) {
		wxLogDebug(_T("AudioDisplay::Prev: karaoke enabled, going to prev syllable"));
		int nextSyl = karaoke->curSyllable-1;
		bool needsUpdate = true;

		// First syllable; jump line
		if (nextSyl < 0) {
			wxLogDebug(_T("AudioDisplay::Prev: prev syllable on prev line"));
			// Already first?
			if (line_n == 0) return;

			if (NeedCommit) {
				wxLogDebug(_T("AudioDisplay::Prev: uncommitted karaoke changes"));
				int result = wxMessageBox(_("Do you want to commit your changes? If you choose No, they will be discarded."),_("Commit?"),wxYES_NO | wxCANCEL);
				if (result == wxYES) {
					CommitChanges();
				}
				else if (result == wxCANCEL) {
					karaoke->curSyllable = 0;
					wxLogDebug(_T("AudioDisplay::Prev: cancelled, returning"));
					return;
				}
			}
			wxLogDebug(_T("AudioDisplay::Prev: going to prev line"));
			karaoke->curSyllable = -1;
			ChangeLine(-1);
			needsUpdate = false;
		}

		// Set syllable
		wxLogDebug(_T("AudioDisplay::Prev: set syllable"));
		karaoke->SetSyllable(nextSyl);
		if (needsUpdate) Update();
		int start=0,end=0;
		GetTimesSelection(start,end);
		if (play) Play(start,end);
	}

	// Plain mode
	else {
		wxLogDebug(_T("AudioDisplay::Prev: going to prev line"));
		ChangeLine(-1);
	}

	wxLogDebug(_T("AudioDisplay::Prev: returning"));
}


///////////////////////////////
// Gets syllable at x position
int AudioDisplay::GetSyllableAtX(int x) {
	if (!karaoke->enabled) return -1;
	int ms = GetMSAtX(x);
	size_t syllables = karaoke->syllables.size();;
	int sylstart,sylend;

	// Find a matching syllable
	for (size_t i=0;i<syllables;i++) {
		sylstart = karaoke->syllables.at(i).start_time*10 + curStartMS;
		sylend = karaoke->syllables.at(i).duration*10 + sylstart;
		if (ms >= sylstart && ms < sylend) {
			return (int)i;
		}
	}
	return -1;
}


////////////////
// Focus events
void AudioDisplay::OnGetFocus(wxFocusEvent &event) {
	if (!hasFocus) {
		hasFocus = true;
		UpdateImage(true);
	}
}

void AudioDisplay::OnLoseFocus(wxFocusEvent &event) {
	if (hasFocus && loaded) {
		hasFocus = false;
		UpdateImage(true);
		Refresh(false);
	}
}


//////////////////////////////
// Update time edit controls
void AudioDisplay::UpdateTimeEditCtrls() {
	grid->editBox->StartTime->SetTime(curStartMS,true);
	grid->editBox->EndTime->SetTime(curEndMS,true);
	grid->editBox->Duration->SetTime(curEndMS-curStartMS,true);
}