Skip to content
Extraits de code Groupes Projets
Sélectionner une révision Git
  • 80e55ef8ba5b51eff6477d465b4326d94388ebf0
  • master par défaut protégée
  • rust-playlist-sync
  • rust
  • fix-qt-deprecated-qvariant-type
  • fix-mpris-qtwindow-race-condition
  • rust-appimage-wayland
  • windows-build-rebased
  • v2.5 protégée
  • v2.4 protégée
  • v2.3-1 protégée
  • v2.3 protégée
  • v2.2 protégée
  • v2.1 protégée
  • v2.0 protégée
  • v1.8-3 protégée
  • v1.8-2 protégée
  • v1.8-1 protégée
  • v1.8 protégée
  • v1.7 protégée
  • v1.6 protégée
  • v1.5 protégée
  • v1.4 protégée
  • v1.3 protégée
  • v1.2 protégée
  • v1.1 protégée
  • v1.0 protégée
27 résultats

configure.ac

Blame
  • audio_display.cpp 60,02 Kio
    // 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);
    }