midi_drums
MIDI Drums - Comprehensive drum track generation system.
1"""MIDI Drums - Comprehensive drum track generation system.""" 2 3from midi_drums.core.engine import DrumGenerator 4from midi_drums.models.pattern import Beat, Pattern, TimeSignature 5from midi_drums.models.song import GenerationParameters, Section, Song 6 7__version__ = "1.0.0" 8__all__ = [ 9 "DrumGenerator", 10 "Pattern", 11 "Beat", 12 "TimeSignature", 13 "Song", 14 "Section", 15 "GenerationParameters", 16]
16class DrumGenerator: 17 """Main drum generation engine.""" 18 19 def __init__(self, config_path: Path | None = None): 20 """Initialize drum generator with optional configuration.""" 21 self.plugin_manager = PluginManager() 22 self.drum_kit = DrumKit.create_ezdrummer3_kit() 23 self.midi_engine = MIDIEngine(self.drum_kit) 24 25 # Load plugins 26 self._load_plugins() 27 28 def _load_plugins(self) -> None: 29 """Load all available plugins.""" 30 try: 31 self.plugin_manager.discover_plugins() 32 logger.info( 33 f"Loaded genres: {self.plugin_manager.get_available_genres()}" 34 ) 35 logger.info( 36 f"Loaded drummers: " 37 f"{self.plugin_manager.get_available_drummers()}" 38 ) 39 except Exception as e: 40 logger.error(f"Failed to load plugins: {e}") 41 42 def create_song( 43 self, 44 genre: str, 45 style: str = "default", 46 tempo: int = 120, 47 structure: list[tuple[str, int]] | None = None, 48 drum_kit: DrumKit | None = None, 49 **kwargs, 50 ) -> Song: 51 """Create a complete song structure. 52 53 Args: 54 genre: Genre name (e.g., 'metal', 'rock', 'jazz') 55 style: Style within genre (e.g., 'death', 'power' for metal) 56 tempo: Tempo in BPM 57 structure: List of (section_name, bars) tuples. If None, uses 58 default structure. 59 drum_kit: Optional DrumKit for MIDI mapping. If None, uses 60 current kit. 61 **kwargs: Additional parameters for GenerationParameters 62 63 Returns: 64 Complete Song object with generated patterns 65 """ 66 # Update MIDI engine if new drum kit provided 67 if drum_kit: 68 self.midi_engine = MIDIEngine(drum_kit) 69 self.drum_kit = drum_kit 70 71 # Create generation parameters 72 params = GenerationParameters(genre=genre, style=style, **kwargs) 73 74 # Use default structure if none provided 75 if structure is None: 76 structure = [ 77 ("intro", 4), 78 ("verse", 8), 79 ("chorus", 8), 80 ("verse", 8), 81 ("chorus", 8), 82 ("bridge", 4), 83 ("chorus", 8), 84 ("outro", 4), 85 ] 86 87 # Create song with basic structure 88 song = Song( 89 name=f"{genre}_{style}_song", tempo=tempo, global_parameters=params 90 ) 91 92 # Generate patterns for each section 93 for section_name, bars in structure: 94 pattern = self.generate_pattern( 95 genre, section_name, bars, style=style, **kwargs 96 ) 97 if pattern: 98 section = Section(section_name, pattern, bars) 99 100 # Add variations and fills based on complexity 101 if params.complexity > 0.5: 102 variations = self._generate_variations(pattern, params) 103 section.variations.extend(variations) 104 105 fills = self._generate_fills(genre, params) 106 section.fills.extend(fills) 107 108 song.add_section(section) 109 else: 110 logger.warning( 111 f"Failed to generate pattern for {genre}/{section_name}" 112 ) 113 114 return song 115 116 def generate_pattern( 117 self, genre: str, section: str = "verse", bars: int = 4, **kwargs 118 ) -> Pattern | None: 119 """Generate a single pattern with optional genre context adaptation. 120 121 Args: 122 genre: Genre name 123 section: Section type 124 bars: Number of bars (for multi-bar patterns) 125 **kwargs: Additional generation parameters including: 126 - song_genre_context: Overall song genre for adaptation 127 - context_blend: Blend amount (0.0-1.0) 128 - drummer: Drummer style to apply 129 - humanization: Humanization amount 130 - etc. 131 132 Returns: 133 Generated Pattern or None if generation failed 134 135 Example: 136 # Generate progressive pattern adapted to metal context 137 pattern = generator.generate_pattern( 138 genre="metal", 139 style="progressive", 140 section="bridge", 141 song_genre_context="metal", 142 context_blend=0.3 143 ) 144 """ 145 # Create parameters 146 params = GenerationParameters(genre=genre, **kwargs) 147 148 # Generate base pattern 149 pattern = self.plugin_manager.generate_pattern(genre, section, params) 150 if not pattern: 151 return None 152 153 # Apply genre context blending if specified 154 if params.song_genre_context and params.context_blend > 0: 155 # Only blend if context genre is different from pattern genre 156 if params.song_genre_context != genre: 157 context_plugin = self.plugin_manager.get_genre_plugin( 158 params.song_genre_context 159 ) 160 genre_plugin = self.plugin_manager.get_genre_plugin(genre) 161 162 if context_plugin and genre_plugin: 163 context_profile = context_plugin.intensity_profile 164 pattern = genre_plugin.apply_context_blend( 165 pattern, context_profile, params.context_blend 166 ) 167 logger.debug( 168 f"Applied {params.song_genre_context} context " 169 f"(blend={params.context_blend}) to {genre} pattern" 170 ) 171 172 # Apply drummer style if specified 173 if params.drummer: 174 styled_pattern = self.plugin_manager.apply_drummer_style( 175 pattern, params.drummer 176 ) 177 if styled_pattern: 178 pattern = styled_pattern 179 180 # Apply humanization if requested 181 if params.humanization > 0: 182 timing_var = params.humanization * 0.05 # Scale to reasonable range 183 velocity_var = int(params.humanization * 20) 184 pattern = pattern.humanize(timing_var, velocity_var) 185 186 # Extend pattern for multiple bars if needed 187 if bars > 1: 188 pattern = self._extend_pattern_to_bars(pattern, bars) 189 190 return pattern 191 192 def apply_drummer_style( 193 self, pattern: Pattern, drummer: str 194 ) -> Pattern | None: 195 """Apply drummer-specific style modifications to a pattern.""" 196 return self.plugin_manager.apply_drummer_style(pattern, drummer) 197 198 def export_midi(self, song: Song, output_path: Path) -> None: 199 """Export song as MIDI file.""" 200 self.midi_engine.save_song_midi(song, output_path) 201 logger.info(f"Exported MIDI to: {output_path}") 202 203 def export_pattern_midi( 204 self, 205 pattern: Pattern, 206 output_path: Path, 207 tempo: int = 120, 208 drum_kit: DrumKit | None = None, 209 ) -> None: 210 """Export single pattern as MIDI file.""" 211 # Use provided drum kit or current one 212 engine = self.midi_engine 213 if drum_kit: 214 engine = MIDIEngine(drum_kit) 215 216 engine.save_pattern_midi(pattern, output_path, tempo) 217 logger.info(f"Exported pattern MIDI to: {output_path}") 218 219 def get_available_genres(self) -> list[str]: 220 """Get list of available genres.""" 221 return self.plugin_manager.get_available_genres() 222 223 def get_available_drummers(self) -> list[str]: 224 """Get list of available drummers.""" 225 return self.plugin_manager.get_available_drummers() 226 227 def get_styles_for_genre(self, genre: str) -> list[str]: 228 """Get available styles for a genre.""" 229 return self.plugin_manager.get_styles_for_genre(genre) 230 231 def get_song_info(self, song: Song) -> dict: 232 """Get comprehensive information about a song.""" 233 info = self.midi_engine.get_midi_info(song) 234 info.update( 235 { 236 "genre": ( 237 song.global_parameters.genre 238 if song.global_parameters 239 else "unknown" 240 ), 241 "style": ( 242 song.global_parameters.style 243 if song.global_parameters 244 else "default" 245 ), 246 "drummer": ( 247 song.global_parameters.drummer 248 if song.global_parameters 249 else None 250 ), 251 "sections_count": len(song.sections), 252 "unique_sections": list({s.name for s in song.sections}), 253 } 254 ) 255 return info 256 257 def set_drum_kit(self, kit: DrumKit) -> None: 258 """Set the drum kit configuration.""" 259 self.drum_kit = kit 260 self.midi_engine = MIDIEngine(kit) 261 262 def create_drum_kit(self, kit_type: str) -> DrumKit: 263 """Create a drum kit configuration by type.""" 264 kit_creators = { 265 "ezdrummer3": DrumKit.create_ezdrummer3_kit, 266 "metal": DrumKit.create_metal_kit, 267 "jazz": DrumKit.create_jazz_kit, 268 "standard": DrumKit.create_ezdrummer3_kit, # Alias 269 } 270 271 creator = kit_creators.get(kit_type.lower()) 272 if creator: 273 return creator() 274 else: 275 logger.warning(f"Unknown kit type: {kit_type}, using standard kit") 276 return DrumKit.create_ezdrummer3_kit() 277 278 # Private helper methods 279 def _generate_variations( 280 self, base_pattern: Pattern, params: GenerationParameters 281 ) -> list: 282 """Generate pattern variations based on complexity.""" 283 from midi_drums.models.song import PatternVariation 284 285 variations = [] 286 287 # Create a simplified variation 288 if params.complexity > 0.7: 289 simplified = base_pattern.copy() 290 simplified.name = f"{base_pattern.name}_simple" 291 292 # Remove some hi-hat hits for variation 293 simplified.beats = [ 294 beat 295 for beat in simplified.beats 296 if not ( 297 beat.instrument.name.endswith("HH") 298 and beat.position % 0.5 != 0 299 ) 300 ] 301 302 variations.append(PatternVariation(simplified, 0.3)) 303 304 return variations 305 306 def _generate_fills(self, genre: str, params: GenerationParameters) -> list: 307 """Generate fill patterns for the genre.""" 308 plugin = self.plugin_manager.registry.get_genre_plugin(genre) 309 if plugin: 310 return plugin.get_common_fills() 311 return [] 312 313 def _extend_pattern_to_bars(self, pattern: Pattern, bars: int) -> Pattern: 314 """Extend a pattern to span multiple bars.""" 315 if bars <= 1: 316 return pattern 317 318 extended_pattern = pattern.copy() 319 extended_pattern.name = f"{pattern.name}_{bars}bars" 320 321 original_beats = pattern.beats.copy() 322 beats_per_bar = pattern.time_signature.beats_per_bar 323 324 # Repeat pattern for additional bars with slight variations 325 for bar in range(1, bars): 326 bar_offset = bar * beats_per_bar 327 for beat in original_beats: 328 import random 329 330 from midi_drums.models.pattern import Beat 331 332 new_beat = Beat( 333 position=beat.position + bar_offset, 334 instrument=beat.instrument, 335 velocity=max( 336 1, min(127, beat.velocity + random.randint(-5, 5)) 337 ), # Slight variation with clamping 338 duration=beat.duration, 339 ghost_note=beat.ghost_note, 340 accent=beat.accent, 341 ) 342 extended_pattern.beats.append(new_beat) 343 344 return extended_pattern 345 346 @classmethod 347 def quick_generate( 348 cls, genre: str = "metal", style: str = "heavy", tempo: int = 155 349 ) -> Song: 350 """Quick song generation with sensible defaults. 351 352 This replicates the functionality of the original script. 353 """ 354 generator = cls() 355 return generator.create_song( 356 genre=genre, 357 style=style, 358 tempo=tempo, 359 complexity=0.7, 360 dynamics=0.6, 361 humanization=0.3, 362 )
Main drum generation engine.
19 def __init__(self, config_path: Path | None = None): 20 """Initialize drum generator with optional configuration.""" 21 self.plugin_manager = PluginManager() 22 self.drum_kit = DrumKit.create_ezdrummer3_kit() 23 self.midi_engine = MIDIEngine(self.drum_kit) 24 25 # Load plugins 26 self._load_plugins()
Initialize drum generator with optional configuration.
42 def create_song( 43 self, 44 genre: str, 45 style: str = "default", 46 tempo: int = 120, 47 structure: list[tuple[str, int]] | None = None, 48 drum_kit: DrumKit | None = None, 49 **kwargs, 50 ) -> Song: 51 """Create a complete song structure. 52 53 Args: 54 genre: Genre name (e.g., 'metal', 'rock', 'jazz') 55 style: Style within genre (e.g., 'death', 'power' for metal) 56 tempo: Tempo in BPM 57 structure: List of (section_name, bars) tuples. If None, uses 58 default structure. 59 drum_kit: Optional DrumKit for MIDI mapping. If None, uses 60 current kit. 61 **kwargs: Additional parameters for GenerationParameters 62 63 Returns: 64 Complete Song object with generated patterns 65 """ 66 # Update MIDI engine if new drum kit provided 67 if drum_kit: 68 self.midi_engine = MIDIEngine(drum_kit) 69 self.drum_kit = drum_kit 70 71 # Create generation parameters 72 params = GenerationParameters(genre=genre, style=style, **kwargs) 73 74 # Use default structure if none provided 75 if structure is None: 76 structure = [ 77 ("intro", 4), 78 ("verse", 8), 79 ("chorus", 8), 80 ("verse", 8), 81 ("chorus", 8), 82 ("bridge", 4), 83 ("chorus", 8), 84 ("outro", 4), 85 ] 86 87 # Create song with basic structure 88 song = Song( 89 name=f"{genre}_{style}_song", tempo=tempo, global_parameters=params 90 ) 91 92 # Generate patterns for each section 93 for section_name, bars in structure: 94 pattern = self.generate_pattern( 95 genre, section_name, bars, style=style, **kwargs 96 ) 97 if pattern: 98 section = Section(section_name, pattern, bars) 99 100 # Add variations and fills based on complexity 101 if params.complexity > 0.5: 102 variations = self._generate_variations(pattern, params) 103 section.variations.extend(variations) 104 105 fills = self._generate_fills(genre, params) 106 section.fills.extend(fills) 107 108 song.add_section(section) 109 else: 110 logger.warning( 111 f"Failed to generate pattern for {genre}/{section_name}" 112 ) 113 114 return song
Create a complete song structure.
Arguments:
- genre: Genre name (e.g., 'metal', 'rock', 'jazz')
- style: Style within genre (e.g., 'death', 'power' for metal)
- tempo: Tempo in BPM
- structure: List of (section_name, bars) tuples. If None, uses default structure.
- drum_kit: Optional DrumKit for MIDI mapping. If None, uses current kit.
- **kwargs: Additional parameters for GenerationParameters
Returns:
Complete Song object with generated patterns
116 def generate_pattern( 117 self, genre: str, section: str = "verse", bars: int = 4, **kwargs 118 ) -> Pattern | None: 119 """Generate a single pattern with optional genre context adaptation. 120 121 Args: 122 genre: Genre name 123 section: Section type 124 bars: Number of bars (for multi-bar patterns) 125 **kwargs: Additional generation parameters including: 126 - song_genre_context: Overall song genre for adaptation 127 - context_blend: Blend amount (0.0-1.0) 128 - drummer: Drummer style to apply 129 - humanization: Humanization amount 130 - etc. 131 132 Returns: 133 Generated Pattern or None if generation failed 134 135 Example: 136 # Generate progressive pattern adapted to metal context 137 pattern = generator.generate_pattern( 138 genre="metal", 139 style="progressive", 140 section="bridge", 141 song_genre_context="metal", 142 context_blend=0.3 143 ) 144 """ 145 # Create parameters 146 params = GenerationParameters(genre=genre, **kwargs) 147 148 # Generate base pattern 149 pattern = self.plugin_manager.generate_pattern(genre, section, params) 150 if not pattern: 151 return None 152 153 # Apply genre context blending if specified 154 if params.song_genre_context and params.context_blend > 0: 155 # Only blend if context genre is different from pattern genre 156 if params.song_genre_context != genre: 157 context_plugin = self.plugin_manager.get_genre_plugin( 158 params.song_genre_context 159 ) 160 genre_plugin = self.plugin_manager.get_genre_plugin(genre) 161 162 if context_plugin and genre_plugin: 163 context_profile = context_plugin.intensity_profile 164 pattern = genre_plugin.apply_context_blend( 165 pattern, context_profile, params.context_blend 166 ) 167 logger.debug( 168 f"Applied {params.song_genre_context} context " 169 f"(blend={params.context_blend}) to {genre} pattern" 170 ) 171 172 # Apply drummer style if specified 173 if params.drummer: 174 styled_pattern = self.plugin_manager.apply_drummer_style( 175 pattern, params.drummer 176 ) 177 if styled_pattern: 178 pattern = styled_pattern 179 180 # Apply humanization if requested 181 if params.humanization > 0: 182 timing_var = params.humanization * 0.05 # Scale to reasonable range 183 velocity_var = int(params.humanization * 20) 184 pattern = pattern.humanize(timing_var, velocity_var) 185 186 # Extend pattern for multiple bars if needed 187 if bars > 1: 188 pattern = self._extend_pattern_to_bars(pattern, bars) 189 190 return pattern
Generate a single pattern with optional genre context adaptation.
Arguments:
- genre: Genre name
- section: Section type
- bars: Number of bars (for multi-bar patterns)
- **kwargs: Additional generation parameters including:
- song_genre_context: Overall song genre for adaptation
- context_blend: Blend amount (0.0-1.0)
- drummer: Drummer style to apply
- humanization: Humanization amount
- etc.
Returns:
Generated Pattern or None if generation failed
Example:
Generate progressive pattern adapted to metal context
pattern = generator.generate_pattern( genre="metal", style="progressive", section="bridge", song_genre_context="metal", context_blend=0.3 )
192 def apply_drummer_style( 193 self, pattern: Pattern, drummer: str 194 ) -> Pattern | None: 195 """Apply drummer-specific style modifications to a pattern.""" 196 return self.plugin_manager.apply_drummer_style(pattern, drummer)
Apply drummer-specific style modifications to a pattern.
198 def export_midi(self, song: Song, output_path: Path) -> None: 199 """Export song as MIDI file.""" 200 self.midi_engine.save_song_midi(song, output_path) 201 logger.info(f"Exported MIDI to: {output_path}")
Export song as MIDI file.
203 def export_pattern_midi( 204 self, 205 pattern: Pattern, 206 output_path: Path, 207 tempo: int = 120, 208 drum_kit: DrumKit | None = None, 209 ) -> None: 210 """Export single pattern as MIDI file.""" 211 # Use provided drum kit or current one 212 engine = self.midi_engine 213 if drum_kit: 214 engine = MIDIEngine(drum_kit) 215 216 engine.save_pattern_midi(pattern, output_path, tempo) 217 logger.info(f"Exported pattern MIDI to: {output_path}")
Export single pattern as MIDI file.
219 def get_available_genres(self) -> list[str]: 220 """Get list of available genres.""" 221 return self.plugin_manager.get_available_genres()
Get list of available genres.
223 def get_available_drummers(self) -> list[str]: 224 """Get list of available drummers.""" 225 return self.plugin_manager.get_available_drummers()
Get list of available drummers.
227 def get_styles_for_genre(self, genre: str) -> list[str]: 228 """Get available styles for a genre.""" 229 return self.plugin_manager.get_styles_for_genre(genre)
Get available styles for a genre.
231 def get_song_info(self, song: Song) -> dict: 232 """Get comprehensive information about a song.""" 233 info = self.midi_engine.get_midi_info(song) 234 info.update( 235 { 236 "genre": ( 237 song.global_parameters.genre 238 if song.global_parameters 239 else "unknown" 240 ), 241 "style": ( 242 song.global_parameters.style 243 if song.global_parameters 244 else "default" 245 ), 246 "drummer": ( 247 song.global_parameters.drummer 248 if song.global_parameters 249 else None 250 ), 251 "sections_count": len(song.sections), 252 "unique_sections": list({s.name for s in song.sections}), 253 } 254 ) 255 return info
Get comprehensive information about a song.
257 def set_drum_kit(self, kit: DrumKit) -> None: 258 """Set the drum kit configuration.""" 259 self.drum_kit = kit 260 self.midi_engine = MIDIEngine(kit)
Set the drum kit configuration.
262 def create_drum_kit(self, kit_type: str) -> DrumKit: 263 """Create a drum kit configuration by type.""" 264 kit_creators = { 265 "ezdrummer3": DrumKit.create_ezdrummer3_kit, 266 "metal": DrumKit.create_metal_kit, 267 "jazz": DrumKit.create_jazz_kit, 268 "standard": DrumKit.create_ezdrummer3_kit, # Alias 269 } 270 271 creator = kit_creators.get(kit_type.lower()) 272 if creator: 273 return creator() 274 else: 275 logger.warning(f"Unknown kit type: {kit_type}, using standard kit") 276 return DrumKit.create_ezdrummer3_kit()
Create a drum kit configuration by type.
346 @classmethod 347 def quick_generate( 348 cls, genre: str = "metal", style: str = "heavy", tempo: int = 155 349 ) -> Song: 350 """Quick song generation with sensible defaults. 351 352 This replicates the functionality of the original script. 353 """ 354 generator = cls() 355 return generator.create_song( 356 genre=genre, 357 style=style, 358 tempo=tempo, 359 complexity=0.7, 360 dynamics=0.6, 361 humanization=0.3, 362 )
Quick song generation with sensible defaults.
This replicates the functionality of the original script.
75@dataclass 76class Pattern: 77 """Complete drum pattern with timing and metadata.""" 78 79 name: str 80 beats: list[Beat] = field(default_factory=list) 81 time_signature: TimeSignature = field(default_factory=TimeSignature) 82 subdivision: int = 16 # 16th note resolution 83 swing_ratio: float = 0.0 # 0.0 = straight, 0.5 = triplet swing 84 metadata: dict[str, Any] = field(default_factory=dict) 85 86 def add_beat( 87 self, 88 position: float, 89 instrument: DrumInstrument, 90 velocity: int = 100, 91 **kwargs, 92 ) -> "Pattern": 93 """Add a beat to the pattern.""" 94 beat = Beat( 95 position=position, 96 instrument=instrument, 97 velocity=velocity, 98 **kwargs, 99 ) 100 self.beats.append(beat) 101 return self 102 103 def get_beats_at_position( 104 self, position: float, tolerance: float = 0.01 105 ) -> list[Beat]: 106 """Get all beats at a specific position.""" 107 return [ 108 beat 109 for beat in self.beats 110 if abs(beat.position - position) <= tolerance 111 ] 112 113 def get_beats_by_instrument(self, instrument: DrumInstrument) -> list[Beat]: 114 """Get all beats for a specific instrument.""" 115 return [beat for beat in self.beats if beat.instrument == instrument] 116 117 def duration_bars(self) -> float: 118 """Calculate pattern duration in bars.""" 119 if not self.beats: 120 return 1.0 121 max_position = max(beat.position for beat in self.beats) 122 return max( 123 1.0, (max_position + 1.0) / self.time_signature.beats_per_bar 124 ) 125 126 def humanize( 127 self, timing_variance: float = 0.02, velocity_variance: float = 10 128 ) -> "Pattern": 129 """Apply humanization to timing and velocity.""" 130 humanized_beats = [] 131 for beat in self.beats: 132 # Add slight timing variations 133 timing_offset = random.uniform(-timing_variance, timing_variance) 134 new_position = max(0, beat.position + timing_offset) 135 136 # Add velocity variations 137 velocity_offset = random.randint( 138 -velocity_variance, velocity_variance 139 ) 140 new_velocity = max(1, min(127, beat.velocity + velocity_offset)) 141 142 humanized_beat = Beat( 143 position=new_position, 144 instrument=beat.instrument, 145 velocity=new_velocity, 146 duration=beat.duration, 147 ghost_note=beat.ghost_note, 148 accent=beat.accent, 149 ) 150 humanized_beats.append(humanized_beat) 151 152 return Pattern( 153 name=f"{self.name}_humanized", 154 beats=humanized_beats, 155 time_signature=self.time_signature, 156 subdivision=self.subdivision, 157 swing_ratio=self.swing_ratio, 158 metadata={**self.metadata, "humanized": True}, 159 ) 160 161 def copy(self) -> "Pattern": 162 """Create a deep copy of the pattern. 163 164 Logs warning if pattern has no beats to aid debugging. 165 """ 166 if not self.beats: 167 logger.warning( 168 f"Pattern '{self.name}' has no beats - copying empty pattern. " 169 "This may cause issues with drummer plugins." 170 ) 171 172 return Pattern( 173 name=self.name, 174 beats=[ 175 Beat( 176 position=beat.position, 177 instrument=beat.instrument, 178 velocity=beat.velocity, 179 duration=beat.duration, 180 ghost_note=beat.ghost_note, 181 accent=beat.accent, 182 ) 183 for beat in self.beats 184 ], 185 time_signature=TimeSignature( 186 self.time_signature.numerator, self.time_signature.denominator 187 ), 188 subdivision=self.subdivision, 189 swing_ratio=self.swing_ratio, 190 metadata=self.metadata.copy(), 191 )
Complete drum pattern with timing and metadata.
86 def add_beat( 87 self, 88 position: float, 89 instrument: DrumInstrument, 90 velocity: int = 100, 91 **kwargs, 92 ) -> "Pattern": 93 """Add a beat to the pattern.""" 94 beat = Beat( 95 position=position, 96 instrument=instrument, 97 velocity=velocity, 98 **kwargs, 99 ) 100 self.beats.append(beat) 101 return self
Add a beat to the pattern.
103 def get_beats_at_position( 104 self, position: float, tolerance: float = 0.01 105 ) -> list[Beat]: 106 """Get all beats at a specific position.""" 107 return [ 108 beat 109 for beat in self.beats 110 if abs(beat.position - position) <= tolerance 111 ]
Get all beats at a specific position.
113 def get_beats_by_instrument(self, instrument: DrumInstrument) -> list[Beat]: 114 """Get all beats for a specific instrument.""" 115 return [beat for beat in self.beats if beat.instrument == instrument]
Get all beats for a specific instrument.
117 def duration_bars(self) -> float: 118 """Calculate pattern duration in bars.""" 119 if not self.beats: 120 return 1.0 121 max_position = max(beat.position for beat in self.beats) 122 return max( 123 1.0, (max_position + 1.0) / self.time_signature.beats_per_bar 124 )
Calculate pattern duration in bars.
126 def humanize( 127 self, timing_variance: float = 0.02, velocity_variance: float = 10 128 ) -> "Pattern": 129 """Apply humanization to timing and velocity.""" 130 humanized_beats = [] 131 for beat in self.beats: 132 # Add slight timing variations 133 timing_offset = random.uniform(-timing_variance, timing_variance) 134 new_position = max(0, beat.position + timing_offset) 135 136 # Add velocity variations 137 velocity_offset = random.randint( 138 -velocity_variance, velocity_variance 139 ) 140 new_velocity = max(1, min(127, beat.velocity + velocity_offset)) 141 142 humanized_beat = Beat( 143 position=new_position, 144 instrument=beat.instrument, 145 velocity=new_velocity, 146 duration=beat.duration, 147 ghost_note=beat.ghost_note, 148 accent=beat.accent, 149 ) 150 humanized_beats.append(humanized_beat) 151 152 return Pattern( 153 name=f"{self.name}_humanized", 154 beats=humanized_beats, 155 time_signature=self.time_signature, 156 subdivision=self.subdivision, 157 swing_ratio=self.swing_ratio, 158 metadata={**self.metadata, "humanized": True}, 159 )
Apply humanization to timing and velocity.
161 def copy(self) -> "Pattern": 162 """Create a deep copy of the pattern. 163 164 Logs warning if pattern has no beats to aid debugging. 165 """ 166 if not self.beats: 167 logger.warning( 168 f"Pattern '{self.name}' has no beats - copying empty pattern. " 169 "This may cause issues with drummer plugins." 170 ) 171 172 return Pattern( 173 name=self.name, 174 beats=[ 175 Beat( 176 position=beat.position, 177 instrument=beat.instrument, 178 velocity=beat.velocity, 179 duration=beat.duration, 180 ghost_note=beat.ghost_note, 181 accent=beat.accent, 182 ) 183 for beat in self.beats 184 ], 185 time_signature=TimeSignature( 186 self.time_signature.numerator, self.time_signature.denominator 187 ), 188 subdivision=self.subdivision, 189 swing_ratio=self.swing_ratio, 190 metadata=self.metadata.copy(), 191 )
Create a deep copy of the pattern.
Logs warning if pattern has no beats to aid debugging.
54@dataclass 55class Beat: 56 """Individual drum hit within a pattern.""" 57 58 position: float # Beat position (0.0-4.0 for 4/4) 59 instrument: DrumInstrument 60 velocity: int = 100 # MIDI velocity 0-127 61 duration: float = 0.25 # Note duration in beats 62 ghost_note: bool = False # Quiet accent note 63 accent: bool = False # Emphasized note 64 65 def __post_init__(self): 66 """Validate beat parameters.""" 67 if not 0 <= self.velocity <= 127: 68 raise ValueError(f"Velocity must be 0-127, got {self.velocity}") 69 if self.position < 0: 70 raise ValueError( 71 f"Position cannot be negative, got {self.position}" 72 )
Individual drum hit within a pattern.
39@dataclass 40class TimeSignature: 41 """Time signature representation.""" 42 43 numerator: int = 4 44 denominator: int = 4 45 46 @property 47 def beats_per_bar(self) -> float: 48 return self.numerator * (4.0 / self.denominator) 49 50 def __str__(self) -> str: 51 return f"{self.numerator}/{self.denominator}"
Time signature representation.
108@dataclass 109class Song: 110 """Complete song structure with sections and global parameters.""" 111 112 name: str 113 tempo: int = 120 # BPM 114 time_signature: TimeSignature = field(default_factory=TimeSignature) 115 sections: list[Section] = field(default_factory=list) 116 global_parameters: GenerationParameters | None = None 117 metadata: dict[str, Any] = field(default_factory=dict) 118 119 def __post_init__(self): 120 """Validate song parameters.""" 121 if not 60 <= self.tempo <= 300: 122 raise ValueError( 123 f"Tempo must be between 60-300 BPM, got {self.tempo}" 124 ) 125 126 def add_section(self, section: Section) -> "Song": 127 """Add a section to the song.""" 128 self.sections.append(section) 129 return self 130 131 def total_bars(self) -> int: 132 """Calculate total number of bars in the song.""" 133 return sum(section.bars for section in self.sections) 134 135 def total_duration_seconds(self) -> float: 136 """Calculate total song duration in seconds.""" 137 total_beats = self.total_bars() * self.time_signature.beats_per_bar 138 beats_per_second = self.tempo / 60.0 139 return total_beats / beats_per_second 140 141 def get_section_by_name(self, name: str) -> Section | None: 142 """Find first section with the given name.""" 143 for section in self.sections: 144 if section.name == name: 145 return section 146 return None 147 148 def get_sections_by_name(self, name: str) -> list[Section]: 149 """Find all sections with the given name.""" 150 return [section for section in self.sections if section.name == name] 151 152 @classmethod 153 def create_simple_structure( 154 cls, 155 name: str, 156 tempo: int = 120, 157 genre: str = "rock", 158 style: str = "default", 159 ) -> "Song": 160 """Create a song with basic verse-chorus structure.""" 161 from midi_drums.models.pattern import ( 162 Pattern, # Import here to avoid circular imports 163 ) 164 165 # Create placeholder patterns (will be generated by plugins) 166 verse_pattern = Pattern(f"{genre}_{style}_verse") 167 chorus_pattern = Pattern(f"{genre}_{style}_chorus") 168 169 song = cls(name=name, tempo=tempo) 170 song.global_parameters = GenerationParameters(genre=genre, style=style) 171 172 # Standard pop/rock structure 173 song.add_section(Section("intro", verse_pattern, bars=4)) 174 song.add_section(Section("verse", verse_pattern, bars=8)) 175 song.add_section(Section("chorus", chorus_pattern, bars=8)) 176 song.add_section(Section("verse", verse_pattern, bars=8)) 177 song.add_section(Section("chorus", chorus_pattern, bars=8)) 178 song.add_section(Section("bridge", verse_pattern, bars=4)) 179 song.add_section(Section("chorus", chorus_pattern, bars=8)) 180 song.add_section(Section("outro", chorus_pattern, bars=4)) 181 182 return song
Complete song structure with sections and global parameters.
126 def add_section(self, section: Section) -> "Song": 127 """Add a section to the song.""" 128 self.sections.append(section) 129 return self
Add a section to the song.
131 def total_bars(self) -> int: 132 """Calculate total number of bars in the song.""" 133 return sum(section.bars for section in self.sections)
Calculate total number of bars in the song.
135 def total_duration_seconds(self) -> float: 136 """Calculate total song duration in seconds.""" 137 total_beats = self.total_bars() * self.time_signature.beats_per_bar 138 beats_per_second = self.tempo / 60.0 139 return total_beats / beats_per_second
Calculate total song duration in seconds.
141 def get_section_by_name(self, name: str) -> Section | None: 142 """Find first section with the given name.""" 143 for section in self.sections: 144 if section.name == name: 145 return section 146 return None
Find first section with the given name.
148 def get_sections_by_name(self, name: str) -> list[Section]: 149 """Find all sections with the given name.""" 150 return [section for section in self.sections if section.name == name]
Find all sections with the given name.
152 @classmethod 153 def create_simple_structure( 154 cls, 155 name: str, 156 tempo: int = 120, 157 genre: str = "rock", 158 style: str = "default", 159 ) -> "Song": 160 """Create a song with basic verse-chorus structure.""" 161 from midi_drums.models.pattern import ( 162 Pattern, # Import here to avoid circular imports 163 ) 164 165 # Create placeholder patterns (will be generated by plugins) 166 verse_pattern = Pattern(f"{genre}_{style}_verse") 167 chorus_pattern = Pattern(f"{genre}_{style}_chorus") 168 169 song = cls(name=name, tempo=tempo) 170 song.global_parameters = GenerationParameters(genre=genre, style=style) 171 172 # Standard pop/rock structure 173 song.add_section(Section("intro", verse_pattern, bars=4)) 174 song.add_section(Section("verse", verse_pattern, bars=8)) 175 song.add_section(Section("chorus", chorus_pattern, bars=8)) 176 song.add_section(Section("verse", verse_pattern, bars=8)) 177 song.add_section(Section("chorus", chorus_pattern, bars=8)) 178 song.add_section(Section("bridge", verse_pattern, bars=4)) 179 song.add_section(Section("chorus", chorus_pattern, bars=8)) 180 song.add_section(Section("outro", chorus_pattern, bars=4)) 181 182 return song
Create a song with basic verse-chorus structure.
67@dataclass 68class Section: 69 """Song section (verse, chorus, etc.) with pattern and variations.""" 70 71 name: str # "verse", "chorus", "bridge", "breakdown", "intro", "outro" 72 pattern: Pattern 73 bars: int = 4 74 variations: list[PatternVariation] = field(default_factory=list) 75 fills: list[Fill] = field(default_factory=list) 76 section_parameters: dict[str, Any] = field(default_factory=dict) 77 78 def get_effective_pattern(self, bar_number: int) -> Pattern: 79 """Get the pattern for a specific bar, considering variations.""" 80 # Check if any variations should apply to this bar 81 for variation in self.variations: 82 if variation.bars is None or bar_number in variation.bars: 83 import random 84 85 if random.random() < variation.probability: 86 return variation.pattern 87 return self.pattern 88 89 def should_add_fill( 90 self, bar_number: int, fill_frequency: float 91 ) -> Fill | None: 92 """Determine if a fill should be added at this bar.""" 93 import random 94 95 if random.random() < fill_frequency and self.fills: 96 # Choose fill based on probabilities 97 total_prob = sum(fill.trigger_probability for fill in self.fills) 98 if total_prob > 0: 99 rand_val = random.random() * total_prob 100 current_sum = 0 101 for fill in self.fills: 102 current_sum += fill.trigger_probability 103 if rand_val <= current_sum: 104 return fill 105 return None
Song section (verse, chorus, etc.) with pattern and variations.
78 def get_effective_pattern(self, bar_number: int) -> Pattern: 79 """Get the pattern for a specific bar, considering variations.""" 80 # Check if any variations should apply to this bar 81 for variation in self.variations: 82 if variation.bars is None or bar_number in variation.bars: 83 import random 84 85 if random.random() < variation.probability: 86 return variation.pattern 87 return self.pattern
Get the pattern for a specific bar, considering variations.
89 def should_add_fill( 90 self, bar_number: int, fill_frequency: float 91 ) -> Fill | None: 92 """Determine if a fill should be added at this bar.""" 93 import random 94 95 if random.random() < fill_frequency and self.fills: 96 # Choose fill based on probabilities 97 total_prob = sum(fill.trigger_probability for fill in self.fills) 98 if total_prob > 0: 99 rand_val = random.random() * total_prob 100 current_sum = 0 101 for fill in self.fills: 102 current_sum += fill.trigger_probability 103 if rand_val <= current_sum: 104 return fill 105 return None
Determine if a fill should be added at this bar.
10@dataclass 11class GenerationParameters: 12 """Parameters controlling pattern generation.""" 13 14 genre: str 15 style: str = "default" 16 drummer: str | None = None 17 complexity: float = 0.5 # 0.0-1.0, affects fill density and variation 18 dynamics: float = 0.5 # 0.0-1.0, affects volume variation 19 humanization: float = 0.3 # 0.0-1.0, affects timing/velocity variation 20 fill_frequency: float = 0.2 # 0.0-1.0, how often fills occur 21 swing_ratio: float = 0.0 # 0.0-1.0, swing feel 22 23 # Genre context adaptation (NEW) 24 song_genre_context: str | None = None # Overall song genre for adaptation 25 context_blend: float = 0.0 # 0.0-1.0, how much to blend with context 26 27 custom_parameters: dict[str, Any] = field(default_factory=dict) 28 29 def __post_init__(self): 30 """Validate parameters.""" 31 for param_name, value in [ 32 ("complexity", self.complexity), 33 ("dynamics", self.dynamics), 34 ("humanization", self.humanization), 35 ("fill_frequency", self.fill_frequency), 36 ("swing_ratio", self.swing_ratio), 37 ("context_blend", self.context_blend), 38 ]: 39 if not 0.0 <= value <= 1.0: 40 raise ValueError( 41 f"{param_name} must be between 0.0 and 1.0, got {value}" 42 )
Parameters controlling pattern generation.