""" 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 self.bar_length = 8 # Pattern length defines a "bar" for volume patterns # 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_bar_length(self, length: int): """Set bar length (pattern length) for volume patterns""" self.bar_length = max(1, min(16, length)) 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 - completes one cycle per bar cycle_rate = (2 * math.pi) / self.bar_length # Complete cycle over bar length cycle = math.sin(self.pattern_position * cycle_rate) * 0.5 + 0.5 return cycle elif self.current_pattern == "breathing": # Smooth breathing rhythm - 2 cycles per bar breathing_rate = (4 * math.pi) / self.bar_length # Two breaths per bar cycle = math.sin(self.pattern_position * breathing_rate) * 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 over bar length, then fade back build_cycle_length = self.bar_length * 2 # Build up and fade down over 2 bars build_progress = (self.pattern_position % build_cycle_length) / build_cycle_length * 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 over bar length fade_cycle_progress = (self.pattern_position % self.bar_length) / self.bar_length fade_progress = 1.0 - fade_cycle_progress return fade_progress elif self.current_pattern == "pulse": # Sharp rhythmic pulses - 4 pulses per bar pulse_rate = (8 * math.pi) / self.bar_length # 4 complete cycles per bar pulse = math.sin(self.pattern_position * pulse_rate) return 1.0 if pulse > 0.8 else 0.3 elif self.current_pattern == "alternating": # Alternate between high and low based on bar subdivisions step_within_bar = int(self.pattern_position) % self.bar_length return 1.0 if step_within_bar % 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()