1466
|
1 """
|
|
2 play sounds according to POST requests.
|
|
3 """
|
|
4 from docopt import docopt
|
|
5 import cyclone.web
|
|
6 import sys, tempfile, itertools
|
|
7 from twisted.internet import reactor
|
|
8 from cyclone.httpclient import fetch
|
|
9 from generator import tts
|
|
10 import xml.etree.ElementTree as ET
|
|
11 from twisted.web.static import File
|
|
12 from standardservice.logsetup import log, verboseLogging
|
|
13 import os
|
|
14
|
|
15 soundCount = itertools.count()
|
|
16
|
|
17 def makeSpeech(speech, fast=False):
|
|
18 speechWav = tempfile.NamedTemporaryFile(suffix='.wav')
|
|
19
|
|
20 root = ET.Element("SABLE")
|
|
21 r = ET.SubElement(root, "RATE",
|
|
22 attrib=dict(SPEED="+50%" if fast else "+0%"))
|
|
23 for sentence in speech.split('.'):
|
|
24 div = ET.SubElement(r, "DIV")
|
|
25 div.set("TYPE", "sentence")
|
|
26 div.text = sentence
|
|
27
|
|
28 speechSecs = tts(speech, speechWav.name)
|
|
29 return pygame.mixer.Sound(speechWav.name), speechSecs
|
|
30
|
|
31 class LOADING(object): pass
|
|
32
|
|
33 class SoundEffects(object):
|
|
34 def __init__(self):
|
|
35 self.buffers = {} # URIRef : pygame.mixer.Sound
|
|
36 self.playingSources = []
|
|
37 self.queued = []
|
|
38 self.volume = 1 # level for the next sound that's played (or existing instances of the same sound)
|
|
39
|
|
40 def _getSound(self, uri):
|
|
41 def done(resp):
|
|
42 path = '/tmp/sound_%s' % next(soundCount)
|
|
43 with open(path, 'w') as out:
|
|
44 out.write(resp.body)
|
|
45 log.info('write %s bytes to %s', len(resp.body), path)
|
|
46 self.buffers[uri] = pygame.mixer.Sound(path)
|
|
47
|
|
48 return fetch(uri).addCallback(done).addErrback(log.error)
|
|
49
|
|
50 def playEffect(self, uri):
|
|
51 if uri not in self.buffers:
|
|
52 self.buffers[uri] = LOADING
|
|
53 self._getSound(uri).addCallback(lambda ret: self.playEffect(uri))
|
|
54 return
|
|
55 if self.buffers[uri] is LOADING:
|
|
56 # The first playback loads then plays, but any attempts
|
|
57 # during that load are dropped, not queued.
|
|
58 return
|
|
59 snd = self.buffers[uri]
|
|
60 snd.set_volume(self.volume)
|
|
61 return self.playBuffer(snd)
|
|
62
|
|
63 def playSpeech(self, txt, preEffect=None, postEffect=None, preEffectOverlap=0):
|
|
64 buf, secs = makeSpeech(txt)
|
|
65 t = 0
|
|
66 if preEffect:
|
|
67 t += self.playEffect(preEffect)
|
|
68
|
|
69 self.playingSources.append(buf)
|
|
70 reactor.callLater(secs + .1, self.done, buf)
|
|
71 return secs
|
|
72
|
|
73 def done(self, src):
|
|
74 try:
|
|
75 self.playingSources.remove(src)
|
|
76 except ValueError:
|
|
77 pass
|
|
78
|
|
79 def stopAll(self):
|
|
80 while self.playingSources:
|
|
81 self.playingSources.pop().stop()
|
|
82 for q in self.queued:
|
|
83 q.cancel()
|
|
84 # doesn't cover the callLater ones
|
|
85
|
|
86
|
|
87 class Index(cyclone.web.RequestHandler):
|
|
88 def get(self):
|
|
89 self.render('index.html', effectNames=[
|
|
90 dict(name=k, postUri='effects/%s' % k)
|
|
91 for k in self.settings.sfx.buffers.keys()])
|
|
92
|
|
93 class Speak(cyclone.web.RequestHandler):
|
|
94 def post(self):
|
|
95 self.settings.sfx.playSpeech(self.get_argument('msg'))
|
|
96 return "ok"
|
|
97
|
|
98 class PlaySound(cyclone.web.RequestHandler):
|
|
99 def post(self):
|
|
100 uri = self.get_argument('uri')
|
|
101 self.settings.sfx.playEffect(uri)
|
|
102 return "ok"
|
|
103
|
|
104 class Volume(cyclone.web.RequestHandler):
|
|
105 def put(self):
|
|
106 self.settings.sfx.setVolume(float(self.get_argument('v')))
|
|
107 return "ok"
|
|
108
|
|
109 class StopAll(cyclone.web.RequestHandler):
|
|
110 def post(self):
|
|
111 self.settings.sfx.stopAll()
|
|
112 return "ok"
|
|
113
|
|
114
|
|
115 if __name__ == '__main__':
|
|
116 arg = docopt('''
|
|
117 Usage: playSound.py [options]
|
|
118
|
|
119 -v Verbose
|
|
120 ''')
|
|
121 verboseLogging(arg['-v'])
|
|
122
|
|
123 import pygame
|
|
124 print('mixer init pulse')
|
|
125 import pygame.mixer
|
|
126 pygame.mixer.init()
|
|
127 sfx = SoundEffects()
|
|
128
|
|
129 reactor.listenTCP(9049, cyclone.web.Application(handlers=[
|
|
130 (r'/', Index),
|
|
131 (r'/speak', Speak),
|
|
132 (r'/playSound', PlaySound),
|
|
133 (r'/volume', Volume),
|
|
134 (r'/stopAll', StopAll),
|
|
135 (r'/static/(.*)', cyclone.web.StaticFileHandler, {'path': 'static'}),
|
|
136 ], template_path='.', sfx=sfx))
|
|
137 reactor.run()
|
|
138 server.app.run(endpoint_description=r"tcp6:port=9049:interface=\:\:")
|