mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-09-09 19:57:16 +02:00
Compare commits
4 Commits
08ad58e7c8
...
c1b94778b4
Author | SHA1 | Date | |
---|---|---|---|
c1b94778b4 | |||
338f66352a | |||
d4f9aa21e5 | |||
624566d34e |
124
cli/sounds.py
Normal file
124
cli/sounds.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import numpy as np
|
||||
import simpleaudio as sa
|
||||
|
||||
class Sound:
|
||||
"""
|
||||
Sound effects for the application with enhanced complexity.
|
||||
Each sound uses at least 6 distinct tones and lasts no more than max_length seconds,
|
||||
except the intro sound which is a detailed 26-second Berlin techno-style build-up, 12-second celebration with a descending-fifth chord sequence of 7 chords, and breakdown with melodic background.
|
||||
Transitions between phases now crossfade over 3 seconds for smoother flow.
|
||||
"""
|
||||
|
||||
fs = 44100 # Sampling rate (samples per second)
|
||||
complexity_factor = 10 # Number of harmonics to sum for richer timbres
|
||||
max_length = 2.0 # Maximum total duration of any sound in seconds
|
||||
|
||||
@staticmethod
|
||||
def _generate_complex_wave(frequency: float, duration: float, harmonics: int = None) -> np.ndarray:
|
||||
if harmonics is None:
|
||||
harmonics = Sound.complexity_factor
|
||||
t = np.linspace(0, duration, int(Sound.fs * duration), False)
|
||||
wave = np.zeros_like(t)
|
||||
for n in range(1, harmonics + 1):
|
||||
wave += (1 / n) * np.sin(2 * np.pi * frequency * n * t)
|
||||
# ADSR envelope
|
||||
attack = int(0.02 * Sound.fs)
|
||||
release = int(0.05 * Sound.fs)
|
||||
env = np.ones_like(wave)
|
||||
env[:attack] = np.linspace(0, 1, attack)
|
||||
env[-release:] = np.linspace(1, 0, release)
|
||||
wave *= env
|
||||
wave /= np.max(np.abs(wave))
|
||||
return (wave * (2**15 - 1)).astype(np.int16)
|
||||
|
||||
@staticmethod
|
||||
def _crossfade(w1: np.ndarray, w2: np.ndarray, fade_len: int) -> np.ndarray:
|
||||
# Ensure fade_len less than each
|
||||
fade_len = min(fade_len, len(w1), len(w2))
|
||||
fade_out = np.linspace(1, 0, fade_len)
|
||||
fade_in = np.linspace(0, 1, fade_len)
|
||||
w1_end = w1[-fade_len:] * fade_out
|
||||
w2_start = w2[:fade_len] * fade_in
|
||||
middle = (w1_end + w2_start).astype(np.int16)
|
||||
return np.concatenate([w1[:-fade_len], middle, w2[fade_len:]])
|
||||
|
||||
@staticmethod
|
||||
def _play(wave: np.ndarray):
|
||||
play_obj = sa.play_buffer(wave, 1, 2, Sound.fs)
|
||||
play_obj.wait_done()
|
||||
|
||||
@classmethod
|
||||
def play_cymais_intro_sound(cls):
|
||||
# Phase durations
|
||||
build_time = 10.0
|
||||
celebr_time = 12.0
|
||||
breakdown_time = 10.0
|
||||
overlap = 3.0 # seconds of crossfade
|
||||
bass_seg = 0.125 # 1/8s kick
|
||||
melody_seg = 0.25 # 2/8s melody
|
||||
bass_freq = 65.41 # C2 kick
|
||||
melody_freqs = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25]
|
||||
|
||||
# Build-up phase
|
||||
steps = int(build_time / (bass_seg + melody_seg))
|
||||
build_seq = []
|
||||
for i in range(steps):
|
||||
amp = (i + 1) / steps
|
||||
b = cls._generate_complex_wave(bass_freq, bass_seg).astype(np.float32) * amp
|
||||
m = cls._generate_complex_wave(melody_freqs[i % len(melody_freqs)], melody_seg).astype(np.float32) * amp
|
||||
build_seq.append(b.astype(np.int16))
|
||||
build_seq.append(m.astype(np.int16))
|
||||
build_wave = np.concatenate(build_seq)
|
||||
|
||||
# Celebration phase: 7 descending-fifth chords
|
||||
roots = [523.25, 349.23, 233.08, 155.56, 103.83, 69.30, 46.25]
|
||||
chord_time = celebr_time / len(roots)
|
||||
celebr_seq = []
|
||||
for root in roots:
|
||||
t = np.linspace(0, chord_time, int(cls.fs * chord_time), False)
|
||||
chord = sum(np.sin(2 * np.pi * f * t) for f in [root, root * 5/4, root * 3/2])
|
||||
chord /= np.max(np.abs(chord))
|
||||
celebr_seq.append((chord * (2**15 - 1)).astype(np.int16))
|
||||
celebr_wave = np.concatenate(celebr_seq)
|
||||
|
||||
# Breakdown phase (mirror of build-up)
|
||||
breakdown_wave = np.concatenate(list(reversed(build_seq)))
|
||||
|
||||
# Crossfade transitions
|
||||
fade_samples = int(overlap * cls.fs)
|
||||
bc = cls._crossfade(build_wave, celebr_wave, fade_samples)
|
||||
full = cls._crossfade(bc, breakdown_wave, fade_samples)
|
||||
|
||||
cls._play(full)
|
||||
|
||||
@classmethod
|
||||
def play_start_sound(cls):
|
||||
freqs = [523.25, 659.26, 783.99, 880.00, 1046.50, 1174.66]
|
||||
cls._prepare_and_play(freqs)
|
||||
|
||||
@classmethod
|
||||
def play_finished_successfully_sound(cls):
|
||||
freqs = [523.25, 587.33, 659.26, 783.99, 880.00, 987.77]
|
||||
cls._prepare_and_play(freqs)
|
||||
|
||||
@classmethod
|
||||
def play_finished_failed_sound(cls):
|
||||
freqs = [880.00, 830.61, 783.99, 659.26, 622.25, 523.25]
|
||||
durations = [0.4, 0.3, 0.25, 0.25, 0.25, 0.25]
|
||||
cls._prepare_and_play(freqs, durations)
|
||||
|
||||
@classmethod
|
||||
def play_warning_sound(cls):
|
||||
freqs = [700.00, 550.00, 750.00, 500.00, 800.00, 450.00]
|
||||
cls._prepare_and_play(freqs)
|
||||
|
||||
@classmethod
|
||||
def _prepare_and_play(cls, freqs, durations=None):
|
||||
count = len(freqs)
|
||||
if durations is None:
|
||||
durations = [cls.max_length / count] * count
|
||||
else:
|
||||
total = sum(durations)
|
||||
durations = [d * cls.max_length / total for d in durations]
|
||||
waves = [cls._generate_complex_wave(f, d) for f, d in zip(freqs, durations)]
|
||||
cls._play(np.concatenate(waves))
|
101
main.py
101
main.py
@@ -5,6 +5,10 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
import threading
|
||||
import signal
|
||||
|
||||
from cli.sounds import Sound
|
||||
|
||||
def format_command_help(name, description, indent=2, col_width=36, width=80):
|
||||
prefix = " " * indent + f"{name:<{col_width - indent}}"
|
||||
@@ -22,7 +26,6 @@ def list_cli_commands(cli_dir):
|
||||
)
|
||||
|
||||
def extract_description_via_help(cli_script_path):
|
||||
"""Run `script --help` and extract the first non-usage line after usage block."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, cli_script_path, "--help"],
|
||||
@@ -31,13 +34,10 @@ def extract_description_via_help(cli_script_path):
|
||||
check=True
|
||||
)
|
||||
lines = result.stdout.splitlines()
|
||||
|
||||
# Skip until first empty line after usage block
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip().startswith("usage:"):
|
||||
continue
|
||||
if line.strip() == "":
|
||||
# description usually comes after usage and empty line
|
||||
for j in range(i+1, len(lines)):
|
||||
desc = lines[j].strip()
|
||||
if desc:
|
||||
@@ -46,41 +46,92 @@ def extract_description_via_help(cli_script_path):
|
||||
except Exception:
|
||||
return "-"
|
||||
|
||||
def main():
|
||||
def play_start_intro():
|
||||
Sound.play_start_sound()
|
||||
Sound.play_cymais_intro_sound()
|
||||
|
||||
def failure_with_warning_loop():
|
||||
Sound.play_finished_failed_sound()
|
||||
print("Warning: command failed. Press Ctrl+C to stop sound warnings.")
|
||||
try:
|
||||
while True:
|
||||
Sound.play_warning_sound()
|
||||
except KeyboardInterrupt:
|
||||
print("Warnings stopped by user.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Parse --no-sound early and remove from args
|
||||
no_sound = False
|
||||
if '--no-sound' in sys.argv:
|
||||
no_sound = True
|
||||
sys.argv.remove('--no-sound')
|
||||
|
||||
# Setup segfault handler to catch crashes
|
||||
def segv_handler(signum, frame):
|
||||
if not no_sound:
|
||||
Sound.play_finished_failed_sound()
|
||||
# Loop warning until interrupted
|
||||
try:
|
||||
while True:
|
||||
Sound.play_warning_sound()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
print("Segmentation fault detected. Exiting.")
|
||||
sys.exit(1)
|
||||
signal.signal(signal.SIGSEGV, segv_handler)
|
||||
|
||||
# Play intro sounds
|
||||
if not no_sound:
|
||||
threading.Thread(target=play_start_intro, daemon=True).start()
|
||||
|
||||
# Change to script directory
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
cli_dir = os.path.join(script_dir, "cli")
|
||||
os.chdir(script_dir)
|
||||
|
||||
available_cli_commands = list_cli_commands(cli_dir)
|
||||
|
||||
# Special case: user ran `cymais playbook --help`
|
||||
if len(sys.argv) >= 3 and sys.argv[1] in available_cli_commands and sys.argv[2] == "--help":
|
||||
cli_script_path = os.path.join(cli_dir, f"{sys.argv[1]}.py")
|
||||
subprocess.run([sys.executable, cli_script_path, "--help"])
|
||||
sys.exit(0)
|
||||
|
||||
# Global --help
|
||||
if "--help" in sys.argv or "-h" in sys.argv or len(sys.argv) == 1:
|
||||
print("CyMaIS CLI – proxy to tools in ./cli/\n")
|
||||
print("Usage:")
|
||||
print(" cymais <command> [options]\n")
|
||||
# Handle help invocation
|
||||
if len(sys.argv) == 1 or sys.argv[1] in ('-h', '--help'):
|
||||
print("CyMaIS CLI – proxy to tools in ./cli/")
|
||||
print("Usage: cymais [--no-sound] <command> [options]")
|
||||
print("Options:")
|
||||
print(" --no-sound Suppress all sounds during execution")
|
||||
print(" -h, --help Show this help message and exit")
|
||||
print("Available commands:")
|
||||
for cmd in available_cli_commands:
|
||||
path = os.path.join(cli_dir, f"{cmd}.py")
|
||||
desc = extract_description_via_help(path)
|
||||
print(format_command_help(cmd, desc))
|
||||
print("\nUse 'cymais <command> --help' for details on each command.")
|
||||
sys.exit(0)
|
||||
|
||||
# Default flow
|
||||
# Special-case per-command help
|
||||
if len(sys.argv) >= 3 and sys.argv[1] in available_cli_commands and sys.argv[2] in ('-h', '--help'):
|
||||
subprocess.run([sys.executable, os.path.join(cli_dir, f"{sys.argv[1]}.py"), "--help"])
|
||||
sys.exit(0)
|
||||
|
||||
# Execute chosen command
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument("cli_command", choices=available_cli_commands)
|
||||
parser.add_argument("cli_args", nargs=argparse.REMAINDER)
|
||||
parser.add_argument('cli_command', choices=available_cli_commands)
|
||||
parser.add_argument('cli_args', nargs=argparse.REMAINDER)
|
||||
args = parser.parse_args()
|
||||
|
||||
cli_script_path = os.path.join(cli_dir, f"{args.cli_command}.py")
|
||||
full_cmd = [sys.executable, cli_script_path] + args.cli_args
|
||||
subprocess.run(full_cmd, check=True)
|
||||
cmd_path = os.path.join(cli_dir, f"{args.cli_command}.py")
|
||||
full_cmd = [sys.executable, cmd_path] + args.cli_args
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
try:
|
||||
proc = subprocess.Popen(full_cmd)
|
||||
proc.wait()
|
||||
rc = proc.returncode
|
||||
if rc != 0:
|
||||
print(f"Command '{args.cli_command}' failed with exit code {rc}.")
|
||||
failure_with_warning_loop()
|
||||
sys.exit(rc)
|
||||
else:
|
||||
if not no_sound:
|
||||
Sound.play_finished_successfully_sound()
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"Exception running command: {e}")
|
||||
failure_with_warning_loop()
|
||||
sys.exit(1)
|
||||
|
@@ -1 +1,3 @@
|
||||
colorscheme-generator @ https://github.com/kevinveenbirkenbach/colorscheme-generator/archive/refs/tags/v0.3.0.zip
|
||||
simpleaudio
|
||||
numpy
|
@@ -2,7 +2,7 @@
|
||||
command: >
|
||||
docker-compose exec -T -u www-data application
|
||||
wp core install
|
||||
--url="{{ web_protocol }}://{{ domains | get_domain(application_id)[0] }}"
|
||||
--url="{{ web_protocol }}://{{ domains | get_domain(application_id) }}"
|
||||
--title="{{ applications[application_id].title }}"
|
||||
--admin_user="{{ applications[application_id].users.administrator.username }}"
|
||||
--admin_password="{{ applications[application_id].credentials.administrator_password }}"
|
||||
|
Reference in New Issue
Block a user