--- /dev/null
+/* Copyright 2009-2010 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+using Logging;
+
+namespace Model {
+
+public abstract class Track : Object {
+ protected weak Project project;
+ public Gee.ArrayList<Clip> clips = new Gee.ArrayList<Clip>(); // all clips, sorted by time
+ public string display_name;
+ bool is_selected;
+
+ public signal void clip_added(Clip clip, bool select);
+ public signal void clip_removed(Clip clip);
+
+ public signal void track_renamed(Track track);
+ public signal void track_selection_changed(Track track);
+ public signal void track_hidden(Track track);
+ public signal void track_removed(Track track);
+ public signal void error_occurred(string major_error, string? minor_error);
+
+ public Track(Project project, string display_name) {
+ this.project = project;
+ this.display_name = display_name;
+ }
+
+ protected abstract string name();
+ public abstract MediaType media_type();
+
+ public void hide() {
+ track_hidden(this);
+ }
+
+ public bool contains_clipfile(ClipFile f) {
+ foreach (Clip c in clips) {
+ if (c.clipfile == f)
+ return true;
+ }
+ return false;
+ }
+
+ protected abstract bool check(Clip clip);
+
+ public int64 get_time_from_pos(Clip clip, bool after) {
+ if (after)
+ return clip.start + clip.duration;
+ else
+ return clip.start;
+ }
+
+ int get_clip_from_time(int64 time) {
+ for (int i = 0; i < clips.size; i++) {
+ if (time >= clips[i].start &&
+ time < clips[i].end)
+ return i;
+ }
+ return -1;
+ }
+
+ public int64 snap_clip(Clip c, int64 span) {
+ foreach (Clip cl in clips) {
+ int64 new_start = c.snap(cl, span);
+ if (new_start != c.start) {
+ return new_start;
+ }
+ }
+ return c.start;
+ }
+
+ public bool snap_coord(out int64 coord, int64 span) {
+ foreach (Clip c in clips) {
+ if (c.snap_coord(out coord, span))
+ return true;
+ }
+ return false;
+ }
+
+ public bool clip_is_near(Model.Clip clip, int64 range, out int64 adjustment) {
+ foreach (Clip potential_clip in clips) {
+ if (potential_clip != clip) {
+ int64 difference = clip.start - potential_clip.end;
+ if (difference.abs() < range) {
+ adjustment = -difference;
+ return true;
+ }
+
+ difference = potential_clip.start - clip.end;
+ if (difference.abs() < range) {
+ adjustment = difference;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ int get_insert_index(int64 time) {
+ int end_ret = 0;
+ for (int i = 0; i < clips.size; i++) {
+ Clip c = clips[i];
+
+ if (time >= c.start) {
+ if (time < c.start + c.duration/2)
+ return i;
+ else if (time < c.start + c.duration)
+ return i + 1;
+ else
+ end_ret ++;
+ }
+ }
+ return end_ret;
+ }
+
+ // This is called to find the first gap after a start time
+ public Gap find_first_gap(int64 start) {
+ int64 new_start = 0;
+ int64 new_end = int64.MAX;
+
+ foreach (Clip c in clips) {
+ if (c.start > new_start &&
+ c.start > start) {
+ new_end = c.start;
+ break;
+ }
+ new_start = c.end;
+ }
+ return new Gap(new_start, new_end);
+ }
+
+ // This is always called with the assumption that we are not on a clip
+ int get_gap_index(int64 time) {
+ int i = 0;
+ while (i < clips.size) {
+ if (time <= clips[i].start)
+ break;
+ i++;
+ }
+ return i;
+ }
+
+ // If we are not on a valid gap (as in, a space between two clips or between the start
+ // and the first clip), we return an empty (and invalid) gap
+ public void find_containing_gap(int64 time, out Gap g) {
+ g = new Gap(0, 0);
+
+ int index = get_gap_index(time);
+ if (index < clips.size) {
+ g.start = index > 0 ? clips[index - 1].end : 0;
+ g.end = clips[index].start;
+ }
+ }
+
+ public Clip? find_overlapping_clip(int64 start, int64 length) {
+ for (int i = 0; i < clips.size; i++) {
+ Clip c = clips[i];
+ if (c.overlap_pos(start, length))
+ return c;
+ }
+ return null;
+ }
+
+ public Clip? find_nearest_clip_edge(int64 time, out bool after) {
+ int limit = clips.size * 2;
+ int64 prev_time = clips[0].start;
+
+ for (int i = 1; i < limit; i++) {
+ Clip c = clips[i / 2];
+ int64 t;
+
+ if (i % 2 == 0)
+ t = c.start;
+ else
+ t = c.end;
+
+ if (t > time) {
+ if (t - time < time - prev_time) {
+ after = ((i % 2) != 0);
+ return clips[i / 2];
+ } else {
+ after = ((i % 2) == 0);
+ return clips[(i - 1) / 2];
+ }
+ }
+ prev_time = t;
+ }
+
+ after = true;
+ return clips[clips.size - 1];
+ }
+
+ void do_clip_overwrite(Clip c) {
+ int start_index = get_clip_from_time(c.start);
+ int end_index = get_clip_from_time(c.end);
+
+ if (end_index >= 0) {
+ int64 diff = c.end - clips[end_index].start;
+ if (end_index == start_index) {
+ if (c == clips[end_index]) {
+ return;
+ }
+
+ if (diff > 0) {
+ Clip cl = new Clip(clips[end_index].clipfile, clips[end_index].type,
+ clips[end_index].name, c.end,
+ clips[end_index].media_start + diff,
+ clips[end_index].duration - diff, false);
+ append_at_time(cl, cl.start, false);
+ }
+ } else {
+ trim(clips[end_index], diff, Gdk.WindowEdge.WEST);
+ }
+ }
+ if (start_index >= 0 && clips[start_index] != c) {
+ int64 delta = clips[start_index].end - c.start;
+ trim(clips[start_index], -delta, Gdk.WindowEdge.EAST);
+ }
+
+ int i = 0;
+ // TODO: This code assumes that when a delete happens, it is reflected immediately.
+ // When we are in an undo (or redo) deleting will happen later. It would be better
+ // for callers not to have to deal with this problem. Too large of a change for now.
+ if (!project.undo_manager.in_undo) {
+ while (i < clips.size) {
+ if (clips[i] != c &&
+ clips[i].start >= c.start &&
+ clips[i].end <= c.end) {
+ delete_clip(clips[i]);
+ }
+ else
+ i++;
+ }
+ }
+ }
+
+ public void move(Clip c, int64 pos, int64 original_time) {
+ Command command = new ClipAddCommand(this, c, original_time, pos);
+ project.do_command(command);
+ }
+
+ public void _move(Clip c, int64 pos) {
+ if (pos < 0) {
+ pos = 0;
+ }
+ c.start = pos;
+ do_clip_overwrite(c);
+
+ insert_clip_into_array(c, get_insert_index(c.start));
+ project.reseek();
+ }
+
+ public void add(Clip c, int64 pos, bool select) {
+ if (!check(c))
+ return;
+
+ _move(c, pos);
+ clip_added(c, select);
+ }
+
+ public virtual void on_clip_updated(Clip clip) {
+ emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_updated");
+ }
+
+ public void do_clip_paste(Clip clip, int64 position) {
+ append_at_time(clip, position, true);
+ }
+
+ public Clip? get_clip(int i) {
+ if (i < 0 || i >= clips.size)
+ error("get_clip: Invalid index! %d (%d)", i, clips.size);
+ return clips[i];
+ }
+
+ public int get_clip_index(Clip c) {
+ for (int i = 0; i < clips.size; i++) {
+ if (clips[i] == c) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public Clip? get_clip_by_position(int64 pos) {
+ int length = clips.size;
+
+ for (int i = length - 1; i >= 0; i--)
+ if (clips[i].start < pos)
+ return pos >= clips[i].end ? null : clips[i];
+ return null;
+ }
+
+ public int64 get_length() {
+ return clips.size == 0 ? 0 : clips[clips.size - 1].start + clips[clips.size - 1].duration;
+ }
+
+ public void _append_at_time(Clip c, int64 time, bool select) {
+ add(c, time, select);
+ }
+
+ public void append_at_time(Clip c, int64 time, bool select) {
+ Command command = new ClipCommand(ClipCommand.Action.APPEND, this, c, time, select);
+ project.do_command(command);
+ }
+
+ public void delete_clip(Clip clip) {
+ Command clip_command = new ClipCommand(ClipCommand.Action.DELETE,
+ this, clip, clip.start, false);
+ project.do_command(clip_command);
+ }
+
+ public void _delete_clip(Clip clip) {
+ int index = get_clip_index(clip);
+ assert(index != -1);
+ clips.remove_at(index);
+
+ clip.removed(clip);
+ clip_removed(clip);
+ }
+
+ public void delete_gap(Gap g) {
+ project.reseek();
+ }
+
+ public void remove_clip_from_array(Clip pos) {
+ clips.remove(pos);
+ }
+
+ void insert_clip_into_array(Clip c, int pos) {
+ c.updated.connect(on_clip_updated);
+ clips.insert(pos, c);
+ }
+
+ public void delete_all_clips() {
+ uint size = clips.size;
+ for (int i = 0; i < size; i++) {
+ delete_clip(clips[0]);
+ }
+ project.media_engine.go(0);
+ }
+
+ public void revert_to_original(Clip clip) {
+ Command command = new ClipRevertCommand(this, clip);
+ project.do_command(command);
+ }
+
+ public void _revert_to_original(Clip c) {
+ int index = get_clip_index(c);
+ if (index == -1)
+ error("revert_to_original: Clip not in track array!");
+
+ c.set_media_start_duration(0, c.clipfile.length);
+
+ project.media_engine.go(c.start);
+ }
+
+ public bool are_contiguous_clips(int64 position) {
+ Clip right_clip = get_clip_by_position(position + 1);
+ Clip left_clip = get_clip_by_position(position - 1);
+
+ return left_clip != null && right_clip != null &&
+ left_clip != right_clip &&
+ left_clip.clipfile == right_clip.clipfile &&
+ left_clip.end == right_clip.start;
+ }
+
+ public void split_at(int64 position) {
+ Command command = new ClipSplitCommand(ClipSplitCommand.Action.SPLIT, this, position);
+ project.do_command(command);
+ }
+
+ public void _split_at(int64 position) {
+ Clip c = get_clip_by_position(position);
+ if (c == null)
+ return;
+
+ Clip cn = new Clip(c.clipfile, c.type, c.name, position,
+ (position - c.start) + c.media_start,
+ c.start + c.duration - position, false);
+
+ c.duration = position - c.start;
+
+ add(cn, position, false);
+ }
+
+ public void join(int64 position) {
+ Command command = new ClipSplitCommand(ClipSplitCommand.Action.JOIN, this, position);
+ project.do_command(command);
+ }
+
+ public void _join(int64 position) {
+ assert(are_contiguous_clips(position));
+ if (are_contiguous_clips(position)) {
+ Clip right_clip = get_clip_by_position(position + 1);
+ assert(right_clip != null);
+
+ int right_clip_index = get_clip_index(right_clip);
+ assert(right_clip_index > 0);
+
+ int left_clip_index = right_clip_index - 1;
+ Clip left_clip = get_clip(left_clip_index);
+ assert(left_clip != null);
+ left_clip.duration = right_clip.end - left_clip.start;
+ _delete_clip(right_clip);
+ }
+ }
+
+ public void trim(Clip clip, int64 delta, Gdk.WindowEdge edge) {
+ Command command = new ClipTrimCommand(this, clip, delta, edge);
+ project.do_command(command);
+ }
+
+ public void _trim(Clip clip, int64 delta, Gdk.WindowEdge edge) {
+ clip.trim(delta, edge);
+ do_clip_overwrite(clip);
+ }
+
+ public int64 previous_edit(int64 pos) {
+ for (int i = clips.size - 1; i >= 0 ; --i) {
+ Clip c = clips[i];
+ if (c.end < pos)
+ return c.end;
+ if (c.start < pos)
+ return c.start;
+ }
+ return 0;
+ }
+
+ public int64 next_edit(int64 pos) {
+ foreach (Clip c in clips)
+ if (c.start > pos)
+ return c.start;
+ else if (c.end > pos)
+ return c.end;
+ return get_length();
+ }
+
+ public virtual void write_attributes(FileStream f) {
+ f.printf("type=\"%s\" name=\"%s\" ", name(), get_display_name());
+ }
+
+ public void save(FileStream f) {
+ f.printf(" <track ");
+ write_attributes(f);
+ f.printf(">\n");
+ for (int i = 0; i < clips.size; i++)
+ clips[i].save(f, project.get_clipfile_index(clips[i].clipfile));
+ f.puts(" </track>\n");
+ }
+
+ public string get_display_name() {
+ return display_name;
+ }
+
+ public void set_display_name(string new_display_name) {
+ if (display_name != new_display_name) {
+ display_name = new_display_name;
+ track_renamed(this);
+ }
+ }
+
+ public void set_selected(bool is_selected) {
+ if (this.is_selected != is_selected) {
+ this.is_selected = is_selected;
+ track_selection_changed(this);
+ }
+ }
+
+ public bool get_is_selected() {
+ return is_selected;
+ }
+}
+
+public class AudioTrack : Track {
+ double pan;
+ double volume;
+
+ int default_num_channels;
+ public static const int INVALID_CHANNEL_COUNT = -1;
+
+ public signal void parameter_changed(Parameter parameter, double new_value);
+ public signal void level_changed(double level_left, double level_right);
+ public signal void channel_count_changed(int channel_count);
+
+ public AudioTrack(Project project, string display_name) {
+ base(project, display_name);
+
+ set_default_num_channels(INVALID_CHANNEL_COUNT);
+ _set_pan(0);
+ _set_volume(1.0);
+ }
+
+ protected override string name() { return "audio"; }
+
+ public override MediaType media_type() {
+ return MediaType.AUDIO;
+ }
+
+ public override void write_attributes(FileStream f) {
+ base.write_attributes(f);
+ f.printf("volume=\"%f\" panorama=\"%f\" ", get_volume(), get_pan());
+
+ int channels;
+ if (get_num_channels(out channels) &&
+ channels != INVALID_CHANNEL_COUNT)
+ f.printf("channels=\"%d\" ", channels);
+ }
+
+ public void set_pan(double new_value) {
+ double old_value = get_pan();
+ if (!float_within(new_value - old_value, 0.05)) {
+ ParameterCommand parameter_command =
+ new ParameterCommand(this, Parameter.PAN, new_value, old_value);
+ project.do_command(parameter_command);
+ }
+ }
+
+ public void _set_pan(double new_value) {
+ assert(new_value <= 1.0 && new_value >= -1.0);
+ double old_value = get_pan();
+ if (!float_within(old_value - new_value, 0.05)) {
+ pan = new_value;
+ parameter_changed(Parameter.PAN, new_value);
+ }
+ }
+
+ public double get_pan() {
+ return pan;
+ }
+
+ public void set_volume(double new_volume) {
+ double old_volume = get_volume();
+ if (!float_within(old_volume - new_volume, 0.005)) {
+ ParameterCommand parameter_command =
+ new ParameterCommand(this, Parameter.VOLUME, new_volume, old_volume);
+ project.do_command(parameter_command);
+ }
+ }
+
+ public void _set_volume(double new_volume) {
+ assert(new_volume >= 0.0 && new_volume <= 10.0);
+ double old_volume = get_volume();
+ if (!float_within(old_volume - new_volume, 0.005)) {
+ volume = new_volume;
+ parameter_changed(Parameter.VOLUME, new_volume);
+ }
+ }
+
+ public double get_volume() {
+ return volume;
+ }
+
+ public void set_default_num_channels(int num) {
+ default_num_channels = num;
+ }
+
+ public bool get_num_channels(out int num) {
+ if (clips.size == 0)
+ return false;
+
+ foreach (Clip c in clips) {
+ if (c.clipfile.is_online()) {
+ bool can = c.clipfile.get_num_channels(out num);
+ assert(can);
+
+ return can;
+ }
+ }
+
+ if (default_num_channels == INVALID_CHANNEL_COUNT)
+ return false;
+
+ num = default_num_channels;
+ return true;
+ }
+
+ public override bool check(Clip clip) {
+ if (!clip.clipfile.is_online()) {
+ return true;
+ }
+
+ if (clips.size == 0) {
+ int number_of_channels = 0;
+ if (clip.clipfile.get_num_channels(out number_of_channels)) {
+ channel_count_changed(number_of_channels);
+ }
+ return true;
+ }
+
+ bool good = false;
+ int number_of_channels;
+ if (clip.clipfile.get_num_channels(out number_of_channels)) {
+ int track_channel_count;
+ if (get_num_channels(out track_channel_count)) {
+ good = track_channel_count == number_of_channels;
+ }
+ }
+
+ if (!good) {
+ string sub_error = number_of_channels == 1 ?
+ "Mono clips cannot go on stereo tracks." :
+ "Stereo clips cannot go on mono tracks.";
+ error_occurred("Cannot add clip to track", sub_error);
+ }
+ return good;
+ }
+
+ public void on_level_changed(double level_left, double level_right) {
+ emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_level_changed");
+ level_changed(level_left, level_right);
+ }
+
+ public override void on_clip_updated(Clip clip) {
+ if (clip.clipfile.is_online()) {
+ int number_of_channels = 0;
+ if (get_num_channels(out number_of_channels)) {
+ channel_count_changed(number_of_channels);
+ }
+ }
+ }
+}
+}