diff options
author | Walter Bender <walter.bender@gmail.com> | 2012-05-15 13:29:26 (GMT) |
---|---|---|
committer | Walter Bender <walter.bender@gmail.com> | 2012-05-15 13:29:26 (GMT) |
commit | 0a8b168a8a28f560df6a32a41dc353fd322b3152 (patch) | |
tree | f7302265f311f203871954e5f9deb752557f11b0 | |
parent | 744acc9aff955d1d0eb52a926a0096269b2e9159 (diff) |
add audio record/playback
-rw-r--r-- | StoryActivity.py | 65 | ||||
-rw-r--r-- | grecord.py | 214 | ||||
-rw-r--r-- | icons/media-recording.svg | 11 | ||||
-rw-r--r-- | utils.py | 10 |
4 files changed, 297 insertions, 3 deletions
diff --git a/StoryActivity.py b/StoryActivity.py index 929c76a..ba5c050 100644 --- a/StoryActivity.py +++ b/StoryActivity.py @@ -30,7 +30,8 @@ if _have_toolbox: from sugar.graphics.alert import NotifyAlert from toolbar_utils import button_factory, label_factory, separator_factory -from utils import json_load, json_dump +from utils import json_load, json_dump, play_audio_from_file +from grecord import Grecord import telepathy import dbus @@ -69,6 +70,9 @@ class StoryActivity(activity.Activity): else: self.colors = ['#A0FFA0', '#FF8080'] + self._recording = False + self._grecord = None + self._setup_toolbars(_have_toolbox) self._setup_dispatch_table() @@ -121,11 +125,21 @@ class StoryActivity(activity.Activity): 'view-refresh', self.toolbar, self._new_game_cb, tooltip=_('Load new images.')) + separator_factory(self.toolbar) + self.save_as_image = button_factory( 'image-saveoff', self.toolbar, self.do_save_as_image_cb, tooltip=_('Save as image')) - self.status = label_factory(self.toolbar, '') + separator_factory(self.toolbar) + + self._record_button = button_factory( + 'media-record', self.toolbar, + self._record_cb, tooltip=_('Start recording')) + + self._playback_button = button_factory( + 'media-playback-start-insensitive', self.toolbar, + self._playback_recording_cb, tooltip=_('Nothing to play')) if _have_toolbox: separator_factory(toolbox.toolbar, True, False) @@ -175,6 +189,53 @@ class StoryActivity(activity.Activity): os.remove(file_path) self._notify_successful_save(title=_('Save as image')) + def _record_cb(self, button=None): + ''' Start/stop audio recording ''' + if self._grecord is None: + _logger.debug('setting up grecord') + self._grecord = Grecord(self) + if self._recording: # Was recording, so stop (and save?) + _logger.debug('recording...True. Preparing to save.') + self._grecord.stop_recording_audio() + self._recording = False + self._record_button.set_icon('media-record') + self._record_button.set_tooltip(_('Start recording')) + self._playback_button.set_icon('media-playback-start') + self._playback_button.set_tooltip(_('Play recording')) + # Autosave if there was not already a recording + self._save_recording() + self._notify_successful_save(title=_('Save recording')) + else: # Wasn't recording, so start + _logger.debug('recording...False. Start recording.') + self._grecord.record_audio() + self._recording = True + self._record_button.set_icon('media-recording') + self._record_button.set_tooltip(_('Stop recording')) + + def _playback_recording_cb(self, button=None): + ''' Play back current recording ''' + _logger.debug('Playback current recording from output.ogg...') + play_audio_from_file(os.path.join(activity.get_activity_root(), + 'instance', 'output.ogg')) + return + + def _save_recording(self, button=None): + if os.path.exists(os.path.join(activity.get_activity_root(), + 'instance', 'output.ogg')): + _logger.debug('Saving recording to Journal...') + dsobject = datastore.create() + dsobject.metadata['title'] = _('audio note for %s') % \ + (self.metadata['title']) + dsobject.metadata['icon-color'] = profile.get_color().to_string() + dsobject.metadata['mime_type'] = 'audio/ogg' + dsobject.set_file_path(os.path.join(activity.get_activity_root(), + 'instance', 'output.ogg')) + datastore.write(dsobject) + dsobject.destroy() + else: + _logger.debug('Nothing to save...') + return + def _notify_successful_save(self, title='', msg=''): ''' Notify user when saves are completed ''' diff --git a/grecord.py b/grecord.py new file mode 100644 index 0000000..8f3002d --- /dev/null +++ b/grecord.py @@ -0,0 +1,214 @@ +#Copyright (c) 2008, Media Modifications Ltd. +#Copyright (c) 2011, Walter Bender + +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in +#all copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +#THE SOFTWARE. + +import os +import time + +import gtk +import gst + +import gobject +gobject.threads_init() + + +class Grecord: + + def __init__(self, parent): + self._activity = parent + self._eos_cb = None + + self._can_limit_framerate = False + self._playing = False + + self._audio_transcode_handler = None + self._transcode_id = None + + self._pipeline = gst.Pipeline("Record") + self._create_audiobin() + + bus = self._pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message', self._bus_message_handler) + + def _create_audiobin(self): + 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", + os.path.join(self._activity.get_activity_root(), + 'instance', 'output.wav')) + + 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): + cbuffers = queue.get_property("current-level-buffers") + cbytes = queue.get_property("current-level-bytes") + ctime = queue.get_property("current-level-time") + + def play(self): + if self._get_state() == gst.STATE_PLAYING: + return + + self._pipeline.set_state(gst.STATE_PLAYING) + self._playing = True + + def pause(self): + self._pipeline.set_state(gst.STATE_PAUSED) + self._playing = False + + def stop(self): + self._pipeline.set_state(gst.STATE_NULL) + self._playing = False + + def is_playing(self): + return self._playing + + def _get_state(self): + return self._pipeline.get_state()[1] + + def stop_recording_audio(self): + # We should be able to simply pause and remove the audiobin, but + # this seems to cause a gstreamer segfault. So we stop the whole + # pipeline while manipulating it. + # http://dev.laptop.org/ticket/10183 + self._pipeline.set_state(gst.STATE_NULL) + self._pipeline.remove(self._audiobin) + self.play() + + audio_path = os.path.join(self._activity.get_activity_root(), + 'instance', 'output.wav') + if not os.path.exists(audio_path) or os.path.getsize(audio_path) <= 0: + # FIXME: inform model of failure? + return + + line = 'filesrc location=' + audio_path + ' 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') + audioOggFilepath = os.path.join(self._activity.get_activity_root(), + 'instance', 'output.ogg') + audioFilesink.set_property("location", audioOggFilepath) + + 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 blockedCb(self, x, y, z): + pass + + def record_audio(self): + # we should be able to add the audiobin on the fly, but unfortunately + # this results in several seconds of silence being added at the start + # of the recording. So we stop the whole pipeline while adjusting it. + # SL#2040 + self._pipeline.set_state(gst.STATE_NULL) + self._pipeline.add(self._audiobin) + self.play() + + def _transcodeUpdateCb(self, pipe): + 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): + 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): + 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() + + wavFilepath = os.path.join(self._activity.get_activity_root(), + 'instance', 'output.wav') + oggFilepath = os.path.join(self._activity.get_activity_root(), + 'instance', 'output.ogg') + os.remove( wavFilepath ) + return False + + def _bus_message_handler(self, bus, message): + 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 + diff --git a/icons/media-recording.svg b/icons/media-recording.svg new file mode 100644 index 0000000..2f46799 --- /dev/null +++ b/icons/media-recording.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [ + <!ENTITY stroke_color "#010101"> + <!ENTITY fill_color "#FFFFFF"> +]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="media-record"> + <path d="M27.497,5C15.073,5,4.999,15.075,4.999,27.5c0,12.427,10.074,22.5,22.498,22.5 c12.428,0,22.502-10.073,22.502-22.5C49.999,15.075,39.925,5,27.497,5z M27.501,35.389c-4.361,0-7.89-3.534-7.89-7.889 c0-4.356,3.528-7.889,7.89-7.889c4.357,0,7.889,3.532,7.889,7.889C35.39,31.854,31.858,35.389,27.501,35.389z" display="inline" fill="&fill_color;"/> +<path + d="m -23.783783,9.6621618 a 4.5656371,4.7779922 0 1 1 -9.131275,0 4.5656371,4.7779922 0 1 1 9.131275,0 z" + transform="matrix(1.7522199,0,0,1.6743434,77.174419,11.322223)" + id="path3758" + style="fill:#ff0000;fill-opacity:1;stroke:none" /> +</g></svg>
\ No newline at end of file @@ -9,7 +9,7 @@ # along with this library; if not, write to the Free Software # Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA - +import subprocess from StringIO import StringIO try: OLD_SUGAR_SYSTEM = False @@ -51,3 +51,11 @@ def json_dump(data): _io = StringIO() jdump(data, _io) return _io.getvalue() + + +def play_audio_from_file(file_path): + """ Audio media """ + command_line = ['gst-launch', 'filesrc', 'location=' + file_path, + '! oggdemux', '! vorbisdec', '! audioconvert', + '! alsasink'] + subprocess.call(command_line) |