comparison service/playSound/speechMusic.py @ 1466:bf05f33c3b3a

start rewrite for playSound Ignore-this: a4d4ccceea8ef0ba996ca2d81bf634c6 darcs-hash:76e853b388c95a2e4ba83637170effe872e997a8
author drewp <drewp@bigasterisk.com>
date Thu, 19 Dec 2019 16:45:40 -0800
parents
children
comparison
equal deleted inserted replaced
1465:9d22aa924e8a 1466:bf05f33c3b3a
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=\:\:")