-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Getting the TRACK_NAME for each track in a MIDI file #9
Comments
I have one more related question. I've not been able to figure out how to determine which track events are for when using the midi.play() function. I had the idea of preprocessing the MIDI files and storing the events in an array with the track and timing data. This works fine for smaller files, but a lot cause "MemoryError: memory allocation failed, allocating 16384 bytes" errors. The goal is to have multiple MIDI outputs which are specific to certain tracks all playing back at once in proper time. Here's the preprocessor:
Then in the playback function, I have:
Could you please let me know if this can be achieved in some way by using the .play() method so I wouldn't need to preprocess the whole file before playback? Thank you, |
Hello Cameron, That said, most MIDI editing software output all meta events, including the track name event, at the beginning of each track. So if you just scan a few events at the beginning of each track, you can compile the track names, for example: for track in umidiparser.MidiFile(file_path, buffer=0).tracks:
for event in track:
if not event.is_meta():
break
if ev.status == umidiparser.TRACK_NAME:
self.track_list.append(event.name) I have a collection of about 8000 random MIDI files downloaded from the internet to test umidiparser. This code to get a list of track names worked on all of these files. I hope this helps, this should be a lot faster than the previous method. Please let me know if this approach worked for you. Re: your second question: Usually, MIDI file tracks are used for different instruments, usually one instrument per track. To know which note comes from each track, all notes in the same track usually have the same channel number. So you can look at event.channel and your program will know to which track the note belongs. With this approach, you can use .play() and get each event in time. Then you look at the channel number and decide which notes to sound and which notes to silence. .play() sorts the tracks on the fly: it looks at all tracks and selects the next event from each track. That way no .sort() is needed, because .sort() will certainly cause a MemoryError on most microcontrollers. Does that help for your problem? I.E. can you control or edit the contents of the MIDI files to assign the same channel number to notes of the same track? Usually MIDI editors already work that way. Looking at the first question again, I'd parse each track for a channel event (i.e. a event where event.is_channel() is true) and then make a dict with key=track name and value=channel. Perhaps that could be useful in your context, provided the MIDI files have the appropriate order of meta events and channel events and have one channel per track. self.track_dict = {}
for track in umidiparser.MidiFile(file_path, buffer=0).tracks:
key = None
for event in track:
if ev.status == umidiparser.TRACK_NAME:
self.track_dict.append(event.name)
if event.is_channel():
if key:
self.track_dict[key] = event.channel
break Please let me know if this helps. |
Hello again, bixb922, Yes, that did help. Thank you! I was able to get my project working well enough to get a release up. The project is a MicroPython Tesla Coil Controller (https://github.com/cameronprince/mptcc). Your umidiparser was instrumental in allowing me to provide the MIDI File feature to drive Tesla coils with MIDI tracks. I realize the code as I have it is greatly lacking. This is due to preprocessing the MIDI file and preparing an events array. (see https://github.com/cameronprince/mptcc/blob/main/screens/midi_file/play.py#L62) This inherently limits the size of the MIDI file it can play, which is unacceptable. I did this because I was attempting to share the SPI bus between the display and the SD card reader. At that time, I had erroneously blamed some performance issues on the display being on the I2C bus. I've now switched the display back to the I2C bus and the SPI bus is dedicated to the SD card reader. I can now use the buffer, so I'm hoping the MidiFile.play() function can be used and I can rip out a lot of the code I have in there to rely more on umidiparser. I was speaking about this with a friend and she mentioned this about buffereing:
I'm curious as to your thoughts on this and now my project as a whole. The requirement for me is to fire the four fiber optic transmitters in time with the note_on commands from their mapped tracks, in a buffered way. Please let me know what you think. Cameron |
Fascinating project! Instead of trying to use the track number, I'd use the channel number, which is an attribute available for MIDI note on/note off events. This number can vary from 0 to 15, and is exactly for this purpose: to distinguish different instruments, i.e. in MIDI files, each instrument will have a different channel number. The instrument is usually defined with a "set program" MIDI event, but in your case, I think you can skip using "set program". Track number should not be used as attribute of the event. There are MIDI files where all instruments are merged in one track (MIDI file type 0) and if a MIDI event comes from a keyboard, there will be no track number at all. But there will be always a channel number to distinguish different instruments or things to be controlled (whatever that "thing" is). For example, channel 10 (corresponds to number 9 when numbering from 0 to 15) is normally assigned for drums/percussion. When editing the MIDI file with any MIDI file editor you will be able to set the channel number. Usually, all note on/note off events with the same channel number are placed in the same track. Using channel numbers instead of track numbers will enable to use MidiFile(filename).play(). As your friend points out correctly, tracks are consumed at different rates. This is what umidiparser does: it follows each track at the correct rate, and merges the events of the tracks without need to buffer the whole track nor the complete file. That way, you can play back MIDI files much larger than the available memory. Using .play() will also make the program simpler and shorter. I hope this helps. Please let me know about your project, I'd really like to see that working! BTW, this is something I do with umidiparser: https://www.youtube.com/channel/UCEzqU6sOy_ST1nh15U3dYCg |
Wow, that's cool! So the MIDI is controlling valves for the pipes? It sounds great! The problem with channels is that they are so arbitrary in general MIDI files. I picked four files today at random. One had CH0 for all tracks. Two were 1:1 with a consistent and unique channel per track and the other had multiple channels in the same track. The interface for my controller has a mapping screen where you select a track and then choose the coil you'd like to drive. Since channels don't have names, this would be confusing. Also, Logic Pro and other software I've used, seem more track focused. I'd prefer to be able to use random MIDI downloads and not make custom files if I don't have to. The good news is that I was able to expand umidiparser to include the track number in the event data with very few changes. I'll post a PR shortly. The player works SO much better now. It's night and day. You really nailed the asyncio handling and incremental chunk reading. Now all my files play without out of memory errors. I stripped out all my timing code and allow umidiparser to do the hard work. I do have to figure out the elapsed time display now as it was broken by removing the original timing code, but no big deal. Thanks again so much for this project! |
Here's the PR for the track number: #10 |
Hello,
Thanks for a great project. I've been able to parse a MIDI file and play it on a MIDI output feeding to a keyboard from a Pico.
I was curious if there is a better way to get the track names. This is what I have gotten to work, but it's pretty slow:
I tried several ways to get it from midi.tracks, but haven't been successful.
Please let me know and thanks again,
Cameron
The text was updated successfully, but these errors were encountered: