forked from Robert904/mumblerecbot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmumblerecbot.py
386 lines (321 loc) · 18.4 KB
/
mumblerecbot.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# -*- coding: utf-8 -*-
import time
from threading import Thread
from constants import *
import pymumble
from pymumble.constants import *
import webvtt
class MumbleRecBot:
"""
stay connected on a mumble server. When USER_COUNT are connected, start to record. Stop when the
number of users goes below.
Create webvtt files with chapters, timestamps and speakers captions
understand a number of commands sent through the mumble chat system
"""
def __init__(self):
from os import getpid
pid = str(getpid())
file(PIDFILE,'w').write("%s\n" % pid) # store the process id
self.simplelog = open("/tmp/mumble.simplelog", "a")
self.recording = False # recording ongoing"
self.audio_file = None # output audio_file
self.chapters = None # chapters webvtt object
self.current_chapter = None # insige a chapter (after a /gamestart, before a /gamestop)
self.last_chapter_time = 0
self.captions = None # webvtt object for speakers captions
self.cursor_time = None # time for which the audio is treated
self.force_start = False
self.force_stop = False
self.force_newfile = False
self.exit = False
self.users = dict() # store some local informations about the users session
# Create the mumble instance and assign callbals
self.mumble = pymumble.Mumble(HOST, PORT, USER, PASSWORD, debug=DEBUG)
self.mumble.callbacks.set_callback(PYMUMBLE_CLBK_USERCREATED, self.user_created)
self.mumble.callbacks.set_callback(PYMUMBLE_CLBK_USERUPDATED, self.user_modified)
self.mumble.callbacks.set_callback(PYMUMBLE_CLBK_USERREMOVED, self.user_removed)
self.mumble.callbacks.set_callback(PYMUMBLE_CLBK_TEXTMESSAGERECEIVEDFULL, self.message_received)
self.mumble.start() # start the mumble thread
self.mumble.is_ready() # wait for the end of the connection process
self.home_channel = self.mumble.channels.find_by_name(CHANNEL)
self.home_channel.move_in() # move to the configured channel
self.mumble.users.myself.mute() # mute the user (just to make clear he don't speak)
self.loop()
def user_created(self, user):
"""A user is connected on the server. Create the specific structure with the local informations"""
if user["session"] not in self.users:
self.users[user["session"]] = dict()
self.set_user_stereo(user["session"])
if "name" in user and user["name"] != USER:
if self.captions is not None:
self.captions.add_cue("<c.system>la'oi {user} co'a jorne".format(user=user["name"]), duration=2)
if self.chapters is not None:
self.chapters.add_cue("<c.system>la'oi {user} co'a jorne".format(user=user["name"]), region="timestamp", duration=0)
if "name" in user:
self.users[user["session"]]["name"] = user["name"]
self.simplelog.write("+ " + user["name"] + "\n");
self.simplelog.flush();
if "channel_id" in user:
self.users[user["session"]]["channel_id"] = user["channel_id"]
self.set_user_stereo(user["session"], user["channel_id"])
self.test_for_users()
def set_user_stereo(self, session, channel=None):
if channel is not None and self.mumble.channels[channel]["name"] in STEREO_CHANNELS:
self.users[session]["stereo"] = STEREO_CHANNELS[self.mumble.channels[channel]["name"]]["stereo"]
self.users[session]["region"] = STEREO_CHANNELS[self.mumble.channels[channel]["name"]]["region"]
else:
self.users[session]["stereo"] = (1, 1)
self.users[session]["region"] = None
def user_modified(self, user, actions):
"""A modification was sent by the server about a connected user"""
if "channel_id" in actions:
if "channel_id" not in self.users[user["session"]] or self.users[user["session"]]["channel_id"] != user["channel_id"]:
self.set_user_stereo(user["session"], user["channel_id"])
if self.captions is not None and "name" in self.users[user["session"]]:
self.captions.add_cue("<c.system>la'oi {user} klama la'oi {channel} noi kumfa".format(user=self.users[user["session"]]["name"], channel=self.mumble.channels[user["channel_id"]]["name"]), duration=2)
self.users[user["session"]]["channel_id"] = user["channel_id"]
if "self_mute" in actions:
if "self_mute" not in self.users[user["session"]] or self.users[user["session"]]["self_mute"] != user["self_mute"]:
if self.captions is not None and "name" in self.users[user["session"]]:
if user["self_mute"]:
self.captions.add_cue("<c.system>la'oi {user} co'u cradi".format(user=self.users[user["session"]]["name"]), duration=2)
else:
self.captions.add_cue("<c.system>la'oi {user} co'a cradi".format(user=self.users[user["session"]]["name"]), duration=2)
self.users[user["session"]]["self_mute"] = user["self_mute"]
if "self_deaf" in actions:
if "self_deaf" not in self.users[user["session"]] or self.users[user["session"]]["self_deaf"] != user["self_deaf"]:
if self.captions is not None and "name" in self.users[user["session"]]:
if user["self_deaf"]:
self.captions.add_cue("<c.system>la'oi {user} co'u tinju'i".format(user=self.users[user["session"]]["name"]), duration=2)
else:
self.captions.add_cue("<c.system>la'oi {user} co'a tinju'i".format(user=self.users[user["session"]]["name"]), duration=2)
self.users[user["session"]]["self_deaf"] = user["self_deaf"]
self.test_for_users()
def user_removed(self, user, *args):
"""a user has disconnected"""
if "name" in self.users[user["session"]]:
if self.captions is not None:
self.captions.add_cue("<c.system>la'oi {user} co'u jorne".format(user=self.users[user["session"]]["name"]), duration=2)
if self.chapters is not None:
self.chapters.add_cue("<c.system>la'oi {user} co'u jorne".format(user=user["name"]), region="timestamp", duration=0)
self.simplelog.write("- " + user["name"] + "\n");
self.simplelog.flush();
del self.users[user["session"]]
self.test_for_users()
def test_for_users(self):
"""check the number of connected users to start/stop the recording"""
try:
attending = [user for user in self.mumble.users.values() if (not "self_mute" in user or not user["self_mute"]) and (not "self_deaf" in user or not user["self_deaf"]) and ("channel_id" in user and user["channel_id"] == self.home_channel["channel_id"])]
except AttributeError:
return
if len(attending) > USER_COUNT:
self.recording = True
else:
self.recording = False
def message_received(self, message_obj):
"""receive a text message from the server"""
from re import match
global USER_COUNT
user_count = USER_COUNT + 1
message = message_obj.message
#TODO: check if message is sent only to me
if message in ("/start", "vreji: ko ru'i cupra"): # force the recording
self.force_start = True
self.force_stop = False
self.mumble.users.myself.comment("ca'o se bapli zbasu" + COMMENT_SUFFIX)
elif message in ("/stop", "vreji: ko na jundi"): # prevent the recording
self.force_start = False
self.force_stop = True
self.mumble.users.myself.comment("ca'o se bapli zbasu be na ku" + COMMENT_SUFFIX)
elif message in ("/auto", "vreji: ko zmiku"): # go in auto mode
self.force_start = False
self.force_stop = False
self.mumble.users.myself.comment(autotext % user_count + COMMENT_SUFFIX)
elif match("^/auto=\d+$", message): # go in auto mode and change the connected users threshold
USER_COUNT=int(message.split("=")[1])
user_count = USER_COUNT + 1
self.force_start = False
self.force_stop = False
self.mumble.users.myself.comment(autotext % user_count + COMMENT_SUFFIX)
elif message in ("/newfile", "vreji: ko katna"): # stop the current audio file and start a new one
self.force_newfile = True
self.mumble.users.myself.comment(autotext % user_count + COMMENT_SUFFIX)
elif message in ("/exit", "vreji: ko morsi"): # Stop the application
self.exit = True
elif message == "/gamestart": # signal a game start to be recorded in the chapter webvtt file
if self.chapters is not None and time.time()-self.last_chapter_time > CHAPTER_MIN_INTERVAL:
self.last_chapter_time = time.time()
if self.current_chapter is not None:
self.current_chapter.end()
usernames = list()
for user in self.mumble.users.values():
if user["name"] != USER:
usernames.append(user["name"])
title = "{time} ({users})".format(time=time.ctime(), users=",".join(usernames))
self.current_chapter = self.chapters.add_cue(title)
elif message == "/gamestop": # signal a game stop to be recorded in the chapter webvtt file
self.last_chapter_time = 0
if self.chapters is not None:
if self.current_chapter is not None:
self.current_chapter.end()
elif match("^/timestamp=.*$", message): # create a timestamp in the chapters webvtt file
text = message.split("=", 1)[1]
text = text[:50]
if self.chapters is not None:
self.chapters.add_cue(text, region="timestamp", duration=0)
else:
if self.captions is not None:
self.captions.add_cue(u"<v {user}>sei la'oi {user} ciska se'u {message}".format(user=self.users[message_obj.actor]["name"], message=message), duration=8)
def loop(self):
"""Master loop"""
import os.path
import audioop
silent = "\x00" * STEREO_CHUNK_SIZE
self.mumble.users.myself.comment(autotext % (USER_COUNT + 1) + COMMENT_SUFFIX)
self.mumble.users.myself.texture(self.load_bitmap(STOP_BITMAP))
while self.mumble.is_alive() and not self.exit:
if ( ( self.recording and not self.force_stop ) or self.force_start ) and not self.force_newfile:
if not self.audio_file:
# Start recording
self.mumble.set_receive_sound(True) # ask the pymumble library to handle incoming audio
self.mumble.users.myself.recording() # signal the others I'm recording (to be fair)
self.mumble.users.myself.texture(self.load_bitmap(START_BITMAP)) # Change the recorder avatar
self.cursor_time = time.time() - BUFFER # time of the start of the recording
#create the files
audio_file_name = os.path.join(SAVEDIR, "mumble-%s" % time.strftime("%Y%m%d-%H%M%S"))
self.audio_file = AudioFile(audio_file_name)
if CREATE_WEBVTT:
if CREATE_CHAPTERS:
self.chapters = webvtt.WebVtt(audio_file_name + "-chapters.vtt")
self.captions = webvtt.WebVtt(
audio_file_name + "-captions.vtt",
regions=[
"Region: id=left width=50% regionanchor=0%,100% viewportanchor=0%,100%",
"Region: id=right width=50% regionanchor=100%,100% viewportanchor=100%,100%",
]
)
usernames = list()
for user in self.mumble.users.values():
if user["name"] != USER:
usernames.append(user["name"])
title = "<c.system>co'a veizba sei la'e di'e zvati fa'o {users}".format(users=",".join(usernames))
self.captions.add_cue(title, duration=2)
if self.cursor_time < time.time() - BUFFER: # it's time to check audio
base_sound = None
for user in self.mumble.users.values(): # check the audio queue of each users
session = user["session"]
while ( user.sound.is_sound() and
user.sound.first_sound().time < self.cursor_time):
user.sound.get_sound(FLOAT_RESOLUTION) # forget about too old sounds
if user.sound.is_sound():
if self.captions is not None and "caption" not in self.users[session]:
self.users[session]["caption"] = self.captions.add_cue("<v {user}>{user}".format(user=user["name"]))
if ( user.sound.first_sound().time >= self.cursor_time and
user.sound.first_sound().time < self.cursor_time + FLOAT_RESOLUTION ):
# available sound is to be treated now and not later
sound = user.sound.get_sound(FLOAT_RESOLUTION)
if sound.target == 0: # take care of the stereo feature
stereo_pcm = audioop.tostereo(sound.pcm, 2, *self.users[session]["stereo"])
if self.captions is not None:
self.users[session]["caption"].set_region(self.users[session]["region"])
else:
stereo_pcm = audioop.tostereo(sound.pcm, 2, 1, 1)
if base_sound == None:
base_sound = stereo_pcm
else:
#base_sound = audioop.add(base_sound, sound.pcm, 2)
base_sound = self.add_sound(base_sound, stereo_pcm)
else:
if self.captions is not None and "caption" in self.users[session]:
self.users[session]["caption"].end()
del self.users[session]["caption"]
if base_sound:
self.audio_file.write(base_sound)
else:
self.audio_file.write(silent)
self.cursor_time += FLOAT_RESOLUTION
else:
time.sleep(FLOAT_RESOLUTION)
else:
if self.audio_file:
# finish recording
self.mumble.users.myself.unrecording()
self.mumble.users.myself.texture(self.load_bitmap(STOP_BITMAP))
self.mumble.set_receive_sound(False)
self.cursor_time = None
self.audio_file.close()
self.audio_file = None
if self.current_chapter is not None:
self.current_chapter.end()
self.current_chapter = None
self.chapters = None
for user in self.users.values():
if "caption" in user:
user["caption"].end()
del user["caption"]
self.captions = None
self.force_newfile = False
time.sleep(0.2)
def add_sound(self, s1, s2):
"""add 2 stereo PCM audio streams"""
import struct
from sound_add import sound_add
if len(s1) != len(s2):
raise Exception("both sound must have same length")
return sound_add(s1, s2)
def load_bitmap(self, file):
"""read the bitmap info to update the avatar"""
bitmap = open(file, "rb")
result = ""
rec = bitmap.read(4096)
while rec != "":
result += rec
rec = bitmap.read(4096)
bitmap.close()
return result
class AudioFile():
"""
Manage the audio saving, through a pipe or in a WAV file
"""
def __init__(self, name):
from subprocess import Popen, PIPE
import wave
import sys
self.name = name
self.type = None
self.file_obj = None
try:
try:
final_name = ENCODER % name
except:
final_name = ENCODER
if DEBUG_ENCODER:
stdout = sys.stdout
stderr = sys.stderr
else:
stdout = None
stderr = None
proc = Popen(final_name.split(" "), shell=False, bufsize=-1, stdin=PIPE, stdout=stdout, stderr=stderr)
self.file_obj = proc.stdin
self.type = "pipe"
except Exception as e:
if DEBUG_ENCODER:
print("Cannot execute encoder, falling back to wav files: {0}".format(e.strerror))
self.name += ".wav"
self.file_obj = wave.open(self.name, "wb")
self.file_obj.setparams((2, 2, BITRATE, 0, 'NONE', 'not compressed'))
self.type = "wav"
def write(self, data):
if self.type == "pipe":
self.file_obj.write(data)
else:
self.file_obj.writeframes(data)
def close(self):
self.file_obj.close()
def printHex(string):
mystr=''
for i in range(len(string)):
mystr += "%02x" % ord(string[i])
return mystr
if __name__ == "__main__":
recbot = MumbleRecBot()