Sonic Pi's evolution in 40 minutes - Ever listened to a Git history?

As a first trial, I simply used a few different synths for differnt authors. However, I think this does not sound better then the previous version. You can try it by setting synth_per_author to true.

Further, I now use pan for authors. In the code below, authors are sorted by their contributions (number of commits). The author with the most commits is played with pan: 0. With decreasing contributions, authors are shifted more and more to the left or right, with a randomly selected direction, but consistent for each author. I.e. if an author’s sound appeared to the left once, it will do so for all commits of that author. It is not a particularly sensational effect, but makes it sound a bit more interesting IMO.

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

# Path of the git repository
path = 'path/to/local/repo'

# Notes to play
notes = scale(:f3, :minor_pentatonic, num_octaves: 2)

# should different synths be asociated to different authors?
synth_per_author = false

# synth used if synth_per_author = false
default_synth = :piano
# synths to use for different authors
synths = (ring :piano, :pluck, :saw, :dsaw, :tri, :dtri, :pulse, :dpulse, :fm, :pretty_bell, :beep)

# 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
      
      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
      
      author = parts[1]
      author_id = 0
      if authors.key?(author)
        a = authors[author]
        a[:commits] += 1
        a[:files] += files
        a[:changes] += ins + del
        author_id = a[:id]
      else
        author_id = authors.length
        authors[author] = {id: author_id, commits: 1, files: files, changes: ins + del}
      end
      
      # append "table" row
      table.append({time: timestamp, author: author_id, files: files, ins: ins, del: del})
    end
  end
  lines = nil
  
  # change author IDs to index in descending number of commits order
  # calculate pan based on author index
  idx = 0
  indices = {}
  max_index = authors.length
  for a in authors.to_a.sort_by { |a| -a[1][:commits] }
    indices[a[1][:id]] = [idx, ((1.0 - 1.0 / (idx + 1)) ** 10) * [-1, 1].choose]
    idx += 1
  end
  
  # 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[:time] }
  # adjust and scale time
  if table.length > 0
    t0 = table[0][:time]
    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[:time] = (row[:time] - t0) * time_scale
      ind = indices[row[:author]]
      row[:author] = ind[0]
      row[:pan] = ind[1]
    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][:time]
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|
    while true
      row = table[idx]
      t = row[:time]
      # 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[:ins] + row[:del]
      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[:author]]
      else
        notes[[row[:files], notes.length-1].min]
      end
      
      # get the synth to use (by author, or default)
      synth = synth_per_author ? synths[row[:author]] : default_synth
      
      # play the note at the desired time in the future
      time_warp t - time do
        with_synth synth do
          if synth_per_author
            play note, amp: amp, pan: row[:pan], sustain: 0, release: 0.5
          else
            play note, amp: amp, pan: row[:pan]
          end
        end
      end
      
      # increment row/commit index, stop if no more rows
      idx += 1
      if idx >= table.length
        stop
      end
      
    end
  end
  sleep 1
  time += 1
end
1 Like