midigen-lib 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
midigen/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ from .midigen import MidiGen
2
+ from .track import Track
3
+ from .note import Note
4
+ from .chord import Chord, ChordProgression, Arpeggio
5
+ from .key import Key, KEY_MAP
6
+ from .drums import DrumKit
7
+ from .song import Song
8
+ from .section import Section
9
+ from .instruments import INSTRUMENT_MAP
midigen/chord.py ADDED
@@ -0,0 +1,209 @@
1
+ from typing import List
2
+ from midigen.note import Note
3
+ from midigen.key import KEY_MAP, Key
4
+ from enum import Enum
5
+ import music21
6
+
7
+
8
+ class Chord:
9
+ def __init__(self, notes: List[Note]):
10
+ self.notes = notes
11
+ self.root = self.get_root()
12
+ self._calculate_start_time()
13
+ self._calculate_duration()
14
+
15
+ def __str__(self) -> str:
16
+ return f"[{', '.join(str(note) for note in self.notes)}]"
17
+
18
+ def _calculate_start_time(self) -> int:
19
+ """
20
+ Calculate the start time of the chord.
21
+
22
+ Returns:
23
+ The start time of the chord.
24
+ """
25
+ self.time = min(note.time for note in self.notes) if self.notes else 0
26
+ return self.time
27
+
28
+ def _calculate_duration(self) -> int:
29
+ """
30
+ Calculate the duration of the chord.
31
+
32
+ Returns:
33
+ The duration of the longest note in the chord.
34
+ """
35
+ if not self.notes:
36
+ return 0
37
+
38
+ # Find the earliest start time of any note in the chord
39
+ earliest_start_time = min(note.time for note in self.notes)
40
+ # Find the latest ending time, calculated as start time plus duration for each note
41
+ latest_end_time = max(note.time + note.duration for note in self.notes)
42
+
43
+ # The duration of the chord is the difference between the earliest start and the latest end
44
+ self.duration = latest_end_time - earliest_start_time
45
+ return self.duration
46
+
47
+ def add_note(self, note: Note) -> None:
48
+ self.notes.append(note)
49
+ self._calculate_duration()
50
+ self._calculate_start_time()
51
+
52
+ def get_chord(self) -> List[Note]:
53
+ return self.notes
54
+
55
+ def get_root(self) -> Note:
56
+ if self.notes:
57
+ self.root = self.notes[0]
58
+ return self.root
59
+ return None
60
+
61
+ def major_triad(self) -> List[Note]:
62
+ return [self.root, self.root + 4, self.root + 7]
63
+
64
+ def minor_triad(self) -> List[Note]:
65
+ return [self.root, self.root + 3, self.root + 7]
66
+
67
+ def dominant_seventh(self) -> List[Note]:
68
+ return self.major_triad() + [self.root + 10]
69
+
70
+ def major_seventh(self) -> List[Note]:
71
+ return self.major_triad() + [self.root + 11]
72
+
73
+ def minor_seventh(self) -> List[Note]:
74
+ return self.minor_triad() + [self.root + 10]
75
+
76
+ def half_diminished_seventh(self) -> List[Note]:
77
+ return [self.root, self.root + 3, self.root + 6, self.root + 10]
78
+
79
+ def diminished_seventh(self) -> List[Note]:
80
+ return [self.root, self.root + 3, self.root + 6, self.root + 9]
81
+
82
+ def minor_ninth(self) -> List[Note]:
83
+ return self.minor_seventh() + [self.root + 14]
84
+
85
+ def major_ninth(self) -> List[Note]:
86
+ return self.major_seventh() + [self.root + 14]
87
+
88
+ def dominant_ninth(self) -> List[Note]:
89
+ return self.dominant_seventh() + [self.root + 14]
90
+
91
+
92
+ class ChordProgression:
93
+ def __init__(self, chords: List[Chord]):
94
+ self.chords = chords
95
+ self._calculate_start_time()
96
+ self._calculate_duration()
97
+
98
+ def __str__(self) -> str:
99
+ return f"[{', '.join(str(chord) for chord in self.chords)}]"
100
+
101
+ def get_progression(self) -> List[Chord]:
102
+ return self.chords
103
+
104
+ def _calculate_duration(self) -> int:
105
+ self.duration = sum(chord._calculate_duration() for chord in self.chords)
106
+ return self.duration
107
+
108
+ def _calculate_start_time(self) -> int:
109
+ self.time = min(chord._calculate_start_time() for chord in self.chords) if self.chords else 0
110
+ return self.time
111
+
112
+ def __eq__(self, other) -> bool:
113
+ return self.chords == other.chords
114
+
115
+ def add_chord(self, chord: Chord) -> None:
116
+ """
117
+ Add a chord (simultaneous notes) to the track.
118
+
119
+ :param chord: A Chord object.
120
+ """
121
+ self.chords.append(chord)
122
+ self._calculate_duration()
123
+ self._calculate_start_time()
124
+
125
+ @classmethod
126
+ def from_roman_numerals(
127
+ cls,
128
+ key: Key,
129
+ progression_string: str,
130
+ octave: int = 4,
131
+ duration: int = 480,
132
+ time_per_chord: int = 0
133
+ ):
134
+ m21_key = music21.key.Key(key.name, key.mode)
135
+ roman_numerals = progression_string.split('-')
136
+ chords = []
137
+ current_time = 0
138
+ for rn_str in roman_numerals:
139
+ rn = music21.roman.RomanNumeral(rn_str, m21_key)
140
+ pitches = rn.pitches
141
+ notes = []
142
+ for i, pitch in enumerate(pitches):
143
+ note_name = f"{pitch.nameWithOctave}"
144
+ # A simple way to handle octave, might need refinement
145
+ note_name_without_octave = ''.join(filter(str.isalpha, pitch.name))
146
+ full_note_name = f"{note_name_without_octave}{octave}"
147
+ midi_pitch = KEY_MAP.get(full_note_name)
148
+
149
+ # If the note is not in the current octave, try the next one
150
+ if midi_pitch is None:
151
+ full_note_name = f"{note_name_without_octave}{octave + 1}"
152
+ midi_pitch = KEY_MAP.get(full_note_name)
153
+
154
+ if midi_pitch:
155
+ # The first note of the chord starts at `current_time`, subsequent notes start at the same time.
156
+ note_time = current_time if i == 0 else 0
157
+ notes.append(Note(pitch=midi_pitch, velocity=64, duration=duration, time=note_time))
158
+
159
+ if notes:
160
+ chords.append(Chord(notes))
161
+ current_time += time_per_chord
162
+
163
+ return cls(chords)
164
+
165
+
166
+ class ArpeggioPattern(Enum):
167
+ ASCENDING = "ascending"
168
+ DESCENDING = "descending"
169
+ ALTERNATING = "alternating"
170
+
171
+
172
+ class Arpeggio(Chord):
173
+ def __init__(self, notes: List[Note], delay: int = 0, pattern: ArpeggioPattern = ArpeggioPattern.ASCENDING, loops: int = 1):
174
+ """
175
+ :param root_note: The root note of the arpeggio.
176
+ :param delay: The delay between each note in the arpeggio.
177
+ """
178
+ super().__init__(notes)
179
+ self.delay = delay
180
+ self.pattern = pattern
181
+ self.loops = loops
182
+
183
+ def get_notes(self) -> List[Note]:
184
+ return self.notes
185
+
186
+ def get_sequential_notes(self) -> List[Note]:
187
+ """
188
+ Get the sequential notes of the arpeggio based on the pattern, delay, and looping.
189
+
190
+ Returns:
191
+ A list of notes representing the arpeggio.
192
+ """
193
+ sequential_notes = []
194
+ for loop in range(self.loops):
195
+ if self.pattern == ArpeggioPattern.ASCENDING:
196
+ notes = self.notes
197
+ elif self.pattern == ArpeggioPattern.DESCENDING:
198
+ notes = list(reversed(self.notes))
199
+ elif self.pattern == ArpeggioPattern.ALTERNATING:
200
+ notes = self.notes if loop % 2 == 0 else list(reversed(self.notes))
201
+
202
+ for i, note in enumerate(notes):
203
+ # Add an offset to the time for the second and subsequent loops
204
+ time_offset = loop * len(notes) * self.delay
205
+ time = note.time + time_offset if i == 0 else self.delay * i + time_offset
206
+ new_note = Note(note.pitch, note.velocity, note.duration, time)
207
+ sequential_notes.append(new_note)
208
+
209
+ return sequential_notes
midigen/drums.py ADDED
@@ -0,0 +1,104 @@
1
+ from midigen.note import Note
2
+ from typing import List
3
+
4
+ GM1_DRUM_MAP = {
5
+ "Acoustic Bass Drum": 35,
6
+ "Bass Drum 1": 36,
7
+ "Side Stick": 37,
8
+ "Acoustic Snare": 38,
9
+ "Hand Clap": 39,
10
+ "Electric Snare": 40,
11
+ "Low Floor Tom": 41,
12
+ "Closed Hi Hat": 42,
13
+ "High Floor Tom": 43,
14
+ "Pedal Hi-Hat": 44,
15
+ "Low Tom": 45,
16
+ "Open Hi-Hat": 46,
17
+ "Low-Mid Tom": 47,
18
+ "Hi-Mid Tom": 48,
19
+ "Crash Cymbal 1": 49,
20
+ "High Tom": 50,
21
+ "Ride Cymbal 1": 51,
22
+ "Chinese Cymbal": 52,
23
+ "Ride Bell": 53,
24
+ "Tambourine": 54,
25
+ "Splash Cymbal": 55,
26
+ "Cowbell": 56,
27
+ "Crash Cymbal 2": 57,
28
+ "Vibraslap": 58,
29
+ "Ride Cymbal 2": 59,
30
+ "Hi Bongo": 60,
31
+ "Low Bongo": 61,
32
+ "Mute Hi Conga": 62,
33
+ "Open Hi Conga": 63,
34
+ "Low Conga": 64,
35
+ "High Timbale": 65,
36
+ "Low Timbale": 66,
37
+ "High Agogo": 67,
38
+ "Low Agogo": 68,
39
+ "Cabasa": 69,
40
+ "Maracas": 70,
41
+ "Short Whistle": 71,
42
+ "Long Whistle": 72,
43
+ "Short Guiro": 73,
44
+ "Long Guiro": 74,
45
+ "Claves": 75,
46
+ "Hi Wood Block": 76,
47
+ "Low Wood Block": 77,
48
+ "Mute Cuica": 78,
49
+ "Open Cuica": 79,
50
+ "Mute Triangle": 80,
51
+ "Open Triangle": 81,
52
+ }
53
+
54
+
55
+ class Drum:
56
+ """
57
+ A drum sound in a drum kit, represented as a MIDI note.
58
+ """
59
+
60
+ def __init__(self, pitch: int, velocity: int, duration: int, time: int):
61
+ if duration < 0:
62
+ raise ValueError("Duration must be non-negative")
63
+ if velocity < 0:
64
+ raise ValueError("Velocity must be non-negative")
65
+ if not 0 <= pitch <= 127:
66
+ raise ValueError("Pitch must be within the MIDI standard range of 0 to 127")
67
+
68
+ self.note = Note(pitch, velocity, duration, time)
69
+
70
+
71
+ class DrumKit:
72
+ """
73
+ A collection of drum sounds, each represented as a MIDI note.
74
+ """
75
+
76
+ def __init__(self):
77
+ self.drums = []
78
+
79
+ def add_drum(
80
+ self, drum_name: str, velocity: int = 64, duration: int = 1, time: int = 0
81
+ ) -> None:
82
+ """
83
+ Add a drum to the drum kit.
84
+
85
+ :param drum_name: The name of the drum, as defined in the GM1_DRUM_MAP.
86
+ :param velocity: The velocity of the drum sound.
87
+ :param duration: The duration of the drum sound.
88
+ :param time: The time at which the drum sound should start.
89
+ """
90
+ if drum_name not in GM1_DRUM_MAP:
91
+ raise ValueError(
92
+ f"Invalid drum name: {drum_name}. Please use a valid drum name from the GM1_DRUM_MAP."
93
+ )
94
+
95
+ drum = Drum(GM1_DRUM_MAP[drum_name], velocity, duration, time)
96
+ self.drums.append(drum)
97
+
98
+ def get_drums(self) -> List[Note]:
99
+ """
100
+ Get the list of drums in the drum kit.
101
+
102
+ :return: A list of Drum objects.
103
+ """
104
+ return [drum.note for drum in self.drums]
midigen/instruments.py ADDED
@@ -0,0 +1,146 @@
1
+ INSTRUMENT_MAP = {
2
+ # Piano
3
+ "Acoustic Grand Piano": 0,
4
+ "Bright Acoustic Piano": 1,
5
+ "Electric Grand Piano": 2,
6
+ "Honky-tonk Piano": 3,
7
+ "Electric Piano 1": 4,
8
+ "Electric Piano 2": 5,
9
+ "Harpsichord": 6,
10
+ "Clavinet": 7,
11
+ # Chromatic Percussion
12
+ "Celesta": 8,
13
+ "Glockenspiel": 9,
14
+ "Music Box": 10,
15
+ "Vibraphone": 11,
16
+ "Marimba": 12,
17
+ "Xylophone": 13,
18
+ "Tubular Bells": 14,
19
+ "Dulcimer": 15,
20
+ # Organ
21
+ "Drawbar Organ": 16,
22
+ "Percussive Organ": 17,
23
+ "Rock Organ": 18,
24
+ "Church Organ": 19,
25
+ "Reed Organ": 20,
26
+ "Accordion": 21,
27
+ "Harmonica": 22,
28
+ "Tango Accordion": 23,
29
+ # Guitar
30
+ "Acoustic Guitar (nylon)": 24,
31
+ "Acoustic Guitar (steel)": 25,
32
+ "Electric Guitar (jazz)": 26,
33
+ "Electric Guitar (clean)": 27,
34
+ "Electric Guitar (muted)": 28,
35
+ "Overdriven Guitar": 29,
36
+ "Distortion Guitar": 30,
37
+ "Guitar Harmonics": 31,
38
+ # Bass
39
+ "Acoustic Bass": 32,
40
+ "Electric Bass (finger)": 33,
41
+ "Electric Bass (pick)": 34,
42
+ "Fretless Bass": 35,
43
+ "Slap Bass 1": 36,
44
+ "Slap Bass 2": 37,
45
+ "Synth Bass 1": 38,
46
+ "Synth Bass 2": 39,
47
+ # Strings
48
+ "Violin": 40,
49
+ "Viola": 41,
50
+ "Cello": 42,
51
+ "Contrabass": 43,
52
+ "Tremolo Strings": 44,
53
+ "Pizzicato Strings": 45,
54
+ "Orchestral Harp": 46,
55
+ "Timpani": 47,
56
+ # Ensemble
57
+ "String Ensemble 1": 48,
58
+ "String Ensemble 2": 49,
59
+ "Synth Strings 1": 50,
60
+ "Synth Strings 2": 51,
61
+ "Choir Aahs": 52,
62
+ "Voice Oohs": 53,
63
+ "Synth Voice": 54,
64
+ "Orchestra Hit": 55,
65
+ # Brass
66
+ "Trumpet": 56,
67
+ "Trombone": 57,
68
+ "Tuba": 58,
69
+ "Muted Trumpet": 59,
70
+ "French Horn": 60,
71
+ "Brass Section": 61,
72
+ "Synth Brass 1": 62,
73
+ "Synth Brass 2": 63,
74
+ # Reed
75
+ "Soprano Sax": 64,
76
+ "Alto Sax": 65,
77
+ "Tenor Sax": 66,
78
+ "Baritone Sax": 67,
79
+ "Oboe": 68,
80
+ "English Horn": 69,
81
+ "Bassoon": 70,
82
+ "Clarinet": 71,
83
+ # Pipe
84
+ "Piccolo": 72,
85
+ "Flute": 73,
86
+ "Recorder": 74,
87
+ "Pan Flute": 75,
88
+ "Blown Bottle": 76,
89
+ "Shakuhachi": 77,
90
+ "Whistle": 78,
91
+ "Ocarina": 79,
92
+ # Synth Lead
93
+ "Lead 1 (square)": 80,
94
+ "Lead 2 (sawtooth)": 81,
95
+ "Lead 3 (calliope)": 82,
96
+ "Lead 4 (chiff)": 83,
97
+ "Lead 5 (charang)": 84,
98
+ "Lead 6 (voice)": 85,
99
+ "Lead 7 (fifths)": 86,
100
+ "Lead 8 (bass + lead)": 87,
101
+ # Synth Pad
102
+ "Pad 1 (new age)": 88,
103
+ "Pad 2 (warm)": 89,
104
+ "Pad 3 (polysynth)": 90,
105
+ "Pad 4 (choir)": 91,
106
+ "Pad 5 (bowed)": 92,
107
+ "Pad 6 (metallic)": 93,
108
+ "Pad 7 (halo)": 94,
109
+ "Pad 8 (sweep)": 95,
110
+ # Synth Effects
111
+ "FX 1 (rain)": 96,
112
+ "FX 2 (soundtrack)": 97,
113
+ "FX 3 (crystal)": 98,
114
+ "FX 4 (atmosphere)": 99,
115
+ "FX 5 (brightness)": 100,
116
+ "FX 6 (goblins)": 101,
117
+ "FX 7 (echoes)": 102,
118
+ "FX 8 (sci-fi)": 103,
119
+ # Ethnic
120
+ "Sitar": 104,
121
+ "Banjo": 105,
122
+ "Shamisen": 106,
123
+ "Koto": 107,
124
+ "Kalimba": 108,
125
+ "Bagpipe": 109,
126
+ "Fiddle": 110,
127
+ "Shanai": 111,
128
+ # Percussive
129
+ "Tinkle Bell": 112,
130
+ "Agogo": 113,
131
+ "Steel Drums": 114,
132
+ "Woodblock": 115,
133
+ "Taiko Drum": 116,
134
+ "Melodic Tom": 117,
135
+ "Synth Drum": 118,
136
+ "Reverse Cymbal": 119,
137
+ # Sound Effects
138
+ "Guitar Fret Noise": 120,
139
+ "Breath Noise": 121,
140
+ "Seashore": 122,
141
+ "Bird Tweet": 123,
142
+ "Telephone Ring": 124,
143
+ "Helicopter": 125,
144
+ "Applause": 126,
145
+ "Gunshot": 127,
146
+ }
midigen/key.py ADDED
@@ -0,0 +1,203 @@
1
+ VALID_KEYS = [
2
+ (note, mode)
3
+ for note in [
4
+ "A",
5
+ "A#",
6
+ "Ab",
7
+ "B",
8
+ "Bb",
9
+ "C",
10
+ "C#",
11
+ "Cb",
12
+ "D",
13
+ "D#",
14
+ "Db",
15
+ "E",
16
+ "Eb",
17
+ "F",
18
+ "F#",
19
+ "Fb",
20
+ "G",
21
+ "G#",
22
+ "Gb",
23
+ ]
24
+ for mode in ["major", "minor"]
25
+ ]
26
+
27
+ KEY_MAP = {
28
+ "C0": 12,
29
+ "C#0": 13,
30
+ "Db0": 13,
31
+ "D0": 14,
32
+ "D#0": 15,
33
+ "Eb0": 15,
34
+ "E0": 16,
35
+ "F0": 17,
36
+ "F#0": 18,
37
+ "Gb0": 18,
38
+ "G0": 19,
39
+ "G#0": 20,
40
+ "Ab0": 20,
41
+ "A0": 21,
42
+ "A#0": 22,
43
+ "Bb0": 22,
44
+ "B0": 23,
45
+ "C1": 24,
46
+ "C#1": 25,
47
+ "Db1": 25,
48
+ "D1": 26,
49
+ "D#1": 27,
50
+ "Eb1": 27,
51
+ "E1": 28,
52
+ "F1": 29,
53
+ "F#1": 30,
54
+ "Gb1": 30,
55
+ "G1": 31,
56
+ "G#1": 32,
57
+ "Ab1": 32,
58
+ "A1": 33,
59
+ "A#1": 34,
60
+ "Bb1": 34,
61
+ "B1": 35,
62
+ "C2": 36,
63
+ "C#2": 37,
64
+ "Db2": 37,
65
+ "D2": 38,
66
+ "D#2": 39,
67
+ "Eb2": 39,
68
+ "E2": 40,
69
+ "F2": 41,
70
+ "F#2": 42,
71
+ "Gb2": 42,
72
+ "G2": 43,
73
+ "G#2": 44,
74
+ "Ab2": 44,
75
+ "A2": 45,
76
+ "A#2": 46,
77
+ "Bb2": 46,
78
+ "B2": 47,
79
+ "C3": 48,
80
+ "C#3": 49,
81
+ "Db3": 49,
82
+ "D3": 50,
83
+ "D#3": 51,
84
+ "Eb3": 51,
85
+ "E3": 52,
86
+ "F3": 53,
87
+ "F#3": 54,
88
+ "Gb3": 54,
89
+ "G3": 55,
90
+ "G#3": 56,
91
+ "Ab3": 56,
92
+ "A3": 57,
93
+ "A#3": 58,
94
+ "Bb3": 58,
95
+ "B3": 59,
96
+ "C4": 60,
97
+ "C#4": 61,
98
+ "Db4": 61,
99
+ "D4": 62,
100
+ "D#4": 63,
101
+ "Eb4": 63,
102
+ "E4": 64,
103
+ "F4": 65,
104
+ "F#4": 66,
105
+ "Gb4": 66,
106
+ "G4": 67,
107
+ "G#4": 68,
108
+ "Ab4": 68,
109
+ "A4": 69,
110
+ "A#4": 70,
111
+ "Bb4": 70,
112
+ "B4": 71,
113
+ "C5": 72,
114
+ "C#5": 73,
115
+ "Db5": 73,
116
+ "D5": 74,
117
+ "D#5": 75,
118
+ "Eb5": 75,
119
+ "E5": 76,
120
+ "F5": 77,
121
+ "F#5": 78,
122
+ "Gb5": 78,
123
+ "G5": 79,
124
+ "G#5": 80,
125
+ "Ab5": 80,
126
+ "A5": 81,
127
+ "A#5": 82,
128
+ "Bb5": 82,
129
+ "B5": 83,
130
+ "C6": 84,
131
+ "C#6": 85,
132
+ "Db6": 85,
133
+ "D6": 86,
134
+ "D#6": 87,
135
+ "Eb6": 87,
136
+ "E6": 88,
137
+ "F6": 89,
138
+ "F#6": 90,
139
+ "Gb6": 90,
140
+ "G6": 91,
141
+ "G#6": 92,
142
+ "Ab6": 92,
143
+ "A6": 93,
144
+ "A#6": 94,
145
+ "Bb6": 94,
146
+ "B6": 95,
147
+ "C7": 96,
148
+ "C#7": 97,
149
+ "Db7": 97,
150
+ "D7": 98,
151
+ "D#7": 99,
152
+ "Eb7": 99,
153
+ "E7": 100,
154
+ "F7": 101,
155
+ "F#7": 102,
156
+ "Gb7": 102,
157
+ "G7": 103,
158
+ "G#7": 104,
159
+ "Ab7": 104,
160
+ "A7": 105,
161
+ "A#7": 106,
162
+ "Bb7": 106,
163
+ "B7": 107,
164
+ "C8": 108,
165
+ "C#8": 109,
166
+ "Db8": 109,
167
+ "D8": 110,
168
+ "D#8": 111,
169
+ "Eb8": 111,
170
+ "E8": 112,
171
+ "F8": 113,
172
+ "F#8": 114,
173
+ "Gb8": 114,
174
+ "G8": 115,
175
+ "G#8": 116,
176
+ "Ab8": 116,
177
+ "A8": 117,
178
+ "A#8": 118,
179
+ "Bb8": 118,
180
+ "B8": 119,
181
+ }
182
+
183
+
184
+ class Key:
185
+ def __init__(self, name: str, mode: str = "major"):
186
+ if (name, mode) not in VALID_KEYS:
187
+ raise ValueError(
188
+ f"Invalid key. Please use a valid key from the list: {format(VALID_KEYS)}"
189
+ )
190
+
191
+ self.name = name
192
+ self.mode = mode
193
+
194
+ def __str__(self) -> str:
195
+ return f"{self.name}{'' if self.mode == 'major' else 'm'}"
196
+
197
+ def __repr__(self) -> str:
198
+ return f"Key(name='{self.name}', mode='{self.mode}')"
199
+
200
+ def __eq__(self, other) -> bool:
201
+ if isinstance(other, Key):
202
+ return self.name == other.name and self.mode == other.mode
203
+ return False