15
15
pygst.require("0.10")
21
22
import TransportManager
23
import UndoSystem, IncrementalSave
24
25
import xml.dom.minidom as xml
25
26
import Instrument, Event
26
from Monitored import Monitored
30
32
#=========================================================================
32
class Project(Monitored):
34
class Project(gobject.GObject):
34
36
This class maintains all of the information required about single Project. It also
35
37
saves and loads Project files.
38
40
""" The Project structure version. Will be useful for handling old save files. """
39
Globals.VERSION = "0.9"
41
Globals.VERSION = "0.11.1"
41
43
""" The audio playback state enum values """
42
44
AUDIO_STOPPED, AUDIO_RECORDING, AUDIO_PLAYING, AUDIO_PAUSED, AUDIO_EXPORTING = range(5)
46
""" String constants for incremental save """
47
INCREMENTAL_SAVE_EXT = ".incremental"
48
INCREMENTAL_SAVE_DELIMITER = "\n<<delimiter>>\n"
52
"audio-state" -- The status of the audio system has changed. See below:
53
"audio-state::play" -- The audio started playing.
54
"audio-state::pause" -- The audio is paused.
55
"audio-state::record" -- The audio started recording.
56
"audio-state::stop" -- The playback or recording was stopped.
57
"audio-state::export-start" -- The audio is being played to a file.
58
"audio-state::export-stop" -- The export to a file has completed.
59
"bpm" -- The beats per minute value was changed.
60
"click-track" -- The volume of the click track changed.
61
"gst-bus-error" -- An error message was posted to the pipeline. Two strings are also send with the error details.
62
"incremental-save" -- An action was logged to the .incremental file.
63
"instrument" -- The instruments for this project have changed. The instrument instance will be passed as a parameter. See below:
64
"instrument::added" -- An instrument was added to this project.
65
"instrument::removed" -- An instrument was removed from this project.
66
"instrument::reordered" -- The order of the instruments for this project changed.
67
"time-signature" -- The time signature values were changed.
68
"undo" -- The undo or redo stacks for this project have been changed.
69
"view-start" -- The starting position of the view of this project's timeline has changed.
70
"volume" -- This master volume value for this project has changed.
71
"zoom" -- The zoom level of this project's timeline has changed.
75
"audio-state" : ( gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_DETAILED, gobject.TYPE_NONE, () ),
76
"bpm" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
77
"click-track" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_DOUBLE,) ),
78
"gst-bus-error" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gobject.TYPE_STRING) ),
79
"incremental-save" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
80
"instrument" : ( gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_DETAILED, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,) ),
81
"time-signature" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
82
"undo" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
83
"view-start" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
84
"volume" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
85
"zoom" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () )
44
88
#_____________________________________________________________________
48
92
Creates a new instance of Project with default values.
50
Monitored.__init__(self)
94
gobject.GObject.__init__(self)
52
self.author = "" #the author of this project
96
self.author = "" #user specified author of this project
53
97
self.name = "" #the name of this project
98
self.notes = "" #user specified notes for the project
54
99
self.projectfile = "" #the name of the project file, complete with path
101
self.levels_path = ""
55
102
self.___id_list = [] #the list of IDs that have already been used, to avoid collisions
56
103
self.instruments = [] #the list of instruments held by this project
57
104
self.graveyard = [] # The place where deleted instruments are kept, to later be retrieved by undo functions
58
105
#used to delete copied audio files if the event that uses them is not saved in the project file
106
#also contains paths to levels_data files corresponding to those audio files
59
107
self.deleteOnCloseAudioFiles = [] # WARNING: any paths in this list will be deleted on exit!
60
108
self.clipboardList = [] #The list containing the events to cut/copy
61
109
self.viewScale = 25.0 #View scale as pixels per second
68
116
self.meter_nom = 4 # time signature numerator
69
117
self.meter_denom = 4 # time signature denominator
70
118
self.clickbpm = 120 #the number of beats per minute that the click track will play
71
self.clickEnabled = False #True is the click track is currently enabled
119
self.clickVolumeValue = 0 #The value of the click track volume between 0.0 and 1.0
72
120
#Keys are instruments which are recording; values are 3-tuples of the event being recorded, the recording bin and bus handler id
73
121
self.recordingEvents = {} #Dict containing recording information for each recording instrument
74
122
self.volume = 1.0 #The volume setting for the entire project
75
123
self.level = 0.0 #The level of the entire project as reported by the gstreamer element
76
124
self.currentSinkString = None #to keep track if the sink changes or not
126
self.hasDoneIncrementalSave = False # True if we have already written to the .incremental file from this project.
127
self.isDoingIncrementalRestore = False # If we are currently restoring incremental save actions
78
129
# Variables for the undo/redo command system
79
self.unsavedChanges = False #This boolean is to indicate if something which is not on the undo/redo stack needs to be saved
130
self.__unsavedChanges = False #This boolean is to indicate if something which is not on the undo/redo stack needs to be saved
80
131
self.__undoStack = [] #not yet saved undo commands
81
132
self.__redoStack = [] #not yet saved actions that we're undone
82
133
self.__savedUndoStack = [] #undo commands that have already been saved in the project file
90
141
self.mainpipeline = gst.Pipeline("timeline")
91
142
self.playbackbin = gst.Bin("playbackbin")
92
143
self.adder = gst.element_factory_make("adder")
144
self.postAdderConvert = gst.element_factory_make("audioconvert")
93
145
self.masterSink = self.MakeProjectSink()
95
147
self.levelElement = gst.element_factory_make("level", "MasterLevel")
96
148
self.levelElement.set_property("interval", gst.SECOND / 50)
97
149
self.levelElement.set_property("message", True)
99
#Restrict adder's output caps due to adder bug
151
#Restrict adder's output caps due to adder bug 341431
100
152
self.levelElementCaps = gst.element_factory_make("capsfilter", "levelcaps")
101
caps = gst.caps_from_string("audio/x-raw-int,rate=44100,channels=2,width=16,depth=16,signed=(boolean)true")
153
capsString = "audio/x-raw-float,rate=44100,channels=2,width=32,endianness=1234"
154
caps = gst.caps_from_string(capsString)
102
155
self.levelElementCaps.set_property("caps", caps)
104
157
# ADD ELEMENTS TO THE PIPELINE AND/OR THEIR BINS #
105
158
self.mainpipeline.add(self.playbackbin)
106
159
Globals.debug("added project playback bin to the pipeline")
107
for element in [self.adder, self.levelElementCaps, self.levelElement, self.masterSink]:
160
for element in [self.adder, self.levelElementCaps, self.postAdderConvert, self.levelElement, self.masterSink]:
108
161
self.playbackbin.add(element)
109
162
Globals.debug("added %s to project playbackbin" % element.get_name())
111
164
# LINK GSTREAMER ELEMENTS #
112
165
self.adder.link(self.levelElementCaps)
113
self.levelElementCaps.link(self.levelElement)
166
self.levelElementCaps.link(self.postAdderConvert)
167
self.postAdderConvert.link(self.levelElement)
114
168
self.levelElement.link(self.masterSink)
116
170
# CONSTRUCT CLICK TRACK BIN #
206
256
Globals.debug("current state:", self.mainpipeline.get_state(0)[1].value_name)
208
258
#If we've been recording then add new events to instruments
209
for instr, (event, bin, handle) in self.recordingEvents.items():
259
for instr, (event, bin, handle) in self.recordingEvents.iteritems():
210
260
instr.FinalizeRecording(event)
211
261
self.bus.disconnect(handle)
213
263
self.TerminateRecording()
215
#If this is due to end of stream then notify those interested
217
for function in self.EOShandlers:
220
Globals.PrintPipelineDebug("PIPELINE AFTER STOP:", self.mainpipeline)
222
265
#_____________________________________________________________________
255
298
#Add all instruments to the pipeline
256
299
self.recordingEvents = {}
258
for device in AlsaDevices.GetAlsaList("capture").keys():
301
capture_devices = AudioBackend.ListCaptureDevices(probe_name=False)
302
if not capture_devices:
303
capture_devices = ((None,None),)
305
default_device = capture_devices[0][0]
307
for device, deviceName in capture_devices:
259
308
devices[device] = []
260
309
for instr in self.instruments:
261
if instr.isArmed and instr.input == device:
310
if instr.isArmed and (instr.input == device or device is None):
262
311
instr.RemoveAndUnlinkPlaybackbin()
263
312
devices[device].append(instr)
313
elif instr.isArmed and instr.input is None:
314
instr.RemoveAndUnlinkPlaybackbin()
315
devices[default_device].append(instr)
265
for device, recInstruments in devices.items():
318
for device, recInstruments in devices.iteritems():
266
319
if len(recInstruments) == 0:
267
320
#Nothing to record on this device
270
channelsNeeded = AlsaDevices.GetChannelsOffered(device)
272
if channelsNeeded > 1 and not gst.registry_get_default().find_plugin("chansplit"):
273
Globals.debug("Channel splitting element not found when trying to record from multi-input device.")
274
raise AudioInputsError(2)
324
# assume we are using a backend like JACK which does not allow
325
#us to do device selection.
326
channelsNeeded = len(recInstruments)
328
channelsNeeded = AudioBackend.GetChannelsOffered(device)
276
331
if channelsNeeded > 1: #We're recording from a multi-input device
277
recordingbin = gst.Bin()
278
src = gst.element_factory_make("alsasrc")
279
src.set_property("device", device)
332
recordingbin = gst.Bin("recording bin")
333
recordString = Globals.settings.recording["audiosrc"]
334
srcBin = gst.parse_bin_from_description(recordString, True)
336
src_element = recordingbin.iterate_sources().next()
337
except StopIteration:
340
if hasattr(src_element.props, "device"):
341
src_element.set_property("device", device)
343
caps = gst.caps_from_string("audio/x-raw-int;audio/x-raw-float")
345
sampleRate = Globals.settings.recording["samplerate"]
347
sampleRate = int(sampleRate)
350
# 0 means for "autodetect", or more technically "don't use any rate caps".
353
struct.set_value("rate", sampleRate)
356
struct.set_value("channels", channelsNeeded)
358
Globals.debug("recording with capsfilter:", caps.to_string())
281
359
capsfilter = gst.element_factory_make("capsfilter")
282
capsString = "audio/x-raw-int,rate=%s" % Globals.settings.recording["samplerate"]
283
caps = gst.caps_from_string(capsString)
284
360
capsfilter.set_property("caps", caps)
286
split = gst.element_factory_make("chansplit")
288
recordingbin.add(src)
289
recordingbin.add(capsfilter)
290
recordingbin.add(split)
362
split = gst.element_factory_make("deinterleave")
363
convert = gst.element_factory_make("audioconvert")
365
recordingbin.add(srcBin, split, convert, capsfilter)
368
convert.link(capsfilter)
292
369
capsfilter.link(split)
294
371
split.connect("pad-added", self.__RecordingPadAddedCb, recInstruments, recordingbin)
300
377
event = instr.GetRecordingEvent()
302
379
encodeString = Globals.settings.recording["fileformat"]
303
capsString = "audio/x-raw-int,rate=%s" % Globals.settings.recording["samplerate"]
304
pipe = "alsasrc device=%s ! %s ! audioconvert ! level name=recordlevel interval=%d" +\
305
" ! audioconvert ! %s ! filesink location=%s"
306
pipe %= (device, capsString, event.LEVEL_INTERVAL * gst.SECOND, encodeString, event.file.replace(" ", "\ "))
380
recordString = Globals.settings.recording["audiosrc"]
384
sampleRate = int( Globals.settings.recording["samplerate"] )
387
# 0 means for "autodetect", or more technically "don't use any caps".
389
capsString = "audio/x-raw-int,rate=%s ! audioconvert" % sampleRate
391
capsString = "audioconvert"
393
# TODO: get rid of this entire string; do it manually
394
pipe = "%s ! %s ! level name=recordlevel ! audioconvert ! %s ! filesink name=sink"
395
pipe %= (recordString, capsString, encodeString)
308
397
Globals.debug("Using pipeline: %s" % pipe)
310
recordingbin = gst.parse_launch("bin.( %s )" % pipe)
399
recordingbin = gst.parse_bin_from_description(pipe, False)
401
filesink = recordingbin.get_by_name("sink")
402
level = recordingbin.get_by_name("recordlevel")
404
filesink.set_property("location", event.GetAbsFile())
405
level.set_property("interval", int(event.LEVEL_INTERVAL * gst.SECOND))
311
407
#update the levels in real time
312
408
handle = self.bus.connect("message::element", event.recording_bus_level)
411
src_element = recordingbin.iterate_sources().next()
412
except StopIteration:
415
if hasattr(src_element.props, "device"):
416
src_element.set_property("device", device)
314
418
self.recordingEvents[instr] = (event, recordingbin, handle)
316
420
Globals.debug("Recording in single-input mode")
336
440
for wav: "wavenc"
442
#try to create encoder/muxer first, before modifying the main pipeline.
444
self.encodebin = gst.parse_bin_from_description(encodeBin, True)
445
except gobject.GError, e:
446
if e.code == gst.PARSE_ERROR_NO_SUCH_ELEMENT:
447
error_no = ProjectManager.ProjectExportException.MISSING_ELEMENT
449
error_no = ProjectManager.ProjectExportException.INVALID_ENCODE_BIN
450
raise ProjectManager.ProjectExportException(error_no, e.message)
338
452
#stop playback because some elements will be removed from the pipeline
341
455
#remove and unlink the alsasink
342
456
self.playbackbin.remove(self.masterSink, self.levelElement)
343
self.levelElementCaps.unlink(self.levelElement)
457
self.postAdderConvert.unlink(self.levelElement)
344
458
self.levelElement.unlink(self.masterSink)
347
461
self.outfile = gst.element_factory_make("filesink", "export_file")
348
462
self.outfile.set_property("location", filename)
349
463
self.playbackbin.add(self.outfile)
351
#create encoder/muxer
352
self.encodebin = gst.gst_parse_bin_from_description("audioconvert ! %s" % encodeBin, True)
353
465
self.playbackbin.add(self.encodebin)
354
self.levelElementCaps.link(self.encodebin)
466
self.postAdderConvert.link(self.encodebin)
355
467
self.encodebin.link(self.outfile)
357
469
#disconnect the bus message handler so the levels don't change
463
575
self.audioState = newState
464
576
if newState == self.AUDIO_PAUSED:
465
self.StateChanged("pause")
577
self.emit("audio-state::pause")
466
578
elif newState == self.AUDIO_PLAYING:
467
self.StateChanged("play")
579
self.emit("audio-state::play")
468
580
elif newState == self.AUDIO_STOPPED:
469
self.StateChanged("stop")
581
self.emit("audio-state::stop")
470
582
elif newState == self.AUDIO_RECORDING:
471
self.StateChanged("record")
583
self.emit("audio-state::record")
472
584
elif newState == self.AUDIO_EXPORTING:
473
585
self.exportPending = False
486
598
recInstruments -- list with all Instruments currently recording.
487
599
bin -- the bin that stores all the recording elements.
489
match = re.search("(\d+)$", pad.get_name())
601
# SRC template: 'src%d'
602
padname = pad.get_name()
604
index = int(padname[3:])
606
Globals.debug("Cannot start multichannel record: pad name does not match 'src%d':", padname)
492
index = int(match.groups()[0])
493
609
for instr in recInstruments:
494
610
if instr.inTrack == index:
495
611
event = instr.GetRecordingEvent()
613
# TODO: get rid of string concatentation
497
614
encodeString = Globals.settings.recording["fileformat"]
498
pipe = "audioconvert ! level name=eventlevel interval=%d message=true !" +\
499
"audioconvert ! %s ! filesink location=%s"
500
pipe %= (event.LEVEL_INTERVAL, encodeString, event.file.replace(" ", "\ "))
615
pipe = "queue ! audioconvert ! level name=recordlevel ! audioconvert ! %s ! filesink name=sink"
502
encodeBin = gst.gst_parse_bin_from_description(pipe, True)
618
encodeBin = gst.parse_bin_from_description(pipe, True)
503
619
bin.add(encodeBin)
504
620
pad.link(encodeBin.get_pad("sink"))
622
filesink = bin.get_by_name("sink")
623
level = bin.get_by_name("recordlevel")
625
filesink.set_property("location", event.GetAbsFile())
626
level.set_property("interval", int(event.LEVEL_INTERVAL * gst.SECOND))
506
628
handle = self.bus.connect("message::element", event.recording_bus_level)
630
# since we are adding the encodebin to an already playing pipeline, sync up there states
631
encodeBin.set_state(gst.STATE_PLAYING)
508
633
self.recordingEvents[instr] = (event, bin, handle)
634
Globals.debug("Linked recording channel: instrument (%s), track %d" % (instr.name, instr.inTrack))
510
637
#_____________________________________________________________________
573
700
error, debug = message.parse_error()
575
702
Globals.debug("Gstreamer bus error:", str(error), str(debug))
576
self.StateChanged("gst-bus-error", str(error), str(debug))
703
Globals.debug("Domain: %s, Code: %s" % (error.domain, error.code))
704
Globals.debug("Message:", error.message)
706
if error.domain == gst.STREAM_ERROR and Globals.DEBUG_GST:
709
self.emit("gst-bus-error", str(error), str(debug))
578
711
#_____________________________________________________________________
580
def SaveProjectFile(self, path=None):
713
def DumpDotFile(self):
714
basepath, ext = os.path.splitext(self.projectfile)
715
name = "jokosher-pipeline-" + os.path.basename(basepath)
716
gst.DEBUG_BIN_TO_DOT_FILE_WITH_TS(self.mainpipeline, gst.DEBUG_GRAPH_SHOW_ALL, name)
717
Globals.debug("Dumped pipeline to DOT file:", name)
718
Globals.debug("Command to render DOT file: dot -Tsvg -o pipeline.svg <file>")
720
#_____________________________________________________________________
722
def SaveProjectFile(self, path=None, backup=False):
582
724
Saves the Project and its children as an XML file
583
725
to the path specified by file.
590
732
if not self.projectfile:
591
raise "No save path specified!"
733
raise Exception("No save path specified!")
592
734
path = self.projectfile
736
if not self.audio_path:
737
self.audio_path = os.path.join(os.path.dirname(path), "audio")
738
if not self.levels_path:
739
self.levels_path = os.path.join(os.path.dirname(path), "levels")
741
if os.path.exists(self.audio_path):
742
if not os.path.isdir(self.audio_path):
743
raise Exception("Audio save location is not a directory")
745
os.mkdir(self.audio_path)
747
if os.path.exists(self.levels_path):
748
if not os.path.isdir(self.levels_path):
749
raise Exception("Levels save location is not a directory")
751
os.mkdir(self.levels_path)
594
753
if not path.endswith(".jokosher"):
595
754
path = path + ".jokosher"
597
756
#sync the transport's mode with the one which will be saved
598
757
self.transportMode = self.transport.mode
600
self.unsavedChanges = False
601
#purge main undo stack so that it will not prompt to save on exit
602
self.__savedUndoStack.extend(self.__undoStack)
603
self.__undoStack = []
604
#purge savedRedoStack so that it will not prompt to save on exit
605
self.__redoStack.extend(self.__savedRedoStack)
606
self.__savedRedoStack = []
760
self.__unsavedChanges = False
761
#purge main undo stack so that it will not prompt to save on exit
762
self.__savedUndoStack.extend(self.__undoStack)
763
self.__undoStack = []
764
#purge savedRedoStack so that it will not prompt to save on exit
765
self.__redoStack.extend(self.__savedRedoStack)
766
self.__savedRedoStack = []
768
# delete the incremental file since its all safe on disk now
769
basepath, ext = os.path.splitext(self.projectfile)
770
incr_path = basepath + self.INCREMENTAL_SAVE_EXT
772
if os.path.exists(incr_path):
775
Globals.debug("Removal of .incremental failed! Next load we will try to restore unrestorable state!")
608
777
doc = xml.Document()
609
778
head = doc.createElement("JokosherProject")
614
783
params = doc.createElement("Parameters")
615
784
head.appendChild(params)
617
items = ["viewScale", "viewStart", "name", "author", "transportMode", "bpm", "meter_nom", "meter_denom"]
786
items = ["viewScale", "viewStart", "name", "author",
787
"transportMode", "bpm", "meter_nom", "meter_denom", "projectfile"]
619
789
Utils.StoreParametersToXML(self, doc, params, items)
791
notesNode = doc.createElement("Notes")
792
head.appendChild(notesNode)
794
# use repr() because XML will not preserve whitespace charaters such as \n and \t.
795
notesNode.setAttribute("text", repr(self.notes))
621
797
undo = doc.createElement("Undo")
622
798
head.appendChild(undo)
647
823
os.remove(path + "~")
649
825
#if the saving doesn't fail, move it to the proper location
826
if os.path.exists(path):
650
828
os.rename(path + "~", path)
652
self.StateChanged("undo")
832
#_____________________________________________________________________
834
def SaveIncrementalAction(self, action):
835
if self.isDoingIncrementalRestore:
838
if self.__performingUndo or self.__performingRedo:
841
path, ext = os.path.splitext(self.projectfile)
842
filename = path + self.INCREMENTAL_SAVE_EXT
844
if self.hasDoneIncrementalSave:
845
incr_file = open(filename, "a")
847
# if we haven't performed an incremental save yet,
848
# the existing .incremental file is old, so overwrite it.
849
incr_file = open(filename, "w")
850
self.hasDoneIncrementalSave = True
852
incr_file.write(action.StoreToString())
853
incr_file.write(self.INCREMENTAL_SAVE_DELIMITER)
857
self.SetUnsavedChanges()
858
self.emit("incremental-save")
860
#_____________________________________________________________________
862
def CanDoIncrementalRestore(self):
863
path, ext = os.path.splitext(self.projectfile)
864
filename = path + self.INCREMENTAL_SAVE_EXT
865
return os.path.exists(filename)
867
#_____________________________________________________________________
869
def DoIncrementalRestore(self):
871
Loads all the actions from the .incremental file and executes them
872
to restore the project's state.
875
if self.hasDoneIncrementalSave:
876
Globals.debug("Cannot do incremental restore after incremental save.")
879
path, ext = os.path.splitext(self.projectfile)
880
filename = path + self.INCREMENTAL_SAVE_EXT
882
save_action_list = []
884
if os.path.isfile(filename):
885
incr_file = open(filename, "r")
886
filetext = incr_file.read()
888
for incr_xml in filetext.split(self.INCREMENTAL_SAVE_DELIMITER):
889
incr_xml = incr_xml.strip()
891
incr_action = IncrementalSave.LoadFromString(incr_xml)
892
save_action_list.append(incr_action)
894
self.isDoingIncrementalRestore = True
896
IncrementalSave.FilterAndExecuteAll(save_action_list, self)
898
Globals.debug("Exception while restoring incremental save.",
899
"Project state is surely out of sync with .incremental file")
902
# set hasDoneIncrementSave to True because project is now in sync with .incremental file
903
# i.e. we don't have to destory the .incremental file because the states match up.
904
self.hasDoneIncrementalSave = True
905
self.isDoingIncrementalRestore = False
654
908
#_____________________________________________________________________
656
910
def CloseProject(self):
658
912
Closes down this Project.
915
# when closing the file, the user chooses to either save, or discard
916
# in either case, we don't need the incremental save file anymore
917
path, ext = os.path.splitext(self.projectfile)
918
filename = path + self.INCREMENTAL_SAVE_EXT
920
if os.path.exists(filename):
923
Globals.debug("Removal of .incremental failed! Next load we will try to restore unrestorable state!")
660
925
for file in self.deleteOnCloseAudioFiles:
661
926
if os.path.exists(file):
662
927
Globals.debug("Deleting copied audio file:", file)
664
929
self.deleteOnCloseAudioFiles = []
666
self.ClearListeners()
667
self.transport.ClearListeners()
668
931
self.mainpipeline.set_state(gst.STATE_NULL)
670
933
#_____________________________________________________________________
756
1027
def CheckUnsavedChanges(self):
758
Uses boolean self.unsavedChanges and Undo/Redo to
1029
Uses boolean self.__unsavedChanges and Undo/Redo to
759
1030
determine if the program needs to save anything on exit.
762
1033
True -- there's unsaved changes, undoes or redoes
763
1034
False -- the Project can be safely closed.
765
return self.unsavedChanges or \
1036
return self.__unsavedChanges or \
766
1037
len(self.__undoStack) > 0 or \
767
1038
len(self.__savedRedoStack) > 0
769
1040
#_____________________________________________________________________
1042
def SetUnsavedChanges(self):
1043
self.__unsavedChanges = True
1046
#_____________________________________________________________________
771
1048
def CanPerformUndo(self):
773
1050
Whether it's possible to perform an undo operation.
803
1080
newUndoAction = self.NewAtomicUndoAction()
804
1081
for cmdList in reversed(undoAction.GetUndoCommands()):
805
1082
obj = cmdList[0]
807
if obj[0] == "P": # Check if the object is a Project
809
elif obj[0] == "I": # Check if the object is an Instrument
811
target_object = [x for x in self.instruments if x.id==id][0]
812
elif obj[0] == "E": # Check if the object is an Event
814
for instr in self.instruments:
815
# First of all see if it's alive on an instrument
816
n = [x for x in instr.events if x.id==id]
818
# If not, check the graveyard on each instrument
819
n = [x for x in instr.graveyard if x.id==id]
1083
target_object = self.JokosherObjectFromString(obj)
824
1085
getattr(target_object, cmdList[1])(_undoAction_=newUndoAction, *cmdList[2])
826
1087
#_____________________________________________________________________
1089
def JokosherObjectFromString(self, string):
1091
Converts a string used to serialize references to Project, Instrument
1092
and Event instances into a reference to the actual object.
1095
string -- The string to convert such as "P" for project or "I2" for instrument with ID equal to 2.
1097
if string[0] == "P": # Check if the object is a Project
1099
elif string[0] == "I": # Check if the object is an Instrument
1100
id = int(string[1:])
1101
for instr in self.instruments:
1104
elif string[0] == "E": # Check if the object is an Event
1105
id = int(string[1:])
1106
for instr in self.instruments:
1107
for event in instr.events:
1110
for event in instr.graveyard:
1114
#_____________________________________________________________________
828
1116
@UndoSystem.UndoCommand("SetBPM", "temp")
829
1117
def SetBPM(self, bpm):
872
1162
they are all appended to the undo stack as a single atomic action.
875
instrTuples -- a list of tuples containing name, type and pixbuf
1165
instrTuples -- a list of tuples containing name and type
876
1166
that will be passed to AddInstrument().
879
A list of IDs of the added Instruments.
1169
A list of the added Instruments.
882
1172
undoAction = self.NewAtomicUndoAction()
883
for name, type, pixbuf, path in instrTuples:
884
self.AddInstrument(name, type, pixbuf, _undoAction_=undoAction)
1174
for name, type in instrTuples:
1175
instr = self.AddInstrument(name, type, _undoAction_=undoAction)
1176
instrList.append(instr)
886
1179
#_____________________________________________________________________
914
1207
name -- name of the instrument.
915
1208
type -- type of the instrument.
916
pixbuf -- image object corresponding to the instrument.
919
ID of the added Instrument.
1211
The created Instrument object.
1213
pixbuf = Globals.getCachedInstrumentPixbuf(type)
922
1214
instr = Instrument.Instrument(self, name, type, pixbuf)
923
1215
if len(self.instruments) == 0:
924
1216
#If this is the first instrument, arm it by default
925
1217
instr.isArmed = True
926
audio_dir = os.path.join(os.path.split(self.projectfile)[0], "audio")
927
instr.path = os.path.join(audio_dir)
929
1219
self.temp = instr.id
930
1220
self.instruments.append(instr)
1222
self.emit("instrument::added", instr)
934
1225
#_____________________________________________________________________
1026
1320
if not undoAction:
1027
1321
undoAction = self.NewAtomicUndoAction()
1323
uris = [PlatformUtils.pathname2url(filename) for filename in fileList]
1029
1325
name, type, pixbuf, path = [x for x in Globals.getCachedInstruments() if x[1] == "audiofile"][0]
1030
instr = self.AddInstrument(name, type, pixbuf, _undoAction_=undoAction)
1031
instr.AddEventsFromList(0, fileList, copyFile, undoAction)
1326
instr = self.AddInstrument(name, type, _undoAction_=undoAction)
1327
instr.AddEventsFromList(0, uris, copyFile, undoAction)
1033
1329
#_____________________________________________________________________
1201
1500
#_____________________________________________________________________
1203
def EnableClick(self):
1502
def SetClickTrackVolume(self, value):
1205
1504
Unmutes and enables the click track.
1208
self.clickTrackVolume.set_property("mute", False)
1209
self.clickEnabled = True
1211
#_____________________________________________________________________
1213
def DisableClick(self):
1215
Mutes and disables the click track.
1218
self.clickTrackVolume.set_property("mute", True)
1219
self.clickEnabled = False
1507
value -- The volume of the click track between 0.0 and 1.0
1509
if self.clickVolumeValue != value:
1510
self.clickTrackVolume.set_property("mute", (value < 0.01))
1511
# convert the 0.0 to 1.0 range to 0.0 to 2.0 range (to let the user make it twice as loud)
1512
self.clickTrackVolume.set_property("volume", value * 2)
1513
self.clickVolumeValue = value
1514
self.emit("click-track", value)
1221
1516
#_____________________________________________________________________
1266
1561
return self.masterSink
1268
1563
self.currentSinkString = sinkString
1271
if sinkString == "alsasink":
1272
sinkElement = gst.element_factory_make("alsasink")
1273
#Set the alsa device for audio output
1274
outdevice = Globals.settings.playback["devicecardnum"]
1275
if outdevice == "default":
1277
# Select first output device as default to avoid a GStreamer bug which causes
1278
# large amounts of latency with the ALSA 'default' device.
1279
outdevice = AlsaDevices.GetAlsaList("playback").keys()[1]
1282
Globals.debug("Output device: %s" % outdevice)
1283
sinkElement.set_property("device", outdevice)
1284
Globals.debug("Using alsasink for audio output")
1286
elif sinkString != "autoaudiosink":
1288
sinkElement = gst.gst_parse_bin_from_description(sinkString, True)
1289
except gobject.GError:
1290
Globals.debug("Parsing failed: %s" % sinkString)
1292
Globals.debug("Using custom pipeline for audio sink: %s" % sinkString)
1295
# if a sink element has not yet been created, autoaudiosink is our last resort
1296
sinkElement = gst.element_factory_make("autoaudiosink")
1567
sinkBin = gst.parse_bin_from_description(sinkString, True)
1568
except gobject.GError:
1569
Globals.debug("Parsing failed: %s" % sinkString)
1570
# autoaudiosink is our last resort
1571
sinkBin = gst.element_factory_make("autoaudiosink")
1297
1572
Globals.debug("Using autoaudiosink for audio output")
1574
Globals.debug("Using custom pipeline for audio sink: %s" % sinkString)
1301
#_____________________________________________________________________
1303
def AddEndOfStreamHandler(self, function):
1305
Adds a function to the list of functions that need notification of
1306
end-of-stream messages from gstreamer
1309
function -- the function to be added
1311
self.EOShandlers.append(function)
1313
#_____________________________________________________________________
1315
def RemoveEndOfStreamHandler(self, function):
1317
Removes a function from the list of functions that need notification of
1318
end-of-stream messages from gstreamer
1321
function -- the function to be removed
1323
self.EOShandlers.remove(function)
1325
#____________________________________________________________________
1576
sinkElement = sinkBin.sinks().next()
1577
if hasattr(sinkElement.props, "device"):
1578
outdevice = Globals.settings.playback["device"]
1579
Globals.debug("Output device: %s" % outdevice)
1580
sinkElement.set_property("device", outdevice)
1584
#____________________________________________________________________
1586
def OnCaptureBackendChange(self):
1587
for instr in self.instruments:
1591
#____________________________________________________________________
1326
1593
def GetInputFilenames(self):
1328
1595
Obtains a list of all filenames that are to be input to
1335
1602
for instrument in self.instruments:
1336
1603
for event in instrument.events:
1337
fileList.append(event.file)
1604
fileList.append(event.GetAbsFile())
1338
1605
return fileList
1340
1607
#____________________________________________________________________
1609
def GetLocalAudioFilenames(self):
1611
for instrument in self.instruments:
1612
for event in instrument.events:
1613
if not os.path.isabs(event.file):
1614
fileList.append(event.file)
1617
#____________________________________________________________________
1619
def GetLevelsFilenames(self):
1621
for instrument in self.instruments:
1622
for event in instrument.events:
1623
fileList.append(event.levels_file)
1626
#____________________________________________________________________
1629
def SetName(self, name):
1630
if self.name != name:
1632
inc = IncrementalSave.SetName(name)
1633
self.SaveIncrementalAction(inc)
1635
#____________________________________________________________________
1637
def SetAuthor(self, author):
1638
if self.author != author:
1639
self.author = author
1640
inc = IncrementalSave.SetAuthor(author)
1641
self.SaveIncrementalAction(inc)
1643
#____________________________________________________________________
1645
def SetNotes(self, notes):
1646
if self.notes != notes:
1648
inc = IncrementalSave.SetNotes(notes)
1649
self.SaveIncrementalAction(inc)
1651
#____________________________________________________________________
1343
1652
#=========================================================================