changeset 664:28cc07978a71

start rewrite for playSound Ignore-this: a4d4ccceea8ef0ba996ca2d81bf634c6
author drewp@bigasterisk.com
date Thu, 19 Dec 2019 16:45:40 -0800
parents 25b418e2659e
children 9f9cb1d85c08
files service/playSound/Dockerfile service/playSound/Dockerfile.pi service/playSound/pulse-client.conf service/playSound/readme service/playSound/requirements.txt service/playSound/serv.n3 service/playSound/speechMusic.py service/playSound/tasks.py service/speechMusic/Dockerfile service/speechMusic/Dockerfile.pi service/speechMusic/index.jade service/speechMusic/makefile service/speechMusic/pulse-client.conf service/speechMusic/readme service/speechMusic/requirements.txt service/speechMusic/speechMusic.py
diffstat 16 files changed, 261 insertions(+), 338 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/playSound/Dockerfile	Thu Dec 19 16:45:40 2019 -0800
@@ -0,0 +1,28 @@
+FROM bang6:5000/base_x86
+
+WORKDIR /opt
+
+
+        apt install libsdl-mixer-dev
+        apt install libsdl-mixer1.2-dev
+wget https://www.pygame.org/ftp/pygame-1.9.6.tar.gz
+   34  tar xvzf pygame-1.9.6.tar.gz
+        35  cd pygame-1.9.6
+        python3 setup.py  install
+python3 -c 'import pygame; print(pygame.__version__, pygame.__file__); pygame.mixer.init()'
+
+
+RUN touch need-new-update
+RUN apt-get update
+RUN apt-get install --yes libopenal1 libogg0 pulseaudio-utils festival sox python-pygame
+COPY requirements.txt ./
+RUN pip3 install --index-url https://projects.bigasterisk.com/ --extra-index-url https://pypi.org/simple -r requirements.txt
+
+ENV SDL_AUDIODRIVER pulse
+ENV PULSE_SERVER /tmp/pulseaudio
+COPY pulse-client.conf /etc/pulse/client.conf
+COPY *.py req* *.jade ./
+
+EXPOSE 9049
+
+CMD [ "python3", "./playSound.py" ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/playSound/Dockerfile.pi	Thu Dec 19 16:45:40 2019 -0800
@@ -0,0 +1,17 @@
+FROM bang6:5000/base_pi
+
+WORKDIR /opt
+
+RUN apt-get update
+RUN apt-get install --yes libopenal1 libogg0 pulseaudio-utils python-pygame festival sox
+COPY requirements.txt ./
+RUN pip install -r requirements.txt
+
+COPY *.py req* *.jade ./
+COPY pulse-client.conf /etc/pulse/client.conf
+
+ENV PULSE_SERVER /tmp/pulseaudio
+EXPOSE 9049
+RUN mkdir /sounds
+
+CMD [ "python", "./playSound.py" ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/playSound/pulse-client.conf	Thu Dec 19 16:45:40 2019 -0800
@@ -0,0 +1,8 @@
+default-server = /tmp/pulseaudio
+
+# Prevent a server running in the container
+autospawn = no
+daemon-binary = /bin/true
+
+# Prevent the use of shared memory
+enable-shm = false
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/playSound/readme	Thu Dec 19 16:45:40 2019 -0800
@@ -0,0 +1,6 @@
+replacement for playSound; assume we do TTS somewhere else and then have
+this service (and also chromecasts, etc) play it.
+
+It's still a goal for this service to do low-latency stuff like
+feedback beeps for button presses, so it should cache and stay ready
+to play.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/playSound/requirements.txt	Thu Dec 19 16:45:40 2019 -0800
@@ -0,0 +1,10 @@
+cyclone
+mock==1.0.1
+Twisted
+
+# upgrading to 1.9.5 makes it stop working with pulseaudio
+# (maybe this unversioned one picks the system package which
+# has a better SDL build)
+pygame
+
+standardservice==0.6.0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/playSound/serv.n3	Thu Dec 19 16:45:40 2019 -0800
@@ -0,0 +1,24 @@
+@prefix : <http://bigasterisk.com/ns/serv#> .
+@prefix auth: <http://bigasterisk.com/ns/serv/auth#> .
+@prefix serv: <http://bigasterisk.com/services/> .
+
+serv:speechmusic_dash a :Service;
+      :path "/speech_music/dash/";
+      :openid auth:admin;
+      :serverHost "bang";
+      :internalPort 9049;
+      :prodDockerFlags (
+      "-p" "9049:9049"
+      "--net=host");
+      :localDockerFlags (
+      "--cap-add" "SYS_PTRACE"
+      #"--mount type=bind,source=/etc/pulse,target=/etc/pulse"
+      "--mount" "type=tmpfs,destination=/tmp,tmpfs-size=52428800"
+      "--mount" "type=bind,source=/tmp/pulseaudio,target=/tmp/pulseaudio"
+      );
+      :localRunCmdline (
+      "strace" "-ftts" "999"
+      "python3" "playSound.py" "-v");
+      :dockerFile "Dockerfile"
+.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/playSound/speechMusic.py	Thu Dec 19 16:45:40 2019 -0800
@@ -0,0 +1,138 @@
+"""
+play sounds according to POST requests.
+"""
+from docopt import docopt
+import cyclone.web
+import sys, tempfile, itertools
+from twisted.internet import reactor
+from cyclone.httpclient import fetch
+from generator import tts
+import xml.etree.ElementTree as ET
+from twisted.web.static import File
+from standardservice.logsetup import log, verboseLogging
+import os
+
+soundCount = itertools.count()
+
+def makeSpeech(speech, fast=False):
+    speechWav = tempfile.NamedTemporaryFile(suffix='.wav')
+
+    root = ET.Element("SABLE")
+    r = ET.SubElement(root, "RATE",
+                      attrib=dict(SPEED="+50%" if fast else "+0%"))
+    for sentence in speech.split('.'):
+        div = ET.SubElement(r, "DIV")
+        div.set("TYPE", "sentence")
+        div.text = sentence
+
+    speechSecs = tts(speech, speechWav.name)
+    return pygame.mixer.Sound(speechWav.name), speechSecs
+
+class LOADING(object): pass
+
+class SoundEffects(object):
+    def __init__(self):
+        self.buffers = {} # URIRef : pygame.mixer.Sound
+        self.playingSources = []
+        self.queued = []
+        self.volume = 1 # level for the next sound that's played (or existing instances of the same sound)
+
+    def _getSound(self, uri):
+        def done(resp):
+            path = '/tmp/sound_%s' % next(soundCount)
+            with open(path, 'w') as out:
+                out.write(resp.body)
+            log.info('write %s bytes to %s', len(resp.body), path)
+            self.buffers[uri] = pygame.mixer.Sound(path)
+
+        return fetch(uri).addCallback(done).addErrback(log.error)
+
+    def playEffect(self, uri):
+        if uri not in self.buffers:
+            self.buffers[uri] = LOADING
+            self._getSound(uri).addCallback(lambda ret: self.playEffect(uri))
+            return
+        if self.buffers[uri] is LOADING:
+            # The first playback loads then plays, but any attempts
+            # during that load are dropped, not queued.
+            return
+        snd = self.buffers[uri]
+        snd.set_volume(self.volume)
+        return self.playBuffer(snd)
+
+    def playSpeech(self, txt, preEffect=None, postEffect=None, preEffectOverlap=0):
+        buf, secs = makeSpeech(txt)
+        t = 0
+        if preEffect:
+            t += self.playEffect(preEffect)
+
+        self.playingSources.append(buf)
+        reactor.callLater(secs + .1, self.done, buf)
+        return secs
+
+    def done(self, src):
+        try:
+            self.playingSources.remove(src)
+        except ValueError:
+            pass
+
+    def stopAll(self):
+        while self.playingSources:
+            self.playingSources.pop().stop()
+        for q in self.queued:
+            q.cancel()
+        # doesn't cover the callLater ones
+
+
+class Index(cyclone.web.RequestHandler):
+    def get(self):
+        self.render('index.html', effectNames=[
+            dict(name=k, postUri='effects/%s' % k)
+            for k in self.settings.sfx.buffers.keys()])
+
+class Speak(cyclone.web.RequestHandler):
+    def post(self):
+        self.settings.sfx.playSpeech(self.get_argument('msg'))
+        return "ok"
+
+class PlaySound(cyclone.web.RequestHandler):
+    def post(self):
+        uri = self.get_argument('uri')
+        self.settings.sfx.playEffect(uri)
+        return "ok"
+
+class Volume(cyclone.web.RequestHandler):
+    def put(self):
+        self.settings.sfx.setVolume(float(self.get_argument('v')))
+        return "ok"
+
+class StopAll(cyclone.web.RequestHandler):
+    def post(self):
+        self.settings.sfx.stopAll()
+        return "ok"
+
+
+if __name__ == '__main__':
+    arg = docopt('''
+    Usage: playSound.py [options]
+
+    -v                Verbose
+    ''')
+    verboseLogging(arg['-v'])
+
+    import pygame
+    print('mixer init pulse')
+    import pygame.mixer
+    pygame.mixer.init()
+    sfx = SoundEffects()
+
+    reactor.listenTCP(9049, cyclone.web.Application(handlers=[
+        (r'/', Index),
+        (r'/speak', Speak),
+        (r'/playSound', PlaySound),
+        (r'/volume', Volume),
+        (r'/stopAll', StopAll),
+        (r'/static/(.*)', cyclone.web.StaticFileHandler, {'path': 'static'}),
+    ], template_path='.', sfx=sfx))
+    reactor.run()
+server.app.run(endpoint_description=r"tcp6:port=9049:interface=\:\:")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/playSound/tasks.py	Thu Dec 19 16:45:40 2019 -0800
@@ -0,0 +1,30 @@
+from invoke import task, Collection
+
+import sys
+sys.path.append('/my/proj/release')
+from serv_tasks import serv_tasks
+
+ns = Collection()
+serv_tasks(ns, 'serv.n3', 'speechmusic_dash')
+
+
+'''
+pactl_test: build_image
+	docker run --rm -it --cap-add SYS_PTRACE --net=host -v /tmp/pulseaudio:/tmp/pulseaudio ${TAG} pactl stat
+
+paplay_test_that_is_loud: build_image
+	docker run --rm -it --cap-add SYS_PTRACE --net=host -v /tmp/pulseaudio:/tmp/pulseaudio ${TAG} paplay /usr/local/lib/python2.7/dist-packages/pygame/examples/data/whiff.wav
+
+pygame_test: build_image
+	docker run --rm -it --cap-add SYS_PTRACE --net=host -e SDL_AUDIOSERVER=pulseaudio -v /tmp/pulseaudio:/tmp/pulseaudio ${TAG} python -c  'import os; print os.environ; import pygame.mixer; pygame.mixer.init()'
+
+
+local_run: build_image
+	docker run --rm -it -p ${PORT}:${PORT} \
+          --name=$(JOB)_local \
+          --net=host \
+	  --mount type=tmpfs,destination=/tmp,tmpfs-size=52428800 \
+          -v /tmp/pulseaudio:/tmp/pulseaudio \
+          ${TAG} \
+          python playSound.py -v
+'''
--- a/service/speechMusic/Dockerfile	Wed Dec 18 17:23:34 2019 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-FROM bang6:5000/base_x86
-
-WORKDIR /opt
-
-RUN apt-get update
-RUN apt-get install --yes libopenal1 libogg0 pulseaudio-utils python-pygame festival sox
-COPY requirements.txt ./
-RUN pip install -r requirements.txt
-
-ENV SDL_AUDIODRIVER pulse
-ENV PULSE_SERVER /tmp/pulseaudio
-COPY pulse-client.conf /etc/pulse/client.conf
-COPY *.py req* *.jade ./
-
-EXPOSE 9049
-
-CMD [ "python", "./speechMusic.py" ]
--- a/service/speechMusic/Dockerfile.pi	Wed Dec 18 17:23:34 2019 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-FROM bang6:5000/base_pi
-
-WORKDIR /opt
-
-RUN apt-get update
-RUN apt-get install --yes libopenal1 libogg0 pulseaudio-utils python-pygame festival sox
-COPY requirements.txt ./
-RUN pip install -r requirements.txt
-
-COPY sound sound
-COPY *.py req* *.jade ./
-COPY pulse-client.conf /etc/pulse/client.conf
-
-ENV PULSE_SERVER /tmp/pulseaudio
-EXPOSE 9049
-RUN mkdir /sounds
-
-CMD [ "python", "./speechMusic.py" ]
--- a/service/speechMusic/index.jade	Wed Dec 18 17:23:34 2019 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-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="//bigasterisk.com/lib/jquery-2.0.3.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
--- a/service/speechMusic/makefile	Wed Dec 18 17:23:34 2019 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-JOB=speech_music
-PORT=9049
-
-TAG=bang6:5000/${JOB}_x86:latest
-TAG_PI=bang6:5000/${JOB}_pi:latest
-
-prep_ctx:
-	rm -rf tmp_ctx
-	mkdir -p tmp_ctx
-	cp -a Dockerfile* ../../lib/*.py *.py *.jade req* *.conf /my/proj/csigen/generator.py tmp_ctx
-
-build_image: prep_ctx
-	docker build --network=host -t ${TAG} tmp_ctx
-
-push_image: build_image
-	docker push ${TAG}
-
-
-build_image_pi: prep_ctx
-	docker build --network=host --file=tmp_ctx/Dockerfile.pi -t ${TAG_PI} tmp_ctx
-
-push_image_pi: build_image_pi
-	docker push ${TAG_PI}
-
-shell: build_image
-	docker run --rm -it \
-	  --cap-add SYS_PTRACE \
-	  --name=$(JOB)_shell \
-	  --mount type=tmpfs,destination=/tmp,tmpfs-size=52428800 \
-	   --net=host \
-	  -v /tmp/pulseaudio:/tmp/pulseaudio \
-	  ${TAG} \
-	  /bin/bash
-
-pactl_test: build_image
-	docker run --rm -it --cap-add SYS_PTRACE --net=host -v /tmp/pulseaudio:/tmp/pulseaudio ${TAG} pactl stat
-
-paplay_test_that_is_loud: build_image
-	docker run --rm -it --cap-add SYS_PTRACE --net=host -v /tmp/pulseaudio:/tmp/pulseaudio ${TAG} paplay /usr/local/lib/python2.7/dist-packages/pygame/examples/data/whiff.wav
-
-pygame_test: build_image
-	docker run --rm -it --cap-add SYS_PTRACE --net=host -e SDL_AUDIOSERVER=pulseaudio -v /tmp/pulseaudio:/tmp/pulseaudio ${TAG} python -c  'import os; print os.environ; import pygame.mixer; pygame.mixer.init()'
-
-
-local_run: build_image
-	docker run --rm -it -p ${PORT}:${PORT} \
-          --name=$(JOB)_local \
-          --net=host \
-	  --mount type=tmpfs,destination=/tmp,tmpfs-size=52428800 \
-          -v /tmp/pulseaudio:/tmp/pulseaudio \
-          ${TAG} \
-          python speechMusic.py -v
-
-local_run_strace: build_image
-	docker run --rm -it -p ${PORT}:${PORT} \
-          -v `pwd`:/mnt \
-          --name=$(JOB)_local \
-          --net=host \
-	  -v /tmp/pulseaudio:/tmp/pulseaudio \
-	  --mount type=tmpfs,destination=/tmp,tmpfs-size=52428800 \
-          --cap-add SYS_PTRACE \
-          ${TAG} \
-          strace -f -tts 200 python /mnt/speechMusic.py -v
-
-fresh_sudo:
-	sudo -v
-
-redeploy: fresh_sudo push_image push_image_pi
-	sudo /my/proj/ansible/playbook -l with_speakers -t $(JOB)
-	supervisorctl -s http://dash:9001/ restart $(JOB)_$(PORT)
-	supervisorctl -s http://slash:9001/ restart $(JOB)_$(PORT)
-	supervisorctl -s http://bed:9001/ restart $(JOB)_$(PORT)
-	supervisorctl -s http://living:9001/ restart $(JOB)_$(PORT)
-	supervisorctl -s http://kitchen:9001/ restart $(JOB)_$(PORT)
-
--- a/service/speechMusic/pulse-client.conf	Wed Dec 18 17:23:34 2019 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-default-server = /tmp/pulseaudio
-
-# Prevent a server running in the container
-autospawn = no
-daemon-binary = /bin/true
-
-# Prevent the use of shared memory
-enable-shm = false
--- a/service/speechMusic/readme	Wed Dec 18 17:23:34 2019 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-this is meant to be run in multiple rooms with various output device configs
--- a/service/speechMusic/requirements.txt	Wed Dec 18 17:23:34 2019 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-Jinja2==2.7
-klein==17.10.0
-Mako==0.8.1
-MarkupSafe==0.18
-mock==1.0.1
-pyjade==2.0.2
-Twisted
-
-# upgrading to 1.9.5 makes it stop working with pulseaudio
-# (maybe this unversioned one picks the system package which
-# has a better SDL build)
-pygame
--- a/service/speechMusic/speechMusic.py	Wed Dec 18 17:23:34 2019 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,148 +0,0 @@
-#!bin/python
-"""
-play sounds according to POST requests.
-"""
-from __future__ import division
-import sys, tempfile, itertools
-from pyjade.ext.mako import preprocessor as mako_preprocessor
-from mako.lookup import TemplateLookup
-from twisted.internet import reactor
-from cyclone.httpclient import fetch
-from generator import tts
-import xml.etree.ElementTree as ET
-from klein import Klein
-from twisted.web.static import File
-from logsetup import log
-import pygame.mixer
-class URIRef(str): pass
-
-soundCount = itertools.count()
-templates = TemplateLookup(directories=['.'],
-                           preprocessor=mako_preprocessor,
-                           filesystem_checks=True)
-
-def makeSpeech(speech, fast=False):
-    speechWav = tempfile.NamedTemporaryFile(suffix='.wav')
-
-    root = ET.Element("SABLE")
-    r = ET.SubElement(root, "RATE",
-                      attrib=dict(SPEED="+50%" if fast else "+0%"))
-    for sentence in speech.split('.'):
-        div = ET.SubElement(r, "DIV")
-        div.set("TYPE", "sentence")
-        div.text = sentence
-
-    speechSecs = tts(speech, speechWav.name)
-    return pygame.mixer.Sound(speechWav.name), speechSecs
-
-class LOADING(object): pass
-
-class SoundEffects(object):
-    def __init__(self):
-        self.buffers = {} # URIRef : pygame.mixer.Sound
-        self.playingSources = []
-        self.queued = []
-        self.volume = 1 # level for the next sound that's played (or existing instances of the same sound)
-
-    def _getSound(self, uri):
-        def done(resp):
-            path = '/tmp/sound_%s' % next(soundCount)
-            with open(path, 'w') as out:
-                out.write(resp.body)
-            log.info('write %s bytes to %s', len(resp.body), path)
-            self.buffers[uri] = pygame.mixer.Sound(path)
-            
-        return fetch(uri).addCallback(done).addErrback(log.error)
-
-    def playEffect(self, uri):
-        if uri not in self.buffers:
-            self.buffers[uri] = LOADING
-            self._getSound(uri).addCallback(lambda ret: self.playEffect(uri))
-            return
-        if self.buffers[uri] is LOADING:
-            # The first playback loads then plays, but any attempts
-            # during that load are dropped, not queued.
-            return
-        snd = self.buffers[uri]
-        snd.set_volume(self.volume)
-        return self.playBuffer(snd)
-
-    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
-
-        if postEffect:
-            self.playBufferLater(t, self.buffers[postEffect])
-
-    def playBufferLater(self, t, buf):
-        self.queued.append(reactor.callLater(t, self.playBuffer, buf))
-            
-    def playBuffer(self, buf):
-        buf.play()
-
-        secs = buf.get_length()
-        self.playingSources.append(buf)
-        reactor.callLater(secs + .1, self.done, buf)
-        return secs
-
-    def done(self, src):
-        try:
-            self.playingSources.remove(src)
-        except ValueError:
-            pass
-
-    def stopAll(self):
-        while self.playingSources:
-            self.playingSources.pop().stop()
-        for q in self.queued:
-            q.cancel()
-        # doesn't cover the callLater ones
-
-class Server(object):
-    app = Klein()
-    def __init__(self, sfx):
-        self.sfx = sfx
-
-    @app.route('/static/', branch=True)
-    def static(self, request):
-        return File("./static")
-
-    @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()])
-
-    @app.route('/speak', methods=['POST'])
-    def speak(self, request):
-        self.sfx.playSpeech(request.args['msg'][0])
-        return "ok"
-
-    @app.route('/playSound', methods=['POST'])
-    def effect(self, request):
-        uri = request.args['uri'][0]
-        self.sfx.playEffect(uri)
-        return "ok"
-
-    @app.route('/volume', methods=['PUT'])
-    def volume(self, request, name):
-        self.sfx.setVolume(float(request.args['v'][0]))
-        return "ok"
-
-    @app.route('/stopAll', methods=['POST'])
-    def stopAll(self, request):
-        self.sfx.stopAll()
-        return "ok"
-
-pygame.mixer.init()
-sfx = SoundEffects()
-
-server = Server(sfx)
-server.app.run(endpoint_description=r"tcp6:port=9049:interface=\:\:")