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.
9 extern const string _VERSION;
12 public class MediaLoaderHandler : LoaderHandler {
13 protected weak Project the_project;
14 protected Track current_track;
16 Gee.ArrayList<ClipFetcher> clipfetchers = new Gee.ArrayList<ClipFetcher>();
17 int num_clipfiles_complete;
19 public MediaLoaderHandler(Project the_project) {
20 this.the_project = the_project;
24 public override bool commit_marina(string[] attr_names, string[] attr_values) {
25 int number_of_attributes = attr_names.length;
26 if (number_of_attributes != 1 ||
27 attr_names[0] != "version") {
28 load_error("Missing version information");
32 if (the_project.get_file_version() < attr_values[0].to_int()) {
33 load_error("Version mismatch! (File Version: %d, App Version: %d)".printf(
34 the_project.get_file_version(), attr_values[0].to_int()));
38 num_clipfiles_complete = 0;
42 public override bool commit_library(string[] attr_names, string[] attr_values) {
43 // We return true since framerate is an optional parameter
44 if (attr_names.length != 1)
47 if (attr_names[0] != "framerate") {
48 load_error("Missing framerate tag");
52 string[] arr = attr_values[0].split("/");
53 if (arr.length != 2) {
54 load_error("Invalid framerate attribute");
58 the_project.set_default_framerate(Fraction(arr[0].to_int(), arr[1].to_int()));
62 public override bool commit_track(string[] attr_names, string[] attr_values) {
63 assert(current_track == null);
65 int number_of_attributes = attr_names.length;
68 for (int i = 0; i < number_of_attributes; ++i) {
69 switch(attr_names[i]) {
71 type = attr_values[i];
74 name = attr_values[i];
82 load_error("Missing track name");
87 load_error("Missing track type");
91 if (type == "audio") {
92 AudioTrack audio_track = new AudioTrack(the_project, name);
93 current_track = audio_track;
94 the_project.add_track(current_track);
96 for (int i = 0; i < number_of_attributes; ++i) {
97 switch(attr_names[i]) {
99 audio_track._set_pan(attr_values[i].to_double());
102 audio_track._set_volume(attr_values[i].to_double());
105 audio_track.set_default_num_channels(attr_values[i].to_int());
112 } else if (type == "video") {
113 current_track = new VideoTrack(the_project);
114 the_project.add_track(current_track);
117 return base.commit_track(attr_names, attr_values);
120 public override void leave_track() {
121 assert(current_track != null);
122 current_track = null;
125 public override bool commit_clip(string[] attr_names, string[] attr_values) {
126 assert(current_track != null);
128 int number_of_attributes = attr_names.length;
130 string? clip_name = null;
132 int64 media_start = -1;
134 for (int i = 0; i < number_of_attributes; i++) {
135 switch (attr_names[i]) {
137 id = attr_values[i].to_int();
140 clip_name = attr_values[i];
143 start = attr_values[i].to_int64();
146 media_start = attr_values[i].to_int64();
149 duration = attr_values[i].to_int64();
152 // TODO: we need a way to deal with orphaned attributes, for now, reject the file
153 load_error("Unknown attribute %s".printf(attr_names[i]));
159 load_error("missing clip id");
163 if (clip_name == null) {
164 load_error("missing clip_name");
169 load_error("missing start time");
173 if (media_start == -1) {
174 load_error("missing media_start");
178 if (duration == -1) {
179 load_error("missing duration");
183 if (id >= clipfetchers.size) {
184 load_error("clip file id %s was not loaded".printf(clip_name));
188 Clip clip = new Clip(clipfetchers[id].clipfile, current_track.media_type(), clip_name,
189 start, media_start, duration, false);
190 current_track.add(clip, start, false);
194 void fetcher_ready(Fetcher f) {
195 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "fetcher_ready");
196 if (f.error_string != null) {
197 load_error("Could not load %s.".printf(f.clipfile.filename));
198 warning("Could not load %s: %s", f.clipfile.filename, f.error_string);
200 the_project.add_clipfile(f.clipfile);
201 num_clipfiles_complete++;
202 if (num_clipfiles_complete == clipfetchers.size) {
207 public override bool commit_clipfile(string[] attr_names, string[] attr_values) {
208 string filename = null;
211 for (int i = 0; i < attr_names.length; i++) {
212 if (attr_names[i] == "filename") {
213 filename = attr_values[i];
214 } else if (attr_names[i] == "id") {
215 id = attr_values[i].to_int();
219 if (filename == null) {
220 load_error("Invalid clipfile filename");
225 load_error("Invalid clipfile id");
230 ClipFetcher fetcher = new ClipFetcher(filename);
231 fetcher.ready.connect(fetcher_ready);
232 clipfetchers.insert(id, fetcher);
234 load_error(e.message);
240 public override bool commit_tempo_entry(string[] attr_names, string[] attr_values) {
241 if (attr_names[0] != "tempo") {
242 load_error("Invalid attribute on tempo entry");
246 the_project._set_bpm(attr_values[0].to_int());
250 public override bool commit_time_signature_entry(string[] attr_names, string[] attr_values) {
251 if (attr_names[0] != "signature") {
252 load_error("Invalid attribute on time signature");
256 the_project._set_time_signature(Fraction.from_string(attr_values[0]));
260 public override bool commit_click(string[] attr_names, string[] attr_values) {
261 for (int i = 0; i < attr_names.length; ++i) {
262 switch (attr_names[i]) {
264 the_project.click_during_play = attr_values[i] == "true";
267 the_project.click_during_record = attr_values[i] == "true";
270 the_project.click_volume = attr_values[i].to_double();
273 load_error("unknown attribute for click '%s'".printf(attr_names[i]));
280 public override bool commit_library_preference(string[] attr_names, string[] attr_values) {
281 for (int i = 0; i < attr_names.length; ++i) {
282 switch (attr_names[i]) {
284 the_project.library_width = attr_values[i].to_int();
287 the_project.library_visible = attr_values[i] == "true";
290 load_error("unknown attribute for library '%s'".printf(attr_names[i]));
297 public override void leave_library() {
298 if (clipfetchers.size == 0)
303 public abstract class Project : TempoInformation, Object {
304 public const string FILLMORE_FILE_EXTENSION = "fill";
305 public const string FILLMORE_FILE_FILTER = "*." + FILLMORE_FILE_EXTENSION;
306 public const string LOMBARD_FILE_EXTENSION = "lom";
307 public const string LOMBARD_FILE_FILTER = "*." + LOMBARD_FILE_EXTENSION;
309 const string license = """
310 %s is free software; you can redistribute it and/or modify it under the
311 terms of the GNU Lesser General Public License as published by the Free
312 Software Foundation; either version 2.1 of the License, or (at your option)
315 %s is distributed in the hope that it will be useful, but WITHOUT
316 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
317 FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
320 You should have received a copy of the GNU Lesser General Public License
321 along with %s; if not, write to the Free Software Foundation, Inc.,
322 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
325 public const string[] authors = {
326 "Robert Powell <rob@yorba.org>",
327 "Adam Dingle <adam@yorba.org>",
328 "Andrew O'Mahony <andrew.omahony@att.net>",
329 "Dru Moore <usr@dru-id.co.uk>",
333 public Gee.ArrayList<Track> tracks = new Gee.ArrayList<Track>();
334 public Gee.ArrayList<Track> inactive_tracks = new Gee.ArrayList<Track>();
335 Gee.HashSet<ClipFetcher> pending = new Gee.HashSet<ClipFetcher>();
336 Gee.ArrayList<ThumbnailFetcher> pending_thumbs = new Gee.ArrayList<ThumbnailFetcher>();
337 protected Gee.ArrayList<ClipFile> clipfiles = new Gee.ArrayList<ClipFile>();
338 // TODO: media_engine is a member of project only temporarily. It will be
339 // less work to move it to fillmore/lombard once we have a transport class.
340 public View.MediaEngine media_engine;
342 protected string project_file; // may be null if project has not yet been saved
343 public ProjectLoader loader;
345 FetcherCompletion fetcher_completion;
346 public UndoManager undo_manager;
347 public LibraryImporter importer;
349 public Fraction default_framerate;
351 Fraction time_signature = Fraction(4, 4);
352 public bool click_during_play = false;
353 public bool click_during_record = true;
354 public double click_volume = 0.8;
355 public bool library_visible = true;
356 public int library_width = 600;
357 public bool snap_to_clip;
360 * This can't be const since the Vala compiler (0.7.7) crashes if we try to make it a const.
361 * I've filed a bug with the Vala bugzilla for this:
362 * https://bugzilla.gnome.org/show_bug.cgi?id=598204
364 public static Fraction INVALID_FRAME_RATE = Fraction(-1, 1);
366 public signal void playstate_changed(PlayState playstate);
368 public signal void name_changed(string? project_file);
369 public signal void load_error(string error);
370 public virtual signal void load_complete() {
373 public signal void closed();
375 public signal void track_added(Track track);
376 public signal void track_removed(Track track);
377 public signal void error_occurred(string major_message, string? minor_message);
379 public signal void clipfile_added(ClipFile c);
380 public signal void clipfile_removed(ClipFile clip_file);
381 public signal void cleared();
383 public abstract TimeCode get_clip_time(ClipFile f);
385 public Project(string? filename, bool include_video) throws Error {
386 undo_manager = new UndoManager();
387 project_file = filename;
389 media_engine = new View.MediaEngine(this, include_video);
390 track_added.connect(media_engine.on_track_added);
391 media_engine.playstate_changed.connect(on_playstate_changed);
392 media_engine.error_occurred.connect(on_error_occurred);
394 set_default_framerate(INVALID_FRAME_RATE);
397 public void on_playstate_changed() {
398 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_playstate_changed");
399 switch (media_engine.get_play_state()) {
400 case PlayState.STOPPED:
403 case PlayState.CLOSED:
407 playstate_changed(media_engine.get_play_state());
410 public virtual string? get_project_file() {
414 public ClipFile? get_clipfile(int index) {
416 index >= clipfiles.size)
418 return clipfiles[index];
421 public int get_clipfile_index(ClipFile find) {
423 foreach (ClipFile f in clipfiles) {
431 public Track? track_from_clip(Clip clip) {
432 foreach (Track track in tracks) {
433 foreach (Clip match in track.clips) {
442 public void print_graph(Gst.Bin bin, string file_name) {
443 Gst.debug_bin_to_dot_file_with_ts(bin, Gst.DebugGraphDetails.ALL, file_name);
446 public int64 get_length() {
448 foreach (Track track in tracks) {
449 max = int64.max(max, track.get_length());
454 public int64 snap_clip(Clip c, int64 span) {
455 foreach (Track track in tracks) {
456 int64 new_start = track.snap_clip(c, span);
457 if (new_start != c.start) {
464 public void snap_coord(out int64 coord, int64 span) {
465 foreach (Track track in tracks) {
466 if (track.snap_coord(out coord, span)) {
472 Gap get_gap_intersection(Gap gap) {
473 Gap intersection = gap;
475 foreach (Track track in tracks) {
476 intersection = intersection.intersect(track.find_first_gap(intersection.start));
482 public bool can_delete_gap(Gap gap) {
483 Gap intersection = get_gap_intersection(gap);
484 return !intersection.is_empty();
487 public void delete_gap(Gap gap) {
488 Gap intersection = get_gap_intersection(gap);
489 assert(!intersection.is_empty());
491 foreach (Track track in tracks) {
492 track.delete_gap(intersection);
496 protected virtual void do_append(Track track, ClipFile clipfile, string name,
498 switch(track.media_type()) {
499 case MediaType.AUDIO:
500 if (clipfile.audio_caps == null) {
504 case MediaType.VIDEO:
505 if (clipfile.video_caps == null) {
511 Clip clip = new Clip(clipfile, track.media_type(), name, 0, 0, clipfile.length, false);
512 track.append_at_time(clip, insert_time, true);
515 public void append(Track track, ClipFile clipfile) {
516 string name = isolate_filename(clipfile.filename);
517 int64 insert_time = 0;
519 foreach (Track temp_track in tracks) {
520 insert_time = int64.max(insert_time, temp_track.get_length());
522 do_append(track, clipfile, name, insert_time);
525 public void add(Track track, ClipFile clipfile, int64 time) {
526 string name = isolate_filename(clipfile.filename);
527 do_append(track, clipfile, name, time);
530 public void on_clip_removed(Track t, Clip clip) {
531 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_clip_removed");
535 public void split_at_playhead() {
536 string description = "Split At Playhead";
537 undo_manager.start_transaction(description);
538 foreach (Track track in tracks) {
539 if (track.get_clip_by_position(transport_get_position()) != null) {
540 track.split_at(transport_get_position());
543 undo_manager.end_transaction(description);
546 public bool can_trim(out bool left) {
547 Clip first_clip = null;
549 // When trimming multiple clips, we allow trimming left only if both clips already start
550 // at the same position, and trimming right only if both clips already end at the same
555 bool start_same = true;
556 bool end_same = true;
557 foreach (Track track in tracks) {
558 Clip clip = track.get_clip_by_position(transport_get_position());
559 if (first_clip != null && clip != null) {
560 start_same = start_same && start == clip.start;
561 end_same = end_same && end == clip.end;
562 } else if (clip != null) {
564 start = first_clip.start;
565 end = first_clip.end;
569 if (first_clip == null) {
573 if (start_same && !end_same) {
578 if (!start_same && end_same) {
583 // which half of the clip are we closer to?
584 left = (transport_get_position() - first_clip.start < first_clip.duration / 2);
588 public void trim_to_playhead() {
590 if (!can_trim(out left)) {
593 string description = "Trim To Playhead";
594 Clip first_clip = null;
595 undo_manager.start_transaction(description);
596 foreach (Track track in tracks) {
597 Clip clip = track.get_clip_by_position(transport_get_position());
601 delta = transport_get_position() - clip.start;
603 delta = transport_get_position() - clip.end;
605 track.trim(clip, delta, left ? Gdk.WindowEdge.WEST : Gdk.WindowEdge.EAST);
608 undo_manager.end_transaction(description);
610 if (left && first_clip != null) {
611 transport_go(first_clip.start);
615 public void transport_go(int64 position) {
616 media_engine.go(position);
619 public bool transport_is_playing() {
620 return media_engine.playing;
623 public bool transport_is_recording() {
624 return media_engine.get_play_state() == PlayState.PRE_RECORD ||
625 media_engine.get_play_state() == PlayState.RECORDING;
628 public bool playhead_on_clip() {
629 foreach (Track track in tracks) {
630 if (track.get_clip_by_position(transport_get_position()) != null) {
637 public bool playhead_on_contiguous_clip() {
638 foreach (Track track in tracks) {
639 if (track.are_contiguous_clips(transport_get_position())) {
646 public bool is_duplicate_track_name(Track? track, string new_name) {
647 assert(new_name != "");
648 foreach (Track this_track in tracks) {
649 if (track != this_track) {
650 if (this_track.get_display_name() == new_name) {
656 foreach (Track this_track in inactive_tracks) {
657 if (track != this_track) {
658 if (this_track.get_display_name() == new_name) {
666 public virtual void add_track(Track track) {
667 track.clip_removed.connect(on_clip_removed);
668 track.error_occurred.connect(on_error_occurred);
672 public virtual void insert_track(int index, Track track) {
673 if (0 > index) index = 0;
674 if (tracks.size <= index) {
678 track.clip_removed.connect(on_clip_removed);
679 track.error_occurred.connect(on_error_occurred);
680 tracks.insert(index, track);
685 public void add_inactive_track(Track track) {
687 inactive_tracks.add(track);
690 public void remove_track(Track track) {
691 media_engine.pipeline.set_state(Gst.State.NULL);
692 track.track_removed(track);
693 tracks.remove(track);
694 track_removed(track);
697 public void remove_track_at(int index) {
698 remove_track(tracks.get(index));
701 public void add_clipfile(ClipFile clipfile) {
702 Model.Command command = new Model.AddClipCommand(this, clipfile);
706 public void _add_clipfile(ClipFile clipfile) throws Error {
707 clipfiles.add(clipfile);
708 if (clipfile.is_online() && clipfile.is_of_type(MediaType.VIDEO)) {
709 ThumbnailFetcher fetcher = new ThumbnailFetcher(clipfile, 0);
710 fetcher.ready.connect(on_thumbnail_ready);
711 pending_thumbs.add(fetcher);
713 clipfile_added(clipfile);
717 void on_thumbnail_ready(Fetcher f) {
718 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_thumbnail_ready");
719 clipfile_added(f.clipfile);
720 pending_thumbs.remove(f as ThumbnailFetcher);
723 public bool clipfile_on_track(string filename) {
724 ClipFile cf = find_clipfile(filename);
726 foreach (Track t in tracks) {
727 foreach (Clip c in t.clips) {
728 if (c.clipfile == cf)
733 foreach (Track t in inactive_tracks) {
734 foreach (Clip c in t.clips) {
735 if (c.clipfile == cf)
743 void delete_clipfile_from_tracks(ClipFile cf) {
744 foreach (Track t in tracks) {
745 for (int i = 0; i < t.clips.size; i++) {
746 if (t.clips[i].clipfile == cf) {
747 t.delete_clip(t.clips[i]);
753 foreach (Track t in inactive_tracks) {
754 for (int i = 0; i < t.clips.size; i++) {
755 if (t.clips[i].clipfile == cf) {
756 t.delete_clip(t.clips[i]);
763 public void _remove_clipfile(ClipFile cf) {
764 clipfiles.remove(cf);
765 clipfile_removed(cf);
768 public void remove_clipfile(string filename) {
769 ClipFile cf = find_clipfile(filename);
771 string description = "Delete From Library";
772 undo_manager.start_transaction(description);
774 delete_clipfile_from_tracks(cf);
776 Command clipfile_delete = new ClipFileDeleteCommand(this, cf);
777 do_command(clipfile_delete);
779 undo_manager.end_transaction(description);
783 public ClipFile? find_clipfile(string filename) {
784 foreach (ClipFile cf in clipfiles)
785 if (cf.filename == filename)
790 public void reseek() { transport_go(transport_get_position()); }
792 public void go_start() { transport_go(0); }
794 public void go_end() { transport_go(get_length()); }
796 public void go_previous() {
797 int64 start_pos = transport_get_position();
799 // If we're currently playing, we jump to the previous clip if we're within the first
800 // second of the current clip.
801 if (transport_is_playing())
802 start_pos -= 1 * Gst.SECOND;
804 int64 new_position = 0;
805 foreach (Track track in tracks) {
806 new_position = int64.max(new_position, track.previous_edit(start_pos));
808 transport_go(new_position);
811 // Move to the next clip boundary after the current transport position.
812 public void go_next() {
813 int64 start_pos = transport_get_position();
814 int64 new_position = get_length();
815 foreach (Track track in tracks) {
816 if (track.get_length() > start_pos) {
817 new_position = int64.min(new_position, track.next_edit(start_pos));
820 transport_go(new_position);
823 public int64 transport_get_position() {
824 return media_engine.position;
827 public void set_name(string? filename) {
828 if (filename != null) {
829 this.project_file = filename;
831 name_changed(filename);
834 public void set_default_framerate(Fraction rate) {
835 default_framerate = rate;
838 public string get_file_display_name() {
839 string filename = get_project_file();
840 if (filename == null) {
841 return "Unsaved Project - %s".printf(get_app_name());
844 string dir = Path.get_dirname(filename);
845 string name = Path.get_basename(filename);
846 string home_path = GLib.Environment.get_home_dir();
849 dir = GLib.Environment.get_current_dir();
851 if (dir.has_prefix(home_path))
852 dir = "~" + dir.substring(home_path.length);
853 return "%s (%s) - %s".printf(name, dir, get_app_name());
857 public void clear() {
858 media_engine.set_gst_state(Gst.State.NULL);
860 foreach (Track track in tracks) {
861 track.delete_all_clips();
862 track.track_removed(track);
863 track_removed(track);
873 public bool can_export() {
874 if (media_engine.get_play_state() != PlayState.STOPPED) {
877 foreach (Track track in tracks) {
878 if (track.get_length() > 0) {
885 public void on_load_started(string filename) {
886 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_load_started");
887 project_file = filename;
890 void on_load_error(string error) {
891 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_load_error");
895 void on_load_complete() {
896 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_load_complete");
897 undo_manager.reset();
898 set_name(project_file);
902 // Load a project file. The load is asynchronous: it may continue after this method returns.
903 // Any load error will be reported via the load_error signal, which may run either while this
904 // method executes or afterward.
905 public virtual void load(string? fname) {
906 emit(this, Facility.LOADING, Level.INFO, "loading project");
913 loader = new ProjectLoader(new MediaLoaderHandler(this), fname);
915 loader.load_started.connect(on_load_started);
916 loader.load_error.connect(on_load_error);
917 loader.load_complete.connect(on_load_complete);
918 loader.load_complete.connect(media_engine.on_load_complete);
919 media_engine.set_play_state(PlayState.LOADING);
920 media_engine.pipeline.set_state(Gst.State.NULL);
924 public void on_error_occurred(string major_error, string? minor_error) {
925 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_error_occurred");
926 error_occurred(major_error, minor_error);
929 public int get_file_version() {
933 public void save_library(FileStream f) {
934 f.printf(" <library");
936 Fraction r = default_framerate;
938 foreach (Track t in tracks) {
939 if (t.media_type () == MediaType.VIDEO) {
940 VideoTrack video_track = t as VideoTrack;
941 if (video_track.get_framerate(out r))
945 if (!r.equal(INVALID_FRAME_RATE))
946 f.printf(" framerate=\"%d/%d\"", r.numerator,
950 for (int i = 0; i < clipfiles.size; i++) {
951 f.printf(" <clipfile filename=\"%s\" id=\"%d\"/>\n", clipfiles[i].filename, i);
954 f.printf(" </library>\n");
957 public virtual void save(string? filename) {
958 if (filename != null) {
962 FileStream f = FileStream.open(project_file, "w");
964 error_occurred("Could not save project",
965 "%s: %s".printf(project_file, GLib.strerror(GLib.errno)));
968 f.printf("<marina version=\"%d\">\n", get_file_version());
972 f.printf(" <tracks>\n");
973 foreach (Track track in tracks) {
977 foreach (Track track in inactive_tracks) {
980 f.printf(" </tracks>\n");
981 f.printf(" <preferences>\n");
982 f.printf(" <click on_play=\"%s\" on_record=\"%s\" volume=\"%lf\"/>\n",
983 click_during_play ? "true" : "false",
984 click_during_record ? "true" : "false",
986 f.printf(" <library width=\"%d\" visible=\"%s\" />\n",
987 library_width, library_visible ? "true" : "false");
988 f.printf(" </preferences>\n");
989 f.printf(" <maps>\n");
990 f.printf(" <tempo>\n");
991 f.printf(" <entry tempo=\"%d\" />\n", tempo);
992 f.printf(" </tempo>\n");
993 f.printf(" <time_signature>\n");
994 f.printf(" <entry signature=\"%s\" />\n", time_signature.to_string());
995 f.printf(" </time_signature>\n");
996 f.printf(" </maps>\n");
998 f.printf("</marina>\n");
1001 // TODO: clean up responsibility between dirty and undo
1002 undo_manager.mark_clean();
1005 public void close() {
1006 media_engine.close();
1009 public void on_importer_clip_complete(ClipFetcher fetcher) {
1010 if (fetcher.error_string != null) {
1011 error_occurred("Error importing clip", fetcher.error_string);
1013 fetcher_completion.complete(fetcher);
1017 public void create_clip_fetcher(FetcherCompletion fetcher_completion, string filename)
1019 ClipFetcher fetcher = new ClipFetcher(filename);
1020 this.fetcher_completion = fetcher_completion;
1021 fetcher.ready.connect(on_fetcher_ready);
1022 pending.add(fetcher);
1025 // TODO: We should be using Library importer rather than this mechanism for fillmore
1026 void on_fetcher_ready(Fetcher fetcher) {
1027 emit(this, Facility.SIGNAL_HANDLERS, Level.INFO, "on_fetcher_ready");
1028 pending.remove(fetcher as ClipFetcher);
1029 if (fetcher.error_string != null) {
1030 emit(this, Facility.DEVELOPER_WARNINGS, Level.INFO, fetcher.error_string);
1031 error_occurred("Error retrieving clip", fetcher.error_string);
1033 if (get_clipfile_index(fetcher.clipfile) == -1) {
1034 add_clipfile(fetcher.clipfile);
1036 fetcher_completion.complete(fetcher);
1040 public bool is_project_extension(string filename) {
1041 string extension = get_file_extension(filename);
1042 return extension == LOMBARD_FILE_EXTENSION || extension == FILLMORE_FILE_EXTENSION;
1045 public void do_command(Command the_command) {
1046 undo_manager.do_command(the_command);
1049 public void undo() {
1050 undo_manager.undo();
1053 void ClearTrackMeters() {
1054 foreach (Track track in tracks) {
1055 AudioTrack audio_track = track as AudioTrack;
1056 if (audio_track != null) {
1057 audio_track.level_changed(-100, -100);
1062 public void create_clip_importer(Model.Track? track, bool timeline_add,
1063 int64 time_to_add, bool both_tracks, Gtk.Window? progress_window_parent, int number) {
1065 assert(track != null);
1066 importer = new Model.TimelineImporter(track, this, time_to_add, both_tracks);
1068 importer = new Model.LibraryImporter(this);
1070 if (progress_window_parent != null) {
1071 new MultiFileProgress(progress_window_parent, number, "Import",
1077 public string get_version() {
1081 public abstract string get_app_name();
1083 public string get_license() {
1084 return license.printf(get_app_name(), get_app_name(), get_app_name());
1087 public void set_time_signature(Fraction time_signature) {
1088 TimeSignatureCommand command = new TimeSignatureCommand(this, time_signature);
1089 undo_manager.do_command(command);
1092 public void _set_time_signature(Fraction time_signature) {
1093 this.time_signature = time_signature;
1094 time_signature_changed(time_signature);
1097 public Fraction get_time_signature() {
1098 return time_signature;
1101 public void set_bpm(int bpm) {
1102 BpmCommand command = new BpmCommand(this, bpm);
1103 undo_manager.do_command(command);
1106 public void _set_bpm(int bpm) {
1111 public int get_bpm() {
1115 public string get_audio_path() {
1116 string path = get_path();
1117 return path == null ? null : Path.build_filename(path, "audio files");
1121 return project_file == null ? null : Path.get_dirname(project_file);
1124 public VideoTrack? find_video_track() {
1125 foreach (Track track in tracks) {
1126 if (track is VideoTrack) {
1127 return track as VideoTrack;
1133 public AudioTrack? find_audio_track() {
1134 foreach (Track track in tracks) {
1135 if (track is AudioTrack) {
1136 return track as AudioTrack;