From 8e8f68552ff1bf4811c31df841db6f5af4d4da01 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 21 Jan 2024 12:25:00 +0100 Subject: [PATCH] Disentangle and clean the classes of the Audio File Processor (#7069) ## Extract views Extract the classes `AudioFileProcessorWaveView` and `AudioFileProcessorView` into their own files. Reformat the new classes by removing unnecessary whitespace and underscores. Add spaces to if-statements. Cleanup the includes. ## Remove friend relationship Remove the friend relationship between `AudioFileProcessor` and `AudioFileProcessorView`. Introduce getters for entities that the view is interested in. --- .../AudioFileProcessor/AudioFileProcessor.cpp | 852 +----------------- .../AudioFileProcessor/AudioFileProcessor.h | 216 +---- .../AudioFileProcessorView.cpp | 286 ++++++ .../AudioFileProcessorView.h | 85 ++ .../AudioFileProcessorWaveView.cpp | 540 +++++++++++ .../AudioFileProcessorWaveView.h | 181 ++++ plugins/AudioFileProcessor/CMakeLists.txt | 2 +- 7 files changed, 1110 insertions(+), 1052 deletions(-) create mode 100644 plugins/AudioFileProcessor/AudioFileProcessorView.cpp create mode 100644 plugins/AudioFileProcessor/AudioFileProcessorView.h create mode 100644 plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp create mode 100644 plugins/AudioFileProcessor/AudioFileProcessorWaveView.h diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index f50a6df689..7cc5ee3fa6 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -23,37 +23,21 @@ */ #include "AudioFileProcessor.h" +#include "AudioFileProcessorView.h" -#include - -#include -#include -#include -#include - -#include "AudioEngine.h" -#include "ComboBox.h" -#include "ConfigManager.h" -#include "DataFile.h" -#include "Engine.h" -#include "gui_templates.h" #include "InstrumentTrack.h" -#include "NotePlayHandle.h" #include "PathUtil.h" -#include "PixmapButton.h" #include "SampleLoader.h" -#include "SampleWaveform.h" #include "Song.h" -#include "StringPairDrag.h" -#include "Clipboard.h" -#include "embed.h" #include "plugin_export.h" +#include + + namespace lmms { - extern "C" { @@ -441,834 +425,6 @@ void AudioFileProcessor::pointChanged() } - - -namespace gui -{ - - - - -AudioFileProcessorView::AudioFileProcessorView( Instrument * _instrument, - QWidget * _parent ) : - InstrumentViewFixedSize( _instrument, _parent ) -{ - m_openAudioFileButton = new PixmapButton( this ); - m_openAudioFileButton->setCursor( QCursor( Qt::PointingHandCursor ) ); - m_openAudioFileButton->move( 227, 72 ); - m_openAudioFileButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( - "select_file" ) ); - m_openAudioFileButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( - "select_file" ) ); - connect( m_openAudioFileButton, SIGNAL( clicked() ), - this, SLOT( openAudioFile() ) ); - m_openAudioFileButton->setToolTip(tr("Open sample")); - - m_reverseButton = new PixmapButton( this ); - m_reverseButton->setCheckable( true ); - m_reverseButton->move( 164, 105 ); - m_reverseButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( - "reverse_on" ) ); - m_reverseButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( - "reverse_off" ) ); - m_reverseButton->setToolTip(tr("Reverse sample")); - -// loop button group - - auto m_loopOffButton = new PixmapButton(this); - m_loopOffButton->setCheckable( true ); - m_loopOffButton->move( 190, 105 ); - m_loopOffButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( - "loop_off_on" ) ); - m_loopOffButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( - "loop_off_off" ) ); - m_loopOffButton->setToolTip(tr("Disable loop")); - - auto m_loopOnButton = new PixmapButton(this); - m_loopOnButton->setCheckable( true ); - m_loopOnButton->move( 190, 124 ); - m_loopOnButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( - "loop_on_on" ) ); - m_loopOnButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( - "loop_on_off" ) ); - m_loopOnButton->setToolTip(tr("Enable loop")); - - auto m_loopPingPongButton = new PixmapButton(this); - m_loopPingPongButton->setCheckable( true ); - m_loopPingPongButton->move( 216, 124 ); - m_loopPingPongButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( - "loop_pingpong_on" ) ); - m_loopPingPongButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( - "loop_pingpong_off" ) ); - m_loopPingPongButton->setToolTip(tr("Enable ping-pong loop")); - - m_loopGroup = new automatableButtonGroup( this ); - m_loopGroup->addButton( m_loopOffButton ); - m_loopGroup->addButton( m_loopOnButton ); - m_loopGroup->addButton( m_loopPingPongButton ); - - m_stutterButton = new PixmapButton( this ); - m_stutterButton->setCheckable( true ); - m_stutterButton->move( 164, 124 ); - m_stutterButton->setActiveGraphic( PLUGIN_NAME::getIconPixmap( - "stutter_on" ) ); - m_stutterButton->setInactiveGraphic( PLUGIN_NAME::getIconPixmap( - "stutter_off" ) ); - m_stutterButton->setToolTip( - tr( "Continue sample playback across notes" ) ); - - m_ampKnob = new Knob( KnobType::Bright26, this ); - m_ampKnob->setVolumeKnob( true ); - m_ampKnob->move( 5, 108 ); - m_ampKnob->setHintText( tr( "Amplify:" ), "%" ); - - m_startKnob = new AudioFileProcessorWaveView::knob( this ); - m_startKnob->move( 45, 108 ); - m_startKnob->setHintText( tr( "Start point:" ), "" ); - - m_endKnob = new AudioFileProcessorWaveView::knob( this ); - m_endKnob->move( 125, 108 ); - m_endKnob->setHintText( tr( "End point:" ), "" ); - - m_loopKnob = new AudioFileProcessorWaveView::knob( this ); - m_loopKnob->move( 85, 108 ); - m_loopKnob->setHintText( tr( "Loopback point:" ), "" ); - -// interpolation selector - m_interpBox = new ComboBox( this ); - m_interpBox->setGeometry( 142, 62, 82, ComboBox::DEFAULT_HEIGHT ); - m_interpBox->setFont( pointSize<8>( m_interpBox->font() ) ); - -// wavegraph - m_waveView = 0; - newWaveView(); - - connect( castModel(), SIGNAL( isPlaying( lmms::f_cnt_t ) ), - m_waveView, SLOT( isPlaying( lmms::f_cnt_t ) ) ); - - qRegisterMetaType( "lmms::f_cnt_t" ); - - setAcceptDrops( true ); -} - - - - - - - - -void AudioFileProcessorView::dragEnterEvent( QDragEnterEvent * _dee ) -{ - // For mimeType() and MimeType enum class - using namespace Clipboard; - - if( _dee->mimeData()->hasFormat( mimeType( MimeType::StringPair ) ) ) - { - QString txt = _dee->mimeData()->data( - mimeType( MimeType::StringPair ) ); - if( txt.section( ':', 0, 0 ) == QString( "clip_%1" ).arg( - static_cast(Track::Type::Sample) ) ) - { - _dee->acceptProposedAction(); - } - else if( txt.section( ':', 0, 0 ) == "samplefile" ) - { - _dee->acceptProposedAction(); - } - else - { - _dee->ignore(); - } - } - else - { - _dee->ignore(); - } -} - - - - -void AudioFileProcessorView::newWaveView() -{ - if ( m_waveView ) - { - delete m_waveView; - m_waveView = 0; - } - m_waveView = new AudioFileProcessorWaveView(this, 245, 75, &castModel()->m_sample); - m_waveView->move( 2, 172 ); - m_waveView->setKnobs( - dynamic_cast( m_startKnob ), - dynamic_cast( m_endKnob ), - dynamic_cast( m_loopKnob ) ); - m_waveView->show(); -} - - - - -void AudioFileProcessorView::dropEvent( QDropEvent * _de ) -{ - const auto type = StringPairDrag::decodeKey(_de); - const auto value = StringPairDrag::decodeValue(_de); - - if (type == "samplefile") { castModel()->setAudioFile(value); } - else if (type == QString("clip_%1").arg(static_cast(Track::Type::Sample))) - { - DataFile dataFile(value.toUtf8()); - castModel()->setAudioFile(dataFile.content().firstChild().toElement().attribute("src")); - } - else - { - _de->ignore(); - return; - } - - m_waveView->updateSampleRange(); - Engine::getSong()->setModified(); - _de->accept(); -} - -void AudioFileProcessorView::paintEvent( QPaintEvent * ) -{ - QPainter p( this ); - - static auto s_artwork = PLUGIN_NAME::getIconPixmap("artwork"); - p.drawPixmap(0, 0, s_artwork); - - auto a = castModel(); - - QString file_name = ""; - - int idx = a->m_sample.sampleFile().length(); - - p.setFont( pointSize<8>( font() ) ); - - QFontMetrics fm( p.font() ); - - // simple algorithm for creating a text from the filename that - // matches in the white rectangle - while( idx > 0 && - fm.size( Qt::TextSingleLine, file_name + "..." ).width() < 210 ) - { - file_name = a->m_sample.sampleFile()[--idx] + file_name; - } - - if( idx > 0 ) - { - file_name = "..." + file_name; - } - - p.setPen( QColor( 255, 255, 255 ) ); - p.drawText( 8, 99, file_name ); -} - - - - -void AudioFileProcessorView::sampleUpdated() -{ - m_waveView->updateSampleRange(); - m_waveView->update(); - update(); -} - - - - - -void AudioFileProcessorView::openAudioFile() -{ - QString af = SampleLoader::openAudioFile(); - if (af.isEmpty()) { return; } - - castModel()->setAudioFile(af); - Engine::getSong()->setModified(); - m_waveView->updateSampleRange(); -} - - - - -void AudioFileProcessorView::modelChanged() -{ - auto a = castModel(); - connect(a, &AudioFileProcessor::sampleUpdated, this, &AudioFileProcessorView::sampleUpdated); - m_ampKnob->setModel( &a->m_ampModel ); - m_startKnob->setModel( &a->m_startPointModel ); - m_endKnob->setModel( &a->m_endPointModel ); - m_loopKnob->setModel( &a->m_loopPointModel ); - m_reverseButton->setModel( &a->m_reverseModel ); - m_loopGroup->setModel( &a->m_loopModel ); - m_stutterButton->setModel( &a->m_stutterModel ); - m_interpBox->setModel( &a->m_interpolationModel ); - sampleUpdated(); -} - - - - -void AudioFileProcessorWaveView::updateSampleRange() -{ - if (m_sample->sampleSize() > 1) - { - const f_cnt_t marging = (m_sample->endFrame() - m_sample->startFrame()) * 0.1; - m_from = qMax(0, m_sample->startFrame() - marging); - m_to = qMin(m_sample->endFrame() + marging, m_sample->sampleSize()); - } -} - -AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget * _parent, int _w, int _h, Sample* buf) : - QWidget( _parent ), - m_sample(buf), - m_graph( QPixmap( _w - 2 * s_padding, _h - 2 * s_padding ) ), - m_from( 0 ), - m_to(m_sample->sampleSize()), - m_last_from( 0 ), - m_last_to( 0 ), - m_last_amp( 0 ), - m_startKnob( 0 ), - m_endKnob( 0 ), - m_loopKnob( 0 ), - m_isDragging( false ), - m_reversed( false ), - m_framesPlayed( 0 ), - m_animation(ConfigManager::inst()->value("ui", "animateafp").toInt()) -{ - setFixedSize( _w, _h ); - setMouseTracking( true ); - - updateSampleRange(); - - m_graph.fill( Qt::transparent ); - update(); - updateCursor(); -} - - - - -void AudioFileProcessorWaveView::isPlaying( f_cnt_t _current_frame ) -{ - m_framesPlayed = _current_frame; - update(); -} - - - - -void AudioFileProcessorWaveView::enterEvent( QEvent * _e ) -{ - updateCursor(); -} - - - - -void AudioFileProcessorWaveView::leaveEvent( QEvent * _e ) -{ - updateCursor(); -} - - - - -void AudioFileProcessorWaveView::mousePressEvent( QMouseEvent * _me ) -{ - m_isDragging = true; - m_draggingLastPoint = _me->pos(); - - const int x = _me->x(); - - const int start_dist = qAbs( m_startFrameX - x ); - const int end_dist = qAbs( m_endFrameX - x ); - const int loop_dist = qAbs( m_loopFrameX - x ); - - DraggingType dt = DraggingType::SampleLoop; int md = loop_dist; - if( start_dist < loop_dist ) { dt = DraggingType::SampleStart; md = start_dist; } - else if( end_dist < loop_dist ) { dt = DraggingType::SampleEnd; md = end_dist; } - - if( md < 4 ) - { - m_draggingType = dt; - } - else - { - m_draggingType = DraggingType::Wave; - updateCursor(_me); - } -} - - - - -void AudioFileProcessorWaveView::mouseReleaseEvent( QMouseEvent * _me ) -{ - m_isDragging = false; - if( m_draggingType == DraggingType::Wave ) - { - updateCursor(_me); - } -} - - - - -void AudioFileProcessorWaveView::mouseMoveEvent( QMouseEvent * _me ) -{ - if( ! m_isDragging ) - { - updateCursor(_me); - return; - } - - const int step = _me->x() - m_draggingLastPoint.x(); - switch( m_draggingType ) - { - case DraggingType::SampleStart: - slideSamplePointByPx( Point::Start, step ); - break; - case DraggingType::SampleEnd: - slideSamplePointByPx( Point::End, step ); - break; - case DraggingType::SampleLoop: - slideSamplePointByPx( Point::Loop, step ); - break; - case DraggingType::Wave: - default: - if( qAbs( _me->y() - m_draggingLastPoint.y() ) - < 2 * qAbs( _me->x() - m_draggingLastPoint.x() ) ) - { - slide( step ); - } - else - { - zoom( _me->y() < m_draggingLastPoint.y() ); - } - } - - m_draggingLastPoint = _me->pos(); - update(); -} - - - - -void AudioFileProcessorWaveView::wheelEvent( QWheelEvent * _we ) -{ - zoom( _we->angleDelta().y() > 0 ); - update(); -} - - - - -void AudioFileProcessorWaveView::paintEvent( QPaintEvent * _pe ) -{ - QPainter p( this ); - - p.drawPixmap( s_padding, s_padding, m_graph ); - - const QRect graph_rect( s_padding, s_padding, width() - 2 * s_padding, height() - 2 * s_padding ); - const f_cnt_t frames = m_to - m_from; - m_startFrameX = graph_rect.x() + (m_sample->startFrame() - m_from) * - double( graph_rect.width() ) / frames; - m_endFrameX = graph_rect.x() + (m_sample->endFrame() - m_from) * - double( graph_rect.width() ) / frames; - m_loopFrameX = graph_rect.x() + (m_sample->loopStartFrame() - m_from) * - double( graph_rect.width() ) / frames; - const int played_width_px = ( m_framesPlayed - m_from ) * - double( graph_rect.width() ) / frames; - - // loop point line - p.setPen( QColor( 0x7F, 0xFF, 0xFF ) ); //TODO: put into a qproperty - p.drawLine( m_loopFrameX, graph_rect.y(), - m_loopFrameX, - graph_rect.height() + graph_rect.y() ); - - // start/end lines - p.setPen( QColor( 0xFF, 0xFF, 0xFF ) ); //TODO: put into a qproperty - p.drawLine( m_startFrameX, graph_rect.y(), - m_startFrameX, - graph_rect.height() + graph_rect.y() ); - p.drawLine( m_endFrameX, graph_rect.y(), - m_endFrameX, - graph_rect.height() + graph_rect.y() ); - - - if( m_endFrameX - m_startFrameX > 2 ) - { - p.fillRect( - m_startFrameX + 1, - graph_rect.y(), - m_endFrameX - m_startFrameX - 1, - graph_rect.height() + graph_rect.y(), - QColor( 95, 175, 255, 50 ) //TODO: put into a qproperty - ); - if( m_endFrameX - m_loopFrameX > 2 ) - p.fillRect( - m_loopFrameX + 1, - graph_rect.y(), - m_endFrameX - m_loopFrameX - 1, - graph_rect.height() + graph_rect.y(), - QColor( 95, 205, 255, 65 ) //TODO: put into a qproperty - ); - - if( m_framesPlayed && m_animation) - { - QLinearGradient g( m_startFrameX, 0, played_width_px, 0 ); - const QColor c( 0, 120, 255, 180 ); //TODO: put into a qproperty - g.setColorAt( 0, Qt::transparent ); - g.setColorAt( 0.8, c ); - g.setColorAt( 1, c ); - p.fillRect( - m_startFrameX + 1, - graph_rect.y(), - played_width_px - ( m_startFrameX + 1 ), - graph_rect.height() + graph_rect.y(), - g - ); - p.setPen( QColor( 255, 255, 255 ) ); //TODO: put into a qproperty - p.drawLine( - played_width_px, - graph_rect.y(), - played_width_px, - graph_rect.height() + graph_rect.y() - ); - m_framesPlayed = 0; - } - } - - QLinearGradient g( 0, 0, width() * 0.7, 0 ); - const QColor c( 16, 111, 170, 180 ); - g.setColorAt( 0, c ); - g.setColorAt( 0.4, c ); - g.setColorAt( 1, Qt::transparent ); - p.fillRect( s_padding, s_padding, m_graph.width(), 14, g ); - - p.setPen( QColor( 255, 255, 255 ) ); - p.setFont( pointSize<8>( font() ) ); - - QString length_text; - const int length = m_sample->sampleDuration().count(); - - if( length > 20000 ) - { - length_text = QString::number( length / 1000 ) + "s"; - } - else if( length > 2000 ) - { - length_text = QString::number( ( length / 100 ) / 10.0 ) + "s"; - } - else - { - length_text = QString::number( length ) + "ms"; - } - - p.drawText( - s_padding + 2, - s_padding + 10, - tr( "Sample length:" ) + " " + length_text - ); -} - - - - -void AudioFileProcessorWaveView::updateGraph() -{ - if( m_to == 1 ) - { - m_to = m_sample->sampleSize() * 0.7; - slideSamplePointToFrames( Point::End, m_to * 0.7 ); - } - - if (m_from > m_sample->startFrame()) - { - m_from = m_sample->startFrame(); - } - - if (m_to < m_sample->endFrame()) - { - m_to = m_sample->endFrame(); - } - - if (m_sample->reversed() != m_reversed) - { - reverse(); - } - else if (m_last_from == m_from && m_last_to == m_to && m_sample->amplification() == m_last_amp) - { - return; - } - - m_last_from = m_from; - m_last_to = m_to; - m_last_amp = m_sample->amplification(); - - m_graph.fill( Qt::transparent ); - QPainter p( &m_graph ); - p.setPen( QColor( 255, 255, 255 ) ); - - const auto rect = QRect{0, 0, m_graph.width(), m_graph.height()}; - const auto waveform = SampleWaveform::Parameters{ - m_sample->data() + m_from, static_cast(m_to - m_from), m_sample->amplification(), m_sample->reversed()}; - SampleWaveform::visualize(waveform, p, rect); -} - - - - -void AudioFileProcessorWaveView::zoom( const bool _out ) -{ - const f_cnt_t start = m_sample->startFrame(); - const f_cnt_t end = m_sample->endFrame(); - const f_cnt_t frames = m_sample->sampleSize(); - const f_cnt_t d_from = start - m_from; - const f_cnt_t d_to = m_to - end; - - const f_cnt_t step = qMax( 1, qMax( d_from, d_to ) / 10 ); - const f_cnt_t step_from = ( _out ? - step : step ); - const f_cnt_t step_to = ( _out ? step : - step ); - - const double comp_ratio = double( qMin( d_from, d_to ) ) - / qMax( 1, qMax( d_from, d_to ) ); - - f_cnt_t new_from; - f_cnt_t new_to; - - if( ( _out && d_from < d_to ) || ( ! _out && d_to < d_from ) ) - { - new_from = qBound( 0, m_from + step_from, start ); - new_to = qBound( - end, - m_to + f_cnt_t( step_to * ( new_from == m_from ? 1 : comp_ratio ) ), - frames - ); - } - else - { - new_to = qBound( end, m_to + step_to, frames ); - new_from = qBound( - 0, - m_from + f_cnt_t( step_from * ( new_to == m_to ? 1 : comp_ratio ) ), - start - ); - } - - if (static_cast(new_to - new_from) / m_sample->sampleRate() > 0.05) - { - m_from = new_from; - m_to = new_to; - } -} - - - - -void AudioFileProcessorWaveView::slide( int _px ) -{ - const double fact = qAbs( double( _px ) / width() ); - f_cnt_t step = ( m_to - m_from ) * fact; - if( _px > 0 ) - { - step = -step; - } - - f_cnt_t step_from = qBound(0, m_from + step, m_sample->sampleSize()) - m_from; - f_cnt_t step_to = qBound(m_from + 1, m_to + step, m_sample->sampleSize()) - m_to; - - step = qAbs( step_from ) < qAbs( step_to ) ? step_from : step_to; - - m_from += step; - m_to += step; - slideSampleByFrames( step ); -} - - - - -void AudioFileProcessorWaveView::setKnobs( knob * _start, knob * _end, knob * _loop ) -{ - m_startKnob = _start; - m_endKnob = _end; - m_loopKnob = _loop; - - m_startKnob->setWaveView( this ); - m_startKnob->setRelatedKnob( m_endKnob ); - - m_endKnob->setWaveView( this ); - m_endKnob->setRelatedKnob( m_startKnob ); - - m_loopKnob->setWaveView( this ); -} - - - - -void AudioFileProcessorWaveView::slideSamplePointByPx( Point _point, int _px ) -{ - slideSamplePointByFrames( - _point, - f_cnt_t( ( double( _px ) / width() ) * ( m_to - m_from ) ) - ); -} - - - - -void AudioFileProcessorWaveView::slideSamplePointByFrames( Point _point, f_cnt_t _frames, bool _slide_to ) -{ - knob * a_knob = m_startKnob; - switch( _point ) - { - case Point::End: - a_knob = m_endKnob; - break; - case Point::Loop: - a_knob = m_loopKnob; - break; - case Point::Start: - break; - } - if( a_knob == nullptr ) - { - return; - } - else - { - const double v = static_cast(_frames) / m_sample->sampleSize(); - if( _slide_to ) - { - a_knob->slideTo( v ); - } - else - { - a_knob->slideBy( v ); - } - } -} - - - - -void AudioFileProcessorWaveView::slideSampleByFrames( f_cnt_t _frames ) -{ - if (m_sample->sampleSize() <= 1) - { - return; - } - const double v = static_cast( _frames ) / m_sample->sampleSize(); - // update knobs in the right order - // to avoid them clamping each other - if (v < 0) - { - m_startKnob->slideBy(v, false); - m_loopKnob->slideBy(v, false); - m_endKnob->slideBy(v, false); - } - else - { - m_endKnob->slideBy(v, false); - m_loopKnob->slideBy(v, false); - m_startKnob->slideBy(v, false); - } -} - - - - -void AudioFileProcessorWaveView::reverse() -{ - slideSampleByFrames( - m_sample->sampleSize() - - m_sample->endFrame() - - m_sample->startFrame() - ); - - const f_cnt_t from = m_from; - m_from = m_sample->sampleSize() - m_to; - m_to = m_sample->sampleSize() - from; - - m_reversed = ! m_reversed; -} - - - -void AudioFileProcessorWaveView::updateCursor( QMouseEvent * _me ) -{ - bool const waveIsDragged = m_isDragging && (m_draggingType == DraggingType::Wave); - bool const pointerCloseToStartEndOrLoop = (_me != nullptr ) && - ( isCloseTo( _me->x(), m_startFrameX ) || - isCloseTo( _me->x(), m_endFrameX ) || - isCloseTo( _me->x(), m_loopFrameX ) ); - - if( !m_isDragging && pointerCloseToStartEndOrLoop) - setCursor(Qt::SizeHorCursor); - else if( waveIsDragged ) - setCursor(Qt::ClosedHandCursor); - else - setCursor(Qt::OpenHandCursor); -} - - - - -void AudioFileProcessorWaveView::knob::slideTo( double _v, bool _check_bound ) -{ - if( _check_bound && ! checkBound( _v ) ) - { - return; - } - model()->setValue( _v ); - emit sliderMoved( model()->value() ); -} - - - - -float AudioFileProcessorWaveView::knob::getValue( const QPoint & _p ) -{ - const double dec_fact = ! m_waveView ? 1 : - static_cast(m_waveView->m_to - m_waveView->m_from) / m_waveView->m_sample->sampleSize(); - const float inc = Knob::getValue( _p ) * dec_fact; - - return inc; -} - - - - -bool AudioFileProcessorWaveView::knob::checkBound( double _v ) const -{ - if( ! m_relatedKnob || ! m_waveView ) - { - return true; - } - - if( ( m_relatedKnob->model()->value() - _v > 0 ) != - ( m_relatedKnob->model()->value() - model()->value() >= 0 ) ) - return false; - - const double d1 = qAbs( m_relatedKnob->model()->value() - model()->value() ) - * (m_waveView->m_sample->sampleSize()) - / m_waveView->m_sample->sampleRate(); - - const double d2 = qAbs( m_relatedKnob->model()->value() - _v ) - * (m_waveView->m_sample->sampleSize()) - / m_waveView->m_sample->sampleRate(); - - return d1 < d2 || d2 > 0.005; -} - - -} // namespace gui - - - - extern "C" { diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.h b/plugins/AudioFileProcessor/AudioFileProcessor.h index 80a40c56f5..7ade1ec4fc 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.h +++ b/plugins/AudioFileProcessor/AudioFileProcessor.h @@ -26,31 +26,17 @@ #ifndef LMMS_AUDIO_FILE_PROCESSOR_H #define LMMS_AUDIO_FILE_PROCESSOR_H -#include +#include "AutomatableModel.h" #include "ComboBoxModel.h" + #include "Instrument.h" -#include "InstrumentView.h" #include "Sample.h" -#include "SampleBuffer.h" -#include "Knob.h" namespace lmms { -namespace gui -{ -class automatableButtonGroup; -class PluginView; -class InstrumentViewFixedSize; -class Knob; -class PixmapButton; -class ComboBox; -class AudioFileProcessorView; -} - - class AudioFileProcessor : public Instrument { Q_OBJECT @@ -77,6 +63,17 @@ public: gui::PluginView* instantiateView( QWidget * _parent ) override; + Sample const & sample() const { return m_sample; } + + FloatModel & ampModel() { return m_ampModel; } + FloatModel & startPointModel() { return m_startPointModel; } + FloatModel & endPointModel() { return m_endPointModel; } + FloatModel & loopPointModel() { return m_loopPointModel; } + BoolModel & reverseModel() { return m_reverseModel; } + IntModel & loopModel() { return m_loopModel; } + BoolModel & stutterModel() { return m_stutterModel; } + ComboBoxModel & interpolationModel() { return m_interpolationModel; } + public slots: void setAudioFile(const QString& _audio_file, bool _rename = true); @@ -109,195 +106,8 @@ private: f_cnt_t m_nextPlayStartPoint; bool m_nextPlayBackwards; - - friend class gui::AudioFileProcessorView; - } ; - -namespace gui -{ - -class AudioFileProcessorWaveView; - - -class AudioFileProcessorView : public gui::InstrumentViewFixedSize -{ - Q_OBJECT -public: - AudioFileProcessorView( Instrument * _instrument, QWidget * _parent ); - virtual ~AudioFileProcessorView() = default; - - void newWaveView(); -protected slots: - void sampleUpdated(); - void openAudioFile(); - - -protected: - virtual void dragEnterEvent( QDragEnterEvent * _dee ); - virtual void dropEvent( QDropEvent * _de ); - virtual void paintEvent( QPaintEvent * ); - - -private: - virtual void modelChanged(); - - - AudioFileProcessorWaveView * m_waveView; - Knob * m_ampKnob; - Knob * m_startKnob; - Knob * m_endKnob; - Knob * m_loopKnob; - - gui::PixmapButton * m_openAudioFileButton; - PixmapButton * m_reverseButton; - automatableButtonGroup * m_loopGroup; - PixmapButton * m_stutterButton; - ComboBox * m_interpBox; - -} ; - - - -class AudioFileProcessorWaveView : public QWidget -{ - Q_OBJECT -protected: - virtual void enterEvent( QEvent * _e ); - virtual void leaveEvent( QEvent * _e ); - virtual void mousePressEvent( QMouseEvent * _me ); - virtual void mouseReleaseEvent( QMouseEvent * _me ); - virtual void mouseMoveEvent( QMouseEvent * _me ); - virtual void wheelEvent( QWheelEvent * _we ); - virtual void paintEvent( QPaintEvent * _pe ); - - -public: - enum class Point - { - Start, - End, - Loop - } ; - - class knob : public Knob - { - const AudioFileProcessorWaveView * m_waveView; - const Knob * m_relatedKnob; - - - public: - knob( QWidget * _parent ) : - Knob( KnobType::Bright26, _parent ), - m_waveView( 0 ), - m_relatedKnob( 0 ) - { - setFixedSize( 37, 47 ); - } - - void setWaveView( const AudioFileProcessorWaveView * _wv ) - { - m_waveView = _wv; - } - - void setRelatedKnob( const Knob * _knob ) - { - m_relatedKnob = _knob; - } - - void slideBy( double _v, bool _check_bound = true ) - { - slideTo( model()->value() + _v, _check_bound ); - } - - void slideTo( double _v, bool _check_bound = true ); - - - protected: - float getValue( const QPoint & _p ); - - - private: - bool checkBound( double _v ) const; - } ; - - -public slots: - void update() - { - updateGraph(); - QWidget::update(); - } - - void isPlaying( lmms::f_cnt_t _current_frame ); - - -private: - static const int s_padding = 2; - - enum class DraggingType - { - Wave, - SampleStart, - SampleEnd, - SampleLoop - } ; - - Sample* m_sample; - QPixmap m_graph; - f_cnt_t m_from; - f_cnt_t m_to; - f_cnt_t m_last_from; - f_cnt_t m_last_to; - float m_last_amp; - knob * m_startKnob; - knob * m_endKnob; - knob * m_loopKnob; - f_cnt_t m_startFrameX; - f_cnt_t m_endFrameX; - f_cnt_t m_loopFrameX; - bool m_isDragging; - QPoint m_draggingLastPoint; - DraggingType m_draggingType; - bool m_reversed; - f_cnt_t m_framesPlayed; - bool m_animation; - - friend class AudioFileProcessorView; - -public: - AudioFileProcessorWaveView(QWidget * _parent, int _w, int _h, Sample* buf); - void setKnobs(knob *_start, knob *_end, knob *_loop ); - - - void updateSampleRange(); -private: - void zoom( const bool _out = false ); - void slide( int _px ); - void slideSamplePointByPx( Point _point, int _px ); - void slideSamplePointByFrames( Point _point, f_cnt_t _frames, bool _slide_to = false ); - void slideSampleByFrames( f_cnt_t _frames ); - - void slideSamplePointToFrames( Point _point, f_cnt_t _frames ) - { - slideSamplePointByFrames( _point, _frames, true ); - } - - void updateGraph(); - void reverse(); - void updateCursor( QMouseEvent * _me = nullptr ); - - static bool isCloseTo( int _a, int _b ) - { - return qAbs( _a - _b ) < 4; - } - -} ; - - -} // namespace gui - } // namespace lmms #endif // LMMS_AUDIO_FILE_PROCESSOR_H diff --git a/plugins/AudioFileProcessor/AudioFileProcessorView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorView.cpp new file mode 100644 index 0000000000..43882222f4 --- /dev/null +++ b/plugins/AudioFileProcessor/AudioFileProcessorView.cpp @@ -0,0 +1,286 @@ +/* + * AudioFileProcessor.cpp - instrument for using audio files + * + * Copyright (c) 2004-2014 Tobias Doerffel + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "AudioFileProcessorView.h" + +#include "AudioFileProcessor.h" +#include "AudioFileProcessorWaveView.h" + +#include + +#include "ComboBox.h" +#include "DataFile.h" +#include "gui_templates.h" +#include "PixmapButton.h" +#include "SampleLoader.h" +#include "Song.h" +#include "StringPairDrag.h" +#include "Track.h" +#include "Clipboard.h" + + +namespace lmms +{ + +namespace gui +{ + +AudioFileProcessorView::AudioFileProcessorView(Instrument* instrument, + QWidget* parent) : + InstrumentViewFixedSize(instrument, parent) +{ + m_openAudioFileButton = new PixmapButton(this); + m_openAudioFileButton->setCursor(QCursor(Qt::PointingHandCursor)); + m_openAudioFileButton->move(227, 72); + m_openAudioFileButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap( + "select_file")); + m_openAudioFileButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap( + "select_file")); + connect(m_openAudioFileButton, SIGNAL(clicked()), + this, SLOT(openAudioFile())); + m_openAudioFileButton->setToolTip(tr("Open sample")); + + m_reverseButton = new PixmapButton(this); + m_reverseButton->setCheckable(true); + m_reverseButton->move(164, 105); + m_reverseButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap( + "reverse_on")); + m_reverseButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap( + "reverse_off")); + m_reverseButton->setToolTip(tr("Reverse sample")); + +// loop button group + + auto m_loopOffButton = new PixmapButton(this); + m_loopOffButton->setCheckable(true); + m_loopOffButton->move(190, 105); + m_loopOffButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap( + "loop_off_on")); + m_loopOffButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap( + "loop_off_off")); + m_loopOffButton->setToolTip(tr("Disable loop")); + + auto m_loopOnButton = new PixmapButton(this); + m_loopOnButton->setCheckable(true); + m_loopOnButton->move(190, 124); + m_loopOnButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap( + "loop_on_on")); + m_loopOnButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap( + "loop_on_off")); + m_loopOnButton->setToolTip(tr("Enable loop")); + + auto m_loopPingPongButton = new PixmapButton(this); + m_loopPingPongButton->setCheckable(true); + m_loopPingPongButton->move(216, 124); + m_loopPingPongButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap( + "loop_pingpong_on")); + m_loopPingPongButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap( + "loop_pingpong_off")); + m_loopPingPongButton->setToolTip(tr("Enable ping-pong loop")); + + m_loopGroup = new automatableButtonGroup(this); + m_loopGroup->addButton(m_loopOffButton); + m_loopGroup->addButton(m_loopOnButton); + m_loopGroup->addButton(m_loopPingPongButton); + + m_stutterButton = new PixmapButton(this); + m_stutterButton->setCheckable(true); + m_stutterButton->move(164, 124); + m_stutterButton->setActiveGraphic(PLUGIN_NAME::getIconPixmap( + "stutter_on")); + m_stutterButton->setInactiveGraphic(PLUGIN_NAME::getIconPixmap( + "stutter_off")); + m_stutterButton->setToolTip( + tr("Continue sample playback across notes")); + + m_ampKnob = new Knob(KnobType::Bright26, this); + m_ampKnob->setVolumeKnob(true); + m_ampKnob->move(5, 108); + m_ampKnob->setHintText(tr("Amplify:"), "%"); + + m_startKnob = new AudioFileProcessorWaveView::knob(this); + m_startKnob->move(45, 108); + m_startKnob->setHintText(tr("Start point:"), ""); + + m_endKnob = new AudioFileProcessorWaveView::knob(this); + m_endKnob->move(125, 108); + m_endKnob->setHintText(tr("End point:"), ""); + + m_loopKnob = new AudioFileProcessorWaveView::knob(this); + m_loopKnob->move(85, 108); + m_loopKnob->setHintText(tr("Loopback point:"), ""); + +// interpolation selector + m_interpBox = new ComboBox(this); + m_interpBox->setGeometry(142, 62, 82, ComboBox::DEFAULT_HEIGHT); + m_interpBox->setFont(pointSize<8>(m_interpBox->font())); + +// wavegraph + m_waveView = 0; + newWaveView(); + + connect(castModel(), SIGNAL(isPlaying(lmms::f_cnt_t)), + m_waveView, SLOT(isPlaying(lmms::f_cnt_t))); + + qRegisterMetaType("lmms::f_cnt_t"); + + setAcceptDrops(true); +} + +void AudioFileProcessorView::dragEnterEvent(QDragEnterEvent* dee) +{ + // For mimeType() and MimeType enum class + using namespace Clipboard; + + if (dee->mimeData()->hasFormat(mimeType(MimeType::StringPair))) + { + QString txt = dee->mimeData()->data( + mimeType(MimeType::StringPair)); + if (txt.section(':', 0, 0) == QString("clip_%1").arg( + static_cast(Track::Type::Sample))) + { + dee->acceptProposedAction(); + } + else if (txt.section(':', 0, 0) == "samplefile") + { + dee->acceptProposedAction(); + } + else + { + dee->ignore(); + } + } + else + { + dee->ignore(); + } +} + +void AudioFileProcessorView::newWaveView() +{ + if (m_waveView) + { + delete m_waveView; + m_waveView = 0; + } + m_waveView = new AudioFileProcessorWaveView(this, 245, 75, &castModel()->sample()); + m_waveView->move(2, 172); + m_waveView->setKnobs( + dynamic_cast(m_startKnob), + dynamic_cast(m_endKnob), + dynamic_cast(m_loopKnob)); + m_waveView->show(); +} + +void AudioFileProcessorView::dropEvent(QDropEvent* de) +{ + const auto type = StringPairDrag::decodeKey(de); + const auto value = StringPairDrag::decodeValue(de); + + if (type == "samplefile") { castModel()->setAudioFile(value); } + else if (type == QString("clip_%1").arg(static_cast(Track::Type::Sample))) + { + DataFile dataFile(value.toUtf8()); + castModel()->setAudioFile(dataFile.content().firstChild().toElement().attribute("src")); + } + else + { + de->ignore(); + return; + } + + m_waveView->updateSampleRange(); + Engine::getSong()->setModified(); + de->accept(); +} + +void AudioFileProcessorView::paintEvent(QPaintEvent*) +{ + QPainter p(this); + + static auto s_artwork = PLUGIN_NAME::getIconPixmap("artwork"); + p.drawPixmap(0, 0, s_artwork); + + auto a = castModel(); + + QString file_name = ""; + + int idx = a->sample().sampleFile().length(); + + p.setFont(pointSize<8>(font())); + + QFontMetrics fm(p.font()); + + // simple algorithm for creating a text from the filename that + // matches in the white rectangle + while(idx > 0 && + fm.size(Qt::TextSingleLine, file_name + "...").width() < 210) + { + file_name = a->sample().sampleFile()[--idx] + file_name; + } + + if (idx > 0) + { + file_name = "..." + file_name; + } + + p.setPen(QColor(255, 255, 255)); + p.drawText(8, 99, file_name); +} + +void AudioFileProcessorView::sampleUpdated() +{ + m_waveView->updateSampleRange(); + m_waveView->update(); + update(); +} + +void AudioFileProcessorView::openAudioFile() +{ + QString af = SampleLoader::openAudioFile(); + if (af.isEmpty()) { return; } + + castModel()->setAudioFile(af); + Engine::getSong()->setModified(); + m_waveView->updateSampleRange(); +} + +void AudioFileProcessorView::modelChanged() +{ + auto a = castModel(); + connect(a, &AudioFileProcessor::sampleUpdated, this, &AudioFileProcessorView::sampleUpdated); + m_ampKnob->setModel(&a->ampModel()); + m_startKnob->setModel(&a->startPointModel()); + m_endKnob->setModel(&a->endPointModel()); + m_loopKnob->setModel(&a->loopPointModel()); + m_reverseButton->setModel(&a->reverseModel()); + m_loopGroup->setModel(&a->loopModel()); + m_stutterButton->setModel(&a->stutterModel()); + m_interpBox->setModel(&a->interpolationModel()); + sampleUpdated(); +} + +} // namespace gui + +} // namespace lmms diff --git a/plugins/AudioFileProcessor/AudioFileProcessorView.h b/plugins/AudioFileProcessor/AudioFileProcessorView.h new file mode 100644 index 0000000000..039eaab2cc --- /dev/null +++ b/plugins/AudioFileProcessor/AudioFileProcessorView.h @@ -0,0 +1,85 @@ +/* + * AudioFileProcessorView.h - View of the AFP + * + * Copyright (c) 2004-2014 Tobias Doerffel + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_AUDIO_FILE_PROCESSOR_VIEW_H +#define LMMS_AUDIO_FILE_PROCESSOR_VIEW_H + +#include "InstrumentView.h" + + +namespace lmms +{ + +namespace gui +{ + +class automatableButtonGroup; +class Knob; +class PixmapButton; +class ComboBox; +class AudioFileProcessorWaveView; + + +class AudioFileProcessorView : public gui::InstrumentViewFixedSize +{ + Q_OBJECT +public: + AudioFileProcessorView(Instrument* instrument, QWidget* parent); + virtual ~AudioFileProcessorView() = default; + + void newWaveView(); + +protected slots: + void sampleUpdated(); + void openAudioFile(); + +protected: + virtual void dragEnterEvent(QDragEnterEvent* dee); + virtual void dropEvent(QDropEvent* de); + virtual void paintEvent(QPaintEvent*); + + // Private methods +private: + virtual void modelChanged(); + + // Private members +private: + AudioFileProcessorWaveView* m_waveView; + Knob* m_ampKnob; + Knob* m_startKnob; + Knob* m_endKnob; + Knob* m_loopKnob; + + gui::PixmapButton* m_openAudioFileButton; + PixmapButton* m_reverseButton; + automatableButtonGroup* m_loopGroup; + PixmapButton* m_stutterButton; + ComboBox* m_interpBox; +} ; + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_AUDIO_FILE_PROCESSOR_VIEW_H diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp new file mode 100644 index 0000000000..51a4d7ccb2 --- /dev/null +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp @@ -0,0 +1,540 @@ +/* + * AudioFileProcessorWaveView.cpp - Wave renderer of the AFP + * + * Copyright (c) 2004-2014 Tobias Doerffel + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "AudioFileProcessorWaveView.h" + +#include "ConfigManager.h" +#include "gui_templates.h" +#include "SampleWaveform.h" + +#include +#include + + +namespace lmms +{ + +namespace gui +{ + +void AudioFileProcessorWaveView::updateSampleRange() +{ + if (m_sample->sampleSize() > 1) + { + const f_cnt_t marging = (m_sample->endFrame() - m_sample->startFrame()) * 0.1; + m_from = qMax(0, m_sample->startFrame() - marging); + m_to = qMin(m_sample->endFrame() + marging, m_sample->sampleSize()); + } +} + +AudioFileProcessorWaveView::AudioFileProcessorWaveView(QWidget * parent, int w, int h, Sample const * buf) : + QWidget(parent), + m_sample(buf), + m_graph(QPixmap(w - 2 * s_padding, h - 2 * s_padding)), + m_from(0), + m_to(m_sample->sampleSize()), + m_last_from(0), + m_last_to(0), + m_last_amp(0), + m_startKnob(0), + m_endKnob(0), + m_loopKnob(0), + m_isDragging(false), + m_reversed(false), + m_framesPlayed(0), + m_animation(ConfigManager::inst()->value("ui", "animateafp").toInt()) +{ + setFixedSize(w, h); + setMouseTracking(true); + + updateSampleRange(); + + m_graph.fill(Qt::transparent); + update(); + updateCursor(); +} + +void AudioFileProcessorWaveView::isPlaying(f_cnt_t current_frame) +{ + m_framesPlayed = current_frame; + update(); +} + +void AudioFileProcessorWaveView::enterEvent(QEvent * e) +{ + updateCursor(); +} + +void AudioFileProcessorWaveView::leaveEvent(QEvent * e) +{ + updateCursor(); +} + +void AudioFileProcessorWaveView::mousePressEvent(QMouseEvent * me) +{ + m_isDragging = true; + m_draggingLastPoint = me->pos(); + + const int x = me->x(); + + const int start_dist = qAbs(m_startFrameX - x); + const int end_dist = qAbs(m_endFrameX - x); + const int loop_dist = qAbs(m_loopFrameX - x); + + DraggingType dt = DraggingType::SampleLoop; int md = loop_dist; + if (start_dist < loop_dist) { dt = DraggingType::SampleStart; md = start_dist; } + else if (end_dist < loop_dist) { dt = DraggingType::SampleEnd; md = end_dist; } + + if (md < 4) + { + m_draggingType = dt; + } + else + { + m_draggingType = DraggingType::Wave; + updateCursor(me); + } +} + +void AudioFileProcessorWaveView::mouseReleaseEvent(QMouseEvent * me) +{ + m_isDragging = false; + if (m_draggingType == DraggingType::Wave) + { + updateCursor(me); + } +} + +void AudioFileProcessorWaveView::mouseMoveEvent(QMouseEvent * me) +{ + if (! m_isDragging) + { + updateCursor(me); + return; + } + + const int step = me->x() - m_draggingLastPoint.x(); + switch(m_draggingType) + { + case DraggingType::SampleStart: + slideSamplePointByPx(Point::Start, step); + break; + case DraggingType::SampleEnd: + slideSamplePointByPx(Point::End, step); + break; + case DraggingType::SampleLoop: + slideSamplePointByPx(Point::Loop, step); + break; + case DraggingType::Wave: + default: + if (qAbs(me->y() - m_draggingLastPoint.y()) + < 2 * qAbs(me->x() - m_draggingLastPoint.x())) + { + slide(step); + } + else + { + zoom(me->y() < m_draggingLastPoint.y()); + } + } + + m_draggingLastPoint = me->pos(); + update(); +} + +void AudioFileProcessorWaveView::wheelEvent(QWheelEvent * we) +{ + zoom(we->angleDelta().y() > 0); + update(); +} + +void AudioFileProcessorWaveView::paintEvent(QPaintEvent * pe) +{ + QPainter p(this); + + p.drawPixmap(s_padding, s_padding, m_graph); + + const QRect graph_rect(s_padding, s_padding, width() - 2 * s_padding, height() - 2 * s_padding); + const f_cnt_t frames = m_to - m_from; + m_startFrameX = graph_rect.x() + (m_sample->startFrame() - m_from) * + double(graph_rect.width()) / frames; + m_endFrameX = graph_rect.x() + (m_sample->endFrame() - m_from) * + double(graph_rect.width()) / frames; + m_loopFrameX = graph_rect.x() + (m_sample->loopStartFrame() - m_from) * + double(graph_rect.width()) / frames; + const int played_width_px = (m_framesPlayed - m_from) * + double(graph_rect.width()) / frames; + + // loop point line + p.setPen(QColor(0x7F, 0xFF, 0xFF)); //TODO: put into a qproperty + p.drawLine(m_loopFrameX, graph_rect.y(), + m_loopFrameX, + graph_rect.height() + graph_rect.y()); + + // start/end lines + p.setPen(QColor(0xFF, 0xFF, 0xFF)); //TODO: put into a qproperty + p.drawLine(m_startFrameX, graph_rect.y(), + m_startFrameX, + graph_rect.height() + graph_rect.y()); + p.drawLine(m_endFrameX, graph_rect.y(), + m_endFrameX, + graph_rect.height() + graph_rect.y()); + + + if (m_endFrameX - m_startFrameX > 2) + { + p.fillRect( + m_startFrameX + 1, + graph_rect.y(), + m_endFrameX - m_startFrameX - 1, + graph_rect.height() + graph_rect.y(), + QColor(95, 175, 255, 50) //TODO: put into a qproperty + ); + if (m_endFrameX - m_loopFrameX > 2) + p.fillRect( + m_loopFrameX + 1, + graph_rect.y(), + m_endFrameX - m_loopFrameX - 1, + graph_rect.height() + graph_rect.y(), + QColor(95, 205, 255, 65) //TODO: put into a qproperty + ); + + if (m_framesPlayed && m_animation) + { + QLinearGradient g(m_startFrameX, 0, played_width_px, 0); + const QColor c(0, 120, 255, 180); //TODO: put into a qproperty + g.setColorAt(0, Qt::transparent); + g.setColorAt(0.8, c); + g.setColorAt(1, c); + p.fillRect( + m_startFrameX + 1, + graph_rect.y(), + played_width_px - (m_startFrameX + 1), + graph_rect.height() + graph_rect.y(), + g + ); + p.setPen(QColor(255, 255, 255)); //TODO: put into a qproperty + p.drawLine( + played_width_px, + graph_rect.y(), + played_width_px, + graph_rect.height() + graph_rect.y() + ); + m_framesPlayed = 0; + } + } + + QLinearGradient g(0, 0, width() * 0.7, 0); + const QColor c(16, 111, 170, 180); + g.setColorAt(0, c); + g.setColorAt(0.4, c); + g.setColorAt(1, Qt::transparent); + p.fillRect(s_padding, s_padding, m_graph.width(), 14, g); + + p.setPen(QColor(255, 255, 255)); + p.setFont(pointSize<8>(font())); + + QString length_text; + const int length = m_sample->sampleDuration().count(); + + if (length > 20000) + { + length_text = QString::number(length / 1000) + "s"; + } + else if (length > 2000) + { + length_text = QString::number((length / 100) / 10.0) + "s"; + } + else + { + length_text = QString::number(length) + "ms"; + } + + p.drawText( + s_padding + 2, + s_padding + 10, + tr("Sample length:") + " " + length_text + ); +} + +void AudioFileProcessorWaveView::updateGraph() +{ + if (m_to == 1) + { + m_to = m_sample->sampleSize() * 0.7; + slideSamplePointToFrames(Point::End, m_to * 0.7); + } + + if (m_from > m_sample->startFrame()) + { + m_from = m_sample->startFrame(); + } + + if (m_to < m_sample->endFrame()) + { + m_to = m_sample->endFrame(); + } + + if (m_sample->reversed() != m_reversed) + { + reverse(); + } + else if (m_last_from == m_from && m_last_to == m_to && m_sample->amplification() == m_last_amp) + { + return; + } + + m_last_from = m_from; + m_last_to = m_to; + m_last_amp = m_sample->amplification(); + + m_graph.fill(Qt::transparent); + QPainter p(&m_graph); + p.setPen(QColor(255, 255, 255)); + + const auto rect = QRect{0, 0, m_graph.width(), m_graph.height()}; + const auto waveform = SampleWaveform::Parameters{ + m_sample->data() + m_from, static_cast(m_to - m_from), m_sample->amplification(), m_sample->reversed()}; + SampleWaveform::visualize(waveform, p, rect); +} + +void AudioFileProcessorWaveView::zoom(const bool out) +{ + const f_cnt_t start = m_sample->startFrame(); + const f_cnt_t end = m_sample->endFrame(); + const f_cnt_t frames = m_sample->sampleSize(); + const f_cnt_t d_from = start - m_from; + const f_cnt_t d_to = m_to - end; + + const f_cnt_t step = qMax(1, qMax(d_from, d_to) / 10); + const f_cnt_t step_from = (out ? - step : step); + const f_cnt_t step_to = (out ? step : - step); + + const double comp_ratio = double(qMin(d_from, d_to)) + / qMax(1, qMax(d_from, d_to)); + + f_cnt_t new_from; + f_cnt_t new_to; + + if ((out && d_from < d_to) || (! out && d_to < d_from)) + { + new_from = qBound(0, m_from + step_from, start); + new_to = qBound( + end, + m_to + f_cnt_t(step_to * (new_from == m_from ? 1 : comp_ratio)), + frames + ); + } + else + { + new_to = qBound(end, m_to + step_to, frames); + new_from = qBound( + 0, + m_from + f_cnt_t(step_from * (new_to == m_to ? 1 : comp_ratio)), + start + ); + } + + if (static_cast(new_to - new_from) / m_sample->sampleRate() > 0.05) + { + m_from = new_from; + m_to = new_to; + } +} + +void AudioFileProcessorWaveView::slide(int px) +{ + const double fact = qAbs(double(px) / width()); + f_cnt_t step = (m_to - m_from) * fact; + if (px > 0) + { + step = -step; + } + + f_cnt_t step_from = qBound(0, m_from + step, m_sample->sampleSize()) - m_from; + f_cnt_t step_to = qBound(m_from + 1, m_to + step, m_sample->sampleSize()) - m_to; + + step = qAbs(step_from) < qAbs(step_to) ? step_from : step_to; + + m_from += step; + m_to += step; + slideSampleByFrames(step); +} + +void AudioFileProcessorWaveView::setKnobs(knob * start, knob * end, knob * loop) +{ + m_startKnob = start; + m_endKnob = end; + m_loopKnob = loop; + + m_startKnob->setWaveView(this); + m_startKnob->setRelatedKnob(m_endKnob); + + m_endKnob->setWaveView(this); + m_endKnob->setRelatedKnob(m_startKnob); + + m_loopKnob->setWaveView(this); +} + +void AudioFileProcessorWaveView::slideSamplePointByPx(Point point, int px) +{ + slideSamplePointByFrames( + point, + f_cnt_t((double(px) / width()) * (m_to - m_from)) + ); +} + +void AudioFileProcessorWaveView::slideSamplePointByFrames(Point point, f_cnt_t frames, bool slide_to) +{ + knob * a_knob = m_startKnob; + switch(point) + { + case Point::End: + a_knob = m_endKnob; + break; + case Point::Loop: + a_knob = m_loopKnob; + break; + case Point::Start: + break; + } + if (a_knob == nullptr) + { + return; + } + else + { + const double v = static_cast(frames) / m_sample->sampleSize(); + if (slide_to) + { + a_knob->slideTo(v); + } + else + { + a_knob->slideBy(v); + } + } +} + + + + +void AudioFileProcessorWaveView::slideSampleByFrames(f_cnt_t frames) +{ + if (m_sample->sampleSize() <= 1) + { + return; + } + const double v = static_cast(frames) / m_sample->sampleSize(); + // update knobs in the right order + // to avoid them clamping each other + if (v < 0) + { + m_startKnob->slideBy(v, false); + m_loopKnob->slideBy(v, false); + m_endKnob->slideBy(v, false); + } + else + { + m_endKnob->slideBy(v, false); + m_loopKnob->slideBy(v, false); + m_startKnob->slideBy(v, false); + } +} + +void AudioFileProcessorWaveView::reverse() +{ + slideSampleByFrames( + m_sample->sampleSize() + - m_sample->endFrame() + - m_sample->startFrame() + ); + + const f_cnt_t from = m_from; + m_from = m_sample->sampleSize() - m_to; + m_to = m_sample->sampleSize() - from; + + m_reversed = ! m_reversed; +} + +void AudioFileProcessorWaveView::updateCursor(QMouseEvent * me) +{ + bool const waveIsDragged = m_isDragging && (m_draggingType == DraggingType::Wave); + bool const pointerCloseToStartEndOrLoop = (me != nullptr) && + (isCloseTo(me->x(), m_startFrameX) || + isCloseTo(me->x(), m_endFrameX) || + isCloseTo(me->x(), m_loopFrameX)); + + if (!m_isDragging && pointerCloseToStartEndOrLoop) + setCursor(Qt::SizeHorCursor); + else if (waveIsDragged) + setCursor(Qt::ClosedHandCursor); + else + setCursor(Qt::OpenHandCursor); +} + +void AudioFileProcessorWaveView::knob::slideTo(double v, bool check_bound) +{ + if (check_bound && ! checkBound(v)) + { + return; + } + model()->setValue(v); + emit sliderMoved(model()->value()); +} + +float AudioFileProcessorWaveView::knob::getValue(const QPoint & p) +{ + const double dec_fact = ! m_waveView ? 1 : + static_cast(m_waveView->m_to - m_waveView->m_from) / m_waveView->m_sample->sampleSize(); + const float inc = Knob::getValue(p) * dec_fact; + + return inc; +} + +bool AudioFileProcessorWaveView::knob::checkBound(double v) const +{ + if (! m_relatedKnob || ! m_waveView) + { + return true; + } + + if ((m_relatedKnob->model()->value() - v > 0) != + (m_relatedKnob->model()->value() - model()->value() >= 0)) + return false; + + const double d1 = qAbs(m_relatedKnob->model()->value() - model()->value()) + * (m_waveView->m_sample->sampleSize()) + / m_waveView->m_sample->sampleRate(); + + const double d2 = qAbs(m_relatedKnob->model()->value() - v) + * (m_waveView->m_sample->sampleSize()) + / m_waveView->m_sample->sampleRate(); + + return d1 < d2 || d2 > 0.005; +} + +} // namespace gui + +} // namespace lmms diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h new file mode 100644 index 0000000000..83a159725b --- /dev/null +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.h @@ -0,0 +1,181 @@ +/* + * AudioFileProcessorWaveView.h - Wave renderer of the AFP + * + * Copyright (c) 2004-2014 Tobias Doerffel + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_AUDIO_FILE_PROCESSOR_WAVE_VIEW_H +#define LMMS_AUDIO_FILE_PROCESSOR_WAVE_VIEW_H + + +#include "Knob.h" + + +namespace lmms +{ + +class Sample; + +namespace gui +{ + +class AudioFileProcessorView; + +class AudioFileProcessorWaveView : public QWidget +{ + Q_OBJECT +protected: + virtual void enterEvent(QEvent* e); + virtual void leaveEvent(QEvent* e); + virtual void mousePressEvent(QMouseEvent* me); + virtual void mouseReleaseEvent(QMouseEvent* me); + virtual void mouseMoveEvent(QMouseEvent* me); + virtual void wheelEvent(QWheelEvent* we); + virtual void paintEvent(QPaintEvent* pe); + + +public: + enum class Point + { + Start, + End, + Loop + } ; + + class knob : public Knob + { + const AudioFileProcessorWaveView* m_waveView; + const Knob* m_relatedKnob; + + + public: + knob(QWidget* parent) : + Knob(KnobType::Bright26, parent), + m_waveView(0), + m_relatedKnob(0) + { + setFixedSize(37, 47); + } + + void setWaveView(const AudioFileProcessorWaveView* wv) + { + m_waveView = wv; + } + + void setRelatedKnob(const Knob* knob) + { + m_relatedKnob = knob; + } + + void slideBy(double v, bool check_bound = true) + { + slideTo(model()->value() + v, check_bound); + } + + void slideTo(double v, bool check_bound = true); + + + protected: + float getValue(const QPoint & p); + + + private: + bool checkBound(double v) const; + } ; + + +public slots: + void update() + { + updateGraph(); + QWidget::update(); + } + + void isPlaying(lmms::f_cnt_t current_frame); + + +private: + static const int s_padding = 2; + + enum class DraggingType + { + Wave, + SampleStart, + SampleEnd, + SampleLoop + } ; + + Sample const* m_sample; + QPixmap m_graph; + f_cnt_t m_from; + f_cnt_t m_to; + f_cnt_t m_last_from; + f_cnt_t m_last_to; + float m_last_amp; + knob* m_startKnob; + knob* m_endKnob; + knob* m_loopKnob; + f_cnt_t m_startFrameX; + f_cnt_t m_endFrameX; + f_cnt_t m_loopFrameX; + bool m_isDragging; + QPoint m_draggingLastPoint; + DraggingType m_draggingType; + bool m_reversed; + f_cnt_t m_framesPlayed; + bool m_animation; + + friend class AudioFileProcessorView; + +public: + AudioFileProcessorWaveView(QWidget* parent, int w, int h, Sample const* buf); + void setKnobs(knob* start, knob* end, knob* loop); + + + void updateSampleRange(); +private: + void zoom(const bool out = false); + void slide(int px); + void slideSamplePointByPx(Point point, int px); + void slideSamplePointByFrames(Point point, f_cnt_t frames, bool slide_to = false); + void slideSampleByFrames(f_cnt_t frames); + + void slideSamplePointToFrames(Point point, f_cnt_t frames) + { + slideSamplePointByFrames(point, frames, true); + } + + void updateGraph(); + void reverse(); + void updateCursor(QMouseEvent* me = nullptr); + + static bool isCloseTo(int a, int b) + { + return qAbs(a - b) < 4; + } + +} ; + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_AUDIO_FILE_PROCESSOR_WAVE_VIEW_H diff --git a/plugins/AudioFileProcessor/CMakeLists.txt b/plugins/AudioFileProcessor/CMakeLists.txt index 055fad7910..a675321126 100644 --- a/plugins/AudioFileProcessor/CMakeLists.txt +++ b/plugins/AudioFileProcessor/CMakeLists.txt @@ -1,3 +1,3 @@ INCLUDE(BuildPlugin) -BUILD_PLUGIN(audiofileprocessor AudioFileProcessor.cpp AudioFileProcessor.h MOCFILES AudioFileProcessor.h EMBEDDED_RESOURCES *.png) +BUILD_PLUGIN(audiofileprocessor AudioFileProcessor.cpp AudioFileProcessor.h AudioFileProcessorView.cpp AudioFileProcessorView.h AudioFileProcessorWaveView.cpp AudioFileProcessorWaveView.h MOCFILES AudioFileProcessor.h AudioFileProcessorView.h AudioFileProcessorWaveView.h EMBEDDED_RESOURCES *.png)