1 /* Copyright 2009-2010 Yorba Foundation
3 * This software is licensed under the GNU Lesser General Public License
4 * (version 2.1 or later). See the COPYING file in this distribution.
11 public abstract class Track : Object {
12 protected weak Project project;
13 public Gee.ArrayList<Clip> clips = new Gee.ArrayList<Clip>(); // all clips, sorted by time
14 public string display_name;
17 public signal void clip_added(Clip clip, bool select);
18 public signal void clip_removed(Clip clip);
20 public signal void track_renamed(Track track);
21 public signal void track_selection_changed(Track track);
22 public signal void track_hidden(Track track);
23 public signal void track_removed(Track track);
24 public signal void error_occurred(string major_error, string? minor_error);
26 public Track(Project project, string display_name) {
27 this.project = project;
28 this.display_name = display_name;
31 protected abstract string name();
32 public abstract MediaType media_type();
38 public bool contains_clipfile(ClipFile f) {
39 foreach (Clip c in clips) {
46 protected abstract bool check(Clip clip);
48 public int64 get_time_from_pos(Clip clip, bool after) {
50 return clip.start + clip.duration;
55 int get_clip_from_time(int64 time) {
56 for (int i = 0; i < clips.size; i++) {
57 if (time >= clips[i].start &&
64 public int64 snap_clip(Clip c, int64 span) {
65 foreach (Clip cl in clips) {
66 int64 new_start = c.snap(cl, span);
67 if (new_start != c.start) {
74 public bool snap_coord(out int64 coord, int64 span) {
75 foreach (Clip c in clips) {
76 if (c.snap_coord(out coord, span))
82 public bool clip_is_near(Model.Clip clip, int64 range, out int64 adjustment) {
83 foreach (Clip potential_clip in clips) {
84 if (potential_clip != clip) {
85 int64 difference = clip.start - potential_clip.end;
86 if (difference.abs() < range) {
87 adjustment = -difference;
91 difference = potential_clip.start - clip.end;
92 if (difference.abs() < range) {
93 adjustment = difference;
101 int get_insert_index(int64 time) {
103 for (int i = 0; i < clips.size; i++) {
106 if (time >= c.start) {
107 if (time < c.start + c.duration/2)
109 else if (time < c.start + c.duration)
118 // This is called to find the first gap after a start time
119 public Gap find_first_gap(int64 start) {
121 int64 new_end = int64.MAX;
123 foreach (Clip c in clips) {
124 if (c.start > new_start &&
131 return new Gap(new_start, new_end);
134 // This is always called with the assumption that we are not on a clip
135 int get_gap_index(int64 time) {
137 while (i < clips.size) {
138 if (time <= clips[i].start)
145 // If we are not on a valid gap (as in, a space between two clips or between the start
146 // and the first clip), we return an empty (and invalid) gap
147 public void find_containing_gap(int64 time, out Gap g) {
150 int index = get_gap_index(time);
151 if (index < clips.size) {
152 g.start = index > 0 ? clips[index - 1].end : 0;
153 g.end = clips[index].start;
157 public Clip? find_overlapping_clip(int64 start, int64 length) {
158 for (int i = 0; i < clips.size; i++) {
160 if (c.overlap_pos(start, length))
166 public Clip? find_nearest_clip_edge(int64 time, out bool after) {
167 int limit = clips.size * 2;
168 int64 prev_time = clips[0].start;
170 for (int i = 1; i < limit; i++) {
171 Clip c = clips[i / 2];
180 if (t - time < time - prev_time) {
181 after = ((i % 2) != 0);
184 after = ((i % 2) == 0);
185 return clips[(i - 1) / 2];
192 return clips[clips.size - 1];
195 void do_clip_overwrite(Clip c) {
196 int start_index = get_clip_from_time(c.start);
197 int end_index = get_clip_from_time(c.end);
199 if (end_index >= 0) {
200 int64 diff = c.end - clips[end_index].start;
201 if (end_index == start_index) {
202 if (c == clips[end_index]) {
207 Clip cl = new Clip(clips[end_index].clipfile, clips[end_index].type,
208 clips[end_index].name, c.end,
209 clips[end_index].media_start + diff,
210 clips[end_index].duration - diff, false);
211 append_at_time(cl, cl.start, false);
214 trim(clips[end_index], diff, Gdk.WindowEdge.WEST);
217 if (start_index >= 0 && clips[start_index] != c) {
218 int64 delta = clips[start_index].end - c.start;
219 trim(clips[start_index], -delta, Gdk.WindowEdge.EAST);
223 // TODO: This code assumes that when a delete happens, it is reflected immediately.
224 // When we are in an undo (or redo) deleting will happen later. It would be better
225 // for callers not to have to deal with this problem. Too large of a change for now.
226 if (!project.undo_manager.in_undo) {
227 while (i < clips.size) {
229 clips[i].start >= c.start &&
230 clips[i].end <= c.end) {
231 delete_clip(clips[i]);
239 public void move(Clip c, int64 pos, int64 original_time) {
240 Command command = new ClipAddCommand(this, c, original_time, pos);
241 project.do_command(command);
244 public void _move(Clip c, int64 pos) {
249 do_clip_overwrite(c);
251 insert_clip_into_array(c, get_insert_index(c.start));
255 public void add(Clip c, int64 pos, bool select) {
260 clip_added(c, select);
263 public virtual void on_clip_updated(Clip clip) {
264 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_updated");
267 public void do_clip_paste(Clip clip, int64 position) {
268 append_at_time(clip, position, true);
271 public Clip? get_clip(int i) {
272 if (i < 0 || i >= clips.size)
273 error("get_clip: Invalid index! %d (%d)", i, clips.size);
277 public int get_clip_index(Clip c) {
278 for (int i = 0; i < clips.size; i++) {
286 public Clip? get_clip_by_position(int64 pos) {
287 int length = clips.size;
289 for (int i = length - 1; i >= 0; i--)
290 if (clips[i].start < pos)
291 return pos >= clips[i].end ? null : clips[i];
295 public int64 get_length() {
296 return clips.size == 0 ? 0 : clips[clips.size - 1].start + clips[clips.size - 1].duration;
299 public void _append_at_time(Clip c, int64 time, bool select) {
300 add(c, time, select);
303 public void append_at_time(Clip c, int64 time, bool select) {
304 Command command = new ClipCommand(ClipCommand.Action.APPEND, this, c, time, select);
305 project.do_command(command);
308 public void delete_clip(Clip clip) {
309 Command clip_command = new ClipCommand(ClipCommand.Action.DELETE,
310 this, clip, clip.start, false);
311 project.do_command(clip_command);
314 public void _delete_clip(Clip clip) {
315 int index = get_clip_index(clip);
317 clips.remove_at(index);
323 public void delete_gap(Gap g) {
327 public void remove_clip_from_array(Clip pos) {
331 void insert_clip_into_array(Clip c, int pos) {
332 c.updated.connect(on_clip_updated);
333 clips.insert(pos, c);
336 public void delete_all_clips() {
337 uint size = clips.size;
338 for (int i = 0; i < size; i++) {
339 delete_clip(clips[0]);
341 project.media_engine.go(0);
344 public void revert_to_original(Clip clip) {
345 Command command = new ClipRevertCommand(this, clip);
346 project.do_command(command);
349 public void _revert_to_original(Clip c) {
350 int index = get_clip_index(c);
352 error("revert_to_original: Clip not in track array!");
354 c.set_media_start_duration(0, c.clipfile.length);
356 project.media_engine.go(c.start);
359 public bool are_contiguous_clips(int64 position) {
360 Clip right_clip = get_clip_by_position(position + 1);
361 Clip left_clip = get_clip_by_position(position - 1);
363 return left_clip != null && right_clip != null &&
364 left_clip != right_clip &&
365 left_clip.clipfile == right_clip.clipfile &&
366 left_clip.end == right_clip.start;
369 public void split_at(int64 position) {
370 Command command = new ClipSplitCommand(ClipSplitCommand.Action.SPLIT, this, position);
371 project.do_command(command);
374 public void _split_at(int64 position) {
375 Clip c = get_clip_by_position(position);
379 Clip cn = new Clip(c.clipfile, c.type, c.name, position,
380 (position - c.start) + c.media_start,
381 c.start + c.duration - position, false);
383 c.duration = position - c.start;
385 add(cn, position, false);
388 public void join(int64 position) {
389 Command command = new ClipSplitCommand(ClipSplitCommand.Action.JOIN, this, position);
390 project.do_command(command);
393 public void _join(int64 position) {
394 assert(are_contiguous_clips(position));
395 if (are_contiguous_clips(position)) {
396 Clip right_clip = get_clip_by_position(position + 1);
397 assert(right_clip != null);
399 int right_clip_index = get_clip_index(right_clip);
400 assert(right_clip_index > 0);
402 int left_clip_index = right_clip_index - 1;
403 Clip left_clip = get_clip(left_clip_index);
404 assert(left_clip != null);
405 left_clip.duration = right_clip.end - left_clip.start;
406 _delete_clip(right_clip);
410 public void trim(Clip clip, int64 delta, Gdk.WindowEdge edge) {
411 Command command = new ClipTrimCommand(this, clip, delta, edge);
412 project.do_command(command);
415 public void _trim(Clip clip, int64 delta, Gdk.WindowEdge edge) {
416 clip.trim(delta, edge);
417 do_clip_overwrite(clip);
420 public int64 previous_edit(int64 pos) {
421 for (int i = clips.size - 1; i >= 0 ; --i) {
431 public int64 next_edit(int64 pos) {
432 foreach (Clip c in clips)
435 else if (c.end > pos)
440 public virtual void write_attributes(FileStream f) {
441 f.printf("type=\"%s\" name=\"%s\" ", name(), get_display_name());
444 public void save(FileStream f) {
445 f.printf(" <track ");
448 for (int i = 0; i < clips.size; i++)
449 clips[i].save(f, project.get_clipfile_index(clips[i].clipfile));
450 f.puts(" </track>\n");
453 public string get_display_name() {
457 public void set_display_name(string new_display_name) {
458 if (display_name != new_display_name) {
459 display_name = new_display_name;
464 public void set_selected(bool is_selected) {
465 if (this.is_selected != is_selected) {
466 this.is_selected = is_selected;
467 track_selection_changed(this);
471 public bool get_is_selected() {
476 public class AudioTrack : Track {
480 int default_num_channels;
481 public static const int INVALID_CHANNEL_COUNT = -1;
483 public signal void parameter_changed(Parameter parameter, double new_value);
484 public signal void level_changed(double level_left, double level_right);
485 public signal void channel_count_changed(int channel_count);
487 public AudioTrack(Project project, string display_name) {
488 base(project, display_name);
490 set_default_num_channels(INVALID_CHANNEL_COUNT);
495 protected override string name() { return "audio"; }
497 public override MediaType media_type() {
498 return MediaType.AUDIO;
501 public override void write_attributes(FileStream f) {
502 base.write_attributes(f);
503 f.printf("volume=\"%f\" panorama=\"%f\" ", get_volume(), get_pan());
506 if (get_num_channels(out channels) &&
507 channels != INVALID_CHANNEL_COUNT)
508 f.printf("channels=\"%d\" ", channels);
511 public void set_pan(double new_value) {
512 double old_value = get_pan();
513 if (!float_within(new_value - old_value, 0.05)) {
514 ParameterCommand parameter_command =
515 new ParameterCommand(this, Parameter.PAN, new_value, old_value);
516 project.do_command(parameter_command);
520 public void _set_pan(double new_value) {
521 assert(new_value <= 1.0 && new_value >= -1.0);
522 double old_value = get_pan();
523 if (!float_within(old_value - new_value, 0.05)) {
525 parameter_changed(Parameter.PAN, new_value);
529 public double get_pan() {
533 public void set_volume(double new_volume) {
534 double old_volume = get_volume();
535 if (!float_within(old_volume - new_volume, 0.005)) {
536 ParameterCommand parameter_command =
537 new ParameterCommand(this, Parameter.VOLUME, new_volume, old_volume);
538 project.do_command(parameter_command);
542 public void _set_volume(double new_volume) {
543 assert(new_volume >= 0.0 && new_volume <= 10.0);
544 double old_volume = get_volume();
545 if (!float_within(old_volume - new_volume, 0.005)) {
547 parameter_changed(Parameter.VOLUME, new_volume);
551 public double get_volume() {
555 public void set_default_num_channels(int num) {
556 default_num_channels = num;
559 public bool get_num_channels(out int num) {
563 foreach (Clip c in clips) {
564 if (c.clipfile.is_online()) {
565 bool can = c.clipfile.get_num_channels(out num);
572 if (default_num_channels == INVALID_CHANNEL_COUNT)
575 num = default_num_channels;
579 public override bool check(Clip clip) {
580 if (!clip.clipfile.is_online()) {
584 if (clips.size == 0) {
585 int number_of_channels = 0;
586 if (clip.clipfile.get_num_channels(out number_of_channels)) {
587 channel_count_changed(number_of_channels);
593 int number_of_channels;
594 if (clip.clipfile.get_num_channels(out number_of_channels)) {
595 int track_channel_count;
596 if (get_num_channels(out track_channel_count)) {
597 good = track_channel_count == number_of_channels;
602 string sub_error = number_of_channels == 1 ?
603 "Mono clips cannot go on stereo tracks." :
604 "Stereo clips cannot go on mono tracks.";
605 error_occurred("Cannot add clip to track", sub_error);
610 public void on_level_changed(double level_left, double level_right) {
611 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_level_changed");
612 level_changed(level_left, level_right);
615 public override void on_clip_updated(Clip clip) {
616 if (clip.clipfile.is_online()) {
617 int number_of_channels = 0;
618 if (get_num_channels(out number_of_channels)) {
619 channel_count_changed(number_of_channels);