Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWalter 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)
commit0a8b168a8a28f560df6a32a41dc353fd322b3152 (patch)
treef7302265f311f203871954e5f9deb752557f11b0
parent744acc9aff955d1d0eb52a926a0096269b2e9159 (diff)
add audio record/playback
-rw-r--r--StoryActivity.py65
-rw-r--r--grecord.py214
-rw-r--r--icons/media-recording.svg11
-rw-r--r--utils.py10
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
diff --git a/utils.py b/utils.py
index ffab831..230ab45 100644
--- a/utils.py
+++ b/utils.py
@@ -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)