The Nasty Covid-19 Soundtrack of Your Country - Data Sonification

Hopefully I do not annoy anyone by mentioning the nasty virus here…

In my current data sonification project, I started to make the pandemic audible from time series of confirmed cases, deaths and vaccinations. It uses freely available data collected by the Johns Hopkins University, provided through Our World in Data. Due to the extensive dataset, you can listen to the soundtrack for every country in the world.

For an impression of the sound without running the code, here are recodings for Germany (as I live there), and for Israel (to show the sound of vaccinations suppressing the virus). Be warned that it sounds really psycho! It is definitely not music, but my intend was to make it not sound pleasant at all. I think I managed that… :wink:

Germany

Israel

Here is a brief description of which data is represented in which way

  • Each heart beat represents one day.
  • Daily cases are reflected by the noise/waves/wind sound. Further, the dissonant bass represents particularly high number of cases.
  • Daily deaths are represented by the volums and arrhythmia of the heart beat, as well as the lamento of synth prophet playing notes of the D-sharp Minor scale.
  • Cumulative vaccinations are represented by the promising sound of synth dark_ambience playing notes of the F Major scale, with pitch increasing with numbers.

All data is normalized by the maximum of the respective variable, so be aware that an absolute comparison between the versions for different countries is not possible.

To use the below code to listen to other counties, you need to download this CSV file provided by Our World in Data (~ 20MB). Further, at the top of the code set path to the location of the downloaded file, and country to the country you intend to sonify.

Here is the code, with some basic comments:

############ SETTINGS ################################################################################

# Path to data table
# Download worldwide date here:
# https://covid.ourworldindata.org/data/owid-covid-data.csv
path = 'path/to/owid-covid-data.csv'

# Country to sonify
country = 'Germany'

# The day (or week, see below) after the first case to start
start_day = 0

# Based on weekly instead of daily data, for a shorter soundtrack
weekly = false

sec_per_day = 1.2

amp_noise = 0.25
amp_growl = 1.5
amp_beat = 1.2
amp_deaths = 0.25
amp_vacc = 1

smooth_noise = 0.5
smooth_growl = 0.75
smooth_beat = 0.5
smooth_deaths = 0.5
smooth_vacc = 0.75

exp_noise = 0.8
exp_growl = 1
exp_beat = 0.5
exp_deaths = 1
exp_vacc = 0.6

# Parses the csv data (not really interesting...)
define :get_data do |path, country, weekly: false|
  require 'csv'
  
  vacc_total = true
  data = []
  
  found = false
  CSV.foreach(path, headers: true) do |row|
    if row["location"] == country
      cases = row["new_cases"].to_i
      deaths = row["new_deaths"].to_i
      
      vaccs = if vacc_total
        row["total_vaccinations"].to_i
      else
        row["new_vaccinations"].to_i
      end
      
      data.append({
                    date: Time.parse(row["date"]),
                    cases: if cases > 0 then cases else 0 end,
                    deaths: if deaths > 0 then deaths else 0 end,
                    vaccs: if vaccs > 0 then vaccs else 0 end,
      })
      found = true
    else
      if found then break end
    end
  end
  
  if weekly
    data_weekly = []
    temp = {date: 0, cases: 0, deaths: 0, vacc: 0}
    idx = 0
    for row in data
      temp[:date] = row[:date]
      temp[:cases] += row[:cases]
      temp[:deaths] += row[:deaths]
      if vacc_total
        temp[:vaccs] = row[:vaccs]
      else
        temp[:vaccs] += row[:vaccs]
      end
      idx += 1
      if idx % 7 == 0
        data_weekly.append(temp)
        temp = {date: 0, cases: 0, deaths: 0, vacc: 0}
      end
    end
    
    if idx % 7 != 0
      data_weekly.append(temp)
    end
    data_weekly
  else
    data
  end
end

start = Time.now
# get the data
data = get_data(path, country, weekly: weekly)
if data.length == 0
  puts "ERROR: no data found for country '#{country}'. You may have to check for typos."
  stop
end

# get the total number of days dnat the time to play synths
total_days = data.length
total_time = (total_days - start_day) * sec_per_day

# get some basic parameters for normalization, like maxima of table columns
max_cases = 0
max_deaths = 0
max_vaccs = 0
for row in data
  if row[:cases] > max_cases then max_cases = row[:cases] end
  if row[:deaths] > max_deaths then max_deaths = row[:deaths] end
  if row[:vaccs] > max_vaccs then max_vaccs = row[:vaccs] end
end

duration = Time.now - start

# Metronome loop to sync other loops to
index = start_day
end_of_data = false
live_loop :tick, delay: duration + 0.5 do
  puts data[index]
  
  sleep sec_per_day
  index += 1
  if index >= data.length
    end_of_data = true
    stop
  end
