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]
class DrumGenerator:
 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.

DrumGenerator(config_path: pathlib.Path | None = None)
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.

plugin_manager
drum_kit
midi_engine
def create_song( self, genre: str, style: str = 'default', tempo: int = 120, structure: list[tuple[str, int]] | None = None, drum_kit: midi_drums.models.kit.DrumKit | None = None, **kwargs) -> Song:
 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

def generate_pattern( self, genre: str, section: str = 'verse', bars: int = 4, **kwargs) -> Pattern | None:
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 )

def apply_drummer_style( self, pattern: Pattern, drummer: str) -> Pattern | None:
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.

def export_midi( self, song: Song, output_path: pathlib.Path) -> None:
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.

def export_pattern_midi( self, pattern: Pattern, output_path: pathlib.Path, tempo: int = 120, drum_kit: midi_drums.models.kit.DrumKit | None = None) -> None:
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.

def get_available_genres(self) -> list[str]:
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.

def get_available_drummers(self) -> list[str]:
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.

def get_styles_for_genre(self, genre: str) -> list[str]:
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.

def get_song_info(self, song: Song) -> dict:
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.

def set_drum_kit(self, kit: midi_drums.models.kit.DrumKit) -> None:
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.

def create_drum_kit(self, kit_type: str) -> midi_drums.models.kit.DrumKit:
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.

@classmethod
def quick_generate( cls, genre: str = 'metal', style: str = 'heavy', tempo: int = 155) -> Song:
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.

@dataclass
class Pattern:
 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.

Pattern( name: str, beats: list[Beat] = <factory>, time_signature: TimeSignature = <factory>, subdivision: int = 16, swing_ratio: float = 0.0, metadata: dict[str, typing.Any] = <factory>)
name: str
beats: list[Beat]
time_signature: TimeSignature
subdivision: int = 16
swing_ratio: float = 0.0
metadata: dict[str, typing.Any]
def add_beat( self, position: float, instrument: midi_drums.models.pattern.DrumInstrument, velocity: int = 100, **kwargs) -> Pattern:
 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.

def get_beats_at_position( self, position: float, tolerance: float = 0.01) -> list[Beat]:
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.

def get_beats_by_instrument( self, instrument: midi_drums.models.pattern.DrumInstrument) -> list[Beat]:
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.

def duration_bars(self) -> float:
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.

def humanize( self, timing_variance: float = 0.02, velocity_variance: float = 10) -> Pattern:
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.

def copy(self) -> Pattern:
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.

@dataclass
class Beat:
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.

Beat( position: float, instrument: midi_drums.models.pattern.DrumInstrument, velocity: int = 100, duration: float = 0.25, ghost_note: bool = False, accent: bool = False)
position: float
instrument: midi_drums.models.pattern.DrumInstrument
velocity: int = 100
duration: float = 0.25
ghost_note: bool = False
accent: bool = False
@dataclass
class TimeSignature:
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.

TimeSignature(numerator: int = 4, denominator: int = 4)
numerator: int = 4
denominator: int = 4
beats_per_bar: float
46    @property
47    def beats_per_bar(self) -> float:
48        return self.numerator * (4.0 / self.denominator)
@dataclass
class Song:
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.

Song( name: str, tempo: int = 120, time_signature: TimeSignature = <factory>, sections: list[Section] = <factory>, global_parameters: GenerationParameters | None = None, metadata: dict[str, typing.Any] = <factory>)
name: str
tempo: int = 120
time_signature: TimeSignature
sections: list[Section]
global_parameters: GenerationParameters | None = None
metadata: dict[str, typing.Any]
def add_section( self, section: Section) -> Song:
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.

def total_bars(self) -> int:
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.

def total_duration_seconds(self) -> float:
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.

def get_section_by_name(self, name: str) -> Section | None:
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.

def get_sections_by_name(self, name: str) -> list[Section]:
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.

@classmethod
def create_simple_structure( cls, name: str, tempo: int = 120, genre: str = 'rock', style: str = 'default') -> Song:
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.

@dataclass
class Section:
 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.

Section( name: str, pattern: Pattern, bars: int = 4, variations: list[midi_drums.models.song.PatternVariation] = <factory>, fills: list[midi_drums.models.song.Fill] = <factory>, section_parameters: dict[str, typing.Any] = <factory>)
name: str
pattern: Pattern
bars: int = 4
variations: list[midi_drums.models.song.PatternVariation]
fills: list[midi_drums.models.song.Fill]
section_parameters: dict[str, typing.Any]
def get_effective_pattern(self, bar_number: int) -> Pattern:
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.

def should_add_fill( self, bar_number: int, fill_frequency: float) -> midi_drums.models.song.Fill | None:
 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.

@dataclass
class GenerationParameters:
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.

GenerationParameters( genre: str, style: str = 'default', drummer: str | None = None, complexity: float = 0.5, dynamics: float = 0.5, humanization: float = 0.3, fill_frequency: float = 0.2, swing_ratio: float = 0.0, song_genre_context: str | None = None, context_blend: float = 0.0, custom_parameters: dict[str, typing.Any] = <factory>)
genre: str
style: str = 'default'
drummer: str | None = None
complexity: float = 0.5
dynamics: float = 0.5
humanization: float = 0.3
fill_frequency: float = 0.2
swing_ratio: float = 0.0
song_genre_context: str | None = None
context_blend: float = 0.0
custom_parameters: dict[str, typing.Any]