Music21: get track index of a note - python

I have a multi-track midi file that I'm reading with music21:
import music21
f = music21.midi.MidiFile()
f.open('1079-02.mid')
f.read()
stream = music21.midi.translate.midiFileToStream(f).flat
note_filter = music21.stream.filters.ClassFilter('Note')
for n in stream.recurse().addFilter(note_filter):
offset = n.offset # offset from song start in beats
note = n.pitch # letter of the note, e.g. C4, F5
midi_note = n.pitch.midi # midi number of the pitch, e.g. 60, 72
duration = n.duration # duration of the note in beats
instrument = n.activeSite.getInstrument() # instrument voice
I'd like to figure out which track each note in this stream belongs to. E.g. when I open the file in GarageBand, the notes are organized into tracks:
In mido, each MidiFile has a tracks attribute that contains one list of notes for each track.
Is there a way to get the same with music21? Any help would be appreciated!

The music tracks are parsed into separate stream.Part objects, so you can just walk through the parts of the stream.Score that you produced if you avoid flattening it (here, I've just produced a stream with converter.parse():
s = converter.parse('1079-02.mid')
for part in s.parts:
for note in part.recurse().notes:
print("I WAS IN PART ", part)
or look up the containing part:
s = converter.parse('1079-02.mid')
for note in s.recurse().notes:
part = note.sites.getObjByClass('Part')
print("I WAS IN PART ", part)
I doubt you really need to flatten anything. Good luck!

Related

Issue on rejoining splitted AudioSegment

I'm working on a script that splits an AudioSegment into 2 second sub segments and then rejoin them. The final purpose of this is to apply a transform function to each segment before rejoining them but at this point I'm just trying to rejoin the segments and left that transform out of the execution
I'm executing the following code:
song = Auseg.from_file(sys.argv[1])
song = song.set_channels(1)
song = song.set_frame_rate(12000)
fr = song.frame_rate
max = song.max
leng = song.duration_seconds
framecount = int(song.frame_count())
songsegs = song.dice(2) #split AudioSegment into a list of 2s segments
print(len(songsegs))
print(framecount," Frames")
print("Framerate: ", fr)
print("Preparing...")
output = Auseg.empty()
for n in range(0,len(songsegs)):
new_segment = songsegs[n] #leave_only_peaks(songsegs[n])
#print(new_segment)
output = output + new_segment
sys.stdout.write("\r%i segments processed" % n)
sys.stdout.flush()
print("\nExporting as prepared.wav..")
#print(output.tobytes())
output.export("prepared.wav",format="wav")
I was able to confirm that using the export() method on a single segment outputs a 2s part of the song as expected, but when rejoining all segments with the code above (which tecnically should return the original song) I get this weird file that is 1600 hours long. I tried another implementation that concats numpy arrays and then converts the result back to an AudioSegment but got the same result
output file
Thank you very much for reading, I'm open for any suggestion
Oddly enought it seems the issue was generated for concating the segments on top of the segment generated by the empty() method, with this tweak I was able to make it work
output = songsegs[0]
for n in range(1,len(songsegs)):
new_segment = songsegs[n] #leave_only_peaks(songsegs[n])
#print(new_segment)
output = output + songsegs[n]
sys.stdout.write("\r%i segments processed" % n)
sys.stdout.flush()
Still would love to know your takes on this, thanks!

Music21: remove percussion based on channel number

I have some midi files [sample] from which I'd like to remove percussion.
Here's what I've been using to read midi files and then save midi back to disk. The resulting sound is great:
path = 'lld.midi'
score = music21.converter.parse(path,
forceSource=False,
quantizePost=False,
).stripTies(inPlace=True)
score.write('midi', 'score.midi')
Since percussion is stored on channel 10 in midi, I thought I could strip the percussion with something like:
m = music21.midi.MidiFile()
m.open(path)
m.read()
tracks = []
for track in m.tracks:
keep = True
for event in track.events:
if event.channel == 10:
keep = False
if keep:
tracks.append(track)
s = music21.midi.translate.midiTracksToStreams(tracks, quantizePost=False)
s.write('midi', 'no-percussion.midi')
This does strip the percussion, but it seems to mess up the note timing as well:
What am I missing? If others can offer advice as to how I can correct the timings of the MidiFile approach, I'd be very grateful!
Lord have mercy I needed to pass forceSource=False into the midiTracksToStreams call as well:
m = music21.midi.MidiFile()
m.open(path)
m.read()
tracks = [t for t in m.tracks if not any([e.channel == 10 for e in t.events])]
score = music21.stream.Score()
music21.midi.translate.midiTracksToStreams(tracks,
inputM21=score,
forceSource=False,
quantizePost=False,
ticksPerQuarter=m.ticksPerQuarterNote,
quarterLengthDivisors=(4,3),
)
score.write('midi', fp='out.midi')

Music21 make an MIDI stream sound like a particular instrument

I am using the Music21 python module for a project. The output of my code generates a MIDI file. I want this MIDI file to sound like a guitar, but it sounds like a Piano. I saw a similar question here which said I should add instrument.Guitar() before the notes in my output notes sequence.
But still the notes are being played like a keyboard.
This is the code which generates the output notes based on a sequence of input notes:
for pattern in in_notes:
# pattern is a chord
if len(self.intervals)<1:
self.set_intervals()
if ('.' in pattern) or pattern.isdigit():
notes_in_chord = pattern.split('.')
notes = []
for current_note in notes_in_chord:
new_note = note.Note(int(current_note))
notes.append(new_note)
new_chord = chord.Chord(notes)
new_chord.offset = offset
output_notes.append(instrument.ElectricGuitar())
output_notes.append(new_chord)
# pattern is a note
else:
new_note = note.Note(pattern)
new_note.offset = offset
output_notes.append(instrument.ElectricGuitar())
output_notes.append(new_note)
# increase offset each iteration so that notes do not stack
offset += self.intervals[0]
self.intervals.pop(0)
And the part which generates the output MIDI file:
midi_stream = stream.Stream(output_notes)
key = midi_stream.analyze('key')
i=interval.Interval(key.tonic,pitch.Pitch(self.key))
midi = midi_stream.transpose(i)
midi.write('midi', fp=f'{self.file_name}.midi')
How do I make it sound like a guitar?
It seems to work if I try to change the instrument to a violin with output_notes.append(instrument.Violin()) but not for any of the Guitars:- Guitar(), AcousticGuitar(), ElectricGuitar()

How to Extract Individual Chords, Rests, and Notes from a midi file?

I am making a program that should be able to extract the notes, rests, and chords from a certain midi file and write the respective pitch (in midi tone numbers - they go from 0-127) of the notes and chords to a csv file for later use.
For this project, I am using the Python Library "Music21".
from music21 import *
import pandas as pd
#SETUP
path = r"Pirates_TheCarib_midi\1225766-Pirates_of_The_Caribbean_Medley.mid"
#create a function for taking parsing and extracting the notes
def extract_notes(path):
stm = converter.parse(path)
treble = stm[0] #access the first part (if there is only one part)
bass = stm[1]
#note extraction
notes_treble = []
notes_bass = []
for thisNote in treble.getElementsByClass("Note"):
indiv_note = [thisNote.name, thisNote.pitch.midi, thisNote.offset]
notes_treble.append(indiv_note) # print's the note and the note's
offset
for thisNote in bass.getElementsByClass("Note"):
indiv_note = [thisNote.name, thisNote.pitch.midi, thisNote.offset]
notes_bass.append(indiv_note) #add the notes to the bass
return notes_treble, notes_bass
#write to csv
def to_csv(notes_array):
df = pd.DataFrame(notes_array, index=None, columns=None)
df.to_csv("attempt1_v1.csv")
#using the functions
notes_array = extract_notes(path)
#to_csv(notes_array)
#DEBUGGING
stm = converter.parse(path)
print(stm.parts)
Here is the link to the score I am using as a test.
https://musescore.com/user/1699036/scores/1225766
When I run the extract_notes function, it returns two empty arrays and the line:
print(stm.parts)
it returns
<music21.stream.iterator.StreamIterator for Score:0x1b25dead550 #:0>
I am confused as to why it does this. The piece should have two parts, treble and bass. How can I get each note, chord and rest into an array so I can put it in a csv file?
Here is small snippet how I did it. I needed to get all notes, chords and rests for specific instrument. So at first I iterated through part and found specific instrument and afterwards check what kind of type note it is and append it.
you can call this method like
notes = get_notes_chords_rests(keyboard_instruments, "Pirates_of_The_Caribbean.mid")
where keyboard_instruments is list of instruments.
keyboard_nstrument = ["KeyboardInstrument", "Piano", "Harpsichord", "Clavichord", "Celesta", ]
def get_notes_chords_rests(instrument_type, path):
try:
midi = converter.parse(path)
parts = instrument.partitionByInstrument(midi)
note_list = []
for music_instrument in range(len(parts)):
if parts.parts[music_instrument].id in instrument_type:
for element_by_offset in stream.iterator.OffsetIterator(parts[music_instrument]):
for entry in element_by_offset:
if isinstance(entry, note.Note):
note_list.append(str(entry.pitch))
elif isinstance(entry, chord.Chord):
note_list.append('.'.join(str(n) for n in entry.normalOrder))
elif isinstance(entry, note.Rest):
note_list.append('Rest')
return note_list
except Exception as e:
print("failed on ", path)
pass
P.S. It is important to use try block because a lot of midi files on the web are corrupted.

Building AIS Messages Decoder

I used to decode AIS messages with theis package (Python) https://github.com/schwehr/noaadata/tree/master/ais until I started getting a new format of the messages.
As you may know, AIS messages come in two types mostly. one part (one message) or two parts (multi message). Message#5 is always comes in two parts. example:
!AIVDM,2,1,1,A,55?MbV02;H;s<HtKR20EHE:address#hidden#Dn2222222216L961O5Gf0NSQEp6ClRp8,0*1C
!AIVDM,2,2,1,A,88888888880,2*25
I used to decode this just fine using the following piece of code:
nmeamsg = fields.split(',')
if nmeamsg[0] != '!AIVDM':
return
total = eval(nmeamsg[1])
part = eval(nmeamsg[2])
aismsg = nmeamsg[5]
nmeastring = string.join(nmeamsg[0:-1],',')
bv = binary.ais6tobitvec(aismsg)
msgnum = int(bv[0:6])
--
elif (total>1):
# Multi Slot Messages: 5,6,8,12,14,17,19,20?,21,24,26
global multimsg
if total==2:
if msgnum==5:
if nmeastring.count('!AIVDM')==2 and len(nmeamsg)==13: # make sure there are two parts concatenated together
aismsg = nmeamsg[5]+nmeamsg[11]
bv = binary.ais6tobitvec(aismsg)
msg5 = ais_msg_5.decode(bv)
print "message5 :",msg5
return msg5
Now I'm getting a new format of the messages:
!SAVDM,2,1,7,A,55#0hd01sq`pQ3W?O81L5#E:1=0U8U#000000016000006H0004m8523k#Dp,0*2A,1410825672
!SAVDM,2,2,7,A,4hC`2U#C`40,2*76,1410825672,1410825673
Note. the number at the last index is the time in epoch format
I tried to adjust my code to decode this new format. I succeed in decoding messages with one part. My problem is multi message type.
nmeamsg = fields.split(',')
if nmeamsg[0] != '!AIVDM' and nmeamsg[0] != '!SAVDM':
return
total = eval(nmeamsg[1])
part = eval(nmeamsg[2])
aismsg = nmeamsg[5]
nmeastring = string.join(nmeamsg[0:-1],',')
dbtimestring = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(float(nmeamsg[7])))
bv = binary.ais6tobitvec(aismsg)
msgnum = int(bv[0:6])
Decoder can't bring the two lines as one. So decoding fails because message#5 should contain two strings not one. The error i get is in these lines:
if nmeastring.count('!SAVDM')==2 and len(nmeamsg)==13:
aismsg = nmeamsg[5]+nmeamsg[11]
Where len(nmeamsg) is always 8 (second line) and nmeastring.count('!SAVDM') is always 1
I hope I explained this clearly so someone can let me know what I'm missing here.
UPDATE
Okay I think I found the reason. I pass messages from file to script line by line:
for line in file:
i=i+1
try:
doais(line)
Where message#5 should be passed as two lines. Any idea on how can I accomplish that?
UPDATE
I did it by modifying the code a little bit:
for line in file:
i=i+1
try:
nmeamsg = line.split(',')
aismsg = nmeamsg[5]
bv = binary.ais6tobitvec(aismsg)
msgnum = int(bv[0:6])
print msgnum
if nmeamsg[0] != '!AIVDM' and nmeamsg[0] != '!SAVDM':
print "wrong format"
total = eval(nmeamsg[1])
if total == 1:
dbtimestring = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(float(nmeamsg[8])))
doais(line,msgnum,dbtimestring,aismsg)
if total == 2: #Multi-line messages
lines= line+file.next()
nmeamsg = lines.split(',')
dbtimestring = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(float(nmeamsg[15])))
aismsg = nmeamsg[5]+nmeamsg[12]
doais(lines,msgnum,dbtimestring,aismsg)
Be aware that noaadata is my old research code. libais is my production library thst is in use for NOAA's ERMA and WhaleAlert.
I usually make decoding a two pass process. First join multi-line messages. I refer to this as normalization (ais_normalize.py). You have several issues in this step. First the two component lines have different timestamps on the right of the second string. By the USCG old metadata standard, the last one matters. So my code will assume that these two lines are not related. Second, you don't have the required station id field.
Where are you getting the SA from in SAVDM? What device ("talker" in the NMEA vocab) is receiving these messages?
If you're in Ruby, I can recommend the NMEA and AIS decoder ruby gem that I wrote, available on github. It's based on the unofficial AIS spec at catb.org which is maintained by one of Kurt's colleagues.
It handles combining of multipart messages, reads from streams, and supports a large of NMEA and AIS messages. Decoding the 50 binary subtypes of AIS messages 6 and 8 is presently in development.
To handle the nonstandard lines you posted:
!SAVDM,2,1,7,A,55#0hd01sq`pQ3W?O81L5#E:1=0U8U#000000016000006H0004m8523k#Dp,0*2A,1410825672
!SAVDM,2,2,7,A,4hC`2U#C`40,2*76,1410825672,1410825673
It would be necessary to add a new parse rule that accepts fields after the checksum, but aside from that it should go smoothly. In other words, you'd copy the parser line here:
| BANG DATA CSUM { result = NMEAPlus::AISMessageFactory.create(val[0], val[1], val[2]) }
and have something like
| BANG DATA CSUM COMMA DATA { result = NMEAPlus::AISMessageFactory.create(val[0], val[1], val[2], val[4]) }
What do you do with those extra timestamp(s)? It almost looks like they've been appended by whatever software is doing the logging, rather than being part of the actual message.

Categories