end

# delay loops to avoid timing errors
at duration + 0.5 do
  
  # The wind/sea noise sound representing number of cases
  with_synth :pnoise do
    a = 0
    syn = play sustain: total_time, release: 6, amp: 0,
      amp_slide: sec_per_day,
      cutoff_slide: sec_per_day,
      res: 0.2, cutoff: 100
    
    live_loop :cases_noise, sync: :tick do
      if end_of_data then stop end
      
      rel = data[index][:cases] / Float(max_cases + 1)
      a = a = smooth_noise * a +
        (1 - smooth_noise) * amp_noise * (rel ** exp_noise)
      control syn, amp: a, cutoff: 100 - 20 * rel
      sleep sec_per_day
    end
  end
  
  # The bass sound representing particularly high numbers of cases
  with_fx :distortion, distort: 0.5 do
    with_synth :hollow do
      syn = play :f2, sustain: total_time, release: 4, amp: 0,
        amp_slide: sec_per_day,
        note_slide: sec_per_day * 0.5
      
      a = 0
      live_loop :cases_growl, sync: :tick do
        if end_of_data then stop end
        
        rel = data[index][:cases] / Float(max_cases + 1)
        note = :f2 - 8 * (rel ** 0.5)
        
        a = smooth_growl * a +
          (1 - smooth_growl) * amp_growl * (rel ** exp_growl)
        
        control syn, amp: a, note: note
        sleep sec_per_day
      end
    end
  end
  
  # Heart beat giving the basic rhythm and representing deaths
  a_beat = 0
  live_loop :deaths_beat, sync: :tick do
    if end_of_data then stop end
    
    rel = data[index][:deaths] / Float(max_deaths + 1)
    a_beat = smooth_beat * a_beat +
      (1 - smooth_beat) * amp_beat * (rel ** exp_beat)
    
    pause = 0.35 - 0.25 * (rel ** exp_beat)
    
    sample :bd_tek, lpf: 90, amp: (a_beat + 0.1)
    sleep pause
    sample :bd_tek, lpf: 80, amp: 0.7 * (a_beat + 0.1)
    sleep sec_per_day - pause
  end
  
  # High synth lamento representing deaths
  with_synth :prophet do
    notes = (scale :ds3, :minor).reverse
    syn = play notes[0], sustain: total_time, release: 4,
      amp: 0, amp_slide: sec_per_day,
      note_slide: 0.25 * sec_per_day,
      cutoff_slide: sec_per_day
    
    a = 0
    live_loop :deaths_synth, sync: :tick do
      if end_of_data then stop end
      
      rel = data[index][:deaths] / Float(max_deaths + 1)
      a = smooth_deaths * a +
        (1 - smooth_deaths) * amp_deaths * (rel ** exp_deaths)
      
      control syn, amp: a, note: notes[index % 4], cutoff: 80 + 20 * rel
      sleep sec_per_day
    end
  end
  
  # The promising ambient sound representing cumulative vaccinations
  with_fx :reverb, room: 1 do
    with_synth :dark_ambience do
      notes = (scale :f2, :major, num_octaves: 2)
      syn = play notes[0], sustain: total_time + 4, release: 6, amp: 0,
        ring: 0.8, amp_slide: sec_per_day,
        reverb_time: 0
      
      a = 0
      live_loop :vacc, sync: :tick do
        if end_of_data then stop end
        
        rel = data[index][:vaccs] / Float(max_vaccs + 1)
        a = smooth_vacc * a +
          (1 - smooth_vacc) * amp_vacc * (rel ** exp_vacc)
        
        note_index = ((rel ** 0.5) * (notes.length - 4.01)).to_i
        modulo = index % 4
        note = notes[note_index + modulo]
        
        control syn, note: note, amp: a
        sleep sec_per_day
      end
    end
  end
end

I probably should consider to also make a video that shows the current date, as well as dynamic diagrams of the data the sounds are based on. When you run the code in Sonic Pi, date and counts are printed every day.

One thing I might improve, but don’t know how, is to make vaccinations sound less dissonant while case and death numbers are still high.

2 Likes

I’m not annoyed, but I kind of don’t want to listen to the daily death toll :sweat: It’s certainly a neat idea. These data sonifications are kind of like infographics. It looks like you chose appropriately sombre sounds to communicate the data. Maybe this will convince people to get vaccinated, wear masks, and respect their local rules.

1 Like

I can definitely understand that! I also have enough after listening to the around 8 minutes for a single country. There is the nasty sound, and in addition the associations to what it represents…

Indeed, I would be content if those tracks could contribute to motivate only a few people into the right direction.

1 Like