Mercurial > code > home > repos > homeauto
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=\:\:") |