Closed waltribeiro closed 3 months ago
You can certainly open two MIDI files simultaneously, and iterate over both getting the events, for example:
import umidiparser
filename1 = "/tunelib/we wish merry christmas.mid"
filename2 = "/tunelib/youtellm.mid"
mf1 = umidiparser.MidiFile(filename1)
mf2 = umidiparser.MidiFile(filename2)
mf1_iterator = iter(mf1)
mf2_iterator = iter(mf2)
while True:
print("file1:", next(mf1_iterator))
print("file2:", next(mf2_iterator))
This example is rather crude, there is no calculation of times, nor waiting for the time to pass.
What you will have to add is merging the times of the tracks. For each event you will have to calculate the time to the next event and wait that time (which is something umidiparser does for multitrack files).
Perhaps the following code might work, I haven't tested this, and this really isn't part of the scope of this project:
mf1 = umidiparser.MidiFile(filename1)
mf2 = umidiparser.MidiFile(filename2)
mf2.tracks.append(mf1.tracks[0])
for ev in mf2.play():
' process event
print(ev)
This should add the track of midi file 1 to midi file 2 (in memory, the files will be untouched) and then play the combined midi file as a multitrack file. The header of both files should be the same.
Just out of curiosity: why don't you want to combine the two files in one as a standard multitrack file? It is very uncommon to play two files simultaneously.
I'll test this out tonight. Looks like it might work.
As for your question at the end - perhaps it might be better that both instruments were in a single MIDI file. But could I "turn off" and "turn on" each instrument separately? Like, have instrument 1 play at velocity 120 while instrument 2 plays at velocity 0 ?
That might be a better solution.
If you have two instruments in a MIDI file, usually each instrument is assigned a channel, that is, all events of that instrument have the channel attribute set to a distinct value. That is done both for single track and multitrack files. When iterating through the MIDI events, the MidiEvent.channel attribute is a way to distinguish which instrument is playing.
For example:
active_channels = [0,1]
for ev in MidiFile("myfile.mid"):
if ev.is_channel() and ev.channel in active_channels:
... process this event ...
or also:
muted_channels = [0,1]
for ev in MidiFile("myfile.mid"):
if ev.status in (NOTE_ON, NOTE_OFF) and ev.channel in muted_channels:
ev.velocity = 0
... continue processing the event ...
This is usually done to implement muting a channel. Does this apply to your use case?
This documentation helps. However, when I choose "active_channels = [0,1]" or just "active_channels = [1]" then the 2nd channel gets all wacky. The MIDI file I'm playing with is just a simple 2-channel MIDI file with quarter notes and eighth notes. So not sure what's happening.
I have a push button on GP8 that I'd like to bring the velocity from 0 to 127 velocity_button = Pin(11, Pin.IN, Pin.PULL_UP)
So basically, a 2-channel MIDI file is playing, and when a push button event happens, then the Treble (channel 0) is either muted or unmuted while the other channel (channel 1) continues playing at 127 velocity.
Is there documentation for that?
Here's what I have so far:
import umidiparser
from time import sleep_us
from machine import Pin, PWM
pwm = PWM(Pin(20))
led = Pin('LED', Pin.OUT)
button = Pin(21, Pin.IN, Pin.PULL_UP) # Push button on GPIO pin 21
def play_tone(freq, usec):
# play RTTL/midi notes, also flash onboard LED
print('freq = {:6.1f} || usec = {:6.1f}'.format(freq, usec))
if freq > 20:
pwm.freq(int(freq)) # Set frequency
pwm.duty_u16(32767) # 50% duty cycle
led.on()
sleep_us(int(0.9 * usec)) # Play for a number of usec
pwm.duty_u16(0) # Stop playing for gap between notes
led.off()
sleep_us(int(0.1 * usec)) # Pause for a number of usec
# map MIDI notes (0-127) to frequencies. Note 69 is 440 Hz ('A4')
freqs = [440 * 2**((float(x) - 69) / 12) for x in range(128)]
filename1 = "03-01.mid"
filename2 = "03-02.mid"
mf1 = umidiparser.MidiFile(filename1)
mf2 = umidiparser.MidiFile(filename2)
mf1_iterator = iter(mf1)
mf2_iterator = iter(mf2)
active_channels = [0, 1]
# Variables to manage the pause state
paused = False
button_pressed = False
# Function to be called when the button is pressed
def button_callback(pin):
global button_pressed
button_pressed = True
# Initialize the button and attach the interrupt
button.irq(trigger=Pin.IRQ_FALLING, handler=button_callback)
while True:
if button_pressed:
paused = not paused # Toggle the pause state
button_pressed = False
if not paused:
try:
event1 = next(mf1_iterator)
event2 = next(mf2_iterator)
if event1.is_channel() and event1.channel in active_channels:
if event1.status == umidiparser.PROGRAM_CHANGE:
# Handle Program Change events separately
print(f"Program Change on channel {event1.channel}, program: {event1.program}")
elif event1.status == umidiparser.NOTE_OFF:
play_tone(freqs[event1.note], event1.delta_us)
if event2.is_channel() and event2.channel in active_channels:
if event2.status == umidiparser.PROGRAM_CHANGE:
# Handle Program Change events separately
print(f"Program Change on channel {event2.channel}, program: {event2.program}")
elif event2.status == umidiparser.NOTE_OFF:
play_tone(freqs[event2.note], event2.delta_us)
except StopIteration:
# Restart the MIDI file iteration when it reaches the end
mf1_iterator = iter(mf1)
mf2_iterator = iter(mf2)
Nice!
Some comments: I'd moveevent1 = next(mf1_iterator)
just after play_tone(freqs[event1.note], event1.delta_us)
, similarly with event2 = next(mf2_iterator)
just after play_tone(freqs[event2.note], event2.delta_us)
I'd put the try: except: block just around the next(), so you can reset the iterator for each file only when that file comes to a StopIteration.
Is that a mechanical button you are using? If so, mechanical buttons bounce on contact, so you will receive a flurry of interrupts in a row. You need to disregard interrupts for 20 to 100 milliseconds after the first contact to get rid of these excess signals.
And I suppose you need to change the active_channel each time a button is pressed?
Hello, I'm sorry I couldn't answer faster. I think I understand the requirement a bit better. I'd structure the program this way:
import umidiparser
from time import sleep_us, ticks_ms, ticks_diff
from machine import Pin, PWM
from asyncio import ThreadSafeFlag
pwm = PWM(Pin(20))
led = Pin('LED', Pin.OUT)
button = Pin(21, Pin.IN, Pin.PULL_UP) # Push button on GPIO pin 21
def note_on(freq):
# play RTTL/midi notes, also flash onboard LED
print(f'{freq=:6.1f}')
if freq > 20:
pwm.freq(int(freq)) # Set frequency
pwm.duty_u16(32767) # 50% duty cycle
led.on()
def note_off():
pwm.duty_u16(0) # Stop playing for gap between notes
led.off()
# map MIDI notes (0-127) to frequencies. Note 69 is 440 Hz ('A4')
freqs = [440 * 2**((float(x) - 69) / 12) for x in range(128)]
# Function to be called when the button is pressed
last_button_press = ticks_ms()
# Use thread safe event to communicate IRQ level routine with main code:
button_down = ThreadSafeFlag()
def button_callback(pin):
global last_button_press
now = ticks_ms()
# Ignore IRQs during 100 milliseconds after button down detected
# to get rid of contact bouncing
if ticks_diff( now, last_button_press) < 100:
return
last_button_press = now
# Signal button has pressed
button_down.set()
# Initialize the button and attach the interrupt
button.irq(trigger=Pin.IRQ_FALLING, handler=button_callback)
channel1_paused = False
play_note = True
while True:
mf = umidiparser.MidiFile("myfile.mid")
for event in mf.play():
# Toggle channel1_paused if button was pressed
if button_down.state:
button_down.clear()
channel1_paused = not channel1_paused
if event.is_channel():
play_note = event.channel == 0 or (event.channel == 1 and not channel1_paused)
if event.status == umidiparser.NOTE_ON and play_note:
note_on(freqs[event.note])
elif event.status == umidiparser.NOTE_OFF and play_note:
note_off()
I added some debouncing logic for the button. The PWM will not be able to sound two notes at a time, however, so if two notes are played at the same time, only one of those will sound. I have not been able to test this program to hear the sound with a speaker, however it runs and calls note_on and note_off when it should.
The button is checked only when a note is turned off. If this is not acceptable, I'd rewrite the code with asyncio, which is better suited for this than interrupts.
Does this help?
I'm looking into generating polyphonic notes with 2 buzzers using UMIDIParser.
Is this possible? Either through use of internal timer functions, or something else?
I'm not looking to play one 2-track MIDI file. Instead, I'm looking to play 2 single-track MIDI files (therefore the 2nd MIDI track can turn on and off from an event).