Thinking about which kind of data might be suitable to render in an audible way, I came up with the idea that “audioalize” (like visualize) the Git histrories of software projects.
The script below does exactly that. For example, you can listen to the history of Sonic Pi, rendered into a 40 minutes piano piece. For those not familiar with using Git, I recorded the first few minutes (starting at approx. commit 1000):
Each commit produces one note. Timing is proportional to commit time and date, each day represented by one second. The note pitch depends on the number of files changed in the commit, while amp depends on the number of lines changed (actually, the logarithm).
You may try to change note_selection
to 1
, which results in a note associated to each contributor rather than judged by the number of changed files.
There are many more possibilities which data associated with each commit to use to create the sounds, and how. Your ideas on that are highly appreciated!
Sonic Pi’s history is not the most exciting to listen to (due to many single-file commits), but I think it fits well for this forum posts, and some passages are IMO still quite interesting. You may try any other Git repository by cloning it and setting path
to the local repo’s folder. Note that the script requires Git to be installed on your machine!
When playing (with) the code, be aware that the pre-processing is faily slow! On my laptop, processing the 9000 commits of Sonic Pi takes around 30 seconds, before any sound is played. So you need to be a bit patient.
############ SETTINGS ################################################################################
# Path of the git repository
path = 'path/to/local/repo'
# Notes to play
notes = scale(:f3, :minor_pentatonic, num_octaves: 2)
# How to select notes
# 0 ... randomly
# 1 ... fixed note per author
# 2 ... by number of changed files
note_selection = 2
# Playback speed
seconds_per_day = 1
# play the tick sound every x days
days_per_tick = 1
# should ticks (e.g. per day) be audible?
play_ticks = false
# Maximum number of (most recent) commits.
# Use this on repositories with a long histroy, as retrieving and processing data is fairly slow.
# Use -1 to load the entire history.
# For the Sonic Pi repo, we just skip the first few hundred commits until the project gained momentum.
max_commits = 8000
set_sched_ahead_time! 2
######################################################################################################
# gets the history by calling `git log`, and processes it into an array of commits, with:
# [time, author id, #files changed, #insertions, #deletions]
# path: path to repo
# spd: seconds per day
# n: maximum number of (most recent) commits
define :get_history do |path, spd: 1, n: -1|
# get the git log in a parsable format
out = %x|git -C #{path} log --all --pretty=format:"%ad;%an" --date=iso --shortstat -n #{n}|
# modify into a CSV-like format
lines = out.gsub(/\n /, ';').split(/\n/)
out = nil
authors = {}
table = []
# iterate over entries and parse into an array of arrays
for line in lines
parts = line.split(';')
if parts.length > 0
# parse commit date to timestamp
timestamp = Time.parse(parts[0]).to_i
author = parts[1]
author_id = 0
if authors.key?(author)
author_id = authors[author]
else
author_id = authors.length
authors[author] = author_id
end
files = 0
ins = 0
del = 0
# add counts of changed files, insertions and deletions (#lines)
if parts.length > 2
pp = parts[2].split(',')
for p in pp
if p.include? 'file'
files = p.split(' ')[0].to_i
elsif p.include? 'insertion'
ins = p.split(' ')[0].to_i
elsif p.include? 'deletion'
del = p.split(' ')[0].to_i
end
end
end
# addpen "table" row
table.append([timestamp, author_id, files, ins, del])
end
end
lines = nil
# time scale to convert history time into playback time
time_scale = spd / Float(60 * 60 * 24)
# sort by time
table = table.sort_by { |row| row[0] }
# adjust and scale time
if table.length > 0
t0 = table[0][0]
start = Time.at(t0)
# we want time 0 to be midnight before the first commit, not the actual time of the first commit
midnight = Time.new(start.year, start.month, start.day, 0, 0, 0, start.utc_offset)
t0 = midnight.to_i
for row in table
row[0] = (row[0] - t0) * time_scale
end
end
table
end
# Get the data and measure the time it takes, to delay the loops in order to avoid timing errors
start = Time.now
table = get_history(path, spd: seconds_per_day, n: max_commits)
total_time = table[-1][0]
duration = Time.now - start
live_loop :tick, delay: duration + 0.5 do
if play_ticks then sample :elec_plip, rate: 2, amp: 0.2 end
sleep seconds_per_day * days_per_tick
end
idx = 0
time = 0
live_loop :commits, sync: :tick do
puts "#{time} / #{total_time}"
with_fx :reverb, room: 1 do |fx|
with_synth :piano do
while true
row = table[idx]
t = row[0]
# schedule commits one beat ahead, otherwise break
if t > time + 1 then break end
# use the scaled logarithm of the number of changed lines to modulate the volume
changes = row[3] + row[4]
amp = 0.25 * Math.log10(changes + 1)
# select the note to play, based on parameter note_selection
note =
if note_selection == 0
notes.choose
elsif note_selection == 1
notes[row[1]]
else
notes[row[2]]
end
# play the note at the desired time in the future
time_warp t - time do
play note, amp: amp
end
# increment row/commit index, stop if no more rows
idx += 1
if idx >= table.length
stop
end
end
end
end
sleep 1
time += 1
end