Midi
====

Beatsearch provides functionality to import polyphonic rhythm patterns from MIDI files. The MIDI files should comply to
the `General MIDI Level 1 Percussion Key Map <https://www.midi.org/specifications/item/gm-level-1-sound-set>`_. Custom
MIDI drum mappings are also allowed, but require a bit more work. The MIDI import/export functionality is found in the
:class:`beatsearch.rhythm.MidiRhythm` class. For example, to import a MIDI file called `rumba.mid`:

.. code-block:: python

   from beatsearch.rhythm import MidiRhythm

   rhythm = MidiRhythm("rumba.mid")


MIDI drum mappings
__________________

A drum mapping is represented as a :class:`beatsearch.rhythm.MidiDrumMapping` object, which is essentially a collection
of :class:`beatsearch.rhythm.MidiDrumKey` objects. :class:`beatsearch.rhythm.MidiDrumKey` is a struct-like class which
holds information about a single key within a MIDI drum mapping. Each drum key holds information about its
frequency-band, the decay-time and the MIDI pitch. To load a MIDI drum loop with a custom mapping, you could do:

.. code-block:: python

   from beatsearch.rhythm import MidiRhythm, MidiDrumKey, FrequencyBand, DecayTime, create_drum_mapping

   CustomMapping = create_drum_mapping("CustomMapping", [
      MidiDrumKey(60, FrequencyBand.LOW, DecayTime.NORMAL, "Kick", key_id="kck"),
      MidiDrumKey(62, FrequencyBand.MID, DecayTime.NORMAL, "Snare", key_id="snr"),
      MidiDrumKey(64, FrequencyBand.MID, DecayTime.NORMAL, "Tom", key_id="tom"),
      MidiDrumKey(66, FrequencyBand.HIGH, DecayTime.SHORT, "Hi-hat", key_id="hht"),
      MidiDrumKey(70, FrequencyBand.HIGH, DecayTime.LONG, "Crash", key_id="crs")
   ])

   rhythm = MidiRhythm("loop.mid", midi_mapping=CustomMapping)


Instrumentation reduction
_________________________

For analytical purposes it's sometimes useful to reduce the instrument count of the drum patterns. This can be
done setting the ``MidiRhythm`` constructor's  `midi_mapping_reducer_cls` parameter. The MIDI mapping reducer must
be a subclass of ``MidiDrumMappingReducer`` or ``None`` for no instrumentation reduction. Beatsearch provides the
following instrumentation reducers:

FrequencyBandMidiDrumMappingReducer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When this reducer is applied, the instrumentation will be reduced down to three streams, based on the frequency-band
of the MIDI drum keys.

- LOW
- MID
- HIGH

DecayTimeMidiDrumMappingReducer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When this reducer is applied, the instrumentation will be reduced down to three streams, based on the decay-time of the
MIDI drum keys.

- SHORT
- NORMAL
- LONG

UniquePropertyComboMidiDrumMappingReducer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When this reducer is applied, the instrumentation will be reduced down to nine streams. One stream per unique
[frequency-band, decay-time] combination.

- LOW.SHORT
- LOW.NORMAL
- LOW.LONG
- MID.SHORT
- MID.NORMAL
- MID.LONG
- LONG.SHORT
- LONG.NORMAL
- LONG.LONG

Example
~~~~~~~

To load a MIDI rhythm and reduce it down to three instruments: `LOW`, `MID` and `HIGH`, you could do:

.. code-block:: python

   from beatsearch.rhythm import MidiRhythm, FrequencyBandMidiDrumMappingReducer

   rhythm = MidiRhythm(
       "./rumba.mid",
       midi_mapping_reducer_cls=FrequencyBandMidiDrumMappingReducer
   )


Rhythm corpus
_____________

We can use the :class:`beatsearch.rhythm.MidiRhythmCorpus` class to load multiple MIDI files. For example, load all the
MIDI files in a directory called `LOOPS`, you could do:

.. code-block:: python

   from beatsearch.rhythm import MidiRhythmCorpus

   rhythms = MidiRhythmCorpus("./LOOPS")

:class:`beatsearch.rhythm.MidiRhythmCorpus` also provides functionality to export its rhythms as MIDI files to a given
directory with the :meth:`beatsearch.rhythm.MidiRhythmCorpus.export_as_midi_files` method. This can be useful, for
example, to reduce the instrumentation of all the MIDI files in a particular directory.

.. code-block:: python

   from beatsearch.rhythm import MidiRhythmCorpus, FrequencyBandMidiDrumMappingReducer

   loops = MidiRhythmCorpus(
       "./LOOPS",
       midi_mapping_reducer=FrequencyBandMidiDrumMappingReducer
   )

   loops.export_as_midi_files("./LOOPS/reduced")