231 lines
5.7 KiB
Python
231 lines
5.7 KiB
Python
# -*- coding: utf-8
|
|
|
|
import argparse
|
|
import sys
|
|
import os
|
|
|
|
import shutil
|
|
|
|
from midiutil.MidiFile import MIDIFile
|
|
from StringIO import StringIO
|
|
from time import sleep
|
|
from git import Repo
|
|
from git.objects.blob import Blob
|
|
from git.exc import InvalidGitRepositoryError
|
|
|
|
# The original scale was: [60, 62, 64, 65, 67, 69, 71]
|
|
notes = [68, 69, 71, 72, 74, 76, 77]
|
|
notecount = len(notes)
|
|
|
|
|
|
def gen_history(log, commit):
|
|
counter = 0
|
|
to_process = [commit]
|
|
|
|
while len(to_process) > 0:
|
|
counter += 1
|
|
|
|
if counter % 500 == 0:
|
|
print("Done with {} commits".format(counter))
|
|
|
|
commit = to_process.pop()
|
|
|
|
if not commit in log:
|
|
log.append(commit)
|
|
to_process += commit.parents
|
|
|
|
|
|
def gen_volume(deletions, insertions, deviation=10):
|
|
return max(
|
|
deviation,
|
|
min(255 - deviation,
|
|
127 - deletions + insertions))
|
|
|
|
|
|
def sha_to_note(sha):
|
|
note_num = reduce(lambda res, digit: res + int(digit, 16),
|
|
list(str(sha)), 0) % notecount
|
|
|
|
return notes[note_num]
|
|
|
|
|
|
def get_file_sha(commit, file_name):
|
|
elements = file_name.split(os.sep)
|
|
t = commit.tree
|
|
|
|
|
|
while True:
|
|
try:
|
|
t = t[elements.pop(0)]
|
|
except KeyError:
|
|
# The file has been deleted
|
|
return '0000000000000000000000000000000000000000'
|
|
|
|
if isinstance(t, Blob):
|
|
break
|
|
|
|
return t.hexsha
|
|
|
|
def gen_note(commit):
|
|
stat = commit.stats
|
|
note = sha_to_note(commit.hexsha)
|
|
|
|
file_notes = []
|
|
|
|
for file_name, file_stat in stat.files.items():
|
|
file_notes.append({
|
|
'note': sha_to_note(get_file_sha(commit, file_name)) - 12,
|
|
'volume': gen_volume(file_stat['deletions'],
|
|
file_stat['insertions'],
|
|
deviation=10),
|
|
})
|
|
|
|
return {
|
|
'commit_note': sha_to_note(commit.hexsha) - 24,
|
|
'commit_volume': gen_volume(stat.total['deletions'],
|
|
stat.total['insertions'],
|
|
deviation=20),
|
|
'file_notes': file_notes,
|
|
}
|
|
|
|
|
|
class MemMIDI(MIDIFile):
|
|
def __init__(self, tracks=None):
|
|
if tracks is None:
|
|
tracks = [("Sample Track", 120)]
|
|
|
|
self.track_count = len(tracks)
|
|
|
|
MIDIFile.__init__(self, self.track_count)
|
|
|
|
self.mem_file = StringIO()
|
|
|
|
if tracks is None:
|
|
self.addTrackName(0, 0, "Sample Track")
|
|
self.addTempo(0, 0, 120)
|
|
else:
|
|
for idx, (name, tempo) in enumerate(tracks):
|
|
self.addTrackName(idx, 0, name)
|
|
self.addTempo(idx, 0, tempo)
|
|
|
|
def writeMem(self):
|
|
self.writeFile(self.mem_file)
|
|
|
|
def exportFile(self, filename):
|
|
with open(filename, 'w') as f:
|
|
self.mem_file.seek(0)
|
|
shutil.copyfileobj(self.mem_file, f)
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(description='Voice of a Repo')
|
|
|
|
parser.add_argument('repository', type=str)
|
|
parser.add_argument('--branch',
|
|
type=str,
|
|
default='master',
|
|
help="The branch to generate sound for [master]")
|
|
parser.add_argument('--file',
|
|
type=str,
|
|
default=None,
|
|
help="Save the generated MIDI sequence to this file")
|
|
parser.add_argument('--play',
|
|
action='store_true',
|
|
default=False,
|
|
help="Play the generated file (requires pygame with " +
|
|
"MIDI support)")
|
|
parser.add_argument('--verbose',
|
|
action='store_true',
|
|
default=False,
|
|
help="Print messages during execution")
|
|
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
repo = Repo(args.repository)
|
|
except InvalidGitRepositoryError:
|
|
print("{} is not a valid Git repository".format(
|
|
os.path.abspath(args.repository)))
|
|
|
|
sys.exit(1)
|
|
|
|
try:
|
|
branch = repo.heads[args.branch]
|
|
except IndexError:
|
|
print("Branch '{}' does not exist in this repo".format(args.branch))
|
|
|
|
sys.exit(1)
|
|
|
|
orig_log = []
|
|
|
|
if args.verbose:
|
|
print("Generating Git log…")
|
|
|
|
gen_history(orig_log, branch.commit)
|
|
|
|
if args.verbose:
|
|
print("Sorting commits…")
|
|
|
|
orig_log.sort(key=lambda commit: commit.authored_date)
|
|
|
|
if args.verbose:
|
|
print("Generating MIDI data…")
|
|
|
|
log = map(gen_note, orig_log)
|
|
|
|
if args.verbose:
|
|
print("Creating MIDI…")
|
|
|
|
MyMIDI = MemMIDI()
|
|
track = 0
|
|
time = 0
|
|
log_channel = 0
|
|
decor_channel = 1
|
|
|
|
# Duration of one note
|
|
duration = 0.3
|
|
|
|
MyMIDI.addProgramChange(track, log_channel, 0, 104)
|
|
MyMIDI.addProgramChange(track, decor_channel, 0, 115)
|
|
|
|
# WRITE THE SEQUENCE
|
|
for section in log:
|
|
section_len = len(section['file_notes']) * duration
|
|
|
|
# Add a long note
|
|
MyMIDI.addNote(track, log_channel,
|
|
section['commit_note'], time,
|
|
section_len, section['commit_volume'])
|
|
|
|
for i, file_note in enumerate(section['file_notes']):
|
|
MyMIDI.addNote(track, decor_channel,
|
|
file_note['note'], time + i * duration,
|
|
duration, file_note['volume'])
|
|
|
|
time += section_len
|
|
|
|
MyMIDI.writeMem()
|
|
|
|
if args.file:
|
|
if args.verbose:
|
|
print("Saving file to {}".format(args.file))
|
|
|
|
MyMIDI.exportFile(args.file)
|
|
|
|
if args.play:
|
|
if args.verbose:
|
|
print("Playing!")
|
|
|
|
# Import pygame stuff here
|
|
import pygame
|
|
import pygame.mixer
|
|
|
|
# PLAYBACK
|
|
pygame.init()
|
|
pygame.mixer.init()
|
|
MyMIDI.mem_file.seek(0)
|
|
pygame.mixer.music.load(MyMIDI.mem_file)
|
|
pygame.mixer.music.play()
|
|
|
|
while pygame.mixer.music.get_busy():
|
|
sleep(1)
|