Skip to content

Instantly share code, notes, and snippets.

@craffel
Last active October 24, 2015 00:45
Show Gist options
  • Save craffel/3eb7513d4540f4acee93 to your computer and use it in GitHub Desktop.
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
Display the source blob
Display the rendered blob
Raw
{
"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