Add SampleCollection, new repl cmds & buzzword samples

Add "status" & "pause" commands to repl.
This commit is contained in:
Jakob 2019-08-25 07:01:33 +02:00
parent fcaef51d40
commit b4d27dc4c3
7 changed files with 311 additions and 136 deletions

View File

@ -7,7 +7,9 @@ class WrongArgumentCount(Exception):
def __init__(self, command, expected, found): def __init__(self, command, expected, found):
if isinstance(expected, list): if isinstance(expected, list):
expected = "{} to {}".format(**expected) expected = "{} to {}".format(**expected)
msg = "Command {} expects {} arguments, found {}".format(command, expected, found) msg = "Command {} expects {} arguments, found {}".format(
command, expected, found
)
super(WrongArgumentCount, self).__init__(msg) super(WrongArgumentCount, self).__init__(msg)
@ -26,7 +28,7 @@ class AtmoClientHandler(socketserver.StreamRequestHandler):
def handle(self): def handle(self):
try: try:
while not self.quit: while not self.quit:
self.send(">>> ", end='') self.send(">>> ", end="")
data = self.rfile.readline().decode().strip() data = self.rfile.readline().decode().strip()
tokens = data.split() tokens = data.split()
if len(tokens) > 0: if len(tokens) > 0:
@ -42,10 +44,19 @@ class AtmoClientHandler(socketserver.StreamRequestHandler):
self._ensure_args(cmd, 0, args) self._ensure_args(cmd, 0, args)
self.atmo.load_sounds() self.atmo.load_sounds()
self.send("Sounds reloaded") self.send("Sounds reloaded")
elif cmd == "status":
self._ensure_args(cmd, 0, args)
self.send("Currently playing:")
s = self.atmo.get_status()
for line in s.splitlines():
self.send(line)
elif cmd == "pause":
self._ensure_args(cmd, 0, args)
is_paused = self.atmo.pause()
self.send("Paused" if is_paused else "Resume")
elif cmd == "help": elif cmd == "help":
self._ensure_args(cmd, 0, args) self._ensure_args(cmd, 0, args)
self.send("The following commands are available: ") self.send_usage()
self.send(" reload")
elif cmd == "exit": elif cmd == "exit":
self.quit = True self.quit = True
self.send("Bye") self.send("Bye")
@ -53,11 +64,16 @@ class AtmoClientHandler(socketserver.StreamRequestHandler):
raise CommandNotFound(cmd) raise CommandNotFound(cmd)
except CommandNotFound as e: except CommandNotFound as e:
self.send("Error: {}".format(e)) self.send("Error: {}".format(e))
self.send("The following commands are available: ") self.send_usage()
self.send(" reload")
except WrongArgumentCount as e: except WrongArgumentCount as e:
self.send("Error: {}".format(e)) self.send("Error: {}".format(e))
def send_usage(self):
self.send("The following commands are available: ")
self.send(" reload")
self.send(" status")
self.send(" pause")
@staticmethod @staticmethod
def _ensure_args(cmd, expected, args): def _ensure_args(cmd, expected, args):
if isinstance(expected, list): if isinstance(expected, list):
@ -67,7 +83,7 @@ class AtmoClientHandler(socketserver.StreamRequestHandler):
if len(args) != expected: if len(args) != expected:
raise WrongArgumentCount(cmd, expected, len(args)) raise WrongArgumentCount(cmd, expected, len(args))
def send(self, line, end='\n'): def send(self, line, end="\n"):
self.wfile.write("{}{}".format(line, end).encode()) self.wfile.write("{}{}".format(line, end).encode())
@ -77,7 +93,7 @@ class ReusableThreadingTCPServer(socketserver.ThreadingTCPServer):
class AtmoReplRunner(threading.Thread): class AtmoReplRunner(threading.Thread):
def __init__(self, atmo, host='0.0.0.0', port=7723): def __init__(self, atmo, host="0.0.0.0", port=7723):
super(AtmoReplRunner, self).__init__() super(AtmoReplRunner, self).__init__()
AtmoClientHandler.atmo = atmo AtmoClientHandler.atmo = atmo
self._client = ReusableThreadingTCPServer((host, port), AtmoClientHandler) self._client = ReusableThreadingTCPServer((host, port), AtmoClientHandler)

View File

