Files
lmms/src/tracks/SampleTrack.cpp
T

818 lines
18 KiB
C++

/*
* SampleTrack.cpp - implementation of class SampleTrack, a track which
* provides arrangement of samples
*
* Copyright (c) 2005-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>
*
* 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 <QDomElement>
#include <QDropEvent>
#include <QMenu>
#include <QLayout>
#include <QMdiArea>
#include <QMdiSubWindow>
#include <QPainter>
#include <QPushButton>
#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<SampleTrack*>( 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<int>( _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<SampleTrack*>( 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<SampleTCO*>( getTrackContentObject() );
if( sTco )
{
sTco->updateTrackTcos();
}
}
TrackContentObjectView::mousePressEvent( _me );
}
}
void SampleTCOView::mouseReleaseEvent(QMouseEvent *_me)
{
if( _me->button() == Qt::MiddleButton && !_me->modifiers() )
{
SampleTCO * sTco = dynamic_cast<SampleTCO*>( 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<int>( 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<SampleTCO*>( 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<SampleTCO *>( *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<SampleTCO*>( 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<SampleTrack>();
m_volumeKnob->setModel( &st->m_volumeModel );
TrackView::modelChanged();
}