Adding Editing Operations

This document provides a hand-holding guide through the process of adding an editing operation to Ardour. It uses the "fade-range" operation as an example.

Understanding the target

Decide what the target for the operation is. The answer is typically one or more regions, but this doesn't address some important question:

  • How is the target defined? Region selection? Range selection? Intersection with an edit point?
  • What type of derived class? Is this an audio editing operation? Does it apply to all types of data?
In the case of fade-range, the target is the set of regions that intersect with the current range selection (made using the range tool). Range selections have nothing inherently to do with regions, so we will find the regions by first finding all selected tracks (range selection will have selected some or all tracks), and then iterating through their playlists to find regions covered by the range selection.

Of course, at present, the selection is a GUI concept, which means that these iterations often have to start with GUI level objects (e.g. TimeAxisView rather than Track).

Find a good example

Before beginning work on code, think about a similar operation that already exists. Similar should cover the target of the operation and how many time points it requires (e.g. a split requires a single position in time, whereas separate requires two). Find the code for this operation and follow it into libardour to get some sense for how it works.

A good way to start this is to find the text for the name of the operation as it appears in a menu, then grep in gtk2_ardour/editor_actions.cc for that text. You will find something like:

	reg_sens (editor_actions, "editor-fade-range", _("Fade Range Selection"), sigc::mem_fun(*this, &Editor::fade_range));
      
The final argument tells you that the method implementing the operation is Editor::fade_range.

Three stages

You don't have to do things in this order, but it is often a convenient approach.

1: Skeleton in the GUI

Add the basic method to Editor, complete with undo/redo framework. Comment out the imagined call to libardour. Here is what things looked like when I started working on fade range, after I added

void
Editor::fade_range ()
{
	TrackViewList ts = selection->tracks.filter_to_unique_playlists ();

	begin_reversible_command (_("fade range"));

	for (TrackViewList::iterator i = ts.begin(); i != ts.end(); ++i) {
		(*i)->fade_range (selection->time);
	}

	commit_reversible_command ();
}

This was largely copy-and-pasted from some similar operation, in this case Editor::cut_copy_ranges.

There is a complication in the first line: when implementing operations that alter regions or playlists, we have to take into account that tracks can share playlists. We don't want to apply the operation twice, so we first filter the track selection to include a set of tracks that represent each affected playlist just once.

As you can see, we're iterating over a set of track views (TimeAxisView). So we need to add code there as well. In general, we don't want to be doing type-testing all over, so we make the method in TimeAxisView an empty virtual method. This way, just the right kind of TimeAxisView (in this case, RouteTimeAxisView rather than, say, AutomationTimeAxisView) can implement something real:

	virtual void fade_range (TimeSelection&) {}
      

Finally, we create a stub/skeleton in RouteTimeAxisView where the action will really happen. It might look something like this:

void
RouteTimeAxisView::fade_range (TimeSelection& selection)
{
	boost::shared_ptr tr = track ();
	boost::shared_ptr playlist;

	if (tr == 0) {
		/* route is a bus, not a track */
		return;
	}

	playlist = tr->playlist();

        playlist->clear_changes ();
        playlist->clear_owned_changes ();

        //playlist->fade_range (selection);

	vector cmds;
	playlist->rdiff (cmds);
	_session->add_commands (cmds);
	_session->add_command (new StatefulDiffCommand (playlist));

}
We have commented out the actual "action" call to Playlist::fade_range() because we haven't defined that yet. But this function contains the rest of the infrastructure needed to make undo/redo work, and to check that we have a Track rather than a Bus to work with (remember, Busses don't have playlists, so they don't have regions, which means they are not targets for this operation).

Now try a compile to make sure that this all fits together correctly.

2: Implement required infrastructure in libardour

Once we got the skeleton in RouteTimeAxisView implemented, we can see that we need a new method for ARDOUR::Playlist (which is where almost all editing operations can be found). This one is very easy to create:

void
Playlist::fade_range (list<AudioRange>& ranges)
{
	 for (list<AudioRange>::iterator r = ranges.begin(); r != ranges.end(); ++r) {
		 for (RegionList::const_iterator i = regions.begin(); i != regions.end(); ++i) {
			 (*i)->fade_range ((*r).start, (*r).end);
		 }
	 }
}
The code is simple: for each part of the range selection (remember: in Ardour, range selections can be discontiguous), iterate over all regions and call Region::fade_range() on each one.

One complication that we could have considered was whether this is an audio-only operation, in which case it might have been better to add it to AudioPlaylist. However in this specific case, it was possible to imagine some potential use for applying a "fade" to MIDI data, so we added it to Playlist.

Now we know we also need Region::fade_range(), so we add that next. But first we note that whatever the operation does, it is likely to be different depending on the type of data involved. We don't have anything useful to do with MIDI data and "fades" at present, so we make it an empty virtual method in Region:

	virtual void fade_range (framepos_t, framepos_t) {}
      
Our "real" implementation in AudioRegion:
void
AudioRegion::fade_range (framepos_t start, framepos_t end)
{
	framepos_t s, e;

	switch (coverage (start, end)) {
	case Evoral::OverlapStart:
		s = _position;
		e = end;
		set_fade_in (FadeConstantPower, e - s);
		break;
	case Evoral::OverlapEnd:
		s = start;
		e = _position + _length;
		set_fade_out (FadeConstantPower, e - s);
		break;
	default:
		return;
	}
}
The important thing to notice in the above code is not the actual logic, but there are already methods existing to modify every aspect of a Region's properties. The goal in new editing operations is typically to provide new ways to manipulate those properties, but in most cases, once you get into Region or AudioRegion, you will be mostly just calling existing methods to modify the region's properties. In this case that means set_fade_in() and set_fade_out().

3: Finalize GUI aspects

  • Uncomment calls into libardour
  • Compile, test and debug till it works.
  • Consider adding a default key binding (currently in mnemonic-us.bindings.in)
  • Write documentation for the manual

Questions

Find people who know on the Ardour IRC channel and ask a question. Be patient and wait for an answer, especially if you're asking during the night time for the eastern USA timezones.