@ -1,92 +1,65 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import time
import sys
import random
from glob import glob from glob import glob
import os
import random
import sys
from threading import Thread from threading import Thread
import time
from typing import Union, List, Mapping from typing import Union, List, Mapping
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
from pygame import mixer from pygame import mixer
from collection import SampleCollection
from atmorepl import AtmoReplRunner from atmorepl import AtmoReplRunner
VERBOSITY = 0 VERBOSITY = 0
channel_index = 0
class SoundCollection:
def __init__(
self, name, files: Union[List[str], Mapping[str, List[str]]], volume=0.5
):
self.name = name
self.topics = files.keys() if hasattr(files, "keys") else []
if self.topics:
self.files = files
else:
self.files = {"None": files}
self.flat_files = [f for topic in self.files for f in self.files[topic]]
self.volume = volume
self.cur_topic = None
self.load_all_files()
self.sound_indices = {name: 0 for name in self.sounds}
self.flat_sound_index = 0
def load_all_files(self):
self.sounds = {}
if self.topics:
for t in self.topics:
self.load_topic_files(t)
else:
self.load_topic_files("None")
self.flat_sounds = [s for topic in self.sounds for s in self.sounds[topic]]
random.shuffle(self.flat_sounds)
def load_topic_files(self, topic):
self.sounds[topic] = []
for f in self.files[topic]:
sound = mixer.Sound(f)
sound.set_volume(self.volume)
self.sounds[topic].append(sound)
random.shuffle(self.sounds[topic])
def next(self):
if self.cur_topic is None:
if len(self.flat_sounds) == 0:
return
self.flat_sound_index += 1
if self.flat_sound_index >= len(self.flat_sounds):
random.shuffle(self.flat_sounds)
self.flat_sound_index = 0
return self.flat_sounds[self.flat_sound_index]
else:
if len(self.sounds[self.cur_topic]) == 0:
return
self.sound_indices[self.cur_topic] += 1
if self.sound_indices[self.cur_topic] >= len(self.sounds[self.cur_topic]):
random.shuffle(self.sounds[self.cur_topic])
self.sound_indices[self.cur_topic] = 0
return self.sounds[self.cur_topic][self.sound_indices[self.cur_topic]]
def set_topic(self, topic=None):
if self.topics:
self.cur_topic = topic
def __len__(self):
if self.cur_topic is None:
return len(self.flat_sounds)
else:
return len(self.sounds[self.cur_topic])
class CampAtmo: class CampAtmo:
tracks_per_type = 2 tracks_per_type = 2
single_track_types = ["lines"] single_track_types = ["lines", "reactions"]
background_track_types = ["background", "buzzwords"] background_track_types = ["background", "buzzwords"]
type_mix = {
"lines": 0.8,
"reactions": 0.7,
"buzzwords": 0.6,
"background": 0.3,
"other": 0.5,
}
lines_delta_sec = (5, 15) lines_delta_sec = (5, 15)
line_reaction_wait_sec = 0.8 line_reaction_wait_sec = 0.8
def __init__(self): def __init__(self):
self.stopped = False self.stopped = False
mixer.init(frequency=22050 * 2) mixer.init(frequency=22050 * 2)
self.load_sounds() type_dirs = glob("samples/*/")
self.types = {}
for type_dir in type_dirs:
type_name = type_dir.split("/")[1]
self.types[type_name] = SampleCollection(
type_dir, volume=self.type_mix[type_name]
)
i = 0
mixer.set_num_channels(
sum(
[
1 if name in self.single_track_types else self.tracks_per_type
for name in self.types
]
)
)
v(f"{mixer.get_num_channels()} channels set")
self.type_channels = {}
for type_name in self.types:
self.type_channels[type_name] = []
for k in range(
1 if type_name in self.single_track_types else self.tracks_per_type
):
self.type_channels[type_name].append(mixer.Channel(i))
i += 1
def start(self): def start(self):
self.background_thread = Thread( self.background_thread = Thread(
@ -104,80 +77,70 @@ class CampAtmo:
self.lines_thread.join(2) self.lines_thread.join(2)
def load_sounds(self): def load_sounds(self):
type_names = glob("samples/*/") for _, collection in self.types.items():
type_files = {} collection.load()
for type_dir in type_names:
type_name = type_dir.split("/")[1]
flat_files = glob(type_dir + "/*.ogg")
type_files[type_name] = flat_files
topic_files = glob(type_dir + "/*/*.ogg")
if topic_files:
type_files[type_name] = {}
for topic_dir in glob(type_dir + "/*/"):
topic_name = topic_dir.split("/")[2]
type_files[type_name][topic_name] = glob(type_dir + "/*/*.ogg")
self.types = {}
i = 0
mixer.set_num_channels(
sum(
[
1 if name in self.single_track_types else self.tracks_per_type
for name in type_files
]
)
)
v(f"{mixer.get_num_channels()} channels set")
for name in type_files:
self.types[name] = []
for k in range(
1 if name in self.single_track_types else self.tracks_per_type
):
self.types[name].append(mixer.Channel(i))
i += 1
self.sounds = {}
for name, files in type_files.items():
self.sounds[name] = SoundCollection(
name, files, volume=0.1 if name in self.background_track_types else 0.9
)
def manage_background_queue(self, name, type_channels):
for c in type_channels:
if c.get_queue() is not None and len(self.sounds[name]) > 1:
continue
v(name)
c.queue(self.sounds[name].next())
if c.get_queue() is None and len(self.sounds[name]) > 1:
c.queue(self.sounds[name].next())
def run_forever(self): def run_forever(self):
while not self.stopped: while not self.stopped:
for name, type_channels in self.types.items(): for type_name, channels in self.type_channels.items():
if ( if (
name not in self.background_track_types type_name not in self.background_track_types
or len(self.sounds[name]) == 0 or len(self.types[type_name]) == 0
): ):
continue continue
self.manage_background_queue(name, type_channels) self.play_type_sounds(type_name)
time.sleep(1) time.sleep(1)
self.load_sounds()
def play_type_sounds(self, type_name) -> int:
if len(self.sounds[type_name]) == 0:
return
max_sound_length = 0
for channel in self.types[type_name]:
sound = self.sounds[type_name].next()
max_sound_length = max(max_sound_length, sound.get_length())
channel.queue(sound)
return max_sound_length
def run_lines_forever(self): def run_lines_forever(self):
while not self.stopped: while not self.stopped:
time.sleep(random.randrange(*self.lines_delta_sec)) time.sleep(random.randrange(*self.lines_delta_sec))
length_sec = self.play_type_sounds("lines") length_sec = self.play_type_sounds("lines")
time.sleep(length_sec + self.line_reaction_wait_sec) time.sleep(length_sec + self.line_reaction_wait_sec)
length_sec = self.play_type_sounds("reactions")
time.sleep(length_sec + self.line_reaction_wait_sec)
self.play_type_sounds("reactions") self.play_type_sounds("reactions")
def play_type_sounds(self, type_name) -> int:
if type_name in self.types and len(self.types[type_name]) == 0:
return
max_sound_length = 0
for channel in self.type_channels[type_name]:
if channel.get_queue() is not None and len(self.types[type_name]) > 1:
continue
sample = self.types[type_name].next()
max_sound_length = max(max_sound_length, sample.sound.get_length())
channel.queue(sample.sound)
if (
type_name not in self.single_track_types
and channel.get_queue() is None
and len(self.types[type_name]) > 1
):
channel.queue(self.types[type_name].next_sound())
return max_sound_length
def get_status(self):
ret_strs = []
for type_name, channels in self.type_channels.items():
samples = []
for chan in channels:
sample = self.types[type_name].get_sample_from_channel(chan)
if sample is not None:
samples.append(sample)
if samples:
sample_infos = [
f"{s.path} @{s.sound.get_volume():.0%}vol" for s in samples
]
ret_strs.append(f"{type_name}: {sample_infos}")
return "\n".join(ret_strs)
def pause(self):
if mixer.get_busy():
mixer.pause()
return False
else:
mixer.unpause()
return True
def main(): def main():
atmo = CampAtmo() atmo = CampAtmo()
@ -186,7 +149,7 @@ def main():
atmo.start() atmo.start()
try: try:
while True: while True:
time.sleep(100) time.sleep(10)
except KeyboardInterrupt: except KeyboardInterrupt:
print("exiting...") print("exiting...")
atmo.stop() atmo.stop()

161
collection.py Normal file
View File

@ -0,0 +1,161 @@
import os
import time
from random import shuffle
from dataclasses import dataclass, field
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
from pygame import mixer
from typing import Union, Tuple, List
from dirs import list_files_relative
@dataclass
class Sample:
path: str
abspath: str
mtime: float
size: int
sound: mixer.Sound
old_sounds: List[Tuple[mixer.Sound, float]] = field(default_factory=list)
class SampleCollection:
extensions = ["ogg"]
def __init__(self, sample_dir: str, volume: float = 0.7):
self.db = []
self.volume = volume
self.topic_db = {}
self.sample_dir = sample_dir
self.index = 0
self.load()
def __len__(self):
return len(self.db)
def load(self):
prev_prefix = ""
for path, stat in list_files_relative(
self.sample_dir, extensions=self.extensions
):
db_sample = self.get_sample(path)
if (
db_sample is not None
and db_sample.mtime == int(stat.st_mtime)
and db_sample.size == stat.st_size
):
continue
abspath = os.path.join(self.sample_dir, path)
if stat.st_size == 0:
print(f"Warning, empty file: {abspath!r}", file=sys.stderr)
continue
topic = None
last_sep = path.rfind("/")
if last_sep > 0:
prefix = path[:last_sep]
topic = prefix
if prev_prefix != prefix:
prev_prefix = prefix
self.add_sample(path, stat, abspath, topic, db_sample)
def add_sample(
self,
path: str,
stat: os.stat_result,
abspath: str,
topic: Union[str, None],
known_sample: Sample,
):
if known_sample is None:
new_sample = Sample(
path=path,
mtime=stat.st_mtime,
size=stat.st_size,
abspath=abspath,
sound=mixer.Sound(abspath),
)
new_sample.sound.set_volume(self.volume)
self.db.append(new_sample)
if topic is not None:
if topic not in self.topic_db:
self.topic_db[topic] = [new_sample.path]
else:
self.topic_db[topic].append(new_sample.path)
else:
known_sample.old_sounds.append((known_sample.sound, time.time()))
known_sample.sound = mixer.Sound(abspath)
known_sample.sound.set_volume(self.volume)
known_sample.mtime = stat.st_mtime
known_sample.size = stat.st_size
def set_volume(self, volume):
self.volume = volume
for sample in self.db:
sample.sound.set_volume(volume)
def next(self):
if self.index == 0:
shuffle(self.db)
ret_sample = self.db[self.index]
self.index = (self.index + 1) % len(self.db)
return ret_sample
def next_sound(self):
return self.next().sound
def next_by_topic(self, topic: str):
if topic not in self.topic_db:
return
start_index = self.index
sample = self.next()
while sample.path not in self.topic_db[topic] and self.index != start_index:
print(f"trying sample {sample.path!r} for topic {topic!r}")
sample = self.next()
if self.index == start_index:
return
return sample
def next_sound_by_topic(self, topic: str):
next_sample = self.next_by_topic(topic)
if next_sample is not None:
return next_sample.sound
def get_sample(self, path: str) -> Union[dict, None]:
for sample in self.db:
if sample.path == path:
return sample
def get_sample_from_channel(self, channel):
ret_sample = None
c_sound = channel.get_sound()
if c_sound is None:
return None
for sample in self.db:
if sample.sound == c_sound:
ret_sample = sample
if ret_sample is None:
for sample in self.db:
for o_sample, _ in sample.old_sounds:
if o_sample == c_sound:
ret_sample = sample
for sample in self.db:
expired = []
for i, (o_sample, exp_time) in enumerate(sample.old_sounds):
if ((time.time() - exp_time) >= o_sample.get_length()) or (
time.time() - exp_time < 0
):
expired.append(i)
for i in reversed(expired):
sample.old_sounds.pop(i)
return ret_sample
# if __name__ == "__main__":
# mixer.init()
# chan = mixer.Channel(0)
# coll = SampleCollection("/home/jakob/dev/seba-audio/gincloud/samples/buzzwords")
# chan.queue(coll.next_sound_by_topic("agile"))
# chan.queue(coll.next_sound_by_topic("cloud"))
# print(coll.get_sample_from_channel(chan).path)
# time.sleep(7)
# print(coll.get_sample_from_channel(chan).path)

35
dirs.py Normal file
View File

@ -0,0 +1,35 @@
import os
def list_files_absolute(start_dir, extensions=None):
start_dir = os.path.expanduser(start_dir)
return _list_files(start_dir, start_dir, extensions)
def list_files_relative(start_dir, extensions=None):
start_dir = os.path.expanduser(start_dir)
return _list_files(start_dir, start_dir, extensions, relative=True)
def _list_files(start_dir, cur_dir, extensions=None, relative=False):
paths = []
with os.scandir(cur_dir) as scanner:
for entry in scanner:
if entry.is_dir():
paths += _list_files(start_dir, entry.path, extensions, relative)
elif (
extensions is not None
and any([entry.name.endswith("." + ext) for ext in extensions])
) or extensions is None:
if relative:
name = os.path.relpath(entry.path, start=start_dir)
else:
name = entry.path
paths.append((name, entry.stat()))
return paths
if __name__ == "__main__":
import doctest
doctest.testmod()

Binary file not shown.

Binary file not shown.

Binary file not shown.