# HG changeset patch # User drewp@bigasterisk.com # Date 1369812782 25200 # Node ID 1132ab1ade804fdc50bffe5f449d606c4d17af36 # Parent bca6d6c63bdcec939629c7131a015a5e9eb40f5f big rewrite of speechMusic to use klein and openal Ignore-this: 5150fae67462dea7e62424399bda86b3 diff -r bca6d6c63bdc -r 1132ab1ade80 service/speechMusic/index.jade --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/service/speechMusic/index.jade Wed May 29 00:33:02 2013 -0700 @@ -0,0 +1,60 @@ +doctype html +html + head + title + | speechMusic + style + input[type=text] { + font-size: 40px; + font-family: monospace; + font-variant: small-caps; + width: 100%; + } + button { + margin: 10px; + padding: 10px; + } + body + h1 Speech + h2 Speak + form(method='POST', action='speak') + input(type='text', name='msg', autocomplete="off") + button(type='submit') Say + + h2 Effects + ul + each effect in effectNames + li + form(method='POST', action="#{effect['postUri']}") + button(type='submit') + | #{effect['name']} + + h2 Stop all + form(method='POST', action='stopAll') + button(type='submit') Stop all sounds + + p#status + + + script(src="static/jquery-2.0.1.min.js") + script(type="text/javascript") + $("input[type=text]").focus(); + + $("form").submit(function() { + $("#status").text("submitting..."); + $.ajax({ + data: $(this).serialize(), + url: this.action, + type: this.method, + success: function (result) { + $("#status").text(result); + $(this).find("input").val(""); + }.bind(this), + error: function (xhr, status, err) { + $("#status").text("Error: "+err); + } + }); + $("input[type=text]").focus(); + return false; + }); + \ No newline at end of file diff -r bca6d6c63bdc -r 1132ab1ade80 service/speechMusic/speechMusic.py --- a/service/speechMusic/speechMusic.py Wed May 29 00:30:51 2013 -0700 +++ b/service/speechMusic/speechMusic.py Wed May 29 00:33:02 2013 -0700 @@ -1,30 +1,29 @@ #!bin/python - +""" +play sounds according to POST requests. """ -play sounds according to POST requests. cooperate with pubsubhubbub -""" -import web, sys, json, subprocess, os, tempfile, logging -from subprocess import check_call +from __future__ import division +import sys, tempfile, logging, pyjade +from pyjade.ext.mako import preprocessor as mako_preprocessor +from mako.template import Template +from mako.lookup import TemplateLookup +sys.path.append("python-openal") +import openal +from twisted.internet import reactor sys.path.append("/my/proj/csigen") from generator import tts import xml.etree.ElementTree as ET -logging.basicConfig(level=logging.INFO, format="%(created)f %(asctime)s %(levelname)s %(message)s") +from klein import Klein +from twisted.web.static import File +logging.basicConfig(level=logging.INFO, + format="%(created)f %(asctime)s %(levelname)s %(message)s") log = logging.getLogger() -sensorWords = {"wifi" : "why fi", - "bluetooth" : "bluetooth"} +templates = TemplateLookup(directories=['.'], + preprocessor=mako_preprocessor, + filesystem_checks=True) -def aplay(device, filename): - paDeviceName = { - 'garage' : 'alsa_output.pci-0000_01_07.0.analog-stereo', - 'living' : 'alsa_output.pci-0000_00_04.0.analog-stereo', - }[device] - subprocess.call(['paplay', - '-d', paDeviceName, - filename]) - -def soundOut(preSound=None, speech='', postSound=None, fast=False): - +def makeSpeech(speech, fast=False): speechWav = tempfile.NamedTemporaryFile(suffix='.wav') root = ET.Element("SABLE") @@ -35,92 +34,103 @@ div.set("TYPE", "sentence") div.text = sentence - sounds = [] - delays = [] + speechSecs = tts(root, speechWav.name) + return openal.Buffer(speechWav.name), speechSecs + +class SoundEffects(object): + def __init__(self): + # for names to pass to this, see alcGetString with ALC_ALL_DEVICES_SPECIFIER + device = openal.Device() + self.contextlistener = device.ContextListener() + + # also '/my/music/entrance/%s.wav' then speak "Neew %s. %s" % (sensorWords[data['sensor']], data['name']), + + print "loading" + self.buffers = { + 'leave': openal.Buffer('/my/music/entrance/leave.wav'), + 'highlight' : openal.Buffer('/my/music/snd/Oxygen/KDE-Im-Highlight-Msg-44100.wav'), + 'question' : openal.Buffer('/my/music/snd/angel_ogg/angel_question.wav'), + 'jazztrumpet': openal.Buffer('/my/music/snd/sampleswap/MELODIC SAMPLES and LOOPS/Acid Jazz Trumpet Lines/acid-jazz-trumpet-11.wav'), + 'beep1': openal.Buffer('/my/music/snd/bxfr/beep1.wav'), + 'beep2': openal.Buffer('/my/music/snd/bxfr/beep2.wav'), + } + print "loaded sounds" + self.playingSources = [] + self.queued = [] - if preSound is not None: - sounds.append(preSound) - delays.extend([0,0]) # assume stereo - - speechSecs = tts(root, speechWav.name) - sounds.append(speechWav.name) - delays.append(.4) - if postSound is not None: - sounds.append(postSound) - delays.extend([speechSecs + .4]*2) # assume stereo - - if len(sounds) == 1: - outName = sounds[0] - else: - outWav = tempfile.NamedTemporaryFile(suffix='.wav') - check_call(['/usr/bin/sox', '--norm', '--combine', 'merge', - ]+sounds+[ - outWav.name, - 'delay', ]+map(str, delays)+[ - 'channels', '1']) - outName = outWav.name + def playEffect(self, name): + return self.playBuffer(self.buffers[name]) - aplay('living', outName) + def playSpeech(self, txt, preEffect=None, postEffect=None, preEffectOverlap=0): + buf, secs = makeSpeech(txt) + t = 0 + if preEffect: + t += self.playEffect(preEffect) + t -= preEffectOverlap + + reactor.callLater(t, self.playBuffer, buf) + t += secs -class visitorNet(object): - def POST(self): - data = json.loads(web.data()) - if 'name' not in data: - data['name'] = 'unknown' + if postEffect: + self.playBufferLater(t, self.buffers[postEffect]) + + def playBufferLater(self, t, buf): + self.queued.append(reactor.callLater(t, self.playBuffer, buf)) - if data.get('action') == 'arrive': - - snd = ('/my/music/entrance/%s.wav' % - data['name'].replace(' ', '_').replace(':', '_')) - if not os.path.exists(snd): - snd = None + def playBuffer(self, buf): + src = self.contextlistener.get_source() + src.buffer = buf + src.play() - soundOut(preSound="/my/music/snd/angel_ogg/angel_question.wav", - # sic: - speech="Neew %s. %s" % (sensorWords[data['sensor']], - data['name']), - postSound=snd, fast=True) - return 'ok' + secs = buf.size / (buf.frequency * buf.channels * buf.bits / 8) + self.playingSources.append(src) + reactor.callLater(secs + .1, self.done, src) + return secs + + def done(self, src): + try: + self.playingSources.remove(src) + except ValueError: + pass - if data.get('action') == 'leave': - soundOut(preSound='/my/music/entrance/leave.wav', - speech="lost %s. %s" % (sensorWords[data['sensor']], - data['name']), - fast=True) - return 'ok' - - return "nothing to do" + def stopAll(self): + while self.playingSources: + self.playingSources.pop().stop() + for q in self.queued: + q.cancel() -class index(object): - def GET(self): - web.header('Content-type', 'text/html') - return ''' -
- -''' +class Server(object): + app = Klein() + def __init__(self, sfx): + self.sfx = sfx + + @app.route('/static/', branch=True) + def static(self, request): + return File("./static") -class speak(object): - def POST(self): - txt = web.input()['say'] - log.info("speak: %r", txt) - soundOut(preSound='/my/music/snd/Oxygen/KDE-Im-Highlight-Msg-44100.wav', - speech=txt) - return "sent" + @app.route('/', methods=['GET']) + def index(self, request): + t = templates.get_template("index.jade") + return t.render(effectNames=[ + dict(name=k, postUri='effects/%s' % k) + for k in self.sfx.buffers.keys()]) -class testSound(object): - def POST(self): - soundOut(preSound='/my/music/entrance/leave.wav') - return 'ok' + @app.route('/speak', methods=['POST']) + def speak(self, request): + self.sfx.playSpeech(request.args['msg'][0]) + return "ok" -urls = ( - r'/', 'index', - r'/speak', 'speak', - r'/testSound', 'testSound', - r'/visitorNet', 'visitorNet', - ) + @app.route('/effects/