diff options
Diffstat (limited to 'pysamples/grecord.py')
-rw-r--r-- | pysamples/grecord.py | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/pysamples/grecord.py b/pysamples/grecord.py new file mode 100644 index 0000000..84b1a5c --- /dev/null +++ b/pysamples/grecord.py @@ -0,0 +1,227 @@ +#Copyright (c) 2008, Media Modifications Ltd. +#Copyright (c) 2011, Walter Bender + +# This procedure is invoked when the user-definable block on the +# "extras" palette is selected. + +# Usage: Import this code into a Python (user-definable) block; Pass +# it 'start' to start recording; 'stop' to stop recording; 'play' to +# play back your recording; or 'save' to save your recording to the +# Sugar Journal. + + +def myblock(tw, arg): + ''' Record and playback a sound (Sugar only) ''' + import os + import time + + import gtk + import gst + + import gobject + gobject.threads_init() + + from TurtleArt.tautils import get_path + from TurtleArt.tagplay import play_audio_from_file + from sugar.datastore import datastore + from sugar import profile + + from gettext import gettext as _ + + class Grecord: + ''' A class for creating a gstreamer session for recording audio. ''' + + def __init__(self, tw): + ''' Set up the stream. We save to a raw .wav file and then + convert the sound to .ogg for saving. ''' + datapath = get_path(tw.parent, 'instance') + self.capture_file = os.path.join(datapath, 'output.wav') + self.save_file = os.path.join(datapath, 'output.ogg') + self._eos_cb = None + + self._can_limit_framerate = False + self._recording = False + + self._audio_transcode_handler = None + self._transcode_id = None + + self._pipeline = gst.Pipeline("Record") + self._create_audiobin() + self._pipeline.add(self._audiobin) + + bus = self._pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message', self._bus_message_handler) + + def _create_audiobin(self): + ''' Assemble all the pieces we need. ''' + src = gst.element_factory_make("alsasrc", "absrc") + + # attempt to use direct access to the 0,0 device, solving some A/V + # sync issues + src.set_property("device", "plughw:0,0") + hwdev_available = src.set_state(gst.STATE_PAUSED) != \ + gst.STATE_CHANGE_FAILURE + src.set_state(gst.STATE_NULL) + if not hwdev_available: + src.set_property("device", "default") + + srccaps = gst.Caps("audio/x-raw-int,rate=16000,channels=1,depth=16") + + # guarantee perfect stream, important for A/V sync + rate = gst.element_factory_make("audiorate") + + # without a buffer here, gstreamer struggles at the start of the + # recording and then the A/V sync is bad for the whole video + # (possibly a gstreamer/ALSA bug -- even if it gets caught up, it + # should be able to resync without problem) + queue = gst.element_factory_make("queue", "audioqueue") + queue.set_property("leaky", True) # prefer fresh data + queue.set_property("max-size-time", 5000000000) # 5 seconds + queue.set_property("max-size-buffers", 500) + queue.connect("overrun", self._log_queue_overrun) + + enc = gst.element_factory_make("wavenc", "abenc") + + sink = gst.element_factory_make("filesink", "absink") + sink.set_property("location", self.capture_file) + + self._audiobin = gst.Bin("audiobin") + self._audiobin.add(src, rate, queue, enc, sink) + + src.link(rate, srccaps) + gst.element_link_many(rate, queue, enc, sink) + + def _log_queue_overrun(self, queue): + ''' We use a buffer, which may overflow. ''' + cbuffers = queue.get_property("current-level-buffers") + cbytes = queue.get_property("current-level-bytes") + ctime = queue.get_property("current-level-time") + + def is_recording(self): + ''' Are we recording? ''' + return self._recording + + def _get_state(self): + ''' What is the state of our gstreamer pipeline? ''' + return self._pipeline.get_state()[1] + + def start_recording_audio(self): + ''' Start the stream in order to start recording. ''' + if self._get_state() == gst.STATE_PLAYING: + return + self._pipeline.set_state(gst.STATE_PLAYING) + self._recording = True + + def stop_recording_audio(self): + ''' Stop recording and then convert the results into a + .ogg file using a new stream. ''' + self._pipeline.set_state(gst.STATE_NULL) + self._recording = False + + if not os.path.exists(self.capture_file) or \ + os.path.getsize(self.capture_file) <= 0: + return + + # Remove previous transcoding results. + if os.path.exists(self.save_file): + os.remove(self.save_file) + + line = 'filesrc location=' + self.capture_file + ' name=audioFilesrc ! wavparse name=audioWavparse ! audioconvert name=audioAudioconvert ! vorbisenc name=audioVorbisenc ! oggmux name=audioOggmux ! filesink name=audioFilesink' + audioline = gst.parse_launch(line) + + vorbis_enc = audioline.get_by_name('audioVorbisenc') + + audioFilesink = audioline.get_by_name('audioFilesink') + audioFilesink.set_property("location", self.save_file) + + audioBus = audioline.get_bus() + audioBus.add_signal_watch() + self._audio_transcode_handler = audioBus.connect( + 'message', self._onMuxedAudioMessageCb, audioline) + self._transcode_id = gobject.timeout_add( + 200, self._transcodeUpdateCb, audioline) + audioline.set_state(gst.STATE_PLAYING) + + def _transcodeUpdateCb(self, pipe): + ''' Where are we in the transcoding process? ''' + position, duration = self._query_position(pipe) + if position != gst.CLOCK_TIME_NONE: + value = position * 100.0 / duration + value = value/100.0 + return True + + def _query_position(self, pipe): + ''' Where are we in the stream? ''' + try: + position, format = pipe.query_position(gst.FORMAT_TIME) + except: + position = gst.CLOCK_TIME_NONE + + try: + duration, format = pipe.query_duration(gst.FORMAT_TIME) + except: + duration = gst.CLOCK_TIME_NONE + + return (position, duration) + + def _onMuxedAudioMessageCb(self, bus, message, pipe): + ''' Clean up at end of stream.''' + if message.type != gst.MESSAGE_EOS: + return True + + gobject.source_remove(self._audio_transcode_handler) + self._audio_transcode_handler = None + gobject.source_remove(self._transcode_id) + self._transcode_id = None + pipe.set_state(gst.STATE_NULL) + pipe.get_bus().remove_signal_watch() + pipe.get_bus().disable_sync_message_emission() + + os.remove(self.capture_file) + return False + + def _bus_message_handler(self, bus, message): + ''' Handle any messages associated with the stream. ''' + t = message.type + if t == gst.MESSAGE_EOS: + if self._eos_cb: + cb = self._eos_cb + self._eos_cb = None + cb() + elif t == gst.MESSAGE_ERROR: + # TODO: if we come out of suspend/resume with errors, then + # get us back up and running... TODO: handle "No space + # left on the resource.gstfilesink.c" err, debug = + # message.parse_error() + pass + + # We store the audio-record stream instance as tw.grecord so that + # we can use it repeatedly. + if not hasattr(tw, 'grecord'): + tw.grecord = Grecord(tw) + + # Sometime we need to parse multiple arguments, e.g., save, savename + save_name = _('Turtle Art') + ' ' + _('sound') + if type(arg) == type([]): + cmd = arg[0].lower() + if len(arg) > 1: + save_name = str(arg[1]) + else: + cmd = arg.lower() + + if cmd == 'start' or cmd == _('start').lower(): + tw.grecord.start_recording_audio() + elif cmd == 'stop' or cmd == _('stop').lower(): + tw.grecord.stop_recording_audio() + elif cmd == 'play' or cmd == _('play').lower(): + play_audio_from_file(tw.lc, tw.grecord.save_file) + elif cmd == 'save' or cmd == _('save').lower(): + if os.path.exists(tw.grecord.save_file) and tw.running_sugar: + dsobject = datastore.create() + dsobject.metadata['title'] = save_name + dsobject.metadata['icon-color'] = profile.get_color().to_string() + dsobject.metadata['mime_type'] = 'audio/ogg' + dsobject.set_file_path(tw.grecord.save_file) + datastore.write(dsobject) + dsobject.destroy() |