2020-04-15 21:31:24 +02:00
|
|
|
import argparse
|
2020-04-17 02:09:01 +02:00
|
|
|
import audioop
|
|
|
|
import logging
|
2020-04-19 20:03:03 +02:00
|
|
|
import math
|
2020-04-17 02:09:01 +02:00
|
|
|
import sys
|
2020-04-19 20:03:03 +02:00
|
|
|
import struct
|
2020-04-17 02:09:01 +02:00
|
|
|
import time
|
2020-04-19 20:03:03 +02:00
|
|
|
import threading
|
2020-04-17 02:09:01 +02:00
|
|
|
from functools import partial
|
|
|
|
|
|
|
|
import pymumble_py3 as pymumble
|
|
|
|
from pymumble_py3.constants import PYMUMBLE_CLBK_TEXTMESSAGERECEIVED
|
|
|
|
|
|
|
|
is_streaming = False
|
|
|
|
silence_time = 0
|
|
|
|
|
2020-04-19 20:03:03 +02:00
|
|
|
SHORT_NORMALIZE = (1.0/32768.0)
|
|
|
|
|
|
|
|
## peak detection
|
|
|
|
|
|
|
|
def get_rms( block ):
|
|
|
|
# RMS amplitude is defined as the square root of the
|
|
|
|
# mean over time of the square of the amplitude.
|
|
|
|
# so we need to convert this string of bytes into
|
|
|
|
# a string of 16-bit samples...
|
|
|
|
|
|
|
|
# we will get one short out for each
|
|
|
|
# two chars in the string.
|
|
|
|
count = len(block)/2
|
|
|
|
format = "%dh"%(count)
|
|
|
|
shorts = struct.unpack( format, block )
|
|
|
|
|
|
|
|
# iterate over the block.
|
|
|
|
sum_squares = 0.0
|
|
|
|
for sample in shorts:
|
|
|
|
# sample is a signed short in +/- 32768.
|
|
|
|
# normalize it to 1.0
|
|
|
|
n = sample * SHORT_NORMALIZE
|
|
|
|
sum_squares += n*n
|
|
|
|
|
|
|
|
return math.sqrt( sum_squares / count )
|
|
|
|
|
2020-04-06 13:29:25 +02:00
|
|
|
|
2020-04-17 02:09:01 +02:00
|
|
|
def message_received(mumble, message):
|
2020-04-15 21:31:24 +02:00
|
|
|
global is_streaming
|
2020-04-16 21:05:51 +02:00
|
|
|
global silence_time
|
2020-04-17 02:09:01 +02:00
|
|
|
command = message.message
|
2020-04-16 21:05:51 +02:00
|
|
|
if command == "/start":
|
2020-04-15 21:31:24 +02:00
|
|
|
is_streaming = True
|
2020-04-16 21:05:51 +02:00
|
|
|
silence_time = 0
|
2020-04-16 21:47:39 +02:00
|
|
|
mumble.my_channel().send_text_message("Diretta iniziata")
|
2020-04-17 02:09:01 +02:00
|
|
|
logging.info("Diretta iniziata")
|
2020-04-16 21:05:51 +02:00
|
|
|
mumble.users.myself.recording()
|
|
|
|
elif command == "/stop":
|
2020-04-15 21:31:24 +02:00
|
|
|
is_streaming = False
|
2020-04-16 21:47:39 +02:00
|
|
|
mumble.my_channel().send_text_message("Diretta terminata")
|
2020-04-17 02:09:01 +02:00
|
|
|
logging.info("Diretta terminata")
|
2020-04-16 21:05:51 +02:00
|
|
|
mumble.users.myself.unrecording()
|
2020-04-17 02:09:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
def get_parser():
|
|
|
|
parser = argparse.ArgumentParser(description="Regia pienamente automatizzata")
|
|
|
|
parser.add_argument("--channel", help="Set channel", default="")
|
|
|
|
parser.add_argument("--name", help="Set bot nickname", default="RadioRobbot")
|
|
|
|
parser.add_argument("--server", help="Set server", default="mumble.esiliati.org")
|
2020-04-17 13:15:33 +02:00
|
|
|
parser.add_argument("--password", help="Set password", default="")
|
2020-04-17 02:09:01 +02:00
|
|
|
parser.add_argument("--port", help="Set port", type=int, default=64738)
|
|
|
|
parser.add_argument(
|
|
|
|
"--stream",
|
|
|
|
action="store_true",
|
|
|
|
help="Ignore commands in chat and stream everything",
|
|
|
|
)
|
2020-04-19 20:03:03 +02:00
|
|
|
parser.add_argument(
|
|
|
|
"--transmit",
|
|
|
|
action="store_true",
|
|
|
|
help="Transmit from your stdin",
|
|
|
|
)
|
2020-04-17 02:09:01 +02:00
|
|
|
parser.add_argument(
|
|
|
|
"--auto-suspend-stream",
|
|
|
|
action="store_true",
|
|
|
|
help="Ignore commands in chat and stream everything",
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"--max-silence-time", type=int, help="max silence time in seconds", default=30
|
|
|
|
)
|
2020-04-17 13:15:33 +02:00
|
|
|
parser.add_argument(
|
|
|
|
"--tokens",
|
|
|
|
help="Set tokens list",
|
|
|
|
nargs="*"
|
|
|
|
)
|
2020-04-18 13:35:54 +02:00
|
|
|
parser.add_argument(
|
|
|
|
"--ignore",
|
|
|
|
help="List of ignored users",
|
|
|
|
nargs="*",
|
|
|
|
default=[]
|
|
|
|
)
|
2020-04-17 02:09:01 +02:00
|
|
|
return parser
|
|
|
|
|
|
|
|
|
2020-04-19 20:03:03 +02:00
|
|
|
def transmit(name, mumble, chunk_size):
|
|
|
|
buffer = 0
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
data = sys.stdin.buffer.read(int(chunk_size))
|
|
|
|
if(get_rms(data) > 0.09):
|
|
|
|
mumble.sound_output.add_sound(data)
|
2020-04-19 20:12:29 +02:00
|
|
|
buffer = 1000
|
2020-04-19 20:03:03 +02:00
|
|
|
else:
|
|
|
|
# keep the audio open for one second
|
|
|
|
if(buffer >= 0):
|
|
|
|
mumble.sound_output.add_sound(data)
|
|
|
|
buffer -= 10
|
|
|
|
except IOError as e:
|
|
|
|
print( " Error recording: %s"%e )
|
|
|
|
|
2020-04-17 02:09:01 +02:00
|
|
|
def main():
|
|
|
|
global is_streaming
|
|
|
|
global silence_time
|
|
|
|
args = get_parser().parse_args()
|
|
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
|
|
|
2020-04-17 13:15:33 +02:00
|
|
|
pwd = args.password
|
2020-04-17 02:09:01 +02:00
|
|
|
server = args.server
|
|
|
|
nick = args.name
|
|
|
|
channel = args.channel
|
|
|
|
port = args.port
|
2020-04-17 13:15:33 +02:00
|
|
|
tokens = args.tokens
|
2020-04-17 02:09:01 +02:00
|
|
|
is_streaming = False
|
|
|
|
stream_always = args.stream
|
2020-04-18 13:35:54 +02:00
|
|
|
ignored_users = args.ignore
|
|
|
|
|
2020-04-17 02:09:01 +02:00
|
|
|
# Spin up a client and connect to mumble server
|
2020-04-17 13:15:33 +02:00
|
|
|
mumble = pymumble.Mumble(server, nick, password=pwd, port=port, tokens=tokens)
|
2020-04-17 02:09:01 +02:00
|
|
|
|
|
|
|
mumble.callbacks.set_callback(
|
|
|
|
PYMUMBLE_CLBK_TEXTMESSAGERECEIVED, partial(message_received, mumble)
|
|
|
|
)
|
|
|
|
mumble.set_receive_sound(1) # Enable receiving sound from mumble server
|
|
|
|
mumble.start()
|
|
|
|
mumble.is_ready() # Wait for client is ready
|
2020-04-17 13:18:45 +02:00
|
|
|
mumble.channels.find_by_tree(channel.split('/')).move_in()
|
2020-04-19 20:03:03 +02:00
|
|
|
if not args.transmit:
|
|
|
|
mumble.users.myself.mute()
|
2020-04-17 02:09:01 +02:00
|
|
|
|
|
|
|
if is_streaming:
|
|
|
|
mumble.users.myself.recording()
|
|
|
|
|
|
|
|
BUFFER = 0.1
|
|
|
|
BITRATE = 48000
|
|
|
|
RESOLUTION = 10 # in ms
|
|
|
|
FLOAT_RESOLUTION = float(RESOLUTION) / 1000
|
|
|
|
MONO_CHUNK_SIZE = BITRATE * 2 * RESOLUTION / 1000
|
|
|
|
STEREO_CHUNK_SIZE = MONO_CHUNK_SIZE * 2
|
|
|
|
silent = b"\x00" * int(STEREO_CHUNK_SIZE)
|
|
|
|
cursor_time = time.time() - BUFFER
|
|
|
|
|
2020-04-19 20:03:03 +02:00
|
|
|
if args.transmit:
|
2020-04-19 20:12:29 +02:00
|
|
|
transmit_thread = threading.Thread(target=transmit, args=(1,mumble, MONO_CHUNK_SIZE))
|
2020-04-19 20:03:03 +02:00
|
|
|
transmit_thread.start()
|
|
|
|
|
2020-04-17 02:09:01 +02:00
|
|
|
while mumble.is_alive():
|
|
|
|
if cursor_time < time.time() - BUFFER:
|
|
|
|
base_sound = None
|
|
|
|
|
|
|
|
try:
|
|
|
|
for user in mumble.users.values(): # check the audio queue of each user
|
2020-04-18 13:35:54 +02:00
|
|
|
if user.sound.is_sound() and user["name"] not in ignored_users:
|
2020-04-17 02:09:01 +02:00
|
|
|
# available sound is to be treated now and not later
|
|
|
|
sound = user.sound.get_sound(FLOAT_RESOLUTION)
|
|
|
|
stereo_pcm = audioop.tostereo(sound.pcm, 2, 1, 1)
|
|
|
|
if base_sound is None:
|
|
|
|
base_sound = stereo_pcm
|
|
|
|
else:
|
|
|
|
base_sound = audioop.add(base_sound, stereo_pcm, 2)
|
|
|
|
except RuntimeError:
|
|
|
|
print("ignored exception in stderr...", file=sys.stderr)
|
|
|
|
|
|
|
|
if is_streaming or stream_always:
|
|
|
|
if base_sound:
|
|
|
|
silence_time = 0
|
|
|
|
sys.stdout.buffer.write(base_sound)
|
|
|
|
else:
|
|
|
|
silence_time += RESOLUTION
|
|
|
|
sys.stdout.buffer.write(silent)
|
|
|
|
|
|
|
|
if (
|
|
|
|
args.auto_suspend_stream
|
|
|
|
and (silence_time >= args.max_silence_time * 1000)
|
|
|
|
and is_streaming
|
|
|
|
):
|
|
|
|
is_streaming = False
|
|
|
|
logging.info("max-silence-time reached")
|
|
|
|
mumble.my_channel().send_text_message(
|
|
|
|
"Diretta terminata in automatico dopo %d secondi circa di silenzio"
|
|
|
|
% args.max_silence_time
|
|
|
|
)
|
|
|
|
mumble.users.myself.unrecording()
|
|
|
|
|
|
|
|
cursor_time += FLOAT_RESOLUTION
|
|
|
|
else:
|
|
|
|
time.sleep(FLOAT_RESOLUTION)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|