Initial version
This commit is contained in:
commit
ff4dbf3095
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/venv/
|
230
git-sound.py
Normal file
230
git-sound.py
Normal file
@ -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)
|
23
notes.txt
Normal file
23
notes.txt
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user