13
13
#-------------------------------------------------------------------------------
15
15
import xml.dom.minidom as xml
16
import os, sys, os.path
18
18
pygst.require("0.10")
20
from Monitored import Monitored
20
import Utils, LevelsList
21
import UndoSystem, IncrementalSave
27
from elements.singledecodebin import SingleDecodeBin
26
28
_ = gettext.gettext
28
30
#=========================================================================
30
class Event(Monitored):
32
class Event(gobject.GObject):
32
34
This class handles maintaing the information for a single audio
33
35
event, normally, a fragment of a recorded file.
38
"waveform" -- The waveform date for this event has changed.
39
"position" -- The starting position of this event has changed.
40
"length" -- The length of this event has changed.
41
"corrupt" -- The audio file for this event is not playable. Two strings with detailed information are sent.
42
"loading" -- Loading has started or completed.
36
""" State changed types (to be sent through the Monitored class) """
37
WAVEFORM, MOVE, LENGTH, CORRUPT, LOADING = range(5)
46
"waveform" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
47
"position" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
48
"length" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
49
"corrupt" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)),
50
"loading" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
51
"selected" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () )
39
54
""" The level sample interval in seconds """
56
LEVELS_FILE_EXTENSION = ".leveldata"
42
58
#_____________________________________________________________________
52
68
filelabel -- label to print in error messages.
53
69
It can be different from the file parameter.
55
Monitored.__init__(self)
71
gobject.GObject.__init__(self)
73
self.id = instrument.project.GenerateUniqueID(id) #check is id is already taken, then set it.
57
74
self.start = 0.0 # Time in seconds at which the event begins
58
75
self.duration = 0.0 # Duration in seconds of the event
59
76
# The file this event should play (without escaped characters)
60
77
# If you need characters escaped, please do self.file.replace(" ", "\ ")
61
78
# but **do not** assign it to this variable.
80
if self.file and os.path.isabs(self.file) and \
81
PlatformUtils.samefile(instrument.project.audio_path, os.path.dirname(self.file)):
82
# If the file is in the audio dir, just include the filename, not the absolute path
83
Globals.debug("Event() given absolute file, should be relative:", self.file)
84
self.file = os.path.basename(self.file)
86
# levels_file is a filename only, no directory information for levels here.
87
basename = os.path.basename(self.file or "Unknown")
88
self.levels_file = "%s_%d%s" % (basename, self.id, self.LEVELS_FILE_EXTENSION)
64
90
# the label is the filename to print in error messages
65
91
# if it differs from the real filename (i.e its been copied into the project)
66
92
if filelabel != None:
72
98
self.name = "New Event" # Name of this event
74
100
self.selection = [0, 0] # List start and end of selection (for fades, etc) measured in seconds
75
self.levels = [] # Array of audio levels to be drawn for this event
101
self.levels_list = LevelsList.LevelsList() # LevelsList class containing array of audio levels to be drawn for this event
77
self.id = instrument.project.GenerateUniqueID(id) #check is id is already taken, then set it.
78
103
self.instrument = instrument # The parent instrument
79
self.filesrc = None # The gstreamer gnlfilesource object.
104
self.gnlsrc = None # The gstreamer gnlsource object.
105
self.single_decode_bin = None # The gstreamer file decoder element.
81
107
self.offset = 0.0 # Offset through the file in seconds
82
108
self.isLoading = False # True if the event is currently loading level data
99
125
# The list *must* be ordered by time-in-seconds, so when you update it from
100
126
# the dictionary using dict.items(), be sure to sort it again.
101
127
self.audioFadePoints = []
102
#Just like self.levels except with all the levels scaled according to the
128
#Just like self.levels_list except with all the levels scaled according to the
103
129
#points in self.audioFadePoints.
130
self.fadeLevels = LevelsList.LevelsList()
106
132
#_____________________________________________________________________
134
def GetFilename(self):
135
return os.path.basename(self.file)
137
#_____________________________________________________________________
139
def GetAbsFile(self):
140
if os.path.isabs(self.file):
143
return os.path.join(self.instrument.project.audio_path, self.file)
145
#_____________________________________________________________________
147
def GetAbsLevelsFile(self):
148
return os.path.join(self.instrument.project.levels_path, self.levels_file)
150
#_____________________________________________________________________
108
152
def CreateFilesource(self):
110
154
Creates a new GStreamer file source with an unique id.
114
158
Globals.debug("create file source")
116
self.filesrc = gst.element_factory_make("gnlfilesource", "Event_%d"%self.id)
117
if not self.filesrc in list(self.instrument.composition.elements()):
118
self.instrument.composition.add(self.filesrc)
160
self.gnlsrc = gst.element_factory_make("gnlsource", "Event_%d"%self.id)
161
if not self.gnlsrc in list(self.instrument.composition.elements()):
162
self.instrument.composition.add(self.gnlsrc)
120
164
self.SetProperties()
166
#_____________________________________________________________________
168
def DestroyFilesource(self):
170
Removes the Gstreamer file source from the instrument's composition.
172
if self.gnlsrc in list(self.instrument.composition.elements()):
173
self.instrument.composition.remove(self.gnlsrc)
122
175
#_____________________________________________________________________
126
179
Sets basic Event properties like location, start, duration, etc.
182
if self.single_decode_bin:
183
self.gnlsrc.remove(self.single_decode_bin)
185
Globals.debug("creating SingleDecodeBin")
186
caps = gst.caps_from_string("audio/x-raw-int;audio/x-raw-float")
187
f = PlatformUtils.pathname2url(self.GetAbsFile())
188
Globals.debug("file uri is:", f)
189
self.single_decode_bin = SingleDecodeBin(caps=caps, uri=f)
190
self.gnlsrc.add(self.single_decode_bin)
129
191
Globals.debug("setting event properties:")
130
propsDict = {"location" : self.file,
131
194
"start" : long(self.start * gst.SECOND),
132
195
"duration" : long(self.duration * gst.SECOND),
133
196
"media-start" : long(self.offset * gst.SECOND),
163
226
ev.appendChild(params)
165
228
items = ["start", "duration", "isSelected",
166
"name", "offset", "file", "isLoading", "isRecording"
229
"name", "offset", "file", "filelabel", "levels_file",
230
"isLoading", "isRecording"
169
233
#Since we are saving the path to the project file, don't delete it on exit
170
if self.file in self.instrument.project.deleteOnCloseAudioFiles:
171
self.instrument.project.deleteOnCloseAudioFiles.remove(self.file)
173
self.temp = self.file
174
if os.path.samefile(self.instrument.path, os.path.dirname(self.file)):
175
# If the file is in the audio dir, just include the filename, not the absolute path
176
self.file = os.path.basename(self.file)
178
Utils.StoreParametersToXML(self, doc, params, items)
180
# Put self.file back to its absolute path
181
self.file = self.temp
234
if self.GetAbsFile() in self.instrument.project.deleteOnCloseAudioFiles:
235
self.instrument.project.deleteOnCloseAudioFiles.remove(self.GetAbsFile())
237
Utils.StoreParametersToXML(self, doc, params, items)
184
239
xmlPoints = doc.createElement("FadePoints")
185
240
ev.appendChild(xmlPoints)
186
241
Utils.StoreDictionaryToXML(doc, xmlPoints, self.__fadePointsDict, "FadePoint")
189
levelsXML = doc.createElement("Levels")
190
ev.appendChild(levelsXML)
191
stringList = map(str, self.levels)
192
levelsXML.setAttribute("value", ",".join(stringList))
244
self.levels_list.tofile(self.GetAbsLevelsFile())
245
if self.GetAbsLevelsFile() in self.instrument.project.deleteOnCloseAudioFiles:
246
self.instrument.project.deleteOnCloseAudioFiles.remove(self.GetAbsLevelsFile())
194
248
#_____________________________________________________________________
223
277
#_____________________________________________________________________
225
@UndoSystem.UndoCommand("Move", "start", "temp")
226
def Move(self, frm, to):
279
@UndoSystem.UndoCommand("Move", "temp")
280
def Move(self, to, frm=None):
228
282
Moves this Event in time.
285
to -- the time the Event's moving to.
231
286
frm -- the time the Event's moving from.
232
to -- the time the Event's moving to.
289
self.temp = self.start
236
293
self.SetProperties()
238
#_____________________________________________________________________
240
def Split(self, split_point, id=-1):
242
Dummy function kept for compatibility with 0.2 project files.
294
self.emit("position")
296
#_____________________________________________________________________
298
def _Compat09_Move(self, frm, to, _undoAction_):
300
Moves this Event in time.
303
A compatibility method for undo functions from
304
0.1, 0.2 and 0.9 project files. It should not be called
305
explicitly by anyone.
308
frm -- the time the Event's moving from.
309
to -- the time the Event's moving to.
311
self.Move(to, frm, _undoAction_=_undoAction_)
313
#_____________________________________________________________________
315
def _Compat02_Split(self, split_point, id=-1):
317
Function kept for compatibility with 0.2 project files.
243
318
Parameters are the same as SplitEvent().
245
320
self.SplitEvent(split_point)
247
322
#_____________________________________________________________________
249
def Join(self, joinEventID):
324
def _Compat02_Join(self, joinEventID):
251
Dummy function kept for compatibility with 0.2 project files.
326
Function kept for compatibility with 0.2 project files.
252
327
Parameters are the same as JoinEvent().
254
329
self.JoinEvent(joinEventID)
256
331
#_____________________________________________________________________
333
def CopySelection(self, eventID=-1):
335
Only for use with a 2-point selection.
336
Essentially performs a 'fake split' and returns a new event
337
which would be the result of splitting an event at the 2 points.
339
This is used when the user shift-drags an event to create a selection,
340
then chooses 'copy' from the context menu. The new event can be placed
341
wherever the user wishes by right-clicking and choosing 'paste'.
344
e = [x for x in self.instrument.graveyard if x.id == eventID][0]
345
self.instrument.graveyard.remove(e)
347
e = Event(self.instrument, self.file)
350
dur = self.selection[1] - self.selection[0]
352
e.start = self.start + self.selection[0]
353
e.offset = self.selection[0] #+self.offset
358
for key, value in self.__fadePointsDict.iteritems():
359
if key < self.selection[0]:
360
dictLeft[key] = value
361
if key > self.selection[0]:
362
dictRight[key - self.selection[0]] = value
363
#in case there is a fade passing through the split point, recreate half of it on either side
364
splitFadeLevel = self.GetFadeLevelAtPoint(self.selection[0])
365
dictLeft[self.selection[0]] = splitFadeLevel
366
dictRight[0.0] = splitFadeLevel
368
millis = int(self.selection[0] * 1000)
369
e.levels_list = self.levels_list.slice_by_endtime(millis)
370
e.__fadePointsDict = dictRight
372
e.__UpdateAudioFadePoints()
374
self.instrument.events.append(e)
258
382
@UndoSystem.UndoCommand("JoinEvent", "temp", "temp2")
259
383
def SplitEvent(self, split_point, cutRightSide=True, eventID=-1):
383
511
self.__fadePointsDict = newDict
384
512
self.__UpdateAudioFadePoints()
514
#create an undo action that is not attached to the project so that
515
# the following delete will not be undone (it will be re-split not resurrected)
516
nullAction = UndoSystem.AtomicUndoAction()
386
517
# Now that they're joined, move delete the rightEvent
387
if joinEvent in self.instrument.events:
388
self.instrument.events.remove(joinEvent)
389
if not joinEvent in self.instrument.graveyard:
390
self.instrument.graveyard.append(joinEvent)
518
joinEvent.Delete(_undoAction_=nullAction)
392
self.StateChanged(self.LENGTH)
393
self.StateChanged(self.MOVE)
521
self.emit("position")
395
523
self.temp2 = joinToRight
396
524
self.temp3 = joinEvent.id
479
607
self.instrument.ResurrectEvent(self.id)
481
609
#_____________________________________________________________________
611
def install_plugin_cb(self, result):
612
self._installing_plugins = False
613
if result == gst.pbutils.INSTALL_PLUGINS_SUCCESS:
614
gst.update_registry()
615
self.GenerateWaveform()
618
# FIXME: send a better error
619
msg = "failed to install plugins: %s" % result
620
self.emit("corrupt", msg)
622
#_____________________________________________________________________
483
624
def bus_message(self, bus, message):
500
641
st = message.structure
502
if st.get_name() == "level":
503
newLevel = self.__CalculateAudioLevel(st["peak"])
504
self.levels.append(newLevel)
506
end = st["endtime"] / float(gst.SECOND)
507
self.loadingLength = int(end)
509
# Only send events every second processed to reduce GUI load
510
if self.loadingLength != self.lastEnd:
511
self.lastEnd = self.loadingLength
512
self.StateChanged(self.LENGTH) # tell the GUI
645
if st.get_name().startswith('missing-'):
646
self.loadingPipeline.set_state(gst.STATE_NULL)
647
Utils.HandleGstPbutilsMissingMessage(message, self.install_plugin_cb)
649
elif st.get_name() == "level":
650
self.__AppendLevelToList(st)
652
#Truncate so it updates once per second
653
self.loadingLength = st["endtime"] / gst.SECOND
655
# Only send events every second processed to reduce GUI load
656
if self.loadingLength != self.lastEnd:
657
self.lastEnd = self.loadingLength
658
self.emit("length") # tell the GUI
515
661
#_____________________________________________________________________
542
688
self.duration = self.loadingLength
691
final_endtime = self.levels_list[-1][0]
692
if final_endtime > int(self.duration * 1000):
693
Globals.debug("Event %d: duration (%f) is less than last level endtime (%d)."
694
% (self.id, self.duration, final_endtime))
695
self.duration = final_endtime / 1000.0
697
Globals.debug("\tduration has been increased to", self.duration)
544
699
if length and (self.offset > 0 or self.duration != length):
545
dt = int(self.duration * len(self.levels) / length)
546
start = int(self.offset * len(self.levels) / length)
547
self.levels = self.levels[start:start+dt]
700
starttime = int(self.offset * 1000)
701
stoptime = int((self.offset + self.duration) * 1000)
702
self.levels_list = self.levels_list.slice_by_endtime(starttime, stoptime)
549
704
# We're done with the bin so release it
550
705
self.StopGenerateWaveform()
552
707
# Signal to interested objects that we've changed
553
self.StateChanged(self.WAVEFORM)
708
self.emit("waveform")
556
711
#_____________________________________________________________________
591
746
error, debug = message.parse_error()
748
Globals.debug("Event Bus Error Message:")
749
Globals.debug("\tCode:", error.code)
750
Globals.debug("\tDomain:", error.domain)
751
Globals.debug("\tMessage:", error.message)
593
753
Globals.debug("Event bus error:", str(error), str(debug))
594
self.StateChanged(self.CORRUPT, str(error), str(debug))
754
self.emit("corrupt", "%s\n%s" % (error, debug))
596
756
#_____________________________________________________________________
625
785
Renders the level information for the GUI.
627
pipe = """filesrc name=src location=%s ! decodebin ! audioconvert ! level interval=%d message=true ! fakesink"""
628
pipe = pipe % (self.file.replace(" ", "\ "), self.LEVEL_INTERVAL * gst.SECOND)
787
pipe = """filesrc name=src ! decodebin ! audioconvert ! level message=true name=level_element ! fakesink"""
629
788
self.loadingPipeline = gst.parse_launch(pipe)
790
filesrc = self.loadingPipeline.get_by_name("src")
791
level = self.loadingPipeline.get_by_name("level_element")
793
filesrc.set_property("location", self.GetAbsFile())
794
level.set_property("interval", int(self.LEVEL_INTERVAL * gst.SECOND))
631
796
self.bus = self.loadingPipeline.get_bus()
632
797
self.bus.add_signal_watch()
649
814
Copies the audio file to the new file location and reads the levels
650
815
at the same time.
652
if not gst.element_make_from_uri(gst.URI_SRC, uri):
818
urisrc = gst.element_make_from_uri(gst.URI_SRC, uri)
653
820
#This means that here is no gstreamer src element on the system that can handle this URI type.
656
pipe = """%s ! tee name=mytee mytee. ! queue ! filesink location=%s """ +\
657
"""mytee. ! queue ! decodebin ! audioconvert ! level interval=%d message=true ! fakesink"""
658
pipe = pipe % (urllib.quote(uri,":/"), self.file.replace(" ", "\ "), self.LEVEL_INTERVAL * gst.SECOND)
823
pipe = """tee name=mytee mytee. ! queue ! filesink name=sink """ +\
824
"""mytee. ! queue ! decodebin ! audioconvert ! level name=level_element message=true ! fakesink"""
659
825
self.loadingPipeline = gst.parse_launch(pipe)
827
tee = self.loadingPipeline.get_by_name("mytee")
828
filesink = self.loadingPipeline.get_by_name("sink")
829
level = self.loadingPipeline.get_by_name("level_element")
831
self.loadingPipeline.add(urisrc)
834
filesink.set_property("location", self.GetAbsFile())
835
level.set_property("interval", int(self.LEVEL_INTERVAL * gst.SECOND))
661
837
self.bus = self.loadingPipeline.get_bus()
662
838
self.bus.add_signal_watch()
690
866
if self.loadingPipeline:
691
867
self.loadingPipeline.set_state(gst.STATE_NULL)
869
if finishedLoading and self.levels_list:
870
self.levels_list.tofile(self.GetAbsLevelsFile())
871
del_on_close_list = self.instrument.project.deleteOnCloseAudioFiles
872
# this event might not be in the project file yet
873
# if so, levels_file should be deleted when audio file is deleted on exit
874
if self.GetAbsFile() in del_on_close_list:
875
del_on_close_list.append(self.GetAbsLevelsFile())
877
inc = IncrementalSave.CompleteLoading(self.id, self.duration, self.levels_file)
878
self.instrument.project.SaveIncrementalAction(inc)
693
880
if self.isDownloading:
694
881
# If we are currently downloading, we can't restart later,
695
882
# so cancel regardless of the finishedLoading boolean's value.
724
912
st = message.structure
725
913
if st and message.src.get_name() == "recordlevel":
726
newLevel = self.__CalculateAudioLevel(st["peak"])
727
self.levels.append(newLevel)
914
self.__AppendLevelToList(st)
729
end = st["endtime"] / float(gst.SECOND)
916
end = st["endtime"] / float(gst.SECOND) #convert to float representing seconds
730
917
#Round to one decimal place so it updates 10 times per second
731
918
self.loadingLength = round(end, 1)
733
920
# Only send events every second processed to reduce GUI load
734
921
if self.loadingLength != self.lastEnd:
735
922
self.lastEnd = self.loadingLength
736
self.StateChanged(self.LENGTH) # tell the GUI
923
self.emit("length") # tell the GUI
739
926
#_____________________________________________________________________
741
def __CalculateAudioLevel(self, channelLevels):
743
Calculates an average for all channel levels.
746
channelLevels -- list of levels from each channel.
749
an average level, also taking into account negative infinity numbers,
750
which will be discarded in the average.
752
negInf = float("-inf")
755
for peak in channelLevels:
756
#don't add -inf values cause 500 + -inf is still -inf
760
#avoid a divide by zero here
762
peaktotal /= peakcount
763
#it must be put back to -inf if nothing has been added to it, so that the DbToFloat conversion will work
767
#convert to 0...1 float, and return
768
return Utils.DbToFloat(peaktotal)
928
def __AppendLevelToList(self, structure):
929
(end, peaks) = Utils.CalculateAudioLevelFromStructure(structure)
931
# the last level may be sent twice. If timestamp is the same, ignore it.
932
if self.levels_list and self.levels_list[-1][0] == end:
935
# work around GStreamer bug where stream time will be -1 (indicating error)
936
# and then cast to guint64 which results in the maximum 64-bit integer value.
937
# In this case stream-time and endtime are bogus values, but duration is still correct.
938
stream_time = structure["stream-time"]
939
if stream_time == ((2**64) - 1):
940
delta = int(structure["duration"] / Utils.NANO_TO_MILLI_DIVISOR)
941
self.levels_list.append_time_delta(delta, peaks)
943
self.levels_list.append(end, peaks)
770
945
#_____________________________________________________________________
969
1144
list is a cache of faded levels as they will be shown on the screen
970
1145
so that we don't have to calculate them everytime we draw.
972
if not self.audioFadePoints:
1147
if not self.audioFadePoints or len(self.audioFadePoints) < 2:
1148
Globals.debug("Event", self.id, ": no fade points to use")
973
1149
#there are no fade points for us to use
977
oneSecondInLevels = len(self.levels) / self.duration
979
previousFade = self.audioFadePoints[0]
980
for fade in self.audioFadePoints[1:]:
981
#calculate the number of levels that should be in the list between the two points
982
levelsInThisSection = int(round((fade[0] - previousFade[0]) * oneSecondInLevels))
983
if fade[1] == previousFade[1]:
984
# not actually a fade, just two points at the same volume
985
fadePercents.extend([ fade[1] ] * levelsInThisSection)
1153
#oneSecondInLevels = len(self.levels) / self.duration
1155
self.fadeLevels = LevelsList.LevelsList()
1157
iterFadePoints = iter(self.audioFadePoints)
1158
firstFadeTime, firstFadeValue = iterFadePoints.next()
1159
firstFadeTime = int(firstFadeTime * 1000) #convert to milliseconds
1160
secondFadeTime, secondFadeValue = iterFadePoints.next()
1161
secondFadeTime = int(secondFadeTime * 1000) #convert to milliseconds
1162
# if less than one percent difference, assume they are the same
1163
sameValues = abs(firstFadeValue - secondFadeValue) < 0.01
1166
slope = (secondFadeValue - firstFadeValue) / (secondFadeTime - firstFadeTime)
1168
for endtime, peak in self.levels_list:
1169
# check if we have moved into the next fade point pair
1170
# don't care about 1 millisecond difference, its rounding error
1171
if endtime > (secondFadeTime + 1):
1172
firstFadeTime = secondFadeTime
1173
firstFadeValue = secondFadeValue
1175
secondFadeTime, secondFadeValue = iterFadePoints.next()
1176
except StopIteration:
1177
Globals.debug("Event %d: endtime (%d) is after last fade point (%d,%d)"
1178
% (self.id, endtime, secondFadeTime, secondFadeValue))
1180
secondFadeTime = int(secondFadeTime * 1000) #convert to milliseconds
1182
# if less than one percent difference, assume they are the same
1183
sameValues = abs(firstFadeValue - secondFadeValue) < 0.01
1185
# the fade line is not flat, so calculate the slope of it
1186
slope = (secondFadeValue - firstFadeValue) / (secondFadeTime - firstFadeTime)
1189
#no fade here, the same volume continues across
1190
self.fadeLevels.append(endtime, [int(peak * firstFadeValue)])
987
step = (fade[1] - previousFade[1]) / levelsInThisSection
988
floatList = Utils.floatRange(previousFade[1], fade[1], step)
989
#make sure the list of levels does not exceed the calculated length
990
floatList = floatList[:levelsInThisSection]
991
fadePercents.extend(floatList)
994
if len(fadePercents) != len(self.levels):
995
#make sure its not longer than the levels list
996
fadePercents = fadePercents[:len(self.levels)]
997
#make sure its not shorter than the levels list
998
#by copying the last level over again
999
lastLevel = fadePercents[-1]
1000
while len(fadePercents) < len(self.levels):
1001
fadePercents.append(lastLevel)
1192
rel_time = endtime - firstFadeTime
1193
peak_delta = slope * rel_time
1194
new_fade_value = firstFadeValue + peak_delta
1195
assert new_fade_value <= 1.0 # a fade cannot be more than 100%
1196
peak = int(peak * new_fade_value)
1003
self.fadeLevels = []
1004
for i in range(len(self.levels)):
1005
self.fadeLevels.append(fadePercents[i] * self.levels[i])
1198
self.fadeLevels.append(endtime, [peak])
1007
1199
#_____________________________________________________________________
1009
1201
def GetFadeLevels(self):