Add SampleCollection, new repl cmds & buzzword samples
Add "status" & "pause" commands to repl.
This commit is contained in:
parent
fcaef51d40
commit
b4d27dc4c3
32
atmorepl.py
32
atmorepl.py
|
@ -7,7 +7,9 @@ class WrongArgumentCount(Exception):
|
|||
def __init__(self, command, expected, found):
|
||||
if isinstance(expected, list):
|
||||
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)
|
||||
|
||||
|
||||
|
@ -26,7 +28,7 @@ class AtmoClientHandler(socketserver.StreamRequestHandler):
|
|||
def handle(self):
|
||||
try:
|
||||
while not self.quit:
|
||||
self.send(">>> ", end='')
|
||||
self.send(">>> ", end="")
|
||||
data = self.rfile.readline().decode().strip()
|
||||
tokens = data.split()
|
||||
if len(tokens) > 0:
|
||||
|
@ -42,10 +44,19 @@ class AtmoClientHandler(socketserver.StreamRequestHandler):
|
|||
self._ensure_args(cmd, 0, args)
|
||||
self.atmo.load_sounds()
|
||||
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":
|
||||
self._ensure_args(cmd, 0, args)
|
||||
self.send("The following commands are available: ")
|
||||
self.send(" reload")
|
||||
self.send_usage()
|
||||
elif cmd == "exit":
|
||||
self.quit = True
|
||||
self.send("Bye")
|
||||
|
@ -53,11 +64,16 @@ class AtmoClientHandler(socketserver.StreamRequestHandler):
|
|||
raise CommandNotFound(cmd)
|
||||
except CommandNotFound as e:
|
||||
self.send("Error: {}".format(e))
|
||||
self.send("The following commands are available: ")
|
||||
self.send(" reload")
|
||||
self.send_usage()
|
||||
except WrongArgumentCount as 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
|
||||
def _ensure_args(cmd, expected, args):
|
||||
if isinstance(expected, list):
|
||||
|
@ -67,7 +83,7 @@ class AtmoClientHandler(socketserver.StreamRequestHandler):
|
|||
if len(args) != expected:
|
||||
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())
|
||||
|
||||
|
||||
|
@ -77,7 +93,7 @@ class ReusableThreadingTCPServer(socketserver.ThreadingTCPServer):
|
|||
|
||||
|
||||
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__()
|
||||
AtmoClientHandler.atmo = atmo
|
||||
self._client = ReusableThreadingTCPServer((host, port), AtmoClientHandler)
|
||||
|
|
219
campatmo.py
219
campatmo.py
|
@ -1,92 +1,65 @@
|
|||
#!/usr/bin/env python3
|
||||
import time
|
||||
import sys
|
||||
import random
|
||||
from glob import glob
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
from threading import Thread
|
||||
import time
|
||||
from typing import Union, List, Mapping
|
||||
|
||||
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
|
||||
from pygame import mixer
|
||||
|
||||
from collection import SampleCollection
|
||||
from atmorepl import AtmoReplRunner
|
||||
|
||||
VERBOSITY = 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])
|
||||
channel_index = 0
|
||||
|
||||
|
||||
class CampAtmo:
|
||||
tracks_per_type = 2
|
||||
single_track_types = ["lines"]
|
||||
single_track_types = ["lines", "reactions"]
|
||||
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)
|
||||
line_reaction_wait_sec = 0.8
|
||||
|
||||
def __init__(self):
|
||||
self.stopped = False
|
||||
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):
|
||||
self.background_thread = Thread(
|
||||
|
@ -104,80 +77,70 @@ class CampAtmo:
|
|||
self.lines_thread.join(2)
|
||||
|
||||
def load_sounds(self):
|
||||
type_names = glob("samples/*/")
|
||||
type_files = {}
|
||||
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())
|
||||
for _, collection in self.types.items():
|
||||
collection.load()
|
||||
|
||||
def run_forever(self):
|
||||
while not self.stopped:
|
||||
for name, type_channels in self.types.items():
|
||||
for type_name, channels in self.type_channels.items():
|
||||
if (
|
||||
name not in self.background_track_types
|
||||
or len(self.sounds[name]) == 0
|
||||
type_name not in self.background_track_types
|
||||
or len(self.types[type_name]) == 0
|
||||
):
|
||||
continue
|
||||
self.manage_background_queue(name, type_channels)
|
||||
self.play_type_sounds(type_name)
|
||||
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):
|
||||
while not self.stopped:
|
||||
time.sleep(random.randrange(*self.lines_delta_sec))
|
||||
length_sec = self.play_type_sounds("lines")
|
||||
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")
|
||||
|
||||
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():
|
||||
atmo = CampAtmo()
|
||||
|
@ -186,7 +149,7 @@ def main():
|
|||
atmo.start()
|
||||
try:
|
||||
while True:
|
||||
time.sleep(100)
|
||||
time.sleep(10)
|
||||
except KeyboardInterrupt:
|
||||
print("exiting...")
|
||||
atmo.stop()
|
||||
|
|
|
@ -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)
|
|
@ -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.
Loading…
Reference in New Issue