Last active
October 24, 2015 00:45
-
-
Save craffel/3eb7513d4540f4acee93 to your computer and use it in GitHub Desktop.
Functions for drum synthesis, arpeggiation and chiptunes synthesis in pretty_midi. View here: http://nbviewer.ipython.org/gist/craffel/3eb7513d4540f4acee93
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"metadata": { | |
"name": "", | |
"signature": "sha256:a4fa5c255ccaa1be420a73cc68a6625dc7984768ae895075f774c57587c24682" | |
}, | |
"nbformat": 3, | |
"nbformat_minor": 0, | |
"worksheets": [ | |
{ | |
"cells": [ | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"import IPython.display" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"import numpy as np\n", | |
"import scipy.signal\n", | |
"import pretty_midi" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"def tonal(fs, length, frequency, nonlinearity=1.):\n", | |
" '''\n", | |
" Synthesize a tonal drum.\n", | |
" \n", | |
" :parameters:\n", | |
" - fs : int\n", | |
" Sampling frequency\n", | |
" - length : int\n", | |
" Length, in samples, of drum sound\n", | |
" - frequency : float\n", | |
" Frequency, in Hz, of the drum\n", | |
" - nonlinearity : float\n", | |
" Gain to apply for nonlinearity, default 1.\n", | |
" \n", | |
" :returns:\n", | |
" - drum_data : np.ndarray\n", | |
" Synthesized drum data\n", | |
" '''\n", | |
" # Amplitude envelope, decaying exponential\n", | |
" amp_envelope = np.exp(np.linspace(0, -10, length))\n", | |
" # Pitch envelope, starting with linear decay\n", | |
" pitch_envelope = np.linspace(1.0, .99, length)\n", | |
" # Also a quick exponential drop at the beginning for a click\n", | |
" pitch_envelope *= 100*np.exp(np.linspace(0, -100*frequency, length)) + 1\n", | |
" # Generate tone\n", | |
" drum_data = amp_envelope*np.sin(2*np.pi*frequency*pitch_envelope*np.arange(length)/float(fs))\n", | |
" # Filter with leaky integrator with 3db point ~= note frequency\n", | |
" alpha = 1 - np.exp(-2*np.pi*(frequency)/float(fs))\n", | |
" drum_data = scipy.signal.lfilter([alpha], [1, alpha - 1], drum_data)\n", | |
" # Apply nonlinearity\n", | |
" drum_data = np.tanh(nonlinearity*drum_data)\n", | |
" return drum_data" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"def noise(length):\n", | |
" '''\n", | |
" Synthesize a noise drum.\n", | |
" \n", | |
" :parameters:\n", | |
" - length : int\n", | |
" Number of samples to synthesize.\n", | |
" \n", | |
" :returns:\n", | |
" - drum_data : np.ndarray\n", | |
" Synthesized drum data\n", | |
" '''\n", | |
" # Amplitude envelope, decaying exponential\n", | |
" amp_envelope = np.exp(np.linspace(0, -10, length))\n", | |
" # Synthesize gaussian random noise\n", | |
" drum_data = amp_envelope*np.random.randn(length)\n", | |
" return drum_data" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"def synthesize_drum_instrument(instrument, fs=44100):\n", | |
" '''\n", | |
" Synthesize a pretty_midi.Instrument object with drum sounds.\n", | |
" \n", | |
" :parameters:\n", | |
" - instrument : pretty_midi.Instrument\n", | |
" Instrument to synthesize\n", | |
" \n", | |
" :returns:\n", | |
" - synthesized : np.ndarray\n", | |
" Audio data of the instrument synthesized\n", | |
" '''\n", | |
" # Allocate audio data\n", | |
" synthesized = np.zeros(int((instrument.get_end_time() + 1)*fs))\n", | |
" for note in instrument.notes:\n", | |
" # Get the name of the drum\n", | |
" drum_name = pretty_midi.note_number_to_drum_name(note.pitch)\n", | |
" # Based on the drum name, synthesize using the tonal or noise functions\n", | |
" if drum_name in ['Acoustic Bass Drum', 'Bass Drum 1']:\n", | |
" d = tonal(fs, fs/2, 80, 8.)\n", | |
" elif drum_name in ['Side Stick']:\n", | |
" d = tonal(fs, fs/20, 400, 8.)\n", | |
" elif drum_name in ['Acoustic Snare', 'Electric Snare']:\n", | |
" d = .4*tonal(fs, fs/10, 200, 20.) + .6*noise(fs/10)\n", | |
" elif drum_name in ['Hand Clap', 'Vibraslap']:\n", | |
" d = .1*tonal(fs, fs/10, 400, 8.) + .9*noise(fs/10)\n", | |
" elif drum_name in ['Low Floor Tom', 'Low Tom', 'Low Bongo', 'Low Conga', 'Low Timbale']:\n", | |
" d = tonal(fs, fs/4, 120, 8.)\n", | |
" elif drum_name in ['Closed Hi Hat', 'Cabasa', 'Maracas', 'Short Guiro']:\n", | |
" d = noise(fs/20)\n", | |
" elif drum_name in ['High Floor Tom', 'High Tom', 'Hi Bongo', 'Open Hi Conga', 'High Timbale']:\n", | |
" d = tonal(fs, fs/4, 480, 4.)\n", | |
" elif drum_name in ['Pedal Hi Hat', 'Open Hi Hat', 'Crash Cymbal 1',\n", | |
" 'Ride Cymbal 1', 'Chinese Cymbal', 'Crash Cymbal 2',\n", | |
" 'Ride Cymbal 2', 'Tambourine', 'Long Guiro',\n", | |
" 'Splash Cymbal']:\n", | |
" d = .8*noise(fs)\n", | |
" elif drum_name in ['Low-Mid Tom']:\n", | |
" d = tonal(fs, fs/4, 240, 4.)\n", | |
" elif drum_name in ['Hi-Mid Tom']:\n", | |
" d = tonal(fs, fs/4, 360, 4.)\n", | |
" elif drum_name in ['Mute Hi Conga', 'Mute Cuica', 'Cowbell',\n", | |
" 'Low Agogo', 'Low Wood Block']:\n", | |
" d = tonal(fs, fs/10, 480, 4.)\n", | |
" elif drum_name in ['Ride Bell', 'High Agogo', 'Claves', 'Hi Wood Block']:\n", | |
" d = tonal(fs, fs/20, 960, 4.)\n", | |
" elif drum_name in ['Short Whistle']:\n", | |
" d = tonal(fs, fs/4, 480, 1.)\n", | |
" elif drum_name in ['Long Whistle']:\n", | |
" d = tonal(fs, fs, 480, 1.)\n", | |
" elif drum_name in ['Mute Triangle']:\n", | |
" d = tonal(fs, fs/10, 1960, 1.)\n", | |
" elif drum_name in ['Open Triangle']:\n", | |
" d = tonal(fs, fs, 1960, 1.)\n", | |
" else:\n", | |
" if drum_name is not '':\n", | |
" # This should never happen\n", | |
" print 'Unexpected drum {}'.format(drum_name)\n", | |
" continue\n", | |
" # Add in the synthesized waveform\n", | |
" start = int(note.start*fs)\n", | |
" synthesized[start:start+d.size] += d*note.velocity\n", | |
" return synthesized" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"def synthesize_with_drums(midi, fs=44100, wave=np.sin):\n", | |
" '''\n", | |
" Synthesize a pretty_midi.PrettyMIDI object using the \n", | |
" synthesize_drum_instrument function for drum instruments.\n", | |
"\n", | |
" :parameters:\n", | |
" - midi : pretty_midi.PrettyMIDI\n", | |
" PrettyMIDI object to synthesize.\n", | |
" - fs : int\n", | |
" Sampling rate of the synthesized audio signal, default 44100\n", | |
" - wave : function\n", | |
" Function which returns a periodic waveform,\n", | |
" e.g. np.sin, scipy.signal.square, etc. Default np.sin\n", | |
"\n", | |
" :returns:\n", | |
" - synthesized : np.ndarray\n", | |
" Waveform of the MIDI data, synthesized at fs\n", | |
" '''\n", | |
" # If there are no instruments, return an empty array\n", | |
" if len(midi.instruments) == 0:\n", | |
" return np.array([])\n", | |
" # Get synthesized waveform for each instrument\n", | |
" waveforms = []\n", | |
" for inst in midi.instruments:\n", | |
" # Use drum synthesis method\n", | |
" if inst.is_drum:\n", | |
" waveforms.append(synthesize_drum_instrument(inst, fs=fs))\n", | |
" else:\n", | |
" waveforms.append(inst.synthesize(fs=fs, wave=wave))\n", | |
" # Allocate output waveform, with #sample = max length of all waveforms\n", | |
" synthesized = np.zeros(np.max([w.shape[0] for w in waveforms]))\n", | |
" # Sum all waveforms in\n", | |
" for waveform in waveforms:\n", | |
" synthesized[:waveform.shape[0]] += waveform\n", | |
" # Normalize\n", | |
" synthesized /= np.abs(synthesized).max()\n", | |
" return synthesized" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"def arpeggiate_instrument(instrument, arpeggio_time):\n", | |
" '''\n", | |
" Arpeggiate the notes of an instrument.\n", | |
" \n", | |
" :parameters:\n", | |
" - inst : pretty_midi.Instrument\n", | |
" Instrument object.\n", | |
" - arpeggio_time : float\n", | |
" Time, in seconds, of each note in the arpeggio\n", | |
" \n", | |
" :returns:\n", | |
" - inst_arpeggiated : pretty_midi.Instrument\n", | |
" Instrument with the notes arpeggiated.\n", | |
" '''\n", | |
" # Make a copy of the instrument\n", | |
" inst_arpeggiated = pretty_midi.Instrument(program=instrument.program,\n", | |
" is_drum=instrument.is_drum)\n", | |
" for bend in instrument.pitch_bends:\n", | |
" inst_arpeggiated.pitch_bends.append(bend)\n", | |
" n = 0\n", | |
" while n < len(instrument.notes):\n", | |
" # Collect notes which are in this chord\n", | |
" chord_notes = [(instrument.notes[n].pitch, instrument.notes[n].velocity)]\n", | |
" m = n + 1\n", | |
" while m < len(instrument.notes):\n", | |
" # It's in the chord if it starts before the current note ends\n", | |
" if instrument.notes[m].start < instrument.notes[n].end:\n", | |
" # Add in the pitch and velocity\n", | |
" chord_notes.append((instrument.notes[m].pitch, instrument.notes[m].velocity))\n", | |
" # Move the start time of the note up so it gets used next time\n", | |
" if instrument.notes[m].end > instrument.notes[n].end:\n", | |
" instrument.notes[m].start = instrument.notes[n].end\n", | |
" m += 1\n", | |
" # Arpeggiate the collected notes\n", | |
" time = instrument.notes[n].start\n", | |
" pitch_index = 0\n", | |
" if len(chord_notes) > 2:\n", | |
" while time < instrument.notes[n].end:\n", | |
" # Get the pitch and velocity of this note, but mod the index to circulate\n", | |
" pitch, velocity = chord_notes[pitch_index % len(chord_notes)]\n", | |
" # Add this note to the new instrument\n", | |
" inst_arpeggiated.notes.append(pretty_midi.Note(velocity, pitch, time, time + arpeggio_time))\n", | |
" # Next pitch next time\n", | |
" pitch_index += 1\n", | |
" # Move forward by the supplied amount\n", | |
" time += arpeggio_time\n", | |
" else:\n", | |
" inst_arpeggiated.notes.append(instrument.notes[n])\n", | |
" time = instrument.notes[n].end\n", | |
" n += 1\n", | |
" # Find the next chord\n", | |
" while n < len(instrument.notes) and instrument.notes[n].start + arpeggio_time <= time:\n", | |
" n += 1\n", | |
" return inst_arpeggiated" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"def chiptunes_synthesize(midi, fs=44100):\n", | |
" '''\n", | |
" Synthesize a pretty_midi.PrettyMIDI object chiptunes style.\n", | |
"\n", | |
" :parameters:\n", | |
" - midi : pretty_midi.PrettyMIDI\n", | |
" PrettyMIDI object to synthesize\n", | |
" - fs : int\n", | |
" Sampling rate of the synthesized audio signal, default 44100\n", | |
" \n", | |
" :returns:\n", | |
" - synthesized : np.ndarray\n", | |
" Waveform of the MIDI data, synthesized at fs\n", | |
" '''\n", | |
" # If there are no instruments, return an empty array\n", | |
" if len(midi.instruments) == 0:\n", | |
" return np.array([])\n", | |
" # Get synthesized waveform for each instrument\n", | |
" waveforms = []\n", | |
" for inst in midi.instruments:\n", | |
" # Synthesize as drum\n", | |
" if inst.is_drum:\n", | |
" waveforms.append(synthesize_drum_instrument(inst, fs=fs))\n", | |
" else:\n", | |
" # Call it a bass instrument when no notes are over 48 (130hz)\n", | |
" # or the program's name has the word \"bass\" in it\n", | |
" is_bass = (np.max([n.pitch for i in midi.instruments for n in i.notes]) < 48\n", | |
" or 'Bass' in pretty_midi.program_to_instrument_name(inst.program))\n", | |
" if is_bass:\n", | |
" # Synthesize as a sine wave (should be triangle!)\n", | |
" audio = inst.synthesize(fs=fs, wave=np.sin)\n", | |
" # Quantize to 5-bit\n", | |
" audio = np.digitize(audio, np.linspace(-audio.min(), audio.max(), 32))\n", | |
" waveforms.append(audio)\n", | |
" else:\n", | |
" # Otherwise, it's a harmony/lead instrument, so arpeggiate it\n", | |
" # Arpeggio time of 30ms seems to work well\n", | |
" inst_arpeggiated = arpeggiate_instrument(inst, .03)\n", | |
" # These instruments sound louder because they're square, so scale down\n", | |
" waveforms.append(.5*inst_arpeggiated.synthesize(fs=fs, wave=scipy.signal.square))\n", | |
" # Allocate output waveform, with #sample = max length of all waveforms\n", | |
" synthesized = np.zeros(np.max([w.shape[0] for w in waveforms]))\n", | |
" # Sum all waveforms in\n", | |
" for waveform in waveforms:\n", | |
" synthesized[:waveform.shape[0]] += waveform\n", | |
" # Normalize\n", | |
" synthesized /= np.abs(synthesized).max()\n", | |
" return synthesized" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"fs = 22050" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# Drum synthesis example\n", | |
"inst = pretty_midi.Instrument(program=0, is_drum=True)\n", | |
"inst.notes.append(pretty_midi.Note(100, pretty_midi.drum_name_to_note_number('Acoustic Bass Drum'), 0., .5))\n", | |
"inst.notes.append(pretty_midi.Note(100, pretty_midi.drum_name_to_note_number('Closed Hi Hat'), .5, 1.))\n", | |
"inst.notes.append(pretty_midi.Note(100, pretty_midi.drum_name_to_note_number('Acoustic Snare'), 1., 1.5))\n", | |
"inst.notes.append(pretty_midi.Note(100, pretty_midi.drum_name_to_note_number('Closed Hi Hat'), 1.5, 2.))\n", | |
"inst.notes.append(pretty_midi.Note(100, pretty_midi.drum_name_to_note_number('High Tom'), 2., 2.25))\n", | |
"inst.notes.append(pretty_midi.Note(100, pretty_midi.drum_name_to_note_number('Hi-Mid Tom'), 2.25, 2.5))\n", | |
"inst.notes.append(pretty_midi.Note(100, pretty_midi.drum_name_to_note_number('Low-Mid Tom'), 2.5, 2.75))\n", | |
"inst.notes.append(pretty_midi.Note(100, pretty_midi.drum_name_to_note_number('Low Tom'), 2.75, 3.))\n", | |
"d = synthesize_drum_instrument(inst, fs)\n", | |
"IPython.display.Audio(data=d, rate=fs, autoplay=True)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# Arpeggiation example\n", | |
"inst = pretty_midi.Instrument(program=20, is_drum=False)\n", | |
"inst.notes.append(pretty_midi.Note(100, 56, 0., 2.))\n", | |
"inst.notes.append(pretty_midi.Note(100, 60, 0., 2.))\n", | |
"inst.notes.append(pretty_midi.Note(100, 63, 0., 2.))\n", | |
"inst.notes.append(pretty_midi.Note(100, 67, 0., 2.))\n", | |
"d = inst.synthesize(fs=fs)\n", | |
"IPython.display.Audio(data=d, rate=fs, autoplay=True)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"inst_arp = arpeggiate_instrument(inst, .1)\n", | |
"d = inst_arp.synthesize(fs=fs)\n", | |
"IPython.display.Audio(data=d, rate=fs, autoplay=True)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [] | |
}, | |
{ | |
"cell_type": "code", | |
"collapsed": false, | |
"input": [ | |
"# Chiptunes example\n", | |
"filename = \"/Users/craffel/Downloads/ho1217.mid\"\n", | |
"fs = 22050\n", | |
"pm = pretty_midi.PrettyMIDI(filename)\n", | |
"d = chiptunes_synthesize(pm, fs=fs)\n", | |
"IPython.display.Audio(data=d, rate=fs, autoplay=True)" | |
], | |
"language": "python", | |
"metadata": {}, | |
"outputs": [] | |
} | |
], | |
"metadata": {} | |
} | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment