commit ff4dbf3095220c6f329f9867f8879fe43bcb4ee5 Author: Gergely Polonkai Date: Fri May 20 11:10:07 2016 +0200 Initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f21b54 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/venv/ diff --git a/git-sound.py b/git-sound.py new file mode 100644 index 0000000..80ae3e6 --- /dev/null +++ b/git-sound.py @@ -0,0 +1,230 @@ +# -*- 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) diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..ecbfce0 --- /dev/null +++ b/notes.txt @@ -0,0 +1,23 @@ +start: 60 +steps: [0, 2, 2, 1, 2, 2, 2, 1] +Next octave: +12 + + +Notes used in PI music: + +83 o +81 ------------------------------------------o------ +79 #o +77 --------------------------------o---------------- +76 o +74 --------------------- o ------------------------- +72 o +71 ----------- o ----------------------------------- +69 o +67 - #o -------------------------------------------- +65 +64 ------------------------------------------------- +62 +60 ------------------------------------------------- + + 0 1 2 3 4 5 6 7 8 9