Merge pull request #506 from andrewcsmith/patch-2
[supercollider.git] / QtCollider / widgets / soundfileview / view.cpp
blobcfc48ceb3820b85e7db3a14b949b6675be9a2ee9
1 /************************************************************************
3 * Copyright 2010-2012 Jakob Leben (jakob.leben@gmail.com)
5 * This file is part of SuperCollider Qt GUI.
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ************************************************************************/
22 #include "view.hpp"
23 #include "../../QcWidgetFactory.h"
25 #include <QGridLayout>
26 #include <QPainter>
27 #include <QApplication>
28 #include <QPaintEvent>
29 #include <QCursor>
31 #include <climits>
32 #include <cmath>
33 #include <cstring>
35 QC_DECLARE_QWIDGET_FACTORY(QcWaveform);
37 const int kMaxRawFrames = 300000;
38 const int kMaxFramesPerCacheUnit = 128;
40 QcWaveform::QcWaveform( QWidget * parent ) : QWidget( parent ),
41 sf(0),
43 _rangeBeg(0),
44 _rangeDur(0),
45 _rangeEnd(0),
47 _cache(0),
49 _curSel(0),
51 _showCursor(false),
52 _cursorEditable(true),
53 _cursorPos(0),
55 _showGrid(true),
56 _gridResolution(1.0),
57 _gridOffset(0.0),
59 _beg(0.0),
60 _dur(0.0),
61 _yZoom(1.f),
63 pixmap(0),
64 _bkgColor( QColor(0,0,0) ),
65 _cursorColor( QColor(255,0,0) ),
66 _gridColor( QColor(100,100,200) ),
67 _peakColor( QColor(242,178,0) ),
68 _rmsColor( QColor(255,255,0) ),
69 dirty(false),
70 _drawWaveform(true)
72 memset( &sfInfo, 0, sizeof(SF_INFO) );
74 setFocusPolicy( Qt::StrongFocus );
75 setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
76 setAttribute( Qt::WA_OpaquePaintEvent, true );
79 QcWaveform::~QcWaveform()
81 delete _cache;
82 delete pixmap;
83 if( sf ) sf_close( sf );
86 void QcWaveform::load( const QString& filename )
88 qcDebugMsg( 1, "QcWaveform::load( filename )" );
90 SF_INFO new_info;
91 memset( &new_info, 0, sizeof(SF_INFO) );
93 SNDFILE *new_sf = sf_open( filename.toStdString().c_str(), SFM_READ, &new_info );
95 if( !new_sf ) {
96 qcErrorMsg(QString("Could not open soundfile: ") + filename);
97 return;
100 doLoad( new_sf, new_info, 0, new_info.frames );
103 void QcWaveform::load( const QString& filename, int beg, int dur )
105 qcDebugMsg( 1, "QcWaveform::load( filename, beg, dur )" );
107 SF_INFO new_info;
108 memset( &new_info, 0, sizeof(SF_INFO) );
110 SNDFILE *new_sf = sf_open( filename.toStdString().c_str(), SFM_READ, &new_info );
112 if( !new_sf ) {
113 qcErrorMsg(QString("Could not open soundfile: ") + filename);
114 return;
117 doLoad( new_sf, new_info, beg, dur );
120 void QcWaveform::load( const QVector<double> & data, int offset, int ch, int sr )
122 qcDebugMsg( 1, "QcWaveform::load( data, offset, channels )" );
124 if( ch < 1 ) {
125 qcWarningMsg( "QSoundFileView: invalid number of channels!" );
126 return;
129 int ns = data.count();
130 int nf = ns / ch;
132 if( nf * ch != ns ) {
133 qcWarningMsg( "QSoundFileView: size of data not a multiple of channel count!" );
134 return;
137 if( offset < 0 || nf - offset < 1 ) {
138 qcWarningMsg( "QSoundFileView: invalid range of data!" );
139 return;
142 delete _cache;
143 if( sf ) sf_close( sf );
144 sf = 0;
146 SF_INFO new_info;
147 memset( &new_info, 0, sizeof(SF_INFO) );
148 new_info.channels = ch;
149 new_info.samplerate = sr;
150 sfInfo = new_info;
152 _beg = _rangeBeg = 0;
153 _dur = _rangeDur = _rangeEnd = nf - offset;
155 updateFPP();
157 _cache = new SoundCacheStream();
158 connect( _cache, SIGNAL(loadingDone()), this, SIGNAL(loadingDone()) );
159 connect( _cache, SIGNAL(loadingDone()), this, SLOT(redraw()) );
161 _cache->load( data, _rangeDur, offset, ch );
164 void QcWaveform::allocate ( int frames, int ch, int sr )
166 if( ch < 1 ) {
167 qcWarningMsg( "QSoundFileView: invalid number of channels!" );
168 return;
171 delete _cache;
172 if( sf ) sf_close( sf );
173 sf = 0;
175 SF_INFO new_info;
176 memset( &new_info, 0, sizeof(SF_INFO) );
177 new_info.channels = ch;
178 new_info.samplerate = sr;
179 sfInfo = new_info;
181 _beg = _rangeBeg = 0;
182 _dur = _rangeDur = _rangeEnd = frames;
184 updateFPP();
186 _cache = new SoundCacheStream();
187 _cache->allocate( frames, ch );
189 redraw();
192 void QcWaveform::write( const QVector<double> & data, int offset )
194 if( sf ) {
195 qcWarningMsg( "QSoundFileView: can not write data while displaying a sound file!" );
196 return;
199 if( !_cache || !_cache->ready() ) {
200 qcWarningMsg( "QSoundFileView: can not write data; memory has not been allocated yet!" );
201 return;
204 int ch = sfInfo.channels;
205 int ns = data.size();
206 int nf = ns / ch;
208 if( nf * ch != ns ) {
209 qcWarningMsg( "QSoundFileView: can not write data; size not a multiple of channels!" );
210 return;
213 if( offset < 0 || offset + nf > _rangeEnd ) {
214 qcWarningMsg( "QSoundFileView: can not write data; either offset is wrong or data size is too large." );
215 return;
218 _cache->write( data, offset, nf );
220 redraw();
223 void QcWaveform::doLoad( SNDFILE *new_sf, const SF_INFO &new_info, sf_count_t beg, sf_count_t dur )
225 // set up soundfile to scale data in range [-1,1] to int range
226 // when reading floating point data as int
227 sf_command( new_sf, SFC_SET_SCALE_FLOAT_INT_READ, NULL, SF_TRUE );
229 // check beginning and duration validity
231 if( beg < 0 || dur < 1 || beg + dur > new_info.frames ) {
232 qcErrorMsg("Invalid beginning and/or duration.");
233 sf_close( new_sf );
234 return;
237 // cleanup previous state
239 // NOTE we have to delete SoundCacheStream before closing the soundfile, as it might be still
240 // loading it
241 // TODO: should SoundCacheStream open the soundfile on its own?
243 delete _cache;
244 if( sf ) sf_close( sf );
246 sf = new_sf;
247 sfInfo = new_info;
248 _beg = _rangeBeg = beg;
249 _dur = _rangeDur = dur;
250 _rangeEnd = _rangeBeg + _rangeDur;
252 updateFPP();
254 _cache = new SoundCacheStream();
255 connect( _cache, SIGNAL(loadProgress(int)),
256 this, SIGNAL(loadProgress(int)) );
257 connect( _cache, SIGNAL(loadProgress(int)),
258 this, SLOT(update()) );
259 connect( _cache, SIGNAL(loadingDone()), this, SIGNAL(loadingDone()) );
260 connect( _cache, SIGNAL(loadingDone()), this, SLOT(redraw()) );
262 _cache->load( sf, sfInfo, beg, dur, kMaxFramesPerCacheUnit, kMaxRawFrames );
264 redraw();
267 float QcWaveform::loadProgress()
269 return _cache ? _cache->loadProgress() : 1.f;
272 float QcWaveform::zoom()
274 // NOTE We have limited _rangeDur to 1 minimum.
275 return _dur / _rangeDur;
278 float QcWaveform::xZoom()
280 return ( sfInfo.samplerate ? _dur / sfInfo.samplerate : 0 );
283 float QcWaveform::yZoom()
285 return _yZoom;
288 VariantList QcWaveform::selections() const
290 VariantList slist;
291 for( int i = 0; i < 64; ++i ) {
292 slist.data << QVariant::fromValue<VariantList>( selection(i) );
294 return slist;
297 void QcWaveform::setCurrentSelection( int i ) {
298 if( i < 0 || i > 63 ) return;
299 _curSel = i;
300 update();
303 VariantList QcWaveform::selection( int i ) const
305 VariantList l;
306 if( i < 0 || i > 63 ) return l;
307 const Selection &s = _selections[i];
308 l.data << QVariant(static_cast<int>(s.start - _rangeBeg));
309 l.data << QVariant(static_cast<int>(s.size));
310 return l;
313 void QcWaveform::setSelection( int i, sf_count_t a, sf_count_t b )
315 if( i < 0 || i > 63 ) return;
316 Selection& s = _selections[i];
317 s.start = qMin( a, b );
318 s.size = qMax( a, b ) - s.start;
319 update();
322 void QcWaveform::setSelection( int i, VariantList l )
324 if( l.data.count() < 2 ) return;
325 sf_count_t start = l.data[0].toInt() + _rangeBeg;
326 sf_count_t end = start + l.data[1].toInt();
327 setSelection( i, start, end );
330 void QcWaveform::setSelectionStart( int i, sf_count_t frame )
332 if( i < 0 || i > 63 ) return;
333 Selection& s = _selections[i];
334 sf_count_t frame2 = s.start + s.size;
335 s.start = qMin( frame, frame2 );
336 s.size = qMax( frame, frame2 ) - s.start;
337 update();
340 void QcWaveform::setSelectionEnd( int i, sf_count_t frame )
342 if( i < 0 || i > 63 ) return;
343 Selection& s = _selections[i];
344 sf_count_t frame2 = s.start;
345 s.start = qMin( frame, frame2 );
346 s.size = qMax( frame, frame2 ) - s.start;
347 update();
350 void QcWaveform::setSelectionEditable( int i, bool editable )
352 if( i < 0 || i > 63 ) return;
353 _selections[i].editable = editable;
354 update();
357 void QcWaveform::setSelectionColor( int i, const QColor &c )
359 if( i < 0 || i > 63 ) return;
360 _selections[i].color = c;
361 update();
364 VariantList QcWaveform::waveColors() const
366 VariantList clist;
367 Q_FOREACH( QColor clr, _waveColors )
368 clist.data << QVariant(clr);
369 return clist;
372 void QcWaveform::setWaveColors( const VariantList &list )
374 _waveColors.clear();
375 Q_FOREACH( QVariant var, list.data )
376 _waveColors << var.value<QColor>();
377 redraw();
380 void QcWaveform::zoomTo( double z )
382 z = qMax( 0.0, qMin( 1.0, z ) );
384 _dur = qMax( _rangeDur * z, 1.0 );
386 //printf("dur: %Li view: %Li\n", sfInfo.frames, _dur);
387 if( _beg + _dur > _rangeEnd ) _beg = _rangeEnd - _dur;
389 updateFPP();
390 redraw();
393 void QcWaveform::zoomBy( double factor )
395 zoomTo( zoom() * factor );
398 void QcWaveform::zoomAllOut()
400 _beg = _rangeBeg;
401 _dur = _rangeDur;
402 updateFPP();
403 redraw();
406 void QcWaveform::scrollTo( double startFrame )
408 _beg = qBound( (double)_rangeBeg, startFrame, _rangeEnd - _dur );
409 redraw();
412 void QcWaveform::scrollBy( double f )
414 scrollTo( _beg + f );
417 float QcWaveform::scrollPos()
419 double scrollRange = _rangeDur - _dur;
420 return scrollRange > 0.0 ? (_beg - _rangeBeg) / scrollRange : 0.f;
422 void QcWaveform::setScrollPos( double fraction )
424 scrollTo( fraction * (_rangeDur - _dur) + _rangeBeg );
427 void QcWaveform::scrollToStart()
429 scrollTo( _rangeBeg );
432 void QcWaveform::scrollToEnd()
434 scrollTo( _rangeEnd - _dur );
437 void QcWaveform::setXZoom( double seconds )
439 // NOTE We have limited _rangeDur to 1 minimum.
440 double frac = seconds * sfInfo.samplerate / _rangeDur;
441 zoomTo( frac );
444 void QcWaveform::setYZoom( double factor )
446 _yZoom = factor;
447 redraw();
450 void QcWaveform::zoomSelection( int i )
452 if( i < 0 || i > 63 ) return;
454 Selection &s = _selections[i];
456 if( s.start >= _rangeEnd || s.size < 1 || s.start + s.size <= _rangeBeg )
457 return;
459 _beg = qMax( s.start, _rangeBeg );
460 double end = qMin( s.start + s.size, _rangeEnd );
461 _dur = end - _beg;
463 // clear the selection
464 s.size = 0;
466 updateFPP();
467 redraw();
470 void QcWaveform::resizeEvent( QResizeEvent * )
472 delete pixmap;
473 pixmap = new QPixmap( size() );
474 updateFPP();
475 redraw();
478 void QcWaveform::paintEvent( QPaintEvent *ev )
480 Q_ASSERT( pixmap != 0 );
482 QPainter p( this );
484 // if loading draw progress
485 if( _cache && _cache->loading() ) {
486 QRect r( rect() );
487 p.fillRect( r, QColor(100,100,100) );
488 r.setRight( r.right() * _cache->loadProgress() / 100 );
489 p.fillRect( r, QColor( 0, 0, 0 ) );
490 p.setPen( QColor(255,255,255) );
491 QTextOption opt;
492 opt.setAlignment( Qt::AlignCenter );
493 p.drawText( rect(), "loading...", opt );
494 return;
497 p.fillRect( rect(), _bkgColor );
499 // draw waveform on pixmap
501 if( _drawWaveform && dirty ) {
502 draw( pixmap, 0, width(), _beg, _dur );
503 dirty = false;
506 // draw selections
508 p.save();
510 p.scale( (double) width() / _dur, 1.0 );
511 p.translate( _beg * -1.0, 0.0 );
512 p.setPen( Qt::NoPen );
514 int i;
515 for( i = 0; i < 64; ++i ) {
516 const Selection &s = _selections[i];
517 if( s.size > 0 ) {
518 QRectF r ( s.start, 0, s.size, height() );
519 p.setBrush( s.color );
520 p.drawRect( r );
524 p.restore();
526 // draw time grid
528 if( _showGrid && sfInfo.samplerate > 0 && _gridResolution > 0.f ) {
529 p.save();
531 double durSecs = (double) _dur / sfInfo.samplerate;
532 double begSecs = (double) _beg / sfInfo.samplerate;
534 p.scale( width() / durSecs, 1.0 );
535 p.setPen( _gridColor );
537 double offset = _gridOffset - begSecs;
538 offset -= ( floor( offset / _gridResolution ) * _gridResolution );
539 while( offset < durSecs ) {
540 p.drawLine( QLineF( offset, 0.0, offset, height() ) );
541 offset += _gridResolution;
544 p.restore();
547 // paste the waveform pixmap on screen
549 if( _drawWaveform )
550 p.drawPixmap( ev->rect(), *pixmap, ev->rect() );
552 // draw cursor
554 if( _showCursor && _cursorPos >= _beg && _cursorPos < _beg + _dur ) {
555 double cursorX = (_cursorPos - _beg) / _fpp;
556 p.setPen( _cursorColor );
557 p.drawLine( QLineF( cursorX, 0, cursorX, height() ) );
561 void QcWaveform::keyPressEvent( QKeyEvent *ev )
563 if( ev->key() == Qt::Key_Shift && _dragAction == Navigate )
565 _dragPoint = mapFromGlobal(QCursor::pos());
566 _dragData2 = zoom();
568 else
569 ev->ignore();
572 void QcWaveform::mousePressEvent( QMouseEvent *ev )
574 _dragAction = NoDragAction;
575 _dragPoint = ev->pos();
576 _dragFrame = ev->pos().x() * _fpp + _beg;
578 Qt::KeyboardModifiers mods = ev->modifiers();
579 Qt::MouseButton btn = ev->button();
580 #ifdef Q_OS_MAC
581 Qt::KeyboardModifier CTRL = Qt::MetaModifier;
582 #else
583 Qt::KeyboardModifier CTRL = Qt::ControlModifier;
584 #endif
586 if( btn == Qt::LeftButton ) {
588 if( (mods & Qt::ShiftModifier) && ( mods & CTRL ) ) {
589 _dragFrame = _selections[_curSel].start;
590 _dragAction = MoveSelection;
592 else if( mods & Qt::ShiftModifier ) {
593 _dragAction = Select;
594 const Selection &s = _selections[_curSel];
595 if( _dragFrame < s.start + (s.size*0.5) ) {
596 setSelectionStart( _curSel, _dragFrame );
597 _dragFrame = s.start + s.size;
599 else {
600 setSelectionEnd( _curSel, _dragFrame );
601 _dragFrame = s.start;
603 Q_EMIT( action() );
605 else {
606 if( !(mods & CTRL) ) {
607 _dragAction = Select;
608 _selections[_curSel].start = _dragFrame;
609 _selections[_curSel].size = 0;
610 update();
611 Q_EMIT( action() );
613 if( _cursorEditable ) {
614 _cursorPos = _dragFrame;
615 if( mods & CTRL ) _dragAction = MoveCursor;
616 update();
617 Q_EMIT( metaAction() );
622 else if( btn == Qt::RightButton ) {
623 _dragAction = Navigate;
624 _dragData = ev->pos().x() * _fpp + _beg;
625 _dragData2 = zoom();
629 void QcWaveform::mouseDoubleClickEvent ( QMouseEvent * )
631 setSelection( _curSel, _rangeBeg, _rangeEnd );
632 Q_EMIT( action() );
635 void QcWaveform::mouseMoveEvent( QMouseEvent *ev )
637 if( !ev->buttons() ) return;
639 if( _dragAction == Navigate ) {
640 Qt::KeyboardModifiers mods = ev->modifiers();
641 if( mods & Qt::ShiftModifier ) {
642 double factor = pow( 2, (ev->pos().y() - _dragPoint.y()) * 0.008 );
643 // zoom to the initial zoom times the factor based on distance from initial position
644 zoomTo( _dragData2 * factor );
646 // scroll to the clicked frame minus the current mouse position in frames
647 // _fpp has been adjusted by zooming
648 scrollTo( _dragData - (ev->pos().x() * _fpp) );
650 else if( _dragAction == Select ) {
651 sf_count_t frame = qBound( 0, ev->pos().x(), width() ) * _fpp + _beg;
652 setSelection( _curSel, _dragFrame, frame );
653 update();
654 Q_EMIT( action() );
656 else if( _dragAction == MoveSelection ) {
657 double dpos = ev->pos().x() - _dragPoint.x();
658 Selection &s = _selections[_curSel];
659 s.start = _dragFrame + (dpos * _fpp);
660 s.start = qBound( _rangeBeg, s.start, _rangeEnd - s.size );
661 update();
662 Q_EMIT( action() );
664 else if( _dragAction == MoveCursor ) {
665 _cursorPos = qBound( 0, ev->pos().x(), width() ) * _fpp + _beg;
666 update();
667 Q_EMIT( metaAction() );
671 void QcWaveform::rebuildCache ( int maxFPU, int maxRawFrames )
675 void QcWaveform::draw( QPixmap *pix, int x, int width, double f_beg, double f_dur )
677 // FIXME anomaly: when _fpp reaching 1.0 rms can go outside min-max!
679 pix->fill( QColor( 0, 0, 0, 0 ) );
681 QPainter p( pix );
683 if( !_cache || !_cache->ready() ) return;
685 // check for sane situation:
686 if( f_beg < _rangeBeg || f_beg + f_dur > _rangeEnd ) return;
688 // data indexes
689 sf_count_t i_beg = floor(f_beg); // data beginning;
690 sf_count_t i_count = ceil( f_beg + f_dur ) - i_beg; // data count;
691 bool haveOneMore;
692 if( i_beg + i_count < _rangeEnd ) {
693 ++i_count;
694 haveOneMore = true;
696 else {
697 haveOneMore = false;
700 // data source - choose according to horiz. zoom (data-display resolution)
701 bool canUseCache = _fpp < 1.0 ? _cache->fpu() == 1.0 : _fpp >= _cache->fpu();
703 SoundStream *soundStream;
704 SoundFileStream sfStream;
706 if( canUseCache ) {
707 qcDebugMsg( 2, QString("using cache") );
708 soundStream = _cache;
710 else if( sf ) {
711 qcDebugMsg( 2, QString("using file") );
712 soundStream = &sfStream;
713 sfStream.load( sf, sfInfo, i_beg, i_count );
715 else {
716 qcWarningMsg( "QSoundFileView: can't paint waveform: view resolution exceeds data cache resolution,"
717 " and soundfile is not given." );
718 return;
721 // geometry
722 float spacing = pix->height() * 0.15f / (sfInfo.channels + 1);
723 float chHeight = pix->height() * 0.85f / (float) sfInfo.channels;
724 float yScale = -chHeight / 65535.f * _yZoom;
725 //spacing /= yscale;
727 // initial painter setup
728 QPen minMaxPen;
729 QPen rmsPen;
731 float halfChH = chHeight * 0.5;
732 p.translate( 0.f, halfChH + spacing );
734 int waveColorN = _waveColors.count();
735 int ch;
736 for( ch = 0; ch < soundStream->channels(); ++ch ) {
738 if( ch < waveColorN && _waveColors[ch].isValid() ) {
739 QColor clr( _waveColors[ch] );
740 rmsPen.setColor( clr );
741 minMaxPen.setColor( clr.darker( 140 ) );
743 else {
744 rmsPen.setColor( _rmsColor );
745 minMaxPen.setColor( _peakColor );
748 // draw center line
749 p.setPen( QColor(90,90,90) );
750 p.drawLine( x, 0, x + width, 0 );
752 // draw bounding lines
753 p.setPen( QColor(100,100,100) );
754 p.drawLine( x, halfChH, x+width, halfChH );
755 p.drawLine( x, -halfChH, x+width, -halfChH );
757 p.save();
759 p.setClipping(true);
760 p.setClipRect( x, -halfChH, x+width, chHeight );
761 p.scale( 1.f, yScale );
763 if( _fpp > 1.0 ) {
765 // draw min-max regions and RMS
767 short minBuffer[width];
768 short maxBuffer[width];
769 short minRMS[width];
770 short maxRMS[width];
772 bool ok = soundStream->displayData( ch, f_beg, f_dur,
773 minBuffer, maxBuffer,
774 minRMS, maxRMS,
775 width );
776 // printf("integration ok: %i\n", ok);
777 Q_ASSERT( ok );
779 int i;
780 for( i = 0; i < width; ++i ) {
781 short min = minBuffer[i];
782 short max = maxBuffer[i];
783 if( max != min ) {
784 p.setPen( minMaxPen );
785 p.drawLine( x + i, min, x + i, max );
787 else {
788 p.setPen( minMaxPen );
789 p.drawPoint( x + i, min );
791 p.setPen( rmsPen );
792 p.drawLine( x + i, minRMS[i], x + i, maxRMS[i] );
795 else {
797 // draw lines between actual values
799 qreal ppf = 1.0 / _fpp;
800 qreal dx = (i_beg - f_beg) * ppf;
802 bool interleaved = false;
803 short *data = soundStream->rawFrames( ch, i_beg, i_count, &interleaved );
804 //printf("got raw frames ok: %i\n", data != 0 );
805 Q_ASSERT( data != 0 );
806 int step = interleaved ? soundStream->channels() : 1;
808 QPainterPath path;
810 if( i_count ) {
811 QPointF pt( dx, (qreal) *data );
812 path.moveTo( pt );
814 int f; // frame
815 for( f = 1; f < i_count; ++f ) {
816 data += step;
817 QPointF pt( f * ppf + dx, (qreal) *data );
818 path.lineTo( pt );
820 if( i_count && !haveOneMore ) {
821 path.lineTo( QPointF( f * ppf + dx, (qreal)*data ) );
824 p.setPen( rmsPen );
825 p.drawPath( path );
828 p.restore();
830 p.translate( 0.f, chHeight + spacing );