Experiment VI: Embodied Resonance – ecg_to_drum.py

To generate heart-rate–driven drums I set out to write a script that builds a drum pattern whose density depends on HR intensity. I fixed the tempo at 75 BPM because the math is convenient: 0.1 s of data ≈ one 1/32-note at 75 BPM. I pictured a bar with 16 cells and designed a map that decides which cell gets filled next; the higher the HR, the more cells are activated. Before rendering, a helper scans the global HR minimum and maximum and slices that span into 16 equal zones (an 8-zone fallback is available) so the percussion always scales correctly. For extra flexibility, the script also writes SDNN and RMSSD into the MIDI file as two separate CC automation lanes. In Ableton, I can map those controllers to any parameter, making it easy to experiment with the track’s sonic texture.

Let’s take a closer look at the code. If you want to see the full, visit: https://github.com/ninaeba/EmbodiedResonance

This script converts raw heart-rate data into a drum track.
Every 0.1-second CSV sample lines up with a 1⁄32-note at the fixed tempo of 75 BPM, so medical time falls neatly onto a musical grid. The pulse controls how many cells inside each 16-step bar are filled, while SDNN and RMSSD are embedded as two CC lanes for later sound-design tricks.

Default configuration (all constants live in one block)
 STEP_BEAT = 1 / 32  # grid resolution in beats
 NOTE_LENGTH = 1 / 32  # each hit lasts a 32nd-note
 DEF_BPM = 75    # master tempo
 CSV_GRID = 0.1   # CSV step in seconds
 SEC_PER_CELL = 0.1   # duration of one pattern cell
 SIGNATURE = "16/16" # one bar = 16 cells
 CC_SDNN = 11    # long-term HRV
 CC_RMSSD = 1    # short-term HRV

The spatial order of hits is defined by a map that spreads layers across the bar, so extra drums feel balanced instead of bunching at the start

 CELL_MAP = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15]

generate_rules slices the global HR span into sixteen equal zones and binds each zone to one cell-and-note pair; the last rule catches any pulse above the top threshold

 def generate_rules(hr_min, hr_max, cells):
  thresholds = np.linspace(hr_min, hr_max, cells + 1)[1:]
  rules = [(thr, CELL_MAP[i], 36 + i) for i, thr in enumerate(thresholds)]
  rules[-1] = (np.inf, rules[-1][1], rules[-1][2])
  return rules

Inside csv_to_midi the script first writes two continuous-controller streams—one for SDNN, one for RMSSD—normalised to 0-127 at every grid tick

 sdnn_norm = clip((sdnn − min) / (range), 0, 1)
 rmssd_norm = clip((rmssd − min) / (range), 0, 1)
 add CC 11 with value round(sdnn_norm × 127) at time t
 add CC 1 with value round(rmssd_norm × 127) at time t

A cumulative-pattern list is built so rhythmic density rises without ever muting earlier layers

 CUM_PATTERNS = []
 acc = []
 for (thr, cell, note) in rules:
  acc.append((cell, note)) # keep previous hits
  CUM_PATTERNS.append(list(acc)) # zone n holds n+1 hits

During rendering each bar/cell slot is visited, the current HR is interpolated onto that exact moment, its zone is looked up, velocity is set to the rounded pulse value, and only the cells active in that zone fire

 hr = interp(t_sample, times, hr_vals)
 zone = first i where hr ≤ rules[i].threshold
 vel = int(round(hr))

 for (z_cell, note) in CUM_PATTERNS[zone]:
  if z_cell ≠ cell: continue
  beat = (bar × cells + cell) × STEP_BEAT × 4
  t0 = beat × 60 / bpm
  write Note(pitch=note, velocity=vel, start=t0, end=t0 + NOTE_LENGTH in seconds)


Before any writing starts the entry-point scans the whole folder to gather global minima and maxima for HR, SDNN and RMSSD, guaranteeing identical zone limits and CC scaling across every file in the batch

 hr_min, hr_max = min/max of all HR values
 sdnn_min, sdnn_max = min/max of all SDNN values
 rmssd_min, rmssd_max = min/max of all RMSSD values
 rules = generate_rules(hr_min, hr_max, cells)

Run the converter like this:

 python ecg_to_drum.py path/to/csv_folder --tempo 75 --time-signature 16/16

Each CSV becomes a _drums.mid file that contains a single drum-kit track whose note density follows heart-rate zones and whose CC 11 and CC 1 envelopes mirror long- and short-term variability—ready to animate filters, reverbs or whatever you map them to in the DAW.

Here are examples of generated MIDI files with drums. On the left side, there are drums made with individual mapping for every patient separately, and on the right side, mapping is one for all patients. We can see they resemble our HR graphs.

Alina Volkova - a Ukrainian singer, sound producer, and DJ, performing under name Nina Eba. Her musical journey was shaped by her education at a music school, playing in rock bands, composing music for audio stocks, and working in television. In August 2024, she released her debut multi-genre mini-album MORPHO, followed by a remix compilation RE:MORPHIX, created in collaboration with 10 producers from different countries. Now she is master student at FH Joanneum/ KUG Sound Design Program and works on project Embodied Echoes.
Leave a Reply

Your email address will not be published. Required fields are marked *