/* * SampleTrack.cpp - implementation of class SampleTrack, a track which * provides arrangement of samples * * Copyright (c) 2005-2014 Tobias Doerffel * * This file is part of LMMS - http://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 "SampleTrack.h" #include #include #include #include #include #include #include #include #include "gui_templates.h" #include "GuiApplication.h" #include "Song.h" #include "embed.h" #include "Engine.h" #include "ToolTip.h" #include "AudioPort.h" #include "BBTrack.h" #include "SamplePlayHandle.h" #include "SampleRecordHandle.h" #include "SongEditor.h" #include "StringPairDrag.h" #include "TimeLineWidget.h" #include "Knob.h" #include "MainWindow.h" #include "Mixer.h" #include "GuiApplication.h" #include "EffectRackView.h" #include "TrackLabelButton.h" #include "ConfigManager.h" #include "panning_constants.h" #include "volume.h" SampleTCO::SampleTCO( Track * _track ) : TrackContentObject( _track ), m_sampleBuffer( new SampleBuffer ), m_isPlaying( false ) { saveJournallingState( false ); setSampleFile( "" ); restoreJournallingState(); // we need to receive bpm-change-events, because then we have to // change length of this TCO connect( Engine::getSong(), SIGNAL( tempoChanged( bpm_t ) ), this, SLOT( updateLength() ) ); connect( Engine::getSong(), SIGNAL( timeSignatureChanged( int,int ) ), this, SLOT( updateLength() ) ); //care about positionmarker TimeLineWidget * timeLine = Engine::getSong()->getPlayPos( Engine::getSong()->Mode_PlaySong ).m_timeLine; connect( timeLine, SIGNAL( positionMarkerMoved() ), this, SLOT( playbackPositionChanged() ) ); //care about loops connect( Engine::getSong(), SIGNAL( updateSampleTracks() ), this, SLOT( playbackPositionChanged() ) ); //care about mute TCOs connect( this, SIGNAL( dataChanged() ), this, SLOT( playbackPositionChanged() ) ); //care about mute track connect( getTrack()->getMutedModel(), SIGNAL( dataChanged() ),this, SLOT( playbackPositionChanged() ) ); //care about TCO position connect( this, SIGNAL( positionChanged() ), this, SLOT( updateTrackTcos() ) ); //playbutton clicked or space key connect( gui->songEditor(), SIGNAL( playTriggered() ), this, SLOT( playbackPositionChanged() ) ); switch( getTrack()->trackContainer()->type() ) { case TrackContainer::BBContainer: setAutoResize( true ); break; case TrackContainer::SongContainer: // move down default: setAutoResize( false ); break; } updateTrackTcos(); } SampleTCO::~SampleTCO() { SampleTrack * sampletrack = dynamic_cast( getTrack() ); if( sampletrack) { sampletrack->updateTcos(); } sharedObject::unref( m_sampleBuffer ); } void SampleTCO::changeLength( const MidiTime & _length ) { float nom = Engine::getSong()->getTimeSigModel().getNumerator(); float den = Engine::getSong()->getTimeSigModel().getDenominator(); int ticksPerTact = DefaultTicksPerTact * ( nom / den ); TrackContentObject::changeLength( qMax( static_cast( _length ), ticksPerTact ) ); } const QString & SampleTCO::sampleFile() const { return m_sampleBuffer->audioFile(); } void SampleTCO::setSampleBuffer( SampleBuffer* sb ) { sharedObject::unref( m_sampleBuffer ); m_sampleBuffer = sb; updateLength(); emit sampleChanged(); } void SampleTCO::setSampleFile( const QString & _sf ) { m_sampleBuffer->setAudioFile( _sf ); updateLength(); emit sampleChanged(); emit playbackPositionChanged(); } void SampleTCO::toggleRecord() { m_recordModel.setValue( !m_recordModel.value() ); emit dataChanged(); } void SampleTCO::playbackPositionChanged() { Engine::mixer()->removePlayHandlesOfTypes( getTrack(), PlayHandle::TypeSamplePlayHandle ); m_isPlaying = false; } void SampleTCO::updateTrackTcos() { SampleTrack * sampletrack = dynamic_cast( getTrack() ); if( sampletrack) { sampletrack->updateTcos(); } } bool SampleTCO::isPlaying() const { return m_isPlaying; } void SampleTCO::setIsPlaying(bool isPlaying) { m_isPlaying = isPlaying; } void SampleTCO::updateLength() { changeLength( sampleLength() ); } MidiTime SampleTCO::sampleLength() const { return (int)( m_sampleBuffer->frames() / Engine::framesPerTick() ); } void SampleTCO::setSampleStartFrame(f_cnt_t startFrame) { m_sampleBuffer->setStartFrame( startFrame ); } void SampleTCO::setSamplePlayLength(f_cnt_t length) { m_sampleBuffer->setEndFrame( length ); } void SampleTCO::saveSettings( QDomDocument & _doc, QDomElement & _this ) { if( _this.parentNode().nodeName() == "clipboard" ) { _this.setAttribute( "pos", -1 ); } else { _this.setAttribute( "pos", startPosition() ); } _this.setAttribute( "len", length() ); _this.setAttribute( "muted", isMuted() ); _this.setAttribute( "src", sampleFile() ); if( sampleFile() == "" ) { QString s; _this.setAttribute( "data", m_sampleBuffer->toBase64( s ) ); } // TODO: start- and end-frame } void SampleTCO::loadSettings( const QDomElement & _this ) { if( _this.attribute( "pos" ).toInt() >= 0 ) { movePosition( _this.attribute( "pos" ).toInt() ); } setSampleFile( _this.attribute( "src" ) ); if( sampleFile().isEmpty() && _this.hasAttribute( "data" ) ) { m_sampleBuffer->loadFromBase64( _this.attribute( "data" ) ); } changeLength( _this.attribute( "len" ).toInt() ); setMuted( _this.attribute( "muted" ).toInt() ); } TrackContentObjectView * SampleTCO::createView( TrackView * _tv ) { return new SampleTCOView( this, _tv ); } SampleTCOView::SampleTCOView( SampleTCO * _tco, TrackView * _tv ) : TrackContentObjectView( _tco, _tv ), m_tco( _tco ), m_paintPixmap() { // update UI and tooltip updateSample(); // track future changes of SampleTCO connect( m_tco, SIGNAL( sampleChanged() ), this, SLOT( updateSample() ) ); setStyle( QApplication::style() ); } SampleTCOView::~SampleTCOView() { } void SampleTCOView::updateSample() { update(); // set tooltip to filename so that user can see what sample this // sample-tco contains ToolTip::add( this, ( m_tco->m_sampleBuffer->audioFile() != "" ) ? m_tco->m_sampleBuffer->audioFile() : tr( "double-click to select sample" ) ); } void SampleTCOView::contextMenuEvent( QContextMenuEvent * _cme ) { if( _cme->modifiers() ) { return; } QMenu contextMenu( this ); if( fixedTCOs() == false ) { contextMenu.addAction( embed::getIconPixmap( "cancel" ), tr( "Delete (middle mousebutton)" ), this, SLOT( remove() ) ); contextMenu.addSeparator(); contextMenu.addAction( embed::getIconPixmap( "edit_cut" ), tr( "Cut" ), this, SLOT( cut() ) ); } contextMenu.addAction( embed::getIconPixmap( "edit_copy" ), tr( "Copy" ), m_tco, SLOT( copy() ) ); contextMenu.addAction( embed::getIconPixmap( "edit_paste" ), tr( "Paste" ), m_tco, SLOT( paste() ) ); contextMenu.addSeparator(); contextMenu.addAction( embed::getIconPixmap( "muted" ), tr( "Mute/unmute (<%1> + middle click)" ).arg( #ifdef LMMS_BUILD_APPLE "⌘"), #else "Ctrl"), #endif m_tco, SLOT( toggleMute() ) ); /*contextMenu.addAction( embed::getIconPixmap( "record" ), tr( "Set/clear record" ), m_tco, SLOT( toggleRecord() ) );*/ constructContextMenu( &contextMenu ); contextMenu.exec( QCursor::pos() ); } void SampleTCOView::dragEnterEvent( QDragEnterEvent * _dee ) { if( StringPairDrag::processDragEnterEvent( _dee, "samplefile,sampledata" ) == false ) { TrackContentObjectView::dragEnterEvent( _dee ); } } void SampleTCOView::dropEvent( QDropEvent * _de ) { if( StringPairDrag::decodeKey( _de ) == "samplefile" ) { m_tco->setSampleFile( StringPairDrag::decodeValue( _de ) ); _de->accept(); } else if( StringPairDrag::decodeKey( _de ) == "sampledata" ) { m_tco->m_sampleBuffer->loadFromBase64( StringPairDrag::decodeValue( _de ) ); m_tco->updateLength(); update(); _de->accept(); Engine::getSong()->setModified(); } else { TrackContentObjectView::dropEvent( _de ); } } void SampleTCOView::mousePressEvent( QMouseEvent * _me ) { if( _me->button() == Qt::LeftButton && _me->modifiers() & Qt::ControlModifier && _me->modifiers() & Qt::ShiftModifier ) { m_tco->toggleRecord(); } else { if( _me->button() == Qt::MiddleButton && _me->modifiers() == Qt::ControlModifier ) { SampleTCO * sTco = dynamic_cast( getTrackContentObject() ); if( sTco ) { sTco->updateTrackTcos(); } } TrackContentObjectView::mousePressEvent( _me ); } } void SampleTCOView::mouseReleaseEvent(QMouseEvent *_me) { if( _me->button() == Qt::MiddleButton && !_me->modifiers() ) { SampleTCO * sTco = dynamic_cast( getTrackContentObject() ); if( sTco ) { sTco->playbackPositionChanged(); } } TrackContentObjectView::mouseReleaseEvent( _me ); } void SampleTCOView::mouseDoubleClickEvent( QMouseEvent * ) { QString af = m_tco->m_sampleBuffer->openAudioFile(); if( af != "" && af != m_tco->m_sampleBuffer->audioFile() ) { m_tco->setSampleFile( af ); Engine::getSong()->setModified(); } } void SampleTCOView::paintEvent( QPaintEvent * pe ) { QPainter painter( this ); if( !needsUpdate() ) { painter.drawPixmap( 0, 0, m_paintPixmap ); return; } setNeedsUpdate( false ); m_paintPixmap = m_paintPixmap.isNull() == true || m_paintPixmap.size() != size() ? QPixmap( size() ) : m_paintPixmap; QPainter p( &m_paintPixmap ); QLinearGradient lingrad( 0, 0, 0, height() ); QColor c; bool muted = m_tco->getTrack()->isMuted() || m_tco->isMuted(); // state: selected, muted, normal c = isSelected() ? selectedColor() : ( muted ? mutedBackgroundColor() : painter.background().color() ); lingrad.setColorAt( 1, c.darker( 300 ) ); lingrad.setColorAt( 0, c ); if( gradient() ) { p.fillRect( rect(), lingrad ); } else { p.fillRect( rect(), c ); } p.setPen( !muted ? painter.pen().brush().color() : mutedColor() ); const int spacing = TCO_BORDER_WIDTH + 1; const float ppt = fixedTCOs() ? ( parentWidget()->width() - 2 * TCO_BORDER_WIDTH ) / (float) m_tco->length().getTact() : pixelsPerTact(); float nom = Engine::getSong()->getTimeSigModel().getNumerator(); float den = Engine::getSong()->getTimeSigModel().getDenominator(); float ticksPerTact = DefaultTicksPerTact * nom / den; QRect r = QRect( TCO_BORDER_WIDTH, spacing, qMax( static_cast( m_tco->sampleLength() * ppt / ticksPerTact ), 1 ), rect().bottom() - 2 * spacing ); m_tco->m_sampleBuffer->visualize( p, r, pe->rect() ); // disable antialiasing for borders, since its not needed p.setRenderHint( QPainter::Antialiasing, false ); if( r.width() < width() - 1 ) { p.drawLine( r.x(), r.y() + r.height() / 2, rect().right() - TCO_BORDER_WIDTH, r.y() + r.height() / 2 ); } // inner border p.setPen( c.lighter( 160 ) ); p.drawRect( 1, 1, rect().right() - TCO_BORDER_WIDTH, rect().bottom() - TCO_BORDER_WIDTH ); // outer border p.setPen( c.darker( 300 ) ); p.drawRect( 0, 0, rect().right(), rect().bottom() ); // draw the 'muted' pixmap only if the pattern was manualy muted if( m_tco->isMuted() ) { const int spacing = TCO_BORDER_WIDTH; const int size = 14; p.drawPixmap( spacing, height() - ( size + spacing ), embed::getIconPixmap( "muted", size, size ) ); } // recording sample tracks is not possible at the moment /* if( m_tco->isRecord() ) { p.setFont( pointSize<7>( p.font() ) ); p.setPen( textShadowColor() ); p.drawText( 10, p.fontMetrics().height()+1, "Rec" ); p.setPen( textColor() ); p.drawText( 9, p.fontMetrics().height(), "Rec" ); p.setBrush( QBrush( textColor() ) ); p.drawEllipse( 4, 5, 4, 4 ); }*/ p.end(); painter.drawPixmap( 0, 0, m_paintPixmap ); } SampleTrack::SampleTrack( TrackContainer* tc ) : Track( Track::SampleTrack, tc ), m_volumeModel( DefaultVolume, MinVolume, MaxVolume, 0.1f, this, tr( "Volume" ) ), m_panningModel( DefaultPanning, PanningLeft, PanningRight, 0.1f, this, tr( "Panning" ) ), m_audioPort( tr( "Sample track" ), true, &m_volumeModel, &m_panningModel, &m_mutedModel ) { setName( tr( "Sample track" ) ); m_panningModel.setCenterValue( DefaultPanning ); } SampleTrack::~SampleTrack() { Engine::mixer()->removePlayHandlesOfTypes( this, PlayHandle::TypeSamplePlayHandle ); } bool SampleTrack::play( const MidiTime & _start, const fpp_t _frames, const f_cnt_t _offset, int _tco_num ) { m_audioPort.effects()->startRunning(); bool played_a_note = false; // will be return variable tcoVector tcos; ::BBTrack * bb_track = NULL; if( _tco_num >= 0 ) { if( _start != 0 ) { return false; } tcos.push_back( getTCO( _tco_num ) ); bb_track = BBTrack::findBBTrack( _tco_num ); } else { for( int i = 0; i < numOfTCOs(); ++i ) { TrackContentObject * tco = getTCO( i ); SampleTCO * sTco = dynamic_cast( tco ); float framesPerTick = Engine::framesPerTick(); if( _start >= sTco->startPosition() && _start < sTco->endPosition() ) { if( sTco->isPlaying() == false ) { f_cnt_t sampleStart = framesPerTick * ( _start - sTco->startPosition() ); f_cnt_t tcoFrameLength = framesPerTick * ( sTco->endPosition() - sTco->startPosition() ); f_cnt_t sampleBufferLength = sTco->sampleBuffer()->frames(); //if the Tco smaller than the sample length we play only until Tco end //else we play the sample to the end but nothing more f_cnt_t samplePlayLength = tcoFrameLength > sampleBufferLength ? sampleBufferLength : tcoFrameLength; //we only play within the sampleBuffer limits if( sampleStart < sampleBufferLength ) { sTco->setSampleStartFrame( sampleStart ); sTco->setSamplePlayLength( samplePlayLength ); tcos.push_back( sTco ); sTco->setIsPlaying( true ); } } } else { sTco->setIsPlaying( false ); } } } for( tcoVector::Iterator it = tcos.begin(); it != tcos.end(); ++it ) { SampleTCO * st = dynamic_cast( *it ); if( !st->isMuted() ) { PlayHandle* handle; if( st->isRecord() ) { if( !Engine::getSong()->isRecording() ) { return played_a_note; } SampleRecordHandle* smpHandle = new SampleRecordHandle( st ); handle = smpHandle; } else { SamplePlayHandle* smpHandle = new SamplePlayHandle( st ); smpHandle->setVolumeModel( &m_volumeModel ); smpHandle->setBBTrack( bb_track ); handle = smpHandle; } handle->setOffset( _offset ); // send it to the mixer Engine::mixer()->addPlayHandle( handle ); played_a_note = true; } } return played_a_note; } TrackView * SampleTrack::createView( TrackContainerView* tcv ) { return new SampleTrackView( this, tcv ); } TrackContentObject * SampleTrack::createTCO( const MidiTime & ) { return new SampleTCO( this ); } void SampleTrack::saveTrackSpecificSettings( QDomDocument & _doc, QDomElement & _this ) { m_audioPort.effects()->saveState( _doc, _this ); #if 0 _this.setAttribute( "icon", tlb->pixmapFile() ); #endif m_volumeModel.saveSettings( _doc, _this, "vol" ); m_panningModel.saveSettings( _doc, _this, "pan" ); } void SampleTrack::loadTrackSpecificSettings( const QDomElement & _this ) { QDomNode node = _this.firstChild(); m_audioPort.effects()->clear(); while( !node.isNull() ) { if( node.isElement() ) { if( m_audioPort.effects()->nodeName() == node.nodeName() ) { m_audioPort.effects()->restoreState( node.toElement() ); } } node = node.nextSibling(); } m_volumeModel.loadSettings( _this, "vol" ); m_panningModel.loadSettings( _this, "pan" ); } void SampleTrack::updateTcos() { for( int i = 0; i < numOfTCOs(); ++i ) { TrackContentObject * tco = getTCO( i ); SampleTCO * sTco = dynamic_cast( tco ); sTco->playbackPositionChanged(); } } SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) : TrackView( _t, tcv ) { setFixedHeight( 32 ); TrackLabelButton * tlb = new TrackLabelButton( this, getTrackSettingsWidget() ); connect( tlb, SIGNAL( clicked( bool ) ), this, SLOT( showEffects() ) ); tlb->setIcon( embed::getIconPixmap( "sample_track" ) ); tlb->move( 3, 1 ); tlb->show(); m_volumeKnob = new Knob( knobSmall_17, getTrackSettingsWidget(), tr( "Track volume" ) ); m_volumeKnob->setVolumeKnob( true ); m_volumeKnob->setModel( &_t->m_volumeModel ); m_volumeKnob->setHintText( tr( "Channel volume:" ), "%" ); if( ConfigManager::inst()->value( "ui", "compacttrackbuttons" ).toInt() ) { m_volumeKnob->move( DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT-2*24, 2 ); } else { m_volumeKnob->move( DEFAULT_SETTINGS_WIDGET_WIDTH-2*24, 2 ); } m_volumeKnob->setLabel( tr( "VOL" ) ); m_volumeKnob->show(); m_panningKnob = new Knob( knobSmall_17, getTrackSettingsWidget(), tr( "Panning" ) ); m_panningKnob->setModel( &_t->m_panningModel ); m_panningKnob->setHintText( tr( "Panning:" ), "%" ); m_panningKnob->move( DEFAULT_SETTINGS_WIDGET_WIDTH-24, 2 ); m_panningKnob->setLabel( tr( "PAN" ) ); m_panningKnob->show(); m_effectRack = new EffectRackView( _t->audioPort()->effects() ); m_effectRack->setFixedSize( 240, 242 ); m_effWindow = gui->mainWindow()->addWindowedWidget( m_effectRack ); m_effWindow->setAttribute( Qt::WA_DeleteOnClose, false ); m_effWindow->layout()->setSizeConstraint( QLayout::SetFixedSize ); m_effWindow->setWindowTitle( _t->name() ); m_effWindow->hide(); setModel( _t ); } SampleTrackView::~SampleTrackView() { m_effWindow->deleteLater(); } void SampleTrackView::showEffects() { if( m_effWindow->isHidden() ) { m_effectRack->show(); m_effWindow->show(); m_effWindow->raise(); } else { m_effWindow->hide(); } } void SampleTrackView::modelChanged() { SampleTrack * st = castModel(); m_volumeKnob->setModel( &st->m_volumeModel ); TrackView::modelChanged(); }