Using MIDI synthesizer on Android

MIDI synthesizer could be used with the midistream library.

%load_ext pythonhere
%connect-there
%%there
from midistream import Synthesizer
midi = Synthesizer()

Get syntesizer configuration:

%%there
from pprint import pprint 
pprint(midi.config)
{'buildGUID': b'1feda229-b9a8-45e9-96f4-73c0a80e7220',
 'buildTimeStamp': 1195621085,
 'checkedVersion': 0,
 'filterEnabled': 1,
 'libVersion': 50727438,
 'ma3xVoices': 64,
 'mixBufferSize': 128,
 'numChannels': 2,
 'sampleRate': 22050}

Execute MIDI commands

%%there
midi.write([0x90, 60, 127])
  • 0x90 - code for MIDI event Note ON on channel 0

  • 60 - C4 (middle C) note number

  • 127 - maximum velocity

MIDI commands reference:

Off middle C note:

%%there
midi.write([0x80, 60, 127])

Helpers

midistream.helpers module could be used to construct MIDI messages

%%there
from midistream.helpers import (
    Control,
    Note,
    midi_control_change,
    midi_note_on,
    midi_note_off,
    midi_channels,
    midi_instruments,
    midi_program_change,
    note_name,
)
%%there
program = 44
print(f"Change program on channel 0 to: {midi_instruments[program]}")
midi.write(midi_program_change(program))
midi.write(midi_note_on(Note.Es4))
Change program on channel 0 to: Tremolo Strings
%%there -d 1
midi.write(midi_note_off(Note.Es4))

Channels

MIDI channels allows to play different parts at the same time.

16 channels are available, channel 9 is a persussion channel.

%%there
print(list(midi_channels()))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15]

Helpers functions accept an optional channel argument:

%%there
midi.write(midi_program_change(20, channel=1))
midi.write(midi_note_on(Note.A3, channel=1))

midi.write(midi_program_change(21, channel=2))
midi.write(midi_note_on(Note.E4, channel=2))
%%there -d 1
midi.write(midi_note_off(Note.A3, channel=1))

At this point, A3 note is off on channel 1, but channel 2 is continue to sound.

%%there -d 1
midi.write(midi_note_off(Note.E4, channel=2))

Control Change messages

%%there
midi.write(
    midi_control_change(Control.volume, 110) +
    midi_control_change(Control.modulation, 90) +
    midi_program_change(62) + 
    midi_note_on(Note.D3) + midi_note_on(Note.F3)
)
%%there -d 2
midi.write(midi_control_change(Control.all_sound_off))  # Turns off sounds on a channel instantly
midi.write(midi_control_change(Control.modulation, 0))

Master volume and Reverb effect

are controlled with Synthesizer properties:

%%there
from midistream import ReverbPreset
midi.volume = 100  # Set maximum master volume
midi.reverb = ReverbPreset.ROOM  # Enable Room preset

Repeating a note with Kivy Clock

Play note every second:

%%there
from kivy.clock import Clock

beat_program = 33
beat_note = Note.C3

def beat(_):
    midi.write(midi_program_change(beat_program, channel=1))
    midi.write(midi_note_on(beat_note, channel=1))

beat_event = Clock.schedule_interval(beat, .5)

Change playing instrument and note:

%%there -d 2
beat_note += 12
beat_program = 115

Stop the scheduled event:

%%there -d 2
Clock.unschedule(beat_event)

Playing sequence with Kivy Animation

%%there kv
BoxLayout:
%%there
from kivy.properties import NumericProperty
from kivy.uix.label import Label

class NoteLabel(Label):
    """Play and show note."""
    prev_note = 0
    note = NumericProperty(Note.E2)
    font_size = 150

    def on_note(self, obj, note):
        note = int(note)  # animation produce float values
        if note == self.prev_note:  # do not repeat same note
            return
        self.text = f"{note} : {note_name(note):3}"
        self.prev_note = note
        midi.write(midi_note_on(note) + midi_note_on(note + 7))
%%there
from kivy.animation import Animation

root.clear_widgets()
widget = NoteLabel()
root.add_widget(widget)

midi.write(midi_program_change(36))
    
sequence = (
    Animation(note=widget.note + 6, duration=1.5, transition="in_elastic") +
    Animation(note=widget.note + 13, duration=1.5, transition="in_elastic") +
    Animation(note=widget.note, duration=4, transition="out_sine") +
    Animation(note=widget.note, duration=.8)
)
sequence.bind(on_complete=lambda *args: midi.write(midi_control_change(Control.all_sound_off)))
sequence.start(widget)
%there -d8 screenshot -w 200
../_images/midi_36_0.png

Instruments list

%%there kv
#:import midi_instruments midistream.helpers.midi_instruments

<InstrumentButton>:
    group: "instruments"
    text: "[b]{:3}[/b] : {}".format(self.instrument_code, midi_instruments[self.instrument_code])
    markup: True
    size_hint: 1, None
    text_size: self.size
    valign: "center"    
    padding_x: 40

ScrollView:
    do_scroll_x: False
    size_hint: 1, 1
    BoxLayout:
        id: instruments
        orientation: "vertical"
        size_hint: 1, None
        height: self.minimum_size[1]
%%there
from kivy.properties import NumericProperty
from kivy.uix.togglebutton import ToggleButton


class InstrumentButton(ToggleButton):
    instrument_code = NumericProperty()
    notes = Note.A3, Note.D4, Note.A5

    def on_state(self, widget, value):
        if value == "down":
            midi.write(midi_program_change(self.instrument_code))
            for note_code in self.notes:
                midi.write(midi_note_on(note_code))
        else:
            for note_code in self.notes:
                midi.write(midi_note_off(note_code))

for code in list(midi_instruments.keys()):
    root.ids.instruments.add_widget(InstrumentButton(instrument_code=code))
%there -d 1 screenshot -w 200
../_images/midi_40_0.png