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…
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.