Timeline: More locking fixes. Also, build peaks cache levels in threads launched by UI thread, instead of in the record diskthreads.

pull/43/head
Jonathan Moore Liles 2013-03-01 17:05:42 -08:00
parent 88b552ea00
commit 8d282617e6
11 changed files with 179 additions and 146 deletions

View File

@ -618,13 +618,12 @@ Audio_Region::draw ( void )
oend = end; oend = end;
} }
} }
else
if ( _clip->peaks()->needs_more_peaks() && ! transport->rolling )
{ {
if ( ! transport->rolling ) /* maybe create a thread to make the peaks */
{ /* this function will just return if there's nothing to do. */
/* create a thread to make the peaks */
_clip->peaks()->make_peaks_asynchronously( Audio_Region::peaks_ready_callback, this ); _clip->peaks()->make_peaks_asynchronously( Audio_Region::peaks_ready_callback, this );
}
} }
} }
else else

View File

@ -74,7 +74,7 @@ Disk_Stream::~Disk_Stream ( )
{ {
/* it isn't safe to do all this with the RT thread running */ /* it isn't safe to do all this with the RT thread running */
timeline->wrlock(); // timeline->wrlock();
shutdown(); shutdown();
@ -85,7 +85,7 @@ Disk_Stream::~Disk_Stream ( )
for ( int i = channels(); i--; ) for ( int i = channels(); i--; )
jack_ringbuffer_free( _rb[ i ] ); jack_ringbuffer_free( _rb[ i ] );
timeline->unlock(); // timeline->unlock();
} }
@ -144,13 +144,15 @@ Disk_Stream::shutdown ( void )
{ {
if ( _thread.running() ) if ( _thread.running() )
{ {
DMESSAGE( "Sending terminate signal to diskthread." );
_terminate = true; _terminate = true;
/* try to wake the thread so it'll see that it's time to die */ /* try to wake the thread so it'll see that it's time to die */
while ( _terminate ) while ( _terminate )
{ {
usleep( 100 );
block_processed(); block_processed();
usleep( 1000 );
} }
_thread.join(); _thread.join();

View File

@ -104,9 +104,7 @@ class Peakfile
FILE *_fp; FILE *_fp;
nframes_t _chunksize; nframes_t _chunksize;
int _channels; /* number of channels this peakfile represents */ int _channels; /* number of channels this peakfile represents */
// nframes_t _length; /* length, in frames, of the clip this peakfile represents */
off_t _offset; off_t _offset;
// int _blocks;
struct block_descriptor struct block_descriptor
{ {
@ -127,14 +125,17 @@ class Peakfile
public: public:
int nblocks ( void ) const
{
return blocks.size();
}
Peakfile ( ) Peakfile ( )
{ {
// _blocks = 0;
_fp = NULL; _fp = NULL;
_offset = 0; _offset = 0;
_chunksize = 0; _chunksize = 0;
_channels = 0; _channels = 0;
// _length = 0;
} }
~Peakfile ( ) ~Peakfile ( )
@ -143,6 +144,11 @@ public:
close(); close();
} }
void rescan ( void )
{
blocks.clear();
}
/* int blocks ( void ) const { return blocks.size(); } */ /* int blocks ( void ) const { return blocks.size(); } */
/** find the best block for /chunksize/ */ /** find the best block for /chunksize/ */
void void
@ -184,9 +190,6 @@ public:
if ( ! blocks.size() ) if ( ! blocks.size() )
FATAL( "Peak file contains no blocks!" ); FATAL( "Peak file contains no blocks!" );
// DMESSAGE( "peakfile has %d blocks.", blocks.size() );
blocks.sort(); blocks.sort();
/* fall back on the smallest chunksize */ /* fall back on the smallest chunksize */
@ -204,7 +207,6 @@ public:
} }
// DMESSAGE( "using peakfile block for chunksize %lu", _chunksize ); // DMESSAGE( "using peakfile block for chunksize %lu", _chunksize );
// _blocks = blocks.size();
_offset = ftello( _fp ); _offset = ftello( _fp );
} }
@ -239,6 +241,7 @@ public:
bool bool
open ( const char *name, int channels, nframes_t chunksize ) open ( const char *name, int channels, nframes_t chunksize )
{ {
assert( ! _fp );
// _chunksize = 0; // _chunksize = 0;
_channels = channels; _channels = channels;
@ -263,6 +266,8 @@ public:
bool bool
open ( FILE *fp, int channels, nframes_t chunksize ) open ( FILE *fp, int channels, nframes_t chunksize )
{ {
assert( ! _fp );
_fp = fp; _fp = fp;
_chunksize = 0; _chunksize = 0;
_channels = channels; _channels = channels;
@ -364,7 +369,9 @@ public:
Peaks::Peaks ( Audio_File *c ) Peaks::Peaks ( Audio_File *c )
{ {
_pending = false; _rescan_needed = false;
_first_block_pending = false;
_mipmaps_pending = false;
_clip = c; _clip = c;
_peak_writer = NULL; _peak_writer = NULL;
_peakfile = new Peakfile(); _peakfile = new Peakfile();
@ -412,39 +419,50 @@ Peaks::ready ( nframes_t s, nframes_t npeaks, nframes_t chunksize ) const
bool bool
Peaks::peakfile_ready ( void ) const Peaks::peakfile_ready ( void ) const
{ {
return current() && ! _pending; if ( _rescan_needed )
{
DMESSAGE( "Rescanning peakfile" );
_peakfile->rescan();
if ( _peakfile->open( _clip->filename(), _clip->channels(), 256 ) )
_peakfile->close();
_rescan_needed = false;
}
return current() && ! _first_block_pending;
} }
/** start building peaks and/or peak mipmap in another thread. It is
* safe to call this again before the thread finishes. /callback/ will
* be called with /userdata/ FROM THE PEAK BUILDING THREAD when the
* peaks are finished. */
void void
Peaks::make_peaks_asynchronously ( void(*callback)(void*), void *userdata ) const Peaks::make_peaks_asynchronously ( void(*callback)(void*), void *userdata ) const
{ {
/* already working on it... */ /* already working on it... */
if( _pending ) if( _first_block_pending || _mipmaps_pending )
return; return;
// make_peaks(); /* maybe still building mipmaps... */
_first_block_pending = _peakfile->nblocks() < 1;
_pending = true; _mipmaps_pending = _peakfile->nblocks() <= 1;
peak_thread_data *pd = new peak_thread_data(); peak_thread_data *pd = new peak_thread_data();
pd->callback = callback; pd->callback = callback;
pd->userdata = userdata; pd->userdata = userdata;
pd->peaks = const_cast<Peaks*>(this); pd->peaks = const_cast<Peaks*>(this);
_make_peaks_thread.clone( &Peaks::make_peaks, pd ); _make_peaks_thread.clone( &Peaks::make_peaks, pd );
_make_peaks_thread.detach(); _make_peaks_thread.detach();
DMESSAGE( "Starting new peak building thread" );
} }
nframes_t nframes_t
Peaks::read_peakfile_peaks ( Peak *peaks, nframes_t s, nframes_t npeaks, nframes_t chunksize ) const Peaks::read_peakfile_peaks ( Peak *peaks, nframes_t s, nframes_t npeaks, nframes_t chunksize ) const
{ {
/* if ( _pending ) */ if ( ! _peakfile->open( _clip->filename(), _clip->channels(), chunksize ) )
/* return 0; */
// Peakfile _peakfile;
if ( ! _peakfile->open( _clip->filename(), _clip->channels(), chunksize ) )
{ {
DMESSAGE( "Failed to open peakfile!" ); DMESSAGE( "Failed to open peakfile!" );
return 0; return 0;
@ -545,7 +563,7 @@ Peaks::current ( void ) const
{ {
char *pn = peakname( _clip->filename() ); char *pn = peakname( _clip->filename() );
bool b = ! newer( _clip->filename(), pn ); bool b = newer( pn, _clip->filename() );
free( pn ); free( pn );
@ -557,47 +575,40 @@ void *
Peaks::make_peaks ( void *v ) Peaks::make_peaks ( void *v )
{ {
peak_thread_data *pd = (peak_thread_data*)v; peak_thread_data *pd = (peak_thread_data*)v;
pd->peaks->make_peaks();
if ( pd->callback ) if ( pd->peaks->make_peaks() )
pd->callback( pd->userdata ); {
if ( pd->callback )
pd->callback( pd->userdata );
pd->peaks->_rescan_needed = true;
}
delete pd; delete pd;
return NULL; return NULL;
} }
bool
Peaks::needs_more_peaks ( void ) const
{
return _peakfile->nblocks() <= 1 && ! ( _first_block_pending || _mipmaps_pending );
}
bool bool
Peaks::make_peaks ( void ) const Peaks::make_peaks ( void ) const
{ {
Peaks::Builder pb( this ); Peaks::Builder pb( this );
/* make the first block */
int b = pb.make_peaks(); int b = pb.make_peaks();
_pending = false;
return b;
}
/* thread entry point */
void *
Peaks::make_peaks_mipmap ( void *v )
{
((Peaks*)v)->make_peaks_mipmap();
return NULL;
}
bool
Peaks::make_peaks_mipmap ( void ) const
{
Peaks::Builder pb( this );
bool b = pb.make_peaks_mipmap();
_pending = false;
_first_block_pending = false;
b = pb.make_peaks_mipmap();
_mipmaps_pending = false;
return b; return b;
} }
@ -638,8 +649,6 @@ Peaks::finish_writing ( void )
delete _peak_writer; delete _peak_writer;
_peak_writer = NULL; _peak_writer = NULL;
make_peaks_mipmap();
} }
void void
@ -792,15 +801,12 @@ Peaks::Builder::write_block_header ( nframes_t chunksize )
fflush( fp ); fflush( fp );
} }
/** generate additional cache levels for a peakfile with only 1 block (ie. that of a new capture) */ /** generate additional cache levels for a peakfile with only 1 block (ie. that of a new capture) */
bool bool
Peaks::Builder::make_peaks_mipmap ( void ) Peaks::Builder::make_peaks_mipmap ( void )
{ {
if ( ! Peaks::mipmapped_peakfiles ) if ( ! Peaks::mipmapped_peakfiles )
return true; return false;
Audio_File *_clip = _peaks->_clip; Audio_File *_clip = _peaks->_clip;
@ -895,10 +901,7 @@ Peaks::Builder::make_peaks_mipmap ( void )
} }
while ( len > 0 && s < _clip->length() ); while ( len > 0 && s < _clip->length() );
DMESSAGE( "Last sample was %lu", s ); DMESSAGE( "Last sample was %lu", (unsigned long)s );
/* fflush( fp ); */
/* fsync( fileno( fp ) ); */
pf.leave_open(); pf.leave_open();
} }
@ -918,43 +921,46 @@ Peaks::Builder::make_peaks ( void )
const char *filename = _clip->filename(); const char *filename = _clip->filename();
DMESSAGE( "building peaks for \"%s\"", filename ); if ( _peaks->_peakfile && _peaks->_peakfile->nblocks() > 1 )
char *pn = peakname( filename );
if ( ! ( fp = fopen( pn, "w+" ) ) )
{ {
free( pn ); /* this peakfile already has enough blocks */
return false; return false;
} }
else
{
DMESSAGE( "building peaks for \"%s\"", filename );
char *pn = peakname( filename );
if ( ! ( fp = fopen( pn, "w+" ) ) )
{
free( pn );
return false;
}
free( pn );
_clip->seek( 0 );
Peak buf[ _clip->channels() ];
DMESSAGE( "building level 1 peak cache" );
write_block_header( Peaks::cache_minimum );
/* build first level from source */
off_t len;
do {
len = _peaks->read_source_peaks( buf, 1, Peaks::cache_minimum );
fwrite( buf, sizeof( buf ), len, fp );
}
while ( len );
fclose( fp );
free( pn ); DMESSAGE( "done building peaks" );
_clip->seek( 0 );
Peak buf[ _clip->channels() ];
DMESSAGE( "building level 1 peak cache" );
write_block_header( Peaks::cache_minimum );
/* build first level from source */
off_t len;
do {
len = _peaks->read_source_peaks( buf, 1, Peaks::cache_minimum );
fwrite( buf, sizeof( buf ), len, fp );
} }
while ( len );
/* reopen for reading */
/* fflush( fp ); */
/* fsync( fileno( fp ) ); */
fclose( fp );
make_peaks_mipmap();
DMESSAGE( "done building peaks" );
return true; return true;
} }

View File

@ -35,7 +35,9 @@ class Peakfile;
class Peaks class Peaks
{ {
mutable volatile bool _pending; /* true if first block is still being built */
mutable volatile bool _first_block_pending;
mutable volatile bool _mipmaps_pending;
mutable Thread _make_peaks_thread; mutable Thread _make_peaks_thread;
mutable Thread _make_peaks_mipmap_thread; mutable Thread _make_peaks_mipmap_thread;
@ -110,6 +112,8 @@ class Peaks
mutable float _fpp; mutable float _fpp;
volatile mutable bool _rescan_needed;
nframes_t read_peaks ( nframes_t s, nframes_t npeaks, nframes_t chunksize ) const; nframes_t read_peaks ( nframes_t s, nframes_t npeaks, nframes_t chunksize ) const;
nframes_t read_source_peaks ( Peak *peaks, nframes_t s, nframes_t npeaks, nframes_t chunksize ) const; nframes_t read_source_peaks ( Peak *peaks, nframes_t s, nframes_t npeaks, nframes_t chunksize ) const;
nframes_t read_source_peaks ( Peak *peaks, nframes_t npeaks, nframes_t chunksize ) const; nframes_t read_source_peaks ( Peak *peaks, nframes_t npeaks, nframes_t chunksize ) const;
@ -145,11 +149,11 @@ public:
bool ready ( nframes_t s, nframes_t npeaks, nframes_t chunksize ) const; bool ready ( nframes_t s, nframes_t npeaks, nframes_t chunksize ) const;
bool make_peaks ( void ) const; bool make_peaks ( void ) const;
bool make_peaks_mipmap ( void ) const;
void make_peaks_asynchronously ( void(*callback)(void*), void *userdata ) const; void make_peaks_asynchronously ( void(*callback)(void*), void *userdata ) const;
void prepare_for_writing ( void ); void prepare_for_writing ( void );
void finish_writing ( void ); void finish_writing ( void );
void write ( sample_t *buf, nframes_t nframes ); void write ( sample_t *buf, nframes_t nframes );
bool needs_more_peaks ( void ) const;
}; };

View File

@ -34,11 +34,16 @@
bool bool
Timeline::record ( void ) Timeline::record ( void )
{ {
THREAD_ASSERT( UI );
DMESSAGE( "Initiating recording." );
/* FIXME: right place for this? */ /* FIXME: right place for this? */
if ( transport->automatically_create_takes() && if ( transport->automatically_create_takes() &&
! _created_new_takes ) ! _created_new_takes )
{ {
DMESSAGE( "Creating new takes." );
add_take_for_armed_tracks(); add_take_for_armed_tracks();
_created_new_takes = true; _created_new_takes = true;
} }
@ -53,12 +58,16 @@ Timeline::record ( void )
if ( transport->punch_enabled() ) if ( transport->punch_enabled() )
{ {
DMESSAGE( "Finding next punch region following frame %lu...", (unsigned long)frame);
const Sequence_Widget *w = punch_cursor_track->next( frame ); const Sequence_Widget *w = punch_cursor_track->next( frame );
if ( w && w->start() >= frame ) if ( w && w->start() >= frame )
{ {
frame = w->start(); frame = w->start();
_punch_out_frame = w->start() + w->length(); _punch_out_frame = w->start() + w->length();
DMESSAGE( "Punch enabled... Will punch in at frame %lu.", (unsigned long)frame );
} }
} }
@ -94,6 +103,8 @@ Timeline::punch_in ( nframes_t frame )
void void
Timeline::punch_out ( nframes_t frame ) Timeline::punch_out ( nframes_t frame )
{ {
THREAD_ASSERT( UI );
for ( int i = tracks->children(); i-- ; ) for ( int i = tracks->children(); i-- ; )
{ {
Track *t = (Track*)tracks->child( i ); Track *t = (Track*)tracks->child( i );
@ -102,9 +113,11 @@ Timeline::punch_out ( nframes_t frame )
t->record_ds->stop( frame ); t->record_ds->stop( frame );
} }
/* wait until finalization is complete before continuing */ DMESSAGE( "Waiting for record threads to shutdown." );
/* none of the record threads need to call Fl::lock, because we're
* holding up the UI thread waiting for them to join.*/
DMESSAGE( "Waiting for record threads to shutdown" );
for ( int i = tracks->children(); i-- ; ) for ( int i = tracks->children(); i-- ; )
{ {
Track *t = (Track*)tracks->child( i ); Track *t = (Track*)tracks->child( i );
@ -112,6 +125,8 @@ Timeline::punch_out ( nframes_t frame )
if ( t->armed() && t->record_ds ) if ( t->armed() && t->record_ds )
t->record_ds->shutdown(); t->record_ds->shutdown();
} }
DMESSAGE( "All record threads stopped." );
_punched_in = false; _punched_in = false;
_punch_in_frame = 0; _punch_in_frame = 0;
@ -122,6 +137,8 @@ Timeline::punch_out ( nframes_t frame )
void void
Timeline::stop ( void ) Timeline::stop ( void )
{ {
THREAD_ASSERT( UI );
nframes_t frame = transport->frame; nframes_t frame = transport->frame;
if ( transport->punch_enabled() ) if ( transport->punch_enabled() )

View File

@ -272,15 +272,19 @@ Track::record ( Capture *c, nframes_t frame )
/* open it again for reading in the GUI thread */ /* open it again for reading in the GUI thread */
// Audio_File *af = Audio_File::from_file( c->audio_file->name() ); // Audio_File *af = Audio_File::from_file( c->audio_file->name() );
/* must acquire the FLTK lock because adding a widget might interfere with drawing */
Fl::lock();
/* must acquire a write lock because the Audio_Region constructor /* must acquire a write lock because the Audio_Region constructor
* will add the region to the specified sequence */ * will add the region to the specified sequence, which might affect playback */
timeline->wrlock(); timeline->wrlock();
c->region = new Audio_Region( c->audio_file, sequence(), frame ); c->region = new Audio_Region( c->audio_file, sequence(), frame );
timeline->unlock(); timeline->unlock();
Fl::unlock();
c->region->prepare(); c->region->prepare();
} }
@ -305,14 +309,11 @@ Track::finalize ( Capture *c, nframes_t frame )
/* adjust region start for latency */ /* adjust region start for latency */
/* FIXME: is just looking at the first channel good enough? */ /* FIXME: is just looking at the first channel good enough? */
timeline->wrlock();
DMESSAGE( "finalizing audio file" ); DMESSAGE( "finalizing audio file" );
/* must finalize audio before peaks file, otherwise another thread
* might think the peaks are out of date and attempt to regenerate
* them */
c->audio_file->finalize(); c->audio_file->finalize();
timeline->wrlock();
c->region->finalize( frame ); c->region->finalize( frame );
nframes_t capture_offset = 0; nframes_t capture_offset = 0;
@ -333,6 +334,4 @@ Track::finalize ( Capture *c, nframes_t frame )
c->region->offset( capture_offset ); c->region->offset( capture_offset );
timeline->unlock(); timeline->unlock();
// delete c->audio_file;
} }

View File

@ -131,9 +131,7 @@ Project::write_info ( void )
void void
Project::undo ( void ) Project::undo ( void )
{ {
timeline->wrlock();
Loggable::undo(); Loggable::undo();
timeline->unlock();
} }
bool bool
@ -203,12 +201,8 @@ Project::close ( void )
if ( ! save() ) if ( ! save() )
return false; return false;
timeline->wrlock();
Loggable::close(); Loggable::close();
timeline->unlock();
// write_info(); // write_info();
_is_open = false; _is_open = false;

View File

@ -188,14 +188,11 @@ Sequence::add ( Sequence_Widget *r )
{ {
// Logger _log( this ); // Logger _log( this );
if ( r->sequence() ) if ( r->sequence() && r->sequence() != this )
{ {
/* This method can be called from the Capture thread as well as the GUI thread, so we must lock FLTK before redraw */ /* This method can be called from the Capture thread as well as the GUI thread, so we must lock FLTK before redraw */
Fl::lock();
r->redraw(); r->redraw();
r->sequence()->remove( r ); r->sequence()->remove( r );
Fl::unlock();
// r->track()->redraw();
} }
r->sequence( this ); r->sequence( this );
@ -481,28 +478,30 @@ Sequence::handle ( int m )
Sequence_Widget::pushed( NULL ); Sequence_Widget::pushed( NULL );
} }
Loggable::block_start(); if ( _delete_queue.size() )
timeline->wrlock();
while ( _delete_queue.size() )
{ {
Loggable::block_start();
Sequence_Widget *t = _delete_queue.front();
_delete_queue.pop(); while ( _delete_queue.size() )
if ( Sequence_Widget::pushed() == t )
Sequence_Widget::pushed( NULL );
if ( Sequence_Widget::belowmouse() == t )
{ {
Sequence_Widget::belowmouse()->handle( FL_LEAVE ); Sequence_Widget *t = _delete_queue.front();
Sequence_Widget::belowmouse( NULL ); _delete_queue.pop();
}
if ( Sequence_Widget::pushed() == t )
Sequence_Widget::pushed( NULL );
if ( Sequence_Widget::belowmouse() == t )
{
Sequence_Widget::belowmouse()->handle( FL_LEAVE );
Sequence_Widget::belowmouse( NULL );
}
delete t; timeline->wrlock();
delete t;
timeline->unlock();
} }
timeline->unlock();
Loggable::block_end(); Loggable::block_end();
}
if ( m == FL_PUSH ) if ( m == FL_PUSH )
return 1; return 1;
@ -550,6 +549,7 @@ const Sequence_Widget *
Sequence::next ( nframes_t from ) const Sequence::next ( nframes_t from ) const
{ {
for ( list <Sequence_Widget*>::const_iterator i = _widgets.begin(); i != _widgets.end(); i++ ) for ( list <Sequence_Widget*>::const_iterator i = _widgets.begin(); i != _widgets.end(); i++ )
// if ( (*i)->start() >= from )
if ( (*i)->start() > from ) if ( (*i)->start() > from )
return *i; return *i;

View File

@ -396,7 +396,7 @@ Project::compact();}
} { } {
MenuItem {} { MenuItem {} {
label Undo label Undo
callback {Project::undo();} callback {timeline->command_undo();}
xywh {5 5 40 25} shortcut 0x4007a divider xywh {5 5 40 25} shortcut 0x4007a divider
} }
MenuItem {} { MenuItem {} {

View File

@ -1253,6 +1253,8 @@ Timeline::draw_cursors ( void ) const
void void
Timeline::draw ( void ) Timeline::draw ( void )
{ {
/* Any code that might affect the structures used for drawing from
* another thread must use Fl::lock()/unlock()! */
THREAD_ASSERT( UI ); THREAD_ASSERT( UI );
int X, Y, W, H; int X, Y, W, H;
@ -1260,8 +1262,6 @@ Timeline::draw ( void )
int bdx = 0; int bdx = 0;
int bdw = 0; int bdw = 0;
rdlock();
X = tracks->x() + bdx + 1; X = tracks->x() + bdx + 1;
Y = tracks->y(); Y = tracks->y();
W = tracks->w() - bdw - 1; W = tracks->w() - bdw - 1;
@ -1363,8 +1363,6 @@ done:
_old_xposition = xoffset; _old_xposition = xoffset;
_old_yposition = panzoomer->y_value(); _old_yposition = panzoomer->y_value();
unlock();
} }
void void
@ -1438,6 +1436,7 @@ Timeline::redraw_playhead ( void )
/* we've passed one or more punch regions... punch in for the next, if available. */ /* we've passed one or more punch regions... punch in for the next, if available. */
const Sequence_Widget *w = punch_cursor_track->next( transport->frame ); const Sequence_Widget *w = punch_cursor_track->next( transport->frame );
DMESSAGE( "Delayed punch in" );
if ( w && if ( w &&
w->start() > transport->frame ) w->start() > transport->frame )
{ {
@ -1993,15 +1992,27 @@ Timeline::command_remove_track ( Track *track )
} }
void void
Timeline::command_quit ( ) Timeline::command_quit ( void )
{ {
timeline->wrlock();
Project::close(); Project::close();
timeline->unlock();
command_save(); command_save();
while ( Fl::first_window() ) Fl::first_window()->hide(); while ( Fl::first_window() ) Fl::first_window()->hide();
} }
void
Timeline::command_undo ( void )
{
wrlock();
Project::undo();
unlock();
}
bool bool
Timeline::command_load ( const char *name, const char *display_name ) Timeline::command_load ( const char *name, const char *display_name )
{ {

View File

@ -257,6 +257,7 @@ public:
void command_move_track_up ( Track *track ); void command_move_track_up ( Track *track );
void command_move_track_down ( Track *track ); void command_move_track_down ( Track *track );
void command_undo ( void );
int find_track ( const Track * track ) const; int find_track ( const Track * track ) const;