You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

280 lines
12 KiB

"""
Volume Pattern Engine Module
Handles volume and velocity pattern generation for visual lighting effects.
Creates dynamic volume patterns that control both audio levels and lighting brightness.
"""
import math
import random
from typing import Dict, List, Tuple, Optional
from PyQt5.QtCore import QObject, pyqtSignal
class VolumePatternEngine(QObject):
"""
Generates volume and velocity patterns for enhanced visual effects.
Controls both MIDI channel volume (CC7) and note velocity for brightness control.
"""
# Signals for GUI updates
pattern_changed = pyqtSignal(str) # pattern_name
volume_updated = pyqtSignal(int, float) # channel, volume (0.0-1.0)
# Pattern types available
PATTERN_TYPES = [
"static", "swell", "breathing", "wave", "build", "fade",
"pulse", "alternating", "stutter", "cascade", "ripple",
"random_sparkle", "spotlight", "bounce_volume"
]
def __init__(self):
super().__init__()
# Current pattern settings
self.current_pattern = "static"
self.pattern_speed = 1.0 # Speed multiplier
self.pattern_intensity = 1.0 # Intensity multiplier
# Position tracking for patterns
self.pattern_position = 0.0
self.pattern_direction = 1 # 1 for forward, -1 for reverse
# Volume ranges per channel {channel: (min, max)}
self.channel_volume_ranges: Dict[int, Tuple[float, float]] = {}
self.velocity_ranges: Dict[int, Tuple[int, int]] = {}
# Global ranges (applied to all channels if no individual range set)
self.global_volume_range = (0.2, 1.0) # 20% to 100%
self.global_velocity_range = (40, 127) # MIDI velocity range
# Pattern state
self.pattern_phases: Dict[int, float] = {} # Per-channel phase offsets
self.random_states: Dict[int, float] = {} # For random patterns
# Initialize random states for channels
for channel in range(1, 17):
self.pattern_phases[channel] = random.random() * 2 * math.pi
self.random_states[channel] = random.random()
def set_pattern(self, pattern_name: str) -> bool:
"""Set the current volume pattern"""
if pattern_name in self.PATTERN_TYPES:
self.current_pattern = pattern_name
self.pattern_changed.emit(pattern_name)
return True
return False
def set_pattern_speed(self, speed: float):
"""Set pattern speed multiplier (0.1 to 5.0)"""
self.pattern_speed = max(0.1, min(5.0, speed))
def set_pattern_intensity(self, intensity: float):
"""Set pattern intensity multiplier (0.0 to 2.0)"""
self.pattern_intensity = max(0.0, min(2.0, intensity))
def set_channel_volume_range(self, channel: int, min_vol: float, max_vol: float):
"""Set volume range for a specific channel (0.0 to 1.0)"""
min_vol = max(0.0, min(1.0, min_vol))
max_vol = max(min_vol, min(1.0, max_vol))
self.channel_volume_ranges[channel] = (min_vol, max_vol)
def set_velocity_range(self, channel: int, min_vel: int, max_vel: int):
"""Set velocity range for a specific channel (0-127)"""
min_vel = max(0, min(127, min_vel))
max_vel = max(min_vel, min(127, max_vel))
self.velocity_ranges[channel] = (min_vel, max_vel)
def set_global_ranges(self, min_vol: float, max_vol: float, min_vel: int, max_vel: int):
"""Set global volume and velocity ranges"""
self.global_volume_range = (max(0.0, min(1.0, min_vol)), max(0.0, min(1.0, max_vol)))
self.global_velocity_range = (max(0, min(127, min_vel)), max(0, min(127, max_vel)))
def get_channel_volume_range(self, channel: int) -> Tuple[float, float]:
"""Get volume range for a channel (uses global if not set individually)"""
return self.channel_volume_ranges.get(channel, self.global_volume_range)
def get_velocity_range(self, channel: int) -> Tuple[int, int]:
"""Get velocity range for a channel (uses global if not set individually)"""
return self.velocity_ranges.get(channel, self.global_velocity_range)
def update_pattern(self, delta_time: float):
"""Update pattern position based on elapsed time"""
self.pattern_position += delta_time * self.pattern_speed
def get_channel_volume(self, channel: int, active_channel_count: int = 8) -> float:
"""
Calculate current volume for a channel based on active pattern.
Returns volume as float 0.0 to 1.0
"""
min_vol, max_vol = self.get_channel_volume_range(channel)
# Get base pattern value (0.0 to 1.0)
pattern_value = self._calculate_pattern_value(channel, active_channel_count)
# Apply intensity
pattern_value = pattern_value * self.pattern_intensity
pattern_value = max(0.0, min(1.0, pattern_value))
# Scale to channel's volume range
volume = min_vol + (max_vol - min_vol) * pattern_value
return volume
def get_note_velocity(self, base_velocity: int, channel: int, active_channel_count: int = 8) -> int:
"""
Calculate note velocity based on volume pattern and base velocity.
Returns MIDI velocity (0-127)
"""
min_vel, max_vel = self.get_velocity_range(channel)
# Get pattern influence (0.0 to 1.0)
pattern_value = self._calculate_pattern_value(channel, active_channel_count)
pattern_value = pattern_value * self.pattern_intensity
pattern_value = max(0.0, min(1.0, pattern_value))
# Blend base velocity with pattern
pattern_velocity = min_vel + (max_vel - min_vel) * pattern_value
# Combine with base velocity (weighted average)
final_velocity = int((base_velocity + pattern_velocity) / 2)
return max(0, min(127, final_velocity))
def _calculate_pattern_value(self, channel: int, active_channel_count: int) -> float:
"""Calculate raw pattern value (0.0 to 1.0) for a channel"""
if self.current_pattern == "static":
return 1.0
elif self.current_pattern == "swell":
# Gradual swell up and down
cycle = math.sin(self.pattern_position * 0.5) * 0.5 + 0.5
return cycle
elif self.current_pattern == "breathing":
# Smooth breathing rhythm
cycle = math.sin(self.pattern_position) * 0.5 + 0.5
return 0.3 + cycle * 0.7 # Keep minimum at 30%
elif self.current_pattern == "wave":
# Sine wave across channels
phase_offset = (channel - 1) * (2 * math.pi / active_channel_count)
wave = math.sin(self.pattern_position + phase_offset) * 0.5 + 0.5
return wave
elif self.current_pattern == "build":
# Gradual crescendo
build_progress = (self.pattern_position * 0.1) % 2.0
if build_progress > 1.0:
build_progress = 2.0 - build_progress # Fade back down
return build_progress
elif self.current_pattern == "fade":
# Gradual diminuendo
fade_progress = 1.0 - ((self.pattern_position * 0.1) % 1.0)
return fade_progress
elif self.current_pattern == "pulse":
# Sharp rhythmic pulses
pulse = math.sin(self.pattern_position * 2)
return 1.0 if pulse > 0.8 else 0.3
elif self.current_pattern == "alternating":
# Alternate between high and low
return 1.0 if int(self.pattern_position) % 2 == 0 else 0.3
elif self.current_pattern == "stutter":
# Rapid volume changes
stutter = math.sin(self.pattern_position * 8) * 0.5 + 0.5
return stutter
elif self.current_pattern == "cascade":
# Volume cascades across channels
return self._cascade_pattern(channel, active_channel_count)
elif self.current_pattern == "ripple":
# Ripple effect from center
return self._ripple_pattern(channel, active_channel_count)
elif self.current_pattern == "random_sparkle":
# Random sparkle effect
return self._random_sparkle_pattern(channel)
elif self.current_pattern == "spotlight":
# Spotlight effect - one channel bright, others dim
return self._spotlight_pattern(channel, active_channel_count)
elif self.current_pattern == "bounce_volume":
# Volume follows bounce pattern
return self._bounce_volume_pattern(channel, active_channel_count)
return 1.0 # Default fallback
def _cascade_pattern(self, channel: int, active_channel_count: int) -> float:
"""Volume cascade across channels"""
cascade_position = (self.pattern_position * 0.5) % active_channel_count
distance = min(
abs(channel - 1 - cascade_position),
active_channel_count - abs(channel - 1 - cascade_position)
)
return max(0.2, 1.0 - (distance / active_channel_count) * 0.8)
def _ripple_pattern(self, channel: int, active_channel_count: int) -> float:
"""Ripple effect from center"""
center = active_channel_count / 2
distance = abs(channel - 1 - center)
ripple_phase = self.pattern_position - distance * 0.5
ripple = math.sin(ripple_phase) * 0.5 + 0.5
return max(0.2, ripple)
def _random_sparkle_pattern(self, channel: int) -> float:
"""Random sparkle effect"""
# Update random state periodically
if int(self.pattern_position * 4) % 8 == 0:
self.random_states[channel] = random.random()
base_random = self.random_states[channel]
sparkle_threshold = 0.7
if base_random > sparkle_threshold:
# Sparkle! Add some randomness to timing
sparkle_intensity = (base_random - sparkle_threshold) / (1.0 - sparkle_threshold)
return 0.3 + sparkle_intensity * 0.7
else:
return 0.2 + base_random * 0.3
def _spotlight_pattern(self, channel: int, active_channel_count: int) -> float:
"""Spotlight effect - one channel bright, others dim"""
spotlight_channel = int(self.pattern_position * 0.3) % active_channel_count + 1
if channel == spotlight_channel:
return 1.0
else:
return 0.2
def _bounce_volume_pattern(self, channel: int, active_channel_count: int) -> float:
"""Volume follows bounce pattern between first and last channels"""
bounce_position = (self.pattern_position * 0.8) % (2 * (active_channel_count - 1))
if bounce_position <= active_channel_count - 1:
target_channel = bounce_position + 1
else:
target_channel = 2 * active_channel_count - bounce_position - 1
distance = abs(channel - target_channel)
return max(0.3, 1.0 - distance * 0.3)
def get_all_channel_volumes(self, active_channel_count: int) -> Dict[int, float]:
"""Get volume values for all active channels"""
volumes = {}
for channel in range(1, active_channel_count + 1):
volume = self.get_channel_volume(channel, active_channel_count)
volumes[channel] = volume
self.volume_updated.emit(channel, volume)
return volumes
def reset_pattern(self):
"""Reset pattern to beginning"""
self.pattern_position = 0.0
for channel in range(1, 17):
self.pattern_phases[channel] = random.random() * 2 * math.pi
self.random_states[channel] = random.random()