Compare commits
No commits in common. "f47b01eff6e2dd03f65bde8307b67bcc0797a55e" and "3ea19fbb999d5c940577874189c3c8ec7892b7f3" have entirely different histories.
f47b01eff6
...
3ea19fbb99
@ -1,166 +0,0 @@
|
|||||||
# Sound Effects Configuration
|
|
||||||
|
|
||||||
# Audio library path
|
|
||||||
audio:
|
|
||||||
sounds_directory: "/home/seb/saltybot-data/sounds" # Audio files location
|
|
||||||
supported_formats: [".wav", ".ogg"] # Supported file formats
|
|
||||||
default_format: "wav" # Default synthesis format
|
|
||||||
|
|
||||||
# Volume control
|
|
||||||
volume:
|
|
||||||
default_volume: 0.7 # Default playback volume (0.0-1.0)
|
|
||||||
quiet_mode_volume: 0.4 # Quiet mode volume reduction
|
|
||||||
night_mode_volume: 0.3 # Night mode volume reduction
|
|
||||||
max_volume: 1.0 # Absolute maximum
|
|
||||||
min_volume: 0.0 # Absolute minimum
|
|
||||||
fade_in_time: 0.1 # Fade in duration (seconds)
|
|
||||||
fade_out_time: 0.2 # Fade out duration (seconds)
|
|
||||||
|
|
||||||
# Event definitions
|
|
||||||
events:
|
|
||||||
boot_complete:
|
|
||||||
description: "Robot startup chime"
|
|
||||||
filename: "boot_complete.wav"
|
|
||||||
duration: 1.2
|
|
||||||
priority: 100
|
|
||||||
repeat: false
|
|
||||||
|
|
||||||
person_detected:
|
|
||||||
description: "Subtle detection sound"
|
|
||||||
filename: "person_detected.wav"
|
|
||||||
duration: 0.5
|
|
||||||
priority: 20
|
|
||||||
repeat: false
|
|
||||||
|
|
||||||
wake_word:
|
|
||||||
description: "Wake word activation beep"
|
|
||||||
filename: "wake_word.wav"
|
|
||||||
duration: 0.3
|
|
||||||
priority: 80
|
|
||||||
repeat: false
|
|
||||||
|
|
||||||
low_battery:
|
|
||||||
description: "Low battery warning"
|
|
||||||
filename: "low_battery.wav"
|
|
||||||
duration: 1.0
|
|
||||||
priority: 90
|
|
||||||
repeat: false
|
|
||||||
|
|
||||||
obstacle_close:
|
|
||||||
description: "Proximity warning beep"
|
|
||||||
filename: "obstacle_close.wav"
|
|
||||||
duration: 0.4
|
|
||||||
priority: 70
|
|
||||||
repeat: false
|
|
||||||
|
|
||||||
trick_complete:
|
|
||||||
description: "Trick execution jingle"
|
|
||||||
filename: "trick_complete.wav"
|
|
||||||
duration: 1.5
|
|
||||||
priority: 50
|
|
||||||
repeat: false
|
|
||||||
|
|
||||||
error:
|
|
||||||
description: "Error descending tone"
|
|
||||||
filename: "error.wav"
|
|
||||||
duration: 0.8
|
|
||||||
priority: 85
|
|
||||||
repeat: false
|
|
||||||
|
|
||||||
charging_start:
|
|
||||||
description: "Charging initiated power-up"
|
|
||||||
filename: "charging_start.wav"
|
|
||||||
duration: 1.0
|
|
||||||
priority: 60
|
|
||||||
repeat: false
|
|
||||||
|
|
||||||
geofence_warning:
|
|
||||||
description: "Geofence boundary alert"
|
|
||||||
filename: "geofence_warning.wav"
|
|
||||||
duration: 0.9
|
|
||||||
priority: 75
|
|
||||||
repeat: false
|
|
||||||
|
|
||||||
# Programmatic sound generation
|
|
||||||
synthesis:
|
|
||||||
enabled: true # Enable fallback synthesis if files missing
|
|
||||||
sample_rate: 16000 # Sample rate (Hz)
|
|
||||||
bit_depth: 16 # Bit depth (16 or 24)
|
|
||||||
|
|
||||||
# Default tones for each event
|
|
||||||
defaults:
|
|
||||||
boot_complete:
|
|
||||||
type: "chime" # Ascending notes
|
|
||||||
duration: 1.2
|
|
||||||
frequencies: [523.25, 659.25, 783.99] # C5, E5, G5
|
|
||||||
|
|
||||||
person_detected:
|
|
||||||
type: "sine"
|
|
||||||
duration: 0.5
|
|
||||||
frequency: 800 # Hz
|
|
||||||
|
|
||||||
wake_word:
|
|
||||||
type: "beep"
|
|
||||||
duration: 0.3
|
|
||||||
frequency: 1000
|
|
||||||
|
|
||||||
low_battery:
|
|
||||||
type: "warning" # Double beep
|
|
||||||
duration: 1.0
|
|
||||||
frequency: 880
|
|
||||||
|
|
||||||
obstacle_close:
|
|
||||||
type: "proximity" # Rapid beeps
|
|
||||||
duration: 0.4
|
|
||||||
frequency: 1200
|
|
||||||
pulse_rate: 5 # Pulses per second
|
|
||||||
|
|
||||||
trick_complete:
|
|
||||||
type: "jingle" # Ascending arpeggio
|
|
||||||
duration: 1.5
|
|
||||||
frequencies: [523.25, 659.25, 783.99, 987.77] # C5, E5, G5, B5
|
|
||||||
|
|
||||||
error:
|
|
||||||
type: "descending" # Descending tone
|
|
||||||
duration: 0.8
|
|
||||||
frequencies: [800, 600, 400]
|
|
||||||
|
|
||||||
charging_start:
|
|
||||||
type: "power_up" # Rising tone
|
|
||||||
duration: 1.0
|
|
||||||
start_freq: 400
|
|
||||||
end_freq: 1200
|
|
||||||
|
|
||||||
geofence_warning:
|
|
||||||
type: "alert" # Repeating tone
|
|
||||||
duration: 0.9
|
|
||||||
frequency: 950
|
|
||||||
|
|
||||||
# Priority queue settings
|
|
||||||
queue:
|
|
||||||
max_size: 20 # Maximum queued events
|
|
||||||
overlap_allowed: true # Allow sounds to overlap
|
|
||||||
ducking: false # Reduce volume of existing sounds for new ones
|
|
||||||
duck_amount: 0.5 # How much to reduce (0.0-1.0)
|
|
||||||
|
|
||||||
# Topic subscriptions
|
|
||||||
subscriptions:
|
|
||||||
boot_complete: "/saltybot/system/boot_complete"
|
|
||||||
person_detected: "/saltybot/perception/person_detected"
|
|
||||||
wake_word: "/saltybot/voice/wake_word"
|
|
||||||
low_battery: "/saltybot/battery/low_warning"
|
|
||||||
obstacle_close: "/saltybot/obstacle/close"
|
|
||||||
trick_complete: "/saltybot/tricks/complete"
|
|
||||||
error: "/saltybot/system/error"
|
|
||||||
charging_start: "/saltybot/charging/started"
|
|
||||||
geofence_warning: "/saltybot/geofence/warning"
|
|
||||||
|
|
||||||
# Publishing
|
|
||||||
publications:
|
|
||||||
sound_playing: "/saltybot/sound_playing"
|
|
||||||
|
|
||||||
# Mode settings
|
|
||||||
modes:
|
|
||||||
quiet_mode_enabled: false # Global quiet mode
|
|
||||||
night_mode_enabled: false # Night mode affects volume
|
|
||||||
mute_all: false # Mute all sounds
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
"""Launch file for Sound Effects node."""
|
|
||||||
|
|
||||||
from launch import LaunchDescription
|
|
||||||
from launch_ros.actions import Node
|
|
||||||
from launch.substitutions import LaunchConfiguration
|
|
||||||
from launch.actions import DeclareLaunchArgument
|
|
||||||
from ament_index_python.packages import get_package_share_directory
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def generate_launch_description():
|
|
||||||
"""Generate launch description."""
|
|
||||||
package_dir = get_package_share_directory("saltybot_sound_effects")
|
|
||||||
config_file = str(Path(package_dir) / "config" / "sound_config.yaml")
|
|
||||||
|
|
||||||
# Launch arguments
|
|
||||||
config_arg = DeclareLaunchArgument(
|
|
||||||
"config_file",
|
|
||||||
default_value=config_file,
|
|
||||||
description="Path to sound effects configuration file",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sound Effects node
|
|
||||||
sound_node = Node(
|
|
||||||
package="saltybot_sound_effects",
|
|
||||||
executable="sound_effects_node",
|
|
||||||
name="sound_effects",
|
|
||||||
parameters=[
|
|
||||||
{
|
|
||||||
"config_file": LaunchConfiguration("config_file"),
|
|
||||||
}
|
|
||||||
],
|
|
||||||
output="screen",
|
|
||||||
)
|
|
||||||
|
|
||||||
return LaunchDescription([config_arg, sound_node])
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
|
||||||
<package format="3">
|
|
||||||
<name>saltybot_sound_effects</name>
|
|
||||||
<version>0.1.0</version>
|
|
||||||
<description>
|
|
||||||
Sound effects library for SaltyBot. Event-driven audio feedback system with
|
|
||||||
priority queue, volume control, and programmatic sound generation. Supports
|
|
||||||
WAV/OGG formats with fallback synthesis if files missing.
|
|
||||||
</description>
|
|
||||||
<maintainer email="sl-mechanical@saltylab.local">sl-mechanical</maintainer>
|
|
||||||
<license>MIT</license>
|
|
||||||
|
|
||||||
<depend>rclpy</depend>
|
|
||||||
<depend>std_msgs</depend>
|
|
||||||
|
|
||||||
<buildtool_depend>ament_python</buildtool_depend>
|
|
||||||
|
|
||||||
<test_depend>ament_copyright</test_depend>
|
|
||||||
<test_depend>ament_flake8</test_depend>
|
|
||||||
<test_depend>ament_pep257</test_depend>
|
|
||||||
<test_depend>python3-pytest</test_depend>
|
|
||||||
|
|
||||||
<export>
|
|
||||||
<build_type>ament_python</build_type>
|
|
||||||
</export>
|
|
||||||
</package>
|
|
||||||
@ -1,337 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Sound Effects Library for SaltyBot.
|
|
||||||
|
|
||||||
Event-driven audio feedback system with priority queue, volume control,
|
|
||||||
and programmatic sound generation. Supports WAV/OGG formats with fallback
|
|
||||||
synthesis if files missing.
|
|
||||||
|
|
||||||
Subscribed topics (dynamic based on config):
|
|
||||||
/saltybot/system/boot_complete (std_msgs/Bool)
|
|
||||||
/saltybot/perception/person_detected (std_msgs/Bool)
|
|
||||||
/saltybot/voice/wake_word (std_msgs/Bool)
|
|
||||||
... (see config for all)
|
|
||||||
|
|
||||||
Published topics:
|
|
||||||
/saltybot/sound_playing (std_msgs/String) - JSON current sound status
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
from pathlib import Path
|
|
||||||
from dataclasses import dataclass, asdict
|
|
||||||
from typing import Dict, Optional, List, Callable
|
|
||||||
from enum import Enum
|
|
||||||
from collections import deque
|
|
||||||
from queue import PriorityQueue
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
import numpy as np
|
|
||||||
from scipy import signal
|
|
||||||
from scipy.io import wavfile
|
|
||||||
import rclpy
|
|
||||||
from rclpy.node import Node
|
|
||||||
from std_msgs.msg import String, Bool, Float32
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SoundEvent:
|
|
||||||
"""Sound event with priority."""
|
|
||||||
name: str
|
|
||||||
filename: str
|
|
||||||
duration: float
|
|
||||||
priority: int
|
|
||||||
timestamp: float
|
|
||||||
|
|
||||||
def __lt__(self, other):
|
|
||||||
"""Compare by priority (higher priority first)."""
|
|
||||||
return self.priority > other.priority # Inverted for max-heap
|
|
||||||
|
|
||||||
|
|
||||||
class SoundSynthesizer:
|
|
||||||
"""Programmatically generate sounds if files missing."""
|
|
||||||
|
|
||||||
def __init__(self, sample_rate: int = 16000):
|
|
||||||
"""Initialize synthesizer.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sample_rate: Sample rate in Hz
|
|
||||||
"""
|
|
||||||
self.sample_rate = sample_rate
|
|
||||||
|
|
||||||
def generate_sine(self, frequency: float, duration: float,
|
|
||||||
amplitude: float = 0.3) -> np.ndarray:
|
|
||||||
"""Generate sine wave."""
|
|
||||||
t = np.linspace(0, duration, int(self.sample_rate * duration))
|
|
||||||
return (amplitude * np.sin(2 * np.pi * frequency * t)).astype(np.float32)
|
|
||||||
|
|
||||||
def generate_chime(self, frequencies: List[float], duration: float) -> np.ndarray:
|
|
||||||
"""Generate ascending chime."""
|
|
||||||
num_notes = len(frequencies)
|
|
||||||
note_duration = duration / num_notes
|
|
||||||
samples = []
|
|
||||||
|
|
||||||
for freq in frequencies:
|
|
||||||
note = self.generate_sine(freq, note_duration)
|
|
||||||
# Add fade envelope
|
|
||||||
envelope = np.linspace(0, 1, len(note) // 2).tolist() + np.linspace(1, 0, len(note) // 2).tolist()
|
|
||||||
note = note * np.array(envelope)
|
|
||||||
samples.append(note)
|
|
||||||
|
|
||||||
return np.concatenate(samples)
|
|
||||||
|
|
||||||
def generate_descending(self, frequencies: List[float],
|
|
||||||
duration: float) -> np.ndarray:
|
|
||||||
"""Generate descending tone."""
|
|
||||||
num_notes = len(frequencies)
|
|
||||||
note_duration = duration / num_notes
|
|
||||||
samples = []
|
|
||||||
|
|
||||||
for freq in frequencies:
|
|
||||||
note = self.generate_sine(freq, note_duration)
|
|
||||||
envelope = signal.windows.hann(len(note))
|
|
||||||
note = note * envelope
|
|
||||||
samples.append(note)
|
|
||||||
|
|
||||||
return np.concatenate(samples)
|
|
||||||
|
|
||||||
def generate_power_up(self, start_freq: float, end_freq: float,
|
|
||||||
duration: float) -> np.ndarray:
|
|
||||||
"""Generate rising power-up tone."""
|
|
||||||
t = np.linspace(0, duration, int(self.sample_rate * duration))
|
|
||||||
freq = np.linspace(start_freq, end_freq, len(t))
|
|
||||||
phase = 2 * np.pi * np.cumsum(freq) / self.sample_rate
|
|
||||||
return (0.3 * np.sin(phase)).astype(np.float32)
|
|
||||||
|
|
||||||
def generate_beep(self, frequency: float, duration: float) -> np.ndarray:
|
|
||||||
"""Generate simple beep with envelope."""
|
|
||||||
t = np.linspace(0, duration, int(self.sample_rate * duration))
|
|
||||||
sine = 0.3 * np.sin(2 * np.pi * frequency * t)
|
|
||||||
envelope = signal.windows.hann(len(sine))
|
|
||||||
return (sine * envelope).astype(np.float32)
|
|
||||||
|
|
||||||
def generate_proximity(self, frequency: float, duration: float,
|
|
||||||
pulse_rate: float = 5) -> np.ndarray:
|
|
||||||
"""Generate rapid proximity beeps."""
|
|
||||||
pulse_duration = 1.0 / pulse_rate
|
|
||||||
samples = []
|
|
||||||
elapsed = 0
|
|
||||||
|
|
||||||
while elapsed < duration:
|
|
||||||
beep = self.generate_beep(frequency, min(pulse_duration / 2, duration - elapsed))
|
|
||||||
silence = np.zeros(int(self.sample_rate * pulse_duration / 2))
|
|
||||||
samples.append(np.concatenate([beep, silence]))
|
|
||||||
elapsed += pulse_duration
|
|
||||||
|
|
||||||
return np.concatenate(samples[:int(duration * pulse_rate)])
|
|
||||||
|
|
||||||
|
|
||||||
class SoundEffectsNode(Node):
|
|
||||||
"""ROS2 node for sound effects playback."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__("sound_effects")
|
|
||||||
|
|
||||||
# Load config
|
|
||||||
self.declare_parameter("config_file", "sound_config.yaml")
|
|
||||||
config_path = self.get_parameter("config_file").value
|
|
||||||
self.config = self._load_config(config_path)
|
|
||||||
|
|
||||||
# State
|
|
||||||
self.current_sound: Optional[SoundEvent] = None
|
|
||||||
self.sound_queue = PriorityQueue()
|
|
||||||
self.synthesizer = SoundSynthesizer(
|
|
||||||
self.config["synthesis"]["sample_rate"]
|
|
||||||
)
|
|
||||||
self.volume = self.config["volume"]["default_volume"]
|
|
||||||
self.is_playing = False
|
|
||||||
self.quiet_mode = False
|
|
||||||
self.night_mode = False
|
|
||||||
|
|
||||||
# Load sounds
|
|
||||||
self.sounds_cache: Dict[str, np.ndarray] = {}
|
|
||||||
self._load_sounds()
|
|
||||||
|
|
||||||
# Subscribe to event topics
|
|
||||||
self._subscribe_to_events()
|
|
||||||
|
|
||||||
# Publisher
|
|
||||||
self.sound_state_pub = self.create_publisher(
|
|
||||||
String, "/saltybot/sound_playing", 10
|
|
||||||
)
|
|
||||||
|
|
||||||
# Playback thread
|
|
||||||
self.playback_thread = threading.Thread(target=self._playback_loop, daemon=True)
|
|
||||||
self.playback_thread.start()
|
|
||||||
|
|
||||||
# Timer for queue processing
|
|
||||||
self.timer = self.create_timer(0.1, self._process_queue)
|
|
||||||
|
|
||||||
self.get_logger().info("Sound Effects node initialized")
|
|
||||||
|
|
||||||
def _load_config(self, config_path: str) -> Dict:
|
|
||||||
"""Load YAML configuration."""
|
|
||||||
try:
|
|
||||||
with open(config_path) as f:
|
|
||||||
return yaml.safe_load(f)
|
|
||||||
except FileNotFoundError:
|
|
||||||
self.get_logger().warn(f"Config not found: {config_path}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _load_sounds(self):
|
|
||||||
"""Load sound files and cache them."""
|
|
||||||
sounds_dir = Path(self.config.get("audio", {}).get("sounds_directory", "/tmp"))
|
|
||||||
|
|
||||||
for event_name, event_config in self.config.get("events", {}).items():
|
|
||||||
filename = event_config.get("filename")
|
|
||||||
if not filename:
|
|
||||||
continue
|
|
||||||
|
|
||||||
sound_path = sounds_dir / filename
|
|
||||||
if sound_path.exists():
|
|
||||||
try:
|
|
||||||
rate, data = wavfile.read(str(sound_path))
|
|
||||||
# Normalize to float
|
|
||||||
if data.dtype != np.float32:
|
|
||||||
data = data.astype(np.float32) / 32768.0
|
|
||||||
self.sounds_cache[event_name] = data
|
|
||||||
self.get_logger().info(f"Loaded sound: {event_name}")
|
|
||||||
except Exception as e:
|
|
||||||
self.get_logger().warn(f"Failed to load {filename}: {e}")
|
|
||||||
else:
|
|
||||||
self.get_logger().info(f"Sound file missing: {filename}, will use synthesis")
|
|
||||||
|
|
||||||
def _subscribe_to_events(self):
|
|
||||||
"""Subscribe to all event topics from config."""
|
|
||||||
for event_name, topic in self.config.get("subscriptions", {}).items():
|
|
||||||
def callback(msg, event=event_name):
|
|
||||||
if msg.data:
|
|
||||||
self._queue_sound(event)
|
|
||||||
|
|
||||||
self.create_subscription(Bool, topic, callback, 10)
|
|
||||||
|
|
||||||
def _queue_sound(self, event_name: str):
|
|
||||||
"""Queue a sound for playback."""
|
|
||||||
event_config = self.config.get("events", {}).get(event_name)
|
|
||||||
if not event_config:
|
|
||||||
return
|
|
||||||
|
|
||||||
event = SoundEvent(
|
|
||||||
name=event_name,
|
|
||||||
filename=event_config.get("filename", ""),
|
|
||||||
duration=event_config.get("duration", 1.0),
|
|
||||||
priority=event_config.get("priority", 50),
|
|
||||||
timestamp=time.time(),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sound_queue.put(event)
|
|
||||||
self.get_logger().info(f"Queued sound: {event_name} (priority: {event.priority})")
|
|
||||||
|
|
||||||
def _process_queue(self):
|
|
||||||
"""Process sound queue."""
|
|
||||||
if self.is_playing:
|
|
||||||
return # Still playing current sound
|
|
||||||
|
|
||||||
if not self.sound_queue.empty():
|
|
||||||
try:
|
|
||||||
self.current_sound = self.sound_queue.get_nowait()
|
|
||||||
self.is_playing = True
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _playback_loop(self):
|
|
||||||
"""Background thread for audio playback."""
|
|
||||||
while True:
|
|
||||||
if self.current_sound and self.is_playing:
|
|
||||||
audio_data = self._get_audio_data(self.current_sound.name)
|
|
||||||
if audio_data is not None:
|
|
||||||
self._play_audio(audio_data, self.current_sound.name)
|
|
||||||
|
|
||||||
self.is_playing = False
|
|
||||||
self.current_sound = None
|
|
||||||
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
def _get_audio_data(self, event_name: str) -> Optional[np.ndarray]:
|
|
||||||
"""Get audio data for event (cached or synthesized)."""
|
|
||||||
if event_name in self.sounds_cache:
|
|
||||||
return self.sounds_cache[event_name]
|
|
||||||
|
|
||||||
if not self.config.get("synthesis", {}).get("enabled", True):
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Synthesize default sound
|
|
||||||
return self._synthesize_sound(event_name)
|
|
||||||
|
|
||||||
def _synthesize_sound(self, event_name: str) -> Optional[np.ndarray]:
|
|
||||||
"""Synthesize sound programmatically."""
|
|
||||||
defaults = self.config.get("synthesis", {}).get("defaults", {})
|
|
||||||
event_config = defaults.get(event_name)
|
|
||||||
|
|
||||||
if not event_config:
|
|
||||||
return None
|
|
||||||
|
|
||||||
sound_type = event_config.get("type", "sine")
|
|
||||||
duration = event_config.get("duration", 1.0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if sound_type == "sine":
|
|
||||||
freq = event_config.get("frequency", 440)
|
|
||||||
return self.synthesizer.generate_sine(freq, duration)
|
|
||||||
elif sound_type == "chime":
|
|
||||||
freqs = event_config.get("frequencies", [523.25, 659.25])
|
|
||||||
return self.synthesizer.generate_chime(freqs, duration)
|
|
||||||
elif sound_type == "descending":
|
|
||||||
freqs = event_config.get("frequencies", [800, 600, 400])
|
|
||||||
return self.synthesizer.generate_descending(freqs, duration)
|
|
||||||
elif sound_type == "power_up":
|
|
||||||
start = event_config.get("start_freq", 400)
|
|
||||||
end = event_config.get("end_freq", 1200)
|
|
||||||
return self.synthesizer.generate_power_up(start, end, duration)
|
|
||||||
elif sound_type in ["beep", "warning", "alert"]:
|
|
||||||
freq = event_config.get("frequency", 1000)
|
|
||||||
return self.synthesizer.generate_beep(freq, duration)
|
|
||||||
elif sound_type == "proximity":
|
|
||||||
freq = event_config.get("frequency", 1200)
|
|
||||||
pulse = event_config.get("pulse_rate", 5)
|
|
||||||
return self.synthesizer.generate_proximity(freq, duration, pulse)
|
|
||||||
elif sound_type == "jingle":
|
|
||||||
freqs = event_config.get("frequencies", [523.25, 659.25, 783.99])
|
|
||||||
return self.synthesizer.generate_chime(freqs, duration)
|
|
||||||
except Exception as e:
|
|
||||||
self.get_logger().warn(f"Sound synthesis failed for {event_name}: {e}")
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _play_audio(self, audio_data: np.ndarray, event_name: str):
|
|
||||||
"""Play audio data (mock implementation)."""
|
|
||||||
# In real implementation, would use pygame, pyaudio, or similar
|
|
||||||
duration = len(audio_data) / self.config["synthesis"]["sample_rate"]
|
|
||||||
|
|
||||||
state = {
|
|
||||||
"event": event_name,
|
|
||||||
"playing": True,
|
|
||||||
"duration": duration,
|
|
||||||
"volume": self.volume,
|
|
||||||
}
|
|
||||||
self.sound_state_pub.publish(String(data=json.dumps(state)))
|
|
||||||
|
|
||||||
# Simulate playback
|
|
||||||
time.sleep(duration)
|
|
||||||
|
|
||||||
state["playing"] = False
|
|
||||||
self.sound_state_pub.publish(String(data=json.dumps(state)))
|
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
|
||||||
"""Main entry point."""
|
|
||||||
rclpy.init(args=args)
|
|
||||||
node = SoundEffectsNode()
|
|
||||||
rclpy.spin(node)
|
|
||||||
node.destroy_node()
|
|
||||||
rclpy.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
[develop]
|
|
||||||
script_dir=$base/lib/saltybot_sound_effects
|
|
||||||
[bdist_wheel]
|
|
||||||
universal=0
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
from setuptools import setup
|
|
||||||
|
|
||||||
package_name = "saltybot_sound_effects"
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name=package_name,
|
|
||||||
version="0.1.0",
|
|
||||||
packages=[package_name],
|
|
||||||
data_files=[
|
|
||||||
("share/ament_index/resource_index/packages", [f"resource/{package_name}"]),
|
|
||||||
(f"share/{package_name}", ["package.xml"]),
|
|
||||||
(f"share/{package_name}/launch", ["launch/sound_effects.launch.py"]),
|
|
||||||
(f"share/{package_name}/config", ["config/sound_config.yaml"]),
|
|
||||||
],
|
|
||||||
install_requires=["setuptools", "pyyaml", "numpy", "scipy"],
|
|
||||||
zip_safe=True,
|
|
||||||
maintainer="sl-mechanical",
|
|
||||||
maintainer_email="sl-mechanical@saltylab.local",
|
|
||||||
description=(
|
|
||||||
"Sound effects library: event-driven audio feedback, priority queue, "
|
|
||||||
"programmatic synthesis, volume control"
|
|
||||||
),
|
|
||||||
license="MIT",
|
|
||||||
tests_require=["pytest"],
|
|
||||||
entry_points={
|
|
||||||
"console_scripts": [
|
|
||||||
"sound_effects_node = saltybot_sound_effects.sound_effects_node:main",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
Loading…
x
Reference in New Issue
Block a user