Initial commit
[fillmore] / src / marina / track.vala
1 /* Copyright 2009-2010 Yorba Foundation
2  *
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. 
5  */
6
7 using Logging;
8
9 namespace Model {
10
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;
15     bool is_selected;
16
17     public signal void clip_added(Clip clip, bool select);
18     public signal void clip_removed(Clip clip);
19
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);
25
26     public Track(Project project, string display_name) {
27         this.project = project;
28         this.display_name = display_name;
29     }
30
31     protected abstract string name();
32     public abstract MediaType media_type();
33
34     public void hide() {
35         track_hidden(this);
36     }
37
38     public bool contains_clipfile(ClipFile f) {
39         foreach (Clip c in clips) {
40             if (c.clipfile == f)
41                 return true;
42         }
43         return false;
44     }
45
46     protected abstract bool check(Clip clip);
47
48     public int64 get_time_from_pos(Clip clip, bool after) {
49         if (after)
50             return clip.start + clip.duration;
51         else
52             return clip.start;
53     }
54
55     int get_clip_from_time(int64 time) {
56         for (int i = 0; i < clips.size; i++) {
57             if (time >= clips[i].start &&
58                 time < clips[i].end)
59                 return i;
60         }
61         return -1;
62     }
63
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) {
68                 return new_start;
69             }
70         }
71         return c.start;
72     }
73
74     public bool snap_coord(out int64 coord, int64 span) {
75         foreach (Clip c in clips) {
76             if (c.snap_coord(out coord, span))
77                 return true;
78         }
79         return false;
80     }
81
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;
88                     return true;
89                 }
90
91                 difference = potential_clip.start - clip.end;
92                 if (difference.abs() < range) {
93                     adjustment = difference;
94                     return true;
95                 }
96             }
97         }
98         return false;
99     }
100
101     int get_insert_index(int64 time) {
102         int end_ret = 0;
103         for (int i = 0; i < clips.size; i++) {
104             Clip c = clips[i];
105
106             if (time >= c.start) {
107                 if (time < c.start + c.duration/2)
108                     return i;
109                 else if (time < c.start + c.duration)
110                     return i + 1;
111                 else
112                     end_ret ++;
113             }
114         }
115         return end_ret;
116     }
117
118     // This is called to find the first gap after a start time
119     public Gap find_first_gap(int64 start) {
120         int64 new_start = 0;
121         int64 new_end = int64.MAX;
122
123         foreach (Clip c in clips) {
124             if (c.start > new_start &&
125                 c.start > start) {
126                 new_end = c.start;
127                 break;
128             }
129             new_start = c.end;
130         }
131         return new Gap(new_start, new_end);
132     }
133
134     // This is always called with the assumption that we are not on a clip
135     int get_gap_index(int64 time) {
136         int i = 0;
137         while (i < clips.size) {
138             if (time <= clips[i].start)
139                 break;
140             i++;
141         }
142         return i;
143     }
144
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) {
148         g = new Gap(0, 0);
149
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;
154         }
155     }
156
157     public Clip? find_overlapping_clip(int64 start, int64 length) {
158         for (int i = 0; i < clips.size; i++) {
159             Clip c = clips[i];
160             if (c.overlap_pos(start, length))
161                 return c;
162         }
163         return null;
164     }
165
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;
169
170         for (int i = 1; i < limit; i++) {
171             Clip c = clips[i / 2];
172             int64 t;
173
174             if (i % 2 == 0)
175                 t = c.start;
176             else
177                 t = c.end;
178
179             if (t > time) {
180                 if (t - time < time - prev_time) {
181                     after = ((i % 2) != 0);
182                     return clips[i / 2];
183                 } else {
184                     after = ((i % 2) == 0);
185                     return clips[(i - 1) / 2];
186                 }
187             }
188             prev_time = t;
189         }
190
191         after = true;
192         return clips[clips.size - 1];
193     }
194
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);
198
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]) {
203                     return;
204                 }
205
206                 if (diff > 0) {
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);
212                 }
213             } else {
214                 trim(clips[end_index], diff, Gdk.WindowEdge.WEST);
215             }
216         }
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);
220             }
221
222         int i = 0;
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) {
228                 if (clips[i] != c && 
229                     clips[i].start >= c.start &&
230                     clips[i].end <= c.end) {
231                     delete_clip(clips[i]);
232                 }
233                 else
234                     i++;
235             }
236         }
237     }
238
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);
242     }
243
244     public void _move(Clip c, int64 pos) {
245         if (pos < 0) {
246             pos = 0;
247         }
248         c.start = pos;
249         do_clip_overwrite(c);
250
251         insert_clip_into_array(c, get_insert_index(c.start));
252         project.reseek();
253     }
254
255     public void add(Clip c, int64 pos, bool select) {
256         if (!check(c))
257             return;
258
259         _move(c, pos);
260         clip_added(c, select);
261     }
262
263     public virtual void on_clip_updated(Clip clip) {
264         emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_updated");    
265     }
266
267     public void do_clip_paste(Clip clip, int64 position) {
268         append_at_time(clip, position, true);
269     }
270
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);
274         return clips[i];
275     }
276
277     public int get_clip_index(Clip c) {
278         for (int i = 0; i < clips.size; i++) {
279             if (clips[i] == c) {
280                 return i;
281             }
282         }
283         return -1;
284     }
285
286     public Clip? get_clip_by_position(int64 pos) {
287         int length = clips.size;
288
289         for (int i = length - 1; i >= 0; i--)
290             if (clips[i].start < pos)
291                 return pos >= clips[i].end ? null : clips[i];
292         return null;
293     }
294
295     public int64 get_length() {
296         return clips.size == 0 ? 0 : clips[clips.size - 1].start + clips[clips.size - 1].duration;
297     }
298
299     public void _append_at_time(Clip c, int64 time, bool select) {
300         add(c, time, select);
301     }
302
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);
306     }
307
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);
312     }
313
314     public void _delete_clip(Clip clip) {
315         int index = get_clip_index(clip);
316         assert(index != -1);
317         clips.remove_at(index);
318
319         clip.removed(clip);
320         clip_removed(clip);
321     }
322
323     public void delete_gap(Gap g) {
324         project.reseek();
325     }
326
327     public void remove_clip_from_array(Clip pos) {
328         clips.remove(pos);
329     }
330
331     void insert_clip_into_array(Clip c, int pos) {
332         c.updated.connect(on_clip_updated);
333         clips.insert(pos, c);
334     }
335
336     public void delete_all_clips() {
337         uint size = clips.size;
338         for (int i = 0; i < size; i++) { 
339             delete_clip(clips[0]);
340         }
341         project.media_engine.go(0);
342     }
343
344     public void revert_to_original(Clip clip) {
345         Command command = new ClipRevertCommand(this, clip);
346         project.do_command(command);
347     }
348
349     public void _revert_to_original(Clip c) {
350         int index = get_clip_index(c);
351         if (index == -1)
352             error("revert_to_original: Clip not in track array!");
353
354         c.set_media_start_duration(0, c.clipfile.length);
355
356         project.media_engine.go(c.start);
357     }
358
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);
362
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;
367     }
368
369     public void split_at(int64 position) {
370         Command command = new ClipSplitCommand(ClipSplitCommand.Action.SPLIT, this, position);
371         project.do_command(command);
372     }
373
374     public void _split_at(int64 position) {
375         Clip c = get_clip_by_position(position);
376         if (c == null)
377             return;
378
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);
382
383         c.duration = position - c.start;
384
385         add(cn, position, false);
386     }
387
388     public void join(int64 position) {
389         Command command = new ClipSplitCommand(ClipSplitCommand.Action.JOIN, this, position);
390         project.do_command(command);
391     }
392
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);
398
399             int right_clip_index = get_clip_index(right_clip);
400             assert(right_clip_index > 0);
401
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);
407         }
408     }
409
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);
413     }
414
415     public void _trim(Clip clip, int64 delta, Gdk.WindowEdge edge) {
416         clip.trim(delta, edge);
417         do_clip_overwrite(clip);
418     }
419
420     public int64 previous_edit(int64 pos) {
421         for (int i = clips.size - 1; i >= 0 ; --i) {
422             Clip c = clips[i];
423             if (c.end < pos)
424                 return c.end;
425             if (c.start < pos)
426                 return c.start;
427         }
428         return 0;
429     }
430
431     public int64 next_edit(int64 pos) {
432         foreach (Clip c in clips)
433             if (c.start > pos)
434                 return c.start;
435             else if (c.end > pos)
436                 return c.end;
437         return get_length();
438     }
439
440     public virtual void write_attributes(FileStream f) {
441         f.printf("type=\"%s\" name=\"%s\" ", name(), get_display_name());
442     }
443
444     public void save(FileStream f) {
445         f.printf("    <track ");
446         write_attributes(f);
447         f.printf(">\n");
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");
451     }
452
453     public string get_display_name() {
454         return display_name;
455     }
456
457     public void set_display_name(string new_display_name) {
458         if (display_name != new_display_name) {
459             display_name = new_display_name;
460             track_renamed(this);
461         }
462     }
463
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);
468         }
469     }
470
471     public bool get_is_selected() {
472         return is_selected;
473     }
474 }
475
476 public class AudioTrack : Track {
477     double pan;
478     double volume;
479
480     int default_num_channels;
481     public static const int INVALID_CHANNEL_COUNT = -1;
482
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);
486
487     public AudioTrack(Project project, string display_name) {
488         base(project, display_name);
489
490         set_default_num_channels(INVALID_CHANNEL_COUNT);
491         _set_pan(0);
492         _set_volume(1.0);
493     }
494
495     protected override string name() { return "audio"; }
496
497     public override MediaType media_type() {
498         return MediaType.AUDIO;
499     }
500
501     public override void write_attributes(FileStream f) {
502         base.write_attributes(f);
503         f.printf("volume=\"%f\" panorama=\"%f\" ", get_volume(), get_pan());
504
505         int channels;
506         if (get_num_channels(out channels) &&
507             channels != INVALID_CHANNEL_COUNT)
508             f.printf("channels=\"%d\" ", channels);
509     }
510
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);
517         }
518     }
519
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)) {
524             pan = new_value;
525             parameter_changed(Parameter.PAN, new_value);
526         }
527     }
528
529     public double get_pan() {
530         return pan;
531     }
532
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);
539         }
540     }
541
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)) {
546             volume = new_volume;
547             parameter_changed(Parameter.VOLUME, new_volume);
548         }
549     }
550
551     public double get_volume() {
552         return volume;
553     }
554
555     public void set_default_num_channels(int num) {
556         default_num_channels = num;
557     }
558
559     public bool get_num_channels(out int num) {
560         if (clips.size == 0)
561             return false;
562
563         foreach (Clip c in clips) {
564             if (c.clipfile.is_online()) {
565                 bool can = c.clipfile.get_num_channels(out num);
566                 assert(can);
567
568                 return can;
569             }
570         }
571
572         if (default_num_channels == INVALID_CHANNEL_COUNT)
573             return false;
574
575         num = default_num_channels;
576         return true;
577     }
578
579     public override bool check(Clip clip) {
580         if (!clip.clipfile.is_online()) {
581             return true;
582         }
583
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);
588             }
589             return true;
590         }
591
592         bool good = false;
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;
598             }
599         }
600
601         if (!good) {
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);
606         }
607         return good;
608     }
609
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);
613     }
614
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);
620             }
621         }
622     }
623 }
624 }