From 178e80a02986f34845c837c756c844ea9dbd1f01 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 26 Oct 2015 13:29:26 -0200 Subject: [PATCH 01/16] Linux stack and utility commands for PyMusAnim --- .gitignore | 1 + Animator.py | 0 ConvertColor.py | 0 MidiLexer.py | 0 MusAnimLauncher.py | 175 ++++++++++ MusAnimLexer.py | 52 ++- MusAnimRenderer.py | 717 ++++++++++++++++++++------------------- README | 49 +++ RunBeethoven74Anim.py | 0 RunGouldAnim.py | 0 RunGouldAnim1080.py | 0 RunShosti10Anim.py | 0 batch.sh | 32 ++ checklogs.sh | 21 ++ freepats/FillFreepats.py | 61 ++++ freepats/freepats.cfg | 300 ++++++++++++++++ pymusanim.sh | 16 + 17 files changed, 1058 insertions(+), 366 deletions(-) create mode 100644 .gitignore mode change 100644 => 100755 Animator.py mode change 100644 => 100755 ConvertColor.py mode change 100644 => 100755 MidiLexer.py create mode 100755 MusAnimLauncher.py mode change 100644 => 100755 MusAnimLexer.py mode change 100644 => 100755 MusAnimRenderer.py create mode 100644 README mode change 100644 => 100755 RunBeethoven74Anim.py mode change 100644 => 100755 RunGouldAnim.py mode change 100644 => 100755 RunGouldAnim1080.py mode change 100644 => 100755 RunShosti10Anim.py create mode 100755 batch.sh create mode 100755 checklogs.sh create mode 100755 freepats/FillFreepats.py create mode 100755 freepats/freepats.cfg create mode 100755 pymusanim.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/Animator.py b/Animator.py old mode 100644 new mode 100755 diff --git a/ConvertColor.py b/ConvertColor.py old mode 100644 new mode 100755 diff --git a/MidiLexer.py b/MidiLexer.py old mode 100644 new mode 100755 diff --git a/MusAnimLauncher.py b/MusAnimLauncher.py new file mode 100755 index 0000000..12ea519 --- /dev/null +++ b/MusAnimLauncher.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +from MusAnimRenderer import MusAnimRenderer +from MusAnimLexer import MidiLexer +import sys,os.path + +PITCH_GRACE=5 +TRACK_WIDTH=24 + +def main(): + tracks = [ + { 'name': "track0", + 'color': (0.000, 0.996, 0.000), + 'width': TRACK_WIDTH, + 'z-index': 0 + }, + { 'name': "track1", + 'color': (0.996, 0.000, 0.000), + 'width': TRACK_WIDTH, + 'z-index': 1 + }, + { 'name': "track2", + 'color': (0.004, 0.996, 0.992), + 'width': TRACK_WIDTH, + 'z-index': 2 + }, + { 'name': "track3", + 'color': (0.996, 0.855, 0.398), + 'width': TRACK_WIDTH, + 'z-index': 3 + }, + { 'name': "track4", + 'color': (0.563, 0.980, 0.570), + 'width': TRACK_WIDTH, + 'z-index': 4 + }, + { 'name': "track5", + 'color': (0.000, 0.461, 0.996), + 'width': TRACK_WIDTH, + 'z-index': 5 + }, + { 'name': "track6", + 'color': (0.832, 0.996, 0.000), + 'width': TRACK_WIDTH, + 'z-index': 6 + }, + { 'name': "track7", + 'color': (0.996, 0.574, 0.492), + 'width': TRACK_WIDTH, + 'z-index': 7 + }, + { 'name': "track8", + 'color': (0.992, 0.535, 0.000), + 'width': TRACK_WIDTH, + 'z-index': 8 + }, + { 'name': "track9", + 'color': (0.520, 0.660, 0.000), + 'width': TRACK_WIDTH, + 'z-index': 9 + }, + { 'name': "track10", + 'color': (0.000, 0.680, 0.492), + 'width': TRACK_WIDTH, + 'z-index': 10 + }, + { 'name': "track11", + 'color': (0.738, 0.773, 0.996), + 'width': TRACK_WIDTH, + 'z-index': 11 + }, + { 'name': "track12", + 'color': (0.738, 0.824, 0.574), + 'width': TRACK_WIDTH, + 'z-index': 12 + }, + { 'name': "track13", + 'color': (0.000, 0.723, 0.090), + 'width': TRACK_WIDTH, + 'z-index': 13 + }, + { 'name': "track14", + 'color': (0.004, 0.813, 0.996), + 'width': TRACK_WIDTH, + 'z-index': 14 + }, + { 'name': "track15", + 'color': (0.566, 0.813, 0.793), + 'width': TRACK_WIDTH, + 'z-index': 15 + }, + { 'name': "track16", + 'color': (0.730, 0.531, 0.000), + 'width': TRACK_WIDTH, + 'z-index': 16 + }, + { 'name': "track17", + 'color': (0.867, 0.996, 0.453), + 'width': TRACK_WIDTH, + 'z-index': 17 + }, + { 'name': "track18", + 'color': (0.000, 0.996, 0.773), + 'width': TRACK_WIDTH, + 'z-index': 18 + }, + { 'name': "track19", + 'color': (0.996, 0.895, 0.008), + 'width': TRACK_WIDTH, + 'z-index': 19 + }, + { 'name': "track20", + 'color': (0.594, 0.996, 0.320), + 'width': TRACK_WIDTH, + 'z-index': 20 + }, + { 'name': "track21", + 'color': (0.000, 0.996, 0.469), + 'width': TRACK_WIDTH, + 'z-index': 21 + }, + { 'name': "track22", + 'color': (0.996, 0.430, 0.254), + 'width': TRACK_WIDTH, + 'z-index': 22 + }, + { 'name': "track23", + 'color': (0.645, 0.996, 0.820), + 'width': TRACK_WIDTH, + 'z-index': 23 + }, + { 'name': "track24", + 'color': (0.996, 0.691, 0.402), + 'width': TRACK_WIDTH, + 'z-index': 24 + }, + { 'name': "track25", + 'color': (0.000, 0.605, 0.996), + 'width': TRACK_WIDTH, + 'z-index': 25 + }, + ] + + if len(sys.argv)<3: + print("Usage: python MusAnimLauncher.py input.mid outputDirectory") + sys.exit(1) + return + + mid=sys.argv[1] + + lexer = MidiLexer() + lexer.lex(mid) + + frames_dir = sys.argv[2]+os.sep + os.makedirs(frames_dir) + + speed_map = [{'time': 0.0, 'speed': 4}] + + #dimensions = 426, 240 + dimensions = 720, 480 + #dimensions = 1920, 1080 + + fps = 25 + #fps = 29.97 + + renderer=MusAnimRenderer() + renderer.introduction=False + renderer.render(mid, frames_dir, tracks, + speed_map=speed_map, + dimensions=dimensions, + min_pitch=lexer.minPitch-PITCH_GRACE, + max_pitch=lexer.maxPitch+PITCH_GRACE, + fps=fps) + +if __name__ == '__main__': + main() diff --git a/MusAnimLexer.py b/MusAnimLexer.py old mode 100644 new mode 100755 index 15ef1b4..f6d3f54 --- a/MusAnimLexer.py +++ b/MusAnimLexer.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import sys class MidiLexer: @@ -7,6 +8,11 @@ class MidiLexer: # beats, without making any pretenses about figuring out timing in seconds. # That has to be done later, once we have all the timing events sorted midi_events = [] + minPitch = sys.maxint + maxPitch = -sys.maxint-1 + + def debug(self,event): + print 'Unknown event: ' + bin(event) + " (" + hex(event) + ")" def get_v_time(self, data): """Picks off the variable-length time from a block of data and returns @@ -24,14 +30,15 @@ def get_v_time(self, data): return d_time, data[i+1:] def read_midi_event(self, track_data, time, track_num): - # have to read off vtime first! - d_time, track_data = self.get_v_time(track_data) + # have to read off vtime first! + d_time, track_data = self.get_v_time(track_data) time += d_time + #print time + event=((ord(track_data[0]) & 0xF0) >> 4) if track_data[0] == '\xff': # event is meta event, we do nothing unless it's a tempo event - if ord(track_data[1]) == 0x51: - #print track_num, list(track_data) + if ord(track_data[1]) == 0x51: # tempo event mpqn = ((ord(track_data[3]) << 16) + (ord(track_data[4]) << 8) + ord(track_data[5])) # microseconds per quarter note @@ -44,7 +51,7 @@ def read_midi_event(self, track_data, time, track_num): return track_data[length+3:], time # otherwise we we assume it's a midi event - elif ((ord(track_data[0]) & 0xF0) >> 4) == 0x8: + elif event == 0x8: # note off event # don't add a note off event if keyswitch (pitch below 12) pitch = ord(track_data[1]) @@ -53,7 +60,7 @@ def read_midi_event(self, track_data, time, track_num): 'pitch': pitch, 'track_num': track_num}) return track_data[3:], time - elif ((ord(track_data[0]) & 0xF0) >> 4) == 0x9: + elif event == 0x9: # note on event pitch = ord(track_data[1]) if pitch < 12: # it's a keyswitch! @@ -62,19 +69,30 @@ def read_midi_event(self, track_data, time, track_num): elif pitch == 1: mode = "pizz" else: - raise Exception("Unknown keyswitch") - self.midi_events.append({'type': 'keyswitch', 'time': time, - 'track_num': track_num, 'mode': mode}) + print("Unknown keyswitch: "+str(pitch)) + pitch=False + if pitch != False: + self.midi_events.append({'type': 'keyswitch', 'time': time, + 'track_num': track_num, 'mode': mode}) else: + if pitch > self.maxPitch: + self.maxPitch = pitch + if pitch < self.minPitch: + self.minPitch = pitch self.midi_events.append({'type': 'note_on', 'time': time, 'pitch': ord(track_data[1]), 'track_num': track_num}) return track_data[3:], time - elif ((ord(track_data[0]) & 0xF0) >> 4) == 0xC: + elif event == 0xC: return track_data[2:], time # ignore some other events - elif ((ord(track_data[0]) & 0xF0) >> 4) == 0xB: + elif event == 0xB: return track_data[3:], time + #kek + elif event == 0xF or event == 0xD: + self.debug(event) + return track_data[2:], time else: - raise Exception("Unknown midi file data event: " + str(ord(track_data[0]))) + self.debug(event) + return track_data[3:], time def lex(self, filename): """Returns block list for musanim from a midi file given in filename""" @@ -93,7 +111,7 @@ def lex(self, filename): # grab header header = s[0:14] f_format = ord(s[8]) << 8 | ord(s[9]) - num_tracks = ord(s[10]) << 8 | ord(s[11]) + self.num_tracks = ord(s[10]) << 8 | ord(s[11]) self.ticks_per_quarter = ord(s[12]) << 8 | ord(s[13]) tracks_chunk = s[14:] @@ -105,18 +123,18 @@ def lex(self, filename): # parse midi events for a single track while len(track) > 0: # read off midi events and add to midi_events - track, time = self.read_midi_event(track, time, track_num) + track, time = self.read_midi_event(track, time, track_num) track_num += 1 # convert all times from ticks to beats, for convenience for event in self.midi_events: - event['time'] = (event['time'] + 0.0) / 960 + event['time'] = (event['time'] + 0.0) / self.ticks_per_quarter #960 - self.midi_events.sort(lambda a, b: cmp(a['time'], b['time'])) + self.midi_events.sort(lambda a, b: cmp(a['time'], b['time'])) return self.midi_events if __name__ == '__main__': lexer = MidiLexer() blocks = lexer.lex('multitrackmidi01.MID') - print blocks \ No newline at end of file + print blocks diff --git a/MusAnimRenderer.py b/MusAnimRenderer.py old mode 100644 new mode 100755 index 4c383f8..ffa82cb --- a/MusAnimRenderer.py +++ b/MusAnimRenderer.py @@ -1,350 +1,369 @@ -import os -import sys -import colorsys -import cairo -import math -from collections import deque -from MusAnimLexer import MidiLexer - -class MusAnimRenderer: - def lyrics_deque(self, lyrics): - """Turns lyrics as a string into a lyrics deque, splitting by spaces and - removing newlines.""" - lyrics = lyrics.replace("\n", " ") - lyrics_list = lyrics.split(" ") - lyrics_list2 = [] - for word in lyrics_list: - """ - if word and word[-1] == '-': - word = word[0:-1] + ' -'""" - lyrics_list2.append(word) - return deque(lyrics_list2) - - def blockify(self, midi_events): - """Converts list of midi events given by the lexer into block data used - for animating.""" - blocks = [] - bpm = 120.0 - time_seconds = 0 - time_beats = 0 - tracks_mode = ['normal'] * 100 - for event in midi_events: - # increment times based on elapsed time in beats - d_time_beats = event['time'] - time_beats - time_seconds += (d_time_beats * 60.0) / bpm - time_beats = event['time'] - if event['type'] == 'tempo': # set tempo - bpm = event['bpm'] - elif event['type'] == 'note_on': # create new block in list - blocks.append({'start_time': time_seconds, 'pitch': - event['pitch'], 'track_num': event['track_num']}) - # set shape of last block to bar or circle - if tracks_mode[event['track_num']] == 'normal': - blocks[-1]['shape'] = 'bar' - elif tracks_mode[event['track_num']] == 'pizz': - blocks[-1]['shape'] = 'circle' - else: - raise Exception('Unknown track mode') - elif event['type'] == 'note_off': # add end_time to existing block - pitch = event['pitch'] - track_num = event['track_num'] - blocks_w_pitch = [block for block in blocks - if block['pitch'] == pitch and block['track_num'] - == track_num and 'end_time' not in block] - assert(blocks_w_pitch) # assume it has at least one element - # otherwise we have a faulty midi file! - blocks_w_pitch[0]['end_time'] = time_seconds - elif event['type'] == 'keyswitch': - tracks_mode[event['track_num']] = event['mode'] - else: - raise Exception('Unknown midi event') - return blocks - - def add_block_info(self, blocks, tracks, fps, speed_map, dimensions, - min_pitch, max_pitch): - """Adds essential information to each block dict in blocks, also returns - last_block_end to tell when animation is over""" - # need: start_time (seconds), end_time (seconds), pitch, track_num for - # each block - last_block_end = 0 - cur_speed = self.get_speed(speed_map, 0.0) - - for block in blocks: - # get track object that corresponds to block - track = tracks[block['track_num']] - block['width'] = track['width'] # set width - # get speed and calculate x offset from functions - cur_speed = self.get_speed(speed_map, block['start_time']) - x_offset = self.calc_offset(speed_map, block['start_time'], fps) - block['start_x'] = x_offset + dimensions[0] - # length of the block in time (time it stays highlighted) - block['length'] = block['end_time'] - block['start_time'] + 0.0 - # if a circle, length is same as width, otherwise length - # corresponds to time length - if block['shape'] == 'circle': - block['x_length'] = block['width'] - else: - block['x_length'] = block['length'] * fps * cur_speed - block['end_x'] = block['start_x'] + block['x_length'] - # set last_block_end as the end_x of the very rightmost block - if block['end_x'] > last_block_end: - last_block_end = block['end_x'] - # figure out draw coordinates - y_middle = ((0.0 + max_pitch - block['pitch']) / (max_pitch - - min_pitch)) * dimensions[1] - block['top_y'] = y_middle - (block['width'] / 2) - block['bottom_y'] = y_middle + (block['width'] / 2) - if 'z-index' not in track: - track['z-index'] = 0 - block['z-index'] = track['z-index'] - if 'layer' not in track: - track['layer'] = 0 - block['layer'] = track['layer'] - # round stuff for crisp rendering - #block['x_length'] = round(block['x_length']) - block['top_y'] = round(block['top_y']) - #block['start_x'] = round(block['start_x']) - - # sort by track_num so we get proper melisma length counting - blocks.sort(lambda a, b: cmp(a['track_num'], b['track_num'])) - - # can't add lyrics until we add in end_x for all blocks - block_num = 0 - for block in blocks: - track_num = block['track_num'] - track = tracks[block['track_num']] - if 'lyrics' in track and track['lyrics'][0]: - lyrics_text = track['lyrics'][0] - if lyrics_text[0] == '^': - lyrics_text = lyrics_text[1:] - block['lyrics_position'] = 'above' - elif lyrics_text[0] == '_': - lyrics_text = lyrics_text[1:] - block['lyrics_position'] = 'below' - else: - block['lyrics_position'] = 'middle' - - if track['lyrics'][0] != '*': - # for detecting melismas (* in lyrics text) - i = 0 - while (len(track['lyrics']) > (i + 1) - and track['lyrics'][i+1] == '*'): - i += 1 - block['lyrics_end_x'] = blocks[block_num+i]['end_x'] - block['lyrics'] = lyrics_text - - track['lyrics'].popleft() - - block_num += 1 - - # go back to sorting by start time - blocks.sort(lambda a, b: cmp(a['start_time'], b['start_time'])) - - return blocks, last_block_end - - def calc_offset(self, speed_map, time_offset, fps): - """Calculates the x-offset of a block given its time offset and a speed - map. Needed for laying out blocks because of variable block speed in the - animation.""" - x_offset = 0 - i = 0 - # speed is a dict with a speed and a time when we switch to speed - speeds = ([speed for speed in speed_map if speed['time'] < time_offset] - [0:-1]) - # add offsets from previous speed intervals - if speeds: - for speed in speeds: - x_offset += ((speed_map[i+1]['time'] - speed_map[i]['time']) - * fps * speed_map[i]['speed']) - i += 1 - # add offset from current speed - if time_offset > 0: - x_offset += ((time_offset - speed_map[i]['time']) * fps - * speed_map[i]['speed']) - return x_offset - - def get_speed(self, speed_map, time): - """Retrieves the correct block speed for a given point in time from the - speed map.""" - i = len(speed_map) - 1 - while time < speed_map[i]['time'] and i > 0: - i -= 1 - return speed_map[i]['speed'] - - def draw_block_cairo(self, block, tracks, dimensions, cr, transparent=False): - if block['start_x'] < (dimensions[0] / 2) and (block['end_x'] > - (dimensions[0] / 2)): - color = tracks[block['track_num']]['high_color'] - else: - color = tracks[block['track_num']]['color'] - if transparent: - r, g, b = color - cr.set_source_rgba(r, g, b, 0.5) - else: - cr.set_source_rgb(*color) - if block['shape'] == 'circle': - cr.arc(block['start_x'] + block['width']/2, block['top_y'] + block['width']/2, block['width']/2, 0, 2 * math.pi) - else: - cr.rectangle(block['start_x'], block['top_y'], block['x_length'], - block['width']) - cr.fill() - - def draw_lyrics_cairo(self, block, tracks, dimensions, cr): - cr.set_font_size(1.9*block['width']) - text = block['lyrics'] - x_bearing, y_bearing, width, height = cr.text_extents(text)[:4] - cr.set_source_rgba(0, 0, 0, 0.5) - if block['lyrics_position'] == 'above': - rect = (block['start_x'], int(block['top_y'])-0.7*block['width'], width + 2, block['width']+1) - elif block['lyrics_position'] == 'below': - rect = (block['start_x'], int(block['top_y'])+0.7*block['width'], width + 2, block['width']+1) - else: - rect = (block['start_x'], int(block['top_y']), width + 2, block['width']+1) - cr.rectangle(*rect) - cr.fill() - if block['start_x'] < (dimensions[0] / 2) and (block['lyrics_end_x'] > - (dimensions[0] / 2)): - color = (1, 1, 1) - else: - color = tracks[block['track_num']]['lyrics_color'] - cr.set_source_rgb(*color) - if block['lyrics_position'] == 'above': - corner = (block['start_x'] + 1, block['top_y']+0.18*block['width']) - elif block['lyrics_position'] == 'below': - corner = (block['start_x'] + 1, block['top_y']+1.58*block['width']) - else: - corner = (block['start_x'] + 1, block['top_y']+0.88*block['width']) - cr.move_to(*corner) - cr.show_text(text) - - def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=[{'time':0.0,'speed':4}], - dimensions=(720,480), fps=29.97, min_pitch=34, max_pitch=86, first_frame=None, - last_frame=None, every_nth_frame=1, do_render=1): - """Render the animation!""" - - print "Beginning render..." - speed = speed_map[0]['speed'] - if first_frame == None: - first_frame = 0 - if last_frame == None: - last_frame = 10000000 # just a large number - - print "Lexing midi..." - blocks = [] - lexer = MidiLexer() - midi_events = lexer.lex(input_midi_filename) - - print "Blockifying midi..." - blocks = self.blockify(midi_events) # convert into list of blocks - - for track in tracks: - if 'color' in track: - base_color = colorsys.rgb_to_hls(*track['color']) - track['high_color'] = colorsys.hls_to_rgb(base_color[0], 0.95, - base_color[2]) - track['lyrics_color'] = colorsys.hls_to_rgb(base_color[0], 0.7, - base_color[2]) - if 'lyrics' in track: - track['lyrics'] = self.lyrics_deque(track['lyrics']) - - # do some useful calculations on all blocks - blocks, last_block_end = self.add_block_info(blocks, tracks, - fps, speed_map, dimensions, min_pitch, max_pitch) - - # following used for calculating percentage done to print to console - original_end = last_block_end - percent = 0 - last_percent = -1 - - # sort by z-index descending - blocks.sort(lambda a, b: cmp(b['z-index'], a['z-index'])) - - # for naming image files: - frame = 0 - # for keeping track of speed changes: - # need to initialize time - time = -dimensions[0]/(2.0*fps*speed_map[0]['speed']) - - if not do_render: - print "Skipping render pass, Done!" - return - - print "Rendering frames..." - # generate frames while there are blocks on the screen: - while last_block_end > (0 - speed): - # code only for rendering blocks - if frame >= first_frame and frame <= last_frame and frame % every_nth_frame == 0: - # cairo setup stuff - filename = frame_save_dir + ("frame%05i.png" % frame) - surface = cairo.ImageSurface(cairo.FORMAT_RGB24, *dimensions) - #surface = cairo.SVGSurface(filename, *dimensions) - cr = cairo.Context(surface) - cr.set_antialias(cairo.ANTIALIAS_GRAY) - cr.select_font_face("Garamond", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) - cr.set_font_size(19) - # add black background - cr.set_source_rgb(0, 0, 0) - cr.rectangle(0, 0, *dimensions) - cr.fill() - - # need to do two passes of drawing blocks, once in reverse order - # in full opacity, and a second time in ascending order in half- - # opacityto get fully-colored bars that blend together when - # overlapping - - # get list of blocks that are on screen - on_screen_blocks = [block for block in blocks - if block['start_x'] < dimensions[0] and block['end_x'] > 0] - on_screen_layers = list(set([block['layer'] for block in on_screen_blocks])) - - for layer in on_screen_layers: - layer_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, *dimensions) - layer_context = cairo.Context(layer_surface) - - in_layer_blocks = [block for block in on_screen_blocks if block['layer'] == layer] - - # do first drawing pass - for block in in_layer_blocks: - self.draw_block_cairo(block, tracks, dimensions, layer_context) - - # do second drawing pass - on_screen_blocks.reverse() - for block in in_layer_blocks: - self.draw_block_cairo(block, tracks, dimensions, layer_context, transparent=True) - - cr.set_source_surface(layer_surface) - cr.paint() - - # do lyrics pass, sort by start x so starts of words are on top - on_screen_blocks.sort(lambda a, b: cmp(a['start_x'], b['start_x'])) - for block in on_screen_blocks: - if ('lyrics' in block): - self.draw_lyrics_cairo(block, tracks, dimensions, cr) - - #cr.save() - surface.write_to_png(filename) - - # other code needed to advance animation - frame += 1 - # need to set speed - speed = self.get_speed(speed_map, time) - for block in blocks: # move blocks to left - block['start_x'] -= speed - block['end_x'] -= speed - if 'lyrics_end_x' in block: - block['lyrics_end_x'] -= speed - last_block_end -= speed # move video endpoint left as well - percent = int(min((original_end - last_block_end) * 100.0 - / original_end, 100)) - if percent != last_percent: - print percent, "% done" - last_percent = percent - - time += (1/fps) - - print "Done!" - - -if __name__ == '__main__': - print ("Sorry, I don't really do anything useful as an executable, see " +# -*- coding: utf-8 -*- +import os +import sys +import colorsys +import cairo +import math +from collections import deque +from MusAnimLexer import MidiLexer + +class MusAnimRenderer: + first_highlight = False + + def lyrics_deque(self, lyrics): + """Turns lyrics as a string into a lyrics deque, splitting by spaces and + removing newlines.""" + lyrics = lyrics.replace("\n", " ") + lyrics_list = lyrics.split(" ") + lyrics_list2 = [] + for word in lyrics_list: + """ + if word and word[-1] == '-': + word = word[0:-1] + ' -'""" + lyrics_list2.append(word) + return deque(lyrics_list2) + + def blockify(self, midi_events): + """Converts list of midi events given by the lexer into block data used + for animating.""" + blocks = [] + bpm = 120.0 + time_seconds = 0 + time_beats = 0 + tracks_mode = ['normal'] * 100 + for event in midi_events: + # increment times based on elapsed time in beats + d_time_beats = event['time'] - time_beats + time_seconds += (d_time_beats * 60.0) / bpm + time_beats = event['time'] + if event['type'] == 'tempo': # set tempo + bpm = event['bpm'] + elif event['type'] == 'note_on': # create new block in list + blocks.append({'start_time': time_seconds, 'pitch': + event['pitch'], 'track_num': event['track_num']}) + # set shape of last block to bar or circle + if tracks_mode[event['track_num']] == 'normal': + blocks[-1]['shape'] = 'bar' + elif tracks_mode[event['track_num']] == 'pizz': + blocks[-1]['shape'] = 'circle' + else: + raise Exception('Unknown track mode') + elif event['type'] == 'note_off': # add end_time to existing block + pitch = event['pitch'] + track_num = event['track_num'] + blocks_w_pitch = [block for block in blocks + if block['pitch'] == pitch and block['track_num'] + == track_num and 'end_time' not in block] + assert(blocks_w_pitch) # assume it has at least one element + # otherwise we have a faulty midi file! + blocks_w_pitch[0]['end_time'] = time_seconds + elif event['type'] == 'keyswitch': + tracks_mode[event['track_num']] = event['mode'] + else: + raise Exception('Unknown midi event') + return blocks + + def add_block_info(self, blocks, tracks, fps, speed_map, dimensions, + min_pitch, max_pitch): + """Adds essential information to each block dict in blocks, also returns + last_block_end to tell when animation is over""" + # need: start_time (seconds), end_time (seconds), pitch, track_num for + # each block + last_block_end = 0 + cur_speed = self.get_speed(speed_map, 0.0) + + for block in blocks: + # get track object that corresponds to block + track = tracks[block['track_num']] + block['width'] = track['width'] # set width + # get speed and calculate x offset from functions + cur_speed = self.get_speed(speed_map, block['start_time']) + x_offset = self.calc_offset(speed_map, block['start_time'], fps) + block['start_x'] = x_offset + dimensions[0] + # length of the block in time (time it stays highlighted) + block['length'] = block['end_time'] - block['start_time'] + 0.0 + # if a circle, length is same as width, otherwise length + # corresponds to time length + if block['shape'] == 'circle': + block['x_length'] = block['width'] + else: + block['x_length'] = block['length'] * fps * cur_speed + block['end_x'] = block['start_x'] + block['x_length'] + # set last_block_end as the end_x of the very rightmost block + if block['end_x'] > last_block_end: + last_block_end = block['end_x'] + # figure out draw coordinates + y_middle = ((0.0 + max_pitch - block['pitch']) / (max_pitch - + min_pitch)) * dimensions[1] + block['top_y'] = y_middle - (block['width'] / 2) + block['bottom_y'] = y_middle + (block['width'] / 2) + if 'z-index' not in track: + track['z-index'] = 0 + block['z-index'] = track['z-index'] + if 'layer' not in track: + track['layer'] = 0 + block['layer'] = track['layer'] + # round stuff for crisp rendering + #block['x_length'] = round(block['x_length']) + block['top_y'] = round(block['top_y']) + #block['start_x'] = round(block['start_x']) + + # sort by track_num so we get proper melisma length counting + blocks.sort(lambda a, b: cmp(a['track_num'], b['track_num'])) + + # can't add lyrics until we add in end_x for all blocks + block_num = 0 + for block in blocks: + track_num = block['track_num'] + track = tracks[block['track_num']] + if 'lyrics' in track and track['lyrics'][0]: + lyrics_text = track['lyrics'][0] + if lyrics_text[0] == '^': + lyrics_text = lyrics_text[1:] + block['lyrics_position'] = 'above' + elif lyrics_text[0] == '_': + lyrics_text = lyrics_text[1:] + block['lyrics_position'] = 'below' + else: + block['lyrics_position'] = 'middle' + + if track['lyrics'][0] != '*': + # for detecting melismas (* in lyrics text) + i = 0 + while (len(track['lyrics']) > (i + 1) + and track['lyrics'][i+1] == '*'): + i += 1 + block['lyrics_end_x'] = blocks[block_num+i]['end_x'] + block['lyrics'] = lyrics_text + + track['lyrics'].popleft() + + block_num += 1 + + # go back to sorting by start time + blocks.sort(lambda a, b: cmp(a['start_time'], b['start_time'])) + + return blocks, last_block_end + + def calc_offset(self, speed_map, time_offset, fps): + """Calculates the x-offset of a block given its time offset and a speed + map. Needed for laying out blocks because of variable block speed in the + animation.""" + x_offset = 0 + i = 0 + # speed is a dict with a speed and a time when we switch to speed + speeds = ([speed for speed in speed_map if speed['time'] < time_offset] + [0:-1]) + # add offsets from previous speed intervals + if speeds: + for speed in speeds: + x_offset += ((speed_map[i+1]['time'] - speed_map[i]['time']) + * fps * speed_map[i]['speed']) + i += 1 + # add offset from current speed + if time_offset > 0: + x_offset += ((time_offset - speed_map[i]['time']) * fps + * speed_map[i]['speed']) + return x_offset + + def get_speed(self, speed_map, time): + """Retrieves the correct block speed for a given point in time from the + speed map.""" + i = len(speed_map) - 1 + while time < speed_map[i]['time'] and i > 0: + i -= 1 + return speed_map[i]['speed'] + + def draw_block_cairo(self, block, tracks, dimensions, cr, transparent=False): + if block['start_x'] < (dimensions[0] / 2) and (block['end_x'] > + (dimensions[0] / 2)): + color = tracks[block['track_num']]['high_color'] + self.first_highlight = True + else: + color = tracks[block['track_num']]['color'] + if transparent: + r, g, b = color + cr.set_source_rgba(r, g, b, 0.5) + else: + cr.set_source_rgb(*color) + if block['shape'] == 'circle': + cr.arc(block['start_x'] + block['width']/2, block['top_y'] + block['width']/2, block['width']/2, 0, 2 * math.pi) + else: + cr.rectangle(block['start_x'], block['top_y'], block['x_length'], + block['width']) + cr.fill() + + def draw_lyrics_cairo(self, block, tracks, dimensions, cr): + cr.set_font_size(1.9*block['width']) + text = block['lyrics'] + x_bearing, y_bearing, width, height = cr.text_extents(text)[:4] + cr.set_source_rgba(0, 0, 0, 0.5) + if block['lyrics_position'] == 'above': + rect = (block['start_x'], int(block['top_y'])-0.7*block['width'], width + 2, block['width']+1) + elif block['lyrics_position'] == 'below': + rect = (block['start_x'], int(block['top_y'])+0.7*block['width'], width + 2, block['width']+1) + else: + rect = (block['start_x'], int(block['top_y']), width + 2, block['width']+1) + cr.rectangle(*rect) + cr.fill() + if block['start_x'] < (dimensions[0] / 2) and (block['lyrics_end_x'] > + (dimensions[0] / 2)): + color = (1, 1, 1) + else: + color = tracks[block['track_num']]['lyrics_color'] + cr.set_source_rgb(*color) + if block['lyrics_position'] == 'above': + corner = (block['start_x'] + 1, block['top_y']+0.18*block['width']) + elif block['lyrics_position'] == 'below': + corner = (block['start_x'] + 1, block['top_y']+1.58*block['width']) + else: + corner = (block['start_x'] + 1, block['top_y']+0.88*block['width']) + cr.move_to(*corner) + cr.show_text(text) + + speed_map=[{'time':0.0,'speed':4}] + width=720 + height=480 + fps=29.97 + min_pitch=34 + max_pitch=86 + first_frame=None + last_frame=None + every_nth_frame=1 + do_render=1 + introduction=True + + def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_map, + dimensions=(width,height), fps=fps, min_pitch=min_pitch, max_pitch=max_pitch, first_frame=first_frame, + last_frame=last_frame, every_nth_frame=every_nth_frame, do_render=do_render): + """Render the animation!""" + + print "Beginning render..." + speed = speed_map[0]['speed'] + if first_frame == None: + first_frame = 0 + if last_frame == None: + last_frame = 10000000 # just a large number + + print "Lexing midi..." + blocks = [] + lexer = MidiLexer() + midi_events = lexer.lex(input_midi_filename) + + print "Blockifying midi..." + blocks = self.blockify(midi_events) # convert into list of blocks + + for track in tracks: + if 'color' in track: + base_color = colorsys.rgb_to_hls(*track['color']) + track['high_color'] = colorsys.hls_to_rgb(base_color[0], 0.95, + base_color[2]) + track['lyrics_color'] = colorsys.hls_to_rgb(base_color[0], 0.7, + base_color[2]) + if 'lyrics' in track: + track['lyrics'] = self.lyrics_deque(track['lyrics']) + + # do some useful calculations on all blocks + blocks, last_block_end = self.add_block_info(blocks, tracks, + fps, speed_map, dimensions, min_pitch, max_pitch) + + # following used for calculating percentage done to print to console + original_end = last_block_end + percent = 0 + last_percent = -1 + + # sort by z-index descending + blocks.sort(lambda a, b: cmp(b['z-index'], a['z-index'])) + + # for naming image files: + frame = 0 + framefile = 0 + # for keeping track of speed changes: + # need to initialize time + time = -dimensions[0]/(2.0*fps*speed_map[0]['speed']) + + if not do_render: + print "Skipping render pass, Done!" + return + + print "Rendering frames..." + # generate frames while there are blocks on the screen: + while last_block_end > (0 - speed): + # code only for rendering blocks + if frame >= first_frame and frame <= last_frame and frame % every_nth_frame == 0: + # cairo setup stuff + filename = frame_save_dir + ("frame%05i.png" % framefile) + surface = cairo.ImageSurface(cairo.FORMAT_RGB24, *dimensions) + #surface = cairo.SVGSurface(filename, *dimensions) + cr = cairo.Context(surface) + cr.set_antialias(cairo.ANTIALIAS_GRAY) + cr.select_font_face("Garamond", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) + cr.set_font_size(19) + # add black background + cr.set_source_rgb(0, 0, 0) + cr.rectangle(0, 0, *dimensions) + cr.fill() + + # need to do two passes of drawing blocks, once in reverse order + # in full opacity, and a second time in ascending order in half- + # opacityto get fully-colored bars that blend together when + # overlapping + + # get list of blocks that are on screen + on_screen_blocks = [block for block in blocks + if block['start_x'] < dimensions[0] and block['end_x'] > 0] + on_screen_layers = list(set([block['layer'] for block in on_screen_blocks])) + + for layer in on_screen_layers: + layer_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, *dimensions) + layer_context = cairo.Context(layer_surface) + + in_layer_blocks = [block for block in on_screen_blocks if block['layer'] == layer] + + # do first drawing pass + for block in in_layer_blocks: + self.draw_block_cairo(block, tracks, dimensions, layer_context) + + # do second drawing pass + on_screen_blocks.reverse() + for block in in_layer_blocks: + self.draw_block_cairo(block, tracks, dimensions, layer_context, transparent=True) + + cr.set_source_surface(layer_surface) + cr.paint() + + # do lyrics pass, sort by start x so starts of words are on top + on_screen_blocks.sort(lambda a, b: cmp(a['start_x'], b['start_x'])) + for block in on_screen_blocks: + if ('lyrics' in block): + self.draw_lyrics_cairo(block, tracks, dimensions, cr) + + #cr.save() + if self.introduction or self.first_highlight: + framefile += 1 + surface.write_to_png(filename) + + # other code needed to advance animation + frame += 1 + # need to set speed + speed = self.get_speed(speed_map, time) + for block in blocks: # move blocks to left + block['start_x'] -= speed + block['end_x'] -= speed + if 'lyrics_end_x' in block: + block['lyrics_end_x'] -= speed + last_block_end -= speed # move video endpoint left as well + percent = int(min((original_end - last_block_end) * 100.0 + / original_end, 100)) + if percent != last_percent: + print percent, "% done" + last_percent = percent + + time += (1/fps) + + print "Done!" + + +if __name__ == '__main__': + print ("Sorry, I don't really do anything useful as an executable, see " "RunAnim.py for usage") \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..ee20a4f --- /dev/null +++ b/README @@ -0,0 +1,49 @@ +The original Music Animaion Machine and it's open source clone, PyMusAnim, are awesome. Unfortunately though the original project was never open source and ran poorly, on Windows. PyMusAnim, using a totally different approach, doesn't suffer from performance issues - but it's more of a proof of concept than a funcioning tool. This fork extends PyMusAnim so that Linux users can easily output a MPG video given an input MIDI file. + +== DEPENDENCIES == + +You need those external programs installed on your system for this fork to work: + + * Python 2 + * Timidity + * FFMpeg + +On Debian and Ubuntu you can fulfill these requirementes by running the following command: `sudo apt-get install ffmpeg timidity python2.6` + +== HOW IT WORKS == + +The core of PyMusAnim is virtually unchanged, the only difference being that instead of having to create your own Python configuration files the new module MusAnimLauncher does that for you (while still being configurable). This module is automatically called from two Linux command line (bash) utilities: + + ./pymusanim.sh [file.mid] [output directory name] + +Use this to create a video. For example: `pymusanim.sh mysong.mid mysong` will create a MPG file of `mysong.mid` inside the directory `mysong`. + + ./batch.sh midisDirectory [threadsLimit] + +Use this one if you want to create several videos at once. Since PyMusAnim is single-threaded this will let you take advantage of a multi-core CPU if you have one (and you probably do). `midisDirectory` is the directory you have your MIDI files and threadsLimit is an optional argument if you want to explicitally set the number of threads to use (if not set the program will use all available processors). For each MIDI file a sub-directoy will created outside the `output` folder, which will be created one doesn't exist. Example usage: `batch.sh mymidis/` + +Remember to run all these commands from the project's root directory. + +== A NOTE ABOUT PYMUSANIM AND MIDI FILES == + +Unfortunately PyMusAnim is pretty bad at reading MIDI files with weird formats. I would go as far as to say that it's unable to read most MIDI files found on the web. The good news is that as long as the file is well formatted it will work perfectly. + +Well, how is this good news even? + +I have found that if I have trouble opening a MIDI file then I can use TuxGuitar to import that file and then export it again. This way the exported MIDI file will most likely work with PyMusAnim. TuxGuitar even has a batch conversion tool (under the Tools menu) that you can use to import and export many MIDI files at once automatically, making this a very short, if somewhat bothersome step. + +I haven't tried but other programs with MIDI import and export features could potentially work for this as well. + +== LINKS == + +Original PyMusAnim: https://github.com/zhanrnl/PyMusAnim +PyMusAnim on YouTube: https://www.youtube.com/user/PyMusAnim +Original Music Animation Machine by Stephen Malinowski: http://www.musanim.com/player/ +Malinowski on YouTube https://www.youtube.com/user/smalin +TuxGuitar: http://tuxguitar.herac.com.ar/ (also available via `sudo apt-get install tuxguitar` ) + +== TODO == + +* Improve support for reading all formats of MIDI files (this would be huge, if you think you can help please contact me!) +* Make PyMusAnim run on Python 3 too (should be pretty easy) +* A simple visual interface (GUI) so that it would be even easier to create PyMusAnim videos \ No newline at end of file diff --git a/RunBeethoven74Anim.py b/RunBeethoven74Anim.py old mode 100644 new mode 100755 diff --git a/RunGouldAnim.py b/RunGouldAnim.py old mode 100644 new mode 100755 diff --git a/RunGouldAnim1080.py b/RunGouldAnim1080.py old mode 100644 new mode 100755 diff --git a/RunShosti10Anim.py b/RunShosti10Anim.py old mode 100644 new mode 100755 diff --git a/batch.sh b/batch.sh new file mode 100755 index 0000000..a89173e --- /dev/null +++ b/batch.sh @@ -0,0 +1,32 @@ +#!/bin/bash +#Converts a whole directory of MIDI files in parallel +OUT_DIR=output/ + +shopt -s nocaseglob +trap 'kill 0' EXIT +if [ ! $1 ]; then + echo "Usage: batch.sh midisDirectory [threadsLimit]" + exit 1 +fi +mkdir $OUT_DIR +files=`ls -1 $1/*.mid*&>/dev/null` +if [ ! $? == 0 ]; then + echo "No files found, aborting." + exit +fi +for m in $1/*.mid*; do + name=`basename $m .${m#*.}` + nice -n19 ./pymusanim.sh $m $OUT_DIR$name &> $OUT_DIR${name}.log& + echo "Starting $name" + threadsLimit=$2 + if [ ! $threadsLimit ]; then + threadsLimit=`cat /proc/cpuinfo | grep processor | wc -l` + fi + while [ `jobs|grep Running|wc -l` == $threadsLimit ]; do + sleep 1 + done +done +wait +echo "Done" +first=1 +./checklogs.sh $OUT_DIR diff --git a/checklogs.sh b/checklogs.sh new file mode 100755 index 0000000..5fffb8c --- /dev/null +++ b/checklogs.sh @@ -0,0 +1,21 @@ +#!/bin/bash +#Check batch.sh logs. Comment/uncomment/add lines to your needs. +if [ ! $1 ]; then + echo "Usage: checklogs.sh logsDirectory" + exit 1 +fi + +out='' +for log in $1/*.log; do + error="`cat $log|grep -e"Aborting pymusanim.sh" -e"this instrument will not be heard" -e"Drum set .* is undefined"`" + if [ "$error" ]; then + #echo "$log: `echo $error|tail -n1`" #shows filename and first error (default, leave as only uncommented line upon commit) + + echo -e "${error}\n${log}\n\n`cat $log|grep -v '% done'|perl -ne '$H{$_}++ or print'`"|less #analyze each log showing error and filename as header and cleaning progress and duplicate lines + + #out=`echo -e "${out}\n${error}"|sort -u` #list each individual error + fi +done +if [ "$out" ]; then + echo "$out" +fi \ No newline at end of file diff --git a/freepats/FillFreepats.py b/freepats/FillFreepats.py new file mode 100755 index 0000000..8cea391 --- /dev/null +++ b/freepats/FillFreepats.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +#fills missing freepats.cfg tones with existing one from same group +import sys + +GROUP_START=[0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120] + +def addLine(line): + print line + " #filled" + +def write(line): + sys.stdout.write(line) + +def getTone(split): + return split[1][:-1] + +def fillLine(number,file): + addLine(' ' + str(number) + '\t' + file) + +class FillFreepats: + process=False + tone=0 + last=False + + def nextTone(self): + self.tone+=1 + if self.tone in GROUP_START: + self.last=False + print + + def readLine(self,line): + if line=="\n": + return + if not (line.startswith('dir') or line.startswith('#')): + if line.startswith('bank'): + self.process=True + elif self.process: + split=line.split('\t') + lineTone=split[0].lstrip() + if str(self.tone)!=lineTone: + while self.tone Date: Mon, 26 Oct 2015 13:40:14 -0200 Subject: [PATCH 02/16] Revision of README --- README | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README b/README index ee20a4f..b8a5398 100644 --- a/README +++ b/README @@ -1,4 +1,8 @@ -The original Music Animaion Machine and it's open source clone, PyMusAnim, are awesome. Unfortunately though the original project was never open source and ran poorly, on Windows. PyMusAnim, using a totally different approach, doesn't suffer from performance issues - but it's more of a proof of concept than a funcioning tool. This fork extends PyMusAnim so that Linux users can easily output a MPG video given an input MIDI file. +PyMusAnim, a clone of Stephen Malinowski's Music Animation Machine in Python + +This fork extends PyMusAnim so that Linux users can easily output a MPG video given an input MIDI file, using a command-line tool. + +The original Music Animaion Machine and it's open source clone, PyMusAnim, are awesome. Unfortunately though the original project was never open source and ran poorly, on Windows and couldn't create video files by itself. PyMusAnim, using a totally different approach, doesn't suffer from performance issues - but it's orginally more of a proof of concept than a functioning tool. == DEPENDENCIES == @@ -14,23 +18,23 @@ On Debian and Ubuntu you can fulfill these requirementes by running the followin The core of PyMusAnim is virtually unchanged, the only difference being that instead of having to create your own Python configuration files the new module MusAnimLauncher does that for you (while still being configurable). This module is automatically called from two Linux command line (bash) utilities: - ./pymusanim.sh [file.mid] [output directory name] + ./pymusanim.sh [file.mid] [output directory name] Use this to create a video. For example: `pymusanim.sh mysong.mid mysong` will create a MPG file of `mysong.mid` inside the directory `mysong`. - ./batch.sh midisDirectory [threadsLimit] + ./batch.sh midisDirectory [threadsLimit] -Use this one if you want to create several videos at once. Since PyMusAnim is single-threaded this will let you take advantage of a multi-core CPU if you have one (and you probably do). `midisDirectory` is the directory you have your MIDI files and threadsLimit is an optional argument if you want to explicitally set the number of threads to use (if not set the program will use all available processors). For each MIDI file a sub-directoy will created outside the `output` folder, which will be created one doesn't exist. Example usage: `batch.sh mymidis/` +Use this one if you want to create several videos at once. Since PyMusAnim is single-threaded this will let you take advantage of a multi-core CPU if you have one (and you probably do). `midisDirectory` is the directory you have your MIDI files on and threadsLimit` is an optional argument to explicitally set the number of threads to use (if not set the program will use all available processors). For each MIDI file a sub-directoy will be created inside the `output` folder, which will be created if it doesn't exist. For example: `batch.sh mymidis/` Remember to run all these commands from the project's root directory. == A NOTE ABOUT PYMUSANIM AND MIDI FILES == -Unfortunately PyMusAnim is pretty bad at reading MIDI files with weird formats. I would go as far as to say that it's unable to read most MIDI files found on the web. The good news is that as long as the file is well formatted it will work perfectly. +Unfortunately PyMusAnim is pretty bad at reading MIDI files in weird formats. I would go as far as to say that it's unable to read most MIDI files found on the web. The good news is that as long as the file is well formatted it will work perfectly. -Well, how is this good news even? +Well... How is this even good news then? -I have found that if I have trouble opening a MIDI file then I can use TuxGuitar to import that file and then export it again. This way the exported MIDI file will most likely work with PyMusAnim. TuxGuitar even has a batch conversion tool (under the Tools menu) that you can use to import and export many MIDI files at once automatically, making this a very short, if somewhat bothersome step. +I have found that if I have trouble opening a MIDI file then I can use TuxGuitar to import that file and then export it again. This way the exported MIDI file will most likely work with PyMusAnim. TuxGuitar even has a batch conversion tool (under the Tools menu) that you can use to import and export many MIDI files at once automatically, making this a very fast if somewhat bothersome step. I haven't tried but other programs with MIDI import and export features could potentially work for this as well. @@ -42,7 +46,7 @@ Original Music Animation Machine by Stephen Malinowski: http://www.musanim.com/p Malinowski on YouTube https://www.youtube.com/user/smalin TuxGuitar: http://tuxguitar.herac.com.ar/ (also available via `sudo apt-get install tuxguitar` ) -== TODO == +== TO DO == * Improve support for reading all formats of MIDI files (this would be huge, if you think you can help please contact me!) * Make PyMusAnim run on Python 3 too (should be pretty easy) From e76a1edd671343becad316d4b452d74165b01de1 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Mon, 26 Oct 2015 13:43:30 -0200 Subject: [PATCH 03/16] Trying to make the README prettier on Github --- README => README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename README => README.md (98%) diff --git a/README b/README.md similarity index 98% rename from README rename to README.md index b8a5398..54dfab7 100644 --- a/README +++ b/README.md @@ -24,7 +24,7 @@ Use this to create a video. For example: `pymusanim.sh mysong.mid mysong` will c ./batch.sh midisDirectory [threadsLimit] -Use this one if you want to create several videos at once. Since PyMusAnim is single-threaded this will let you take advantage of a multi-core CPU if you have one (and you probably do). `midisDirectory` is the directory you have your MIDI files on and threadsLimit` is an optional argument to explicitally set the number of threads to use (if not set the program will use all available processors). For each MIDI file a sub-directoy will be created inside the `output` folder, which will be created if it doesn't exist. For example: `batch.sh mymidis/` +Use this one if you want to create several videos at once. Since PyMusAnim is single-threaded this will let you take advantage of a multi-core CPU if you have one (and you probably do). `midisDirectory` is the directory you have your MIDI files on and `threadsLimit` is an optional argument to explicitally set the number of threads to use (if not set the program will use all available processors). For each MIDI file a sub-directoy will be created inside the `output` folder, which will be created if it doesn't exist. For example: `batch.sh mymidis/` Remember to run all these commands from the project's root directory. From d9e2d425df6fc6634b684e4b6e013327d3dfa838 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Mon, 26 Oct 2015 13:50:05 -0200 Subject: [PATCH 04/16] Fomatting README --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 54dfab7..90f5bdc 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -PyMusAnim, a clone of Stephen Malinowski's Music Animation Machine in Python +#PyMusAnim + +A clone of Stephen Malinowski's Music Animation Machine in Python This fork extends PyMusAnim so that Linux users can easily output a MPG video given an input MIDI file, using a command-line tool. The original Music Animaion Machine and it's open source clone, PyMusAnim, are awesome. Unfortunately though the original project was never open source and ran poorly, on Windows and couldn't create video files by itself. PyMusAnim, using a totally different approach, doesn't suffer from performance issues - but it's orginally more of a proof of concept than a functioning tool. -== DEPENDENCIES == +## Dependencies You need those external programs installed on your system for this fork to work: @@ -14,7 +16,7 @@ You need those external programs installed on your system for this fork to work: On Debian and Ubuntu you can fulfill these requirementes by running the following command: `sudo apt-get install ffmpeg timidity python2.6` -== HOW IT WORKS == +## How it works The core of PyMusAnim is virtually unchanged, the only difference being that instead of having to create your own Python configuration files the new module MusAnimLauncher does that for you (while still being configurable). This module is automatically called from two Linux command line (bash) utilities: @@ -28,7 +30,7 @@ Use this one if you want to create several videos at once. Since PyMusAnim is si Remember to run all these commands from the project's root directory. -== A NOTE ABOUT PYMUSANIM AND MIDI FILES == +## A note about PyMusAnim and MIDI files Unfortunately PyMusAnim is pretty bad at reading MIDI files in weird formats. I would go as far as to say that it's unable to read most MIDI files found on the web. The good news is that as long as the file is well formatted it will work perfectly. @@ -38,7 +40,7 @@ I have found that if I have trouble opening a MIDI file then I can use TuxGuitar I haven't tried but other programs with MIDI import and export features could potentially work for this as well. -== LINKS == +## Links Original PyMusAnim: https://github.com/zhanrnl/PyMusAnim PyMusAnim on YouTube: https://www.youtube.com/user/PyMusAnim @@ -46,7 +48,7 @@ Original Music Animation Machine by Stephen Malinowski: http://www.musanim.com/p Malinowski on YouTube https://www.youtube.com/user/smalin TuxGuitar: http://tuxguitar.herac.com.ar/ (also available via `sudo apt-get install tuxguitar` ) -== TO DO == +## To do * Improve support for reading all formats of MIDI files (this would be huge, if you think you can help please contact me!) * Make PyMusAnim run on Python 3 too (should be pretty easy) From f26a43bb622240a0e3b042b5343e67703449bbcb Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Mon, 26 Oct 2015 13:51:16 -0200 Subject: [PATCH 05/16] More README formatting --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 90f5bdc..7f071e4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ + #PyMusAnim -A clone of Stephen Malinowski's Music Animation Machine in Python +A clone of Stephen Malinowski's Music Animation Machine in Python. This fork extends PyMusAnim so that Linux users can easily output a MPG video given an input MIDI file, using a command-line tool. @@ -43,9 +44,13 @@ I haven't tried but other programs with MIDI import and export features could po ## Links Original PyMusAnim: https://github.com/zhanrnl/PyMusAnim + PyMusAnim on YouTube: https://www.youtube.com/user/PyMusAnim + Original Music Animation Machine by Stephen Malinowski: http://www.musanim.com/player/ + Malinowski on YouTube https://www.youtube.com/user/smalin + TuxGuitar: http://tuxguitar.herac.com.ar/ (also available via `sudo apt-get install tuxguitar` ) ## To do From 270511b17be5996b2603230c200ea8c1f02f4021 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Wed, 28 Oct 2015 09:57:41 -0200 Subject: [PATCH 06/16] Updating docs --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7f071e4..3283761 100644 --- a/README.md +++ b/README.md @@ -57,4 +57,6 @@ TuxGuitar: http://tuxguitar.herac.com.ar/ (also available via `sudo apt-get inst * Improve support for reading all formats of MIDI files (this would be huge, if you think you can help please contact me!) * Make PyMusAnim run on Python 3 too (should be pretty easy) -* A simple visual interface (GUI) so that it would be even easier to create PyMusAnim videos \ No newline at end of file +* A simple visual interface (GUI) so that it would be even easier to create PyMusAnim videos +* There is a issue with file paths containing whitespace not loading properly +* There may be an issue in batch mode with Timidity blocking the virtual MIDI device, rendering some videos silent but it doesn't seem to happen often From 2b3a48dc65b945641398a77ec24f74d45d014c35 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Sat, 31 Oct 2015 11:23:31 -0200 Subject: [PATCH 07/16] Better handling of generated and temporary files --- batch.sh | 9 +++++++-- pymusanim.sh | 22 +++++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/batch.sh b/batch.sh index a89173e..97ae0b6 100755 --- a/batch.sh +++ b/batch.sh @@ -14,7 +14,7 @@ if [ ! $? == 0 ]; then echo "No files found, aborting." exit fi -for m in $1/*.mid*; do +for m in $1/*.mid*; do #call pymusanim if there is an idle thread name=`basename $m .${m#*.}` nice -n19 ./pymusanim.sh $m $OUT_DIR$name &> $OUT_DIR${name}.log& echo "Starting $name" @@ -26,7 +26,12 @@ for m in $1/*.mid*; do sleep 1 done done +echo "Waiting for last processes to finish..." +while [ `jobs|grep Running|wc -l` != 0 ]; do #wait until end + sleep 1 +done +echo "Cleaning..." wait -echo "Done" +echo "Done!" first=1 ./checklogs.sh $OUT_DIR diff --git a/pymusanim.sh b/pymusanim.sh index 9651a5a..1c56ebc 100755 --- a/pymusanim.sh +++ b/pymusanim.sh @@ -1,16 +1,24 @@ #!/bin/bash # given the same parameters as PyMusAnimLauncher, runs the whole process of creating a video -function doNow() { +function checktime() { now=`date +%s` } -doNow;allStart=$now -python MusAnimLauncher.py $* if [ ! $? == 0 ]; then - echo "usage: ./pymusanim.sh [file.mid] [output directory name]" + echo "Usage: ./pymusanim.sh file.mid outputdirectory/" exit 1 fi -timidity $1 -Ov -o $2/a.ogg +name=`basename $1 .${1#*.}` +if [[ "$1" =~ \ |\' ]]; then + echo "Found an illegal character in this path, please rename: $1" + exit +fi + +checktime;allStart=$now +python MusAnimLauncher.py $* +cp $1 $2 +timidity $1 -Ov -o $2/${name}.ogg cd $2 -ffmpeg -f image2 -i frame%5d.png -i a.ogg -sameq a.mpg #sameq is same_quant in upstream -doNow;echo "Total time: $((($now-$allStart)/60)) minutes" +ffmpeg -f image2 -i frame%5d.png -i ${name}.ogg -sameq ${name}.mpg #sameq is same_quant in upstream +rm *.png +checktime;echo "Total time: $((($now-$allStart)/60)) minutes" From 3b6e504b3ca6667d15fea146c25277f75b5d7cc7 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Fri, 23 Dec 2016 11:34:32 -0200 Subject: [PATCH 08/16] More complete (and messy) freepats --- freepats/README.txt | 8 +++ freepats/freepats.cfg | 156 ++++++++++++++++++++++++++++-------------- 2 files changed, 111 insertions(+), 53 deletions(-) create mode 100755 freepats/README.txt diff --git a/freepats/README.txt b/freepats/README.txt new file mode 100755 index 0000000..0ebe458 --- /dev/null +++ b/freepats/README.txt @@ -0,0 +1,8 @@ +This is a utility you can use to help fill missing sound patches for Timidity's Freepats in case your output or log files shows they are missing. An example file is provided that already fills some missing instruments with similar ones. + +Unfortunately Freepats hasn't been updated since 2006 so unless you're willing to create or find new sound patches yourself this is the best workaround that I could find. + +In Debian the following paths are relevant: + + /etc/timidity/ (where to place your new freepats.cfg) + /usr/share/midi/freepats/ (location of .pat files - you may need to update the freepats.cfg if this is different in your system) diff --git a/freepats/freepats.cfg b/freepats/freepats.cfg index 0bfc5e9..7c23e5a 100755 --- a/freepats/freepats.cfg +++ b/freepats/freepats.cfg @@ -2,15 +2,17 @@ dir /usr/share/midi/freepats # Automatically generated on Sun Feb 19 19:22:39 EST 2006 # by http://freepats.opensrc.org/mkcfg.sh.txt drumset 0 - 25 Drum_000/025_Snare_Roll.pat + 20 Drum_000/035_Kick_1.pat amp=350 #raised from amp=100 + 25 Drum_000/025_Snare_Roll.pat 26 Drum_000/026_Snap.pat 27 Drum_000/027_High_Q.pat + 28 Drum_000/038_Snare_1.pat 31 Drum_000/031_Sticks.pat 32 Drum_000/032_Square_Click.pat 33 Drum_000/033_Metronome_Click.pat 34 Drum_000/034_Metronome_Bell.pat - 35 Drum_000/035_Kick_1.pat amp=100 - 36 Drum_000/036_Kick_2.pat amp=100 + 35 Drum_000/035_Kick_1.pat amp=275 #raised from amp=100 + 36 Drum_000/036_Kick_2.pat amp=350 #raised from amp=100 37 Drum_000/037_Stick_Rim.pat 38 Drum_000/038_Snare_1.pat 39 Drum_000/039_Clap_Hand.pat amp=100 @@ -101,8 +103,8 @@ bank 0 35 Tone_000/035_Fretless_Bass.pat 36 Tone_000/036_Slap_Bass_1.pat 37 Tone_000/037_Slap_Bass_2.pat - 38 Tone_000/038_Synth_Bass_1.pat - 39 Tone_000/038_Synth_Bass_1.pat #filled + 38 Tone_000/038_Synth_Bass_1.pat + 39 Tone_000/038_Synth_Bass_1.pat #filled 40 Tone_000/040_Violin.pat 41 Tone_000/040_Violin.pat #filled @@ -202,99 +204,147 @@ bank 0 125 Tone_000/125_Helicopter.pat 126 Tone_000/125_Helicopter.pat #filled 127 Tone_000/125_Helicopter.pat #filled + drumset 1 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 2 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 3 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 4 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 5 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 6 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 7 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 8 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 9 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 10 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 11 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 12 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 13 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 14 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 15 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 16 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 17 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 18 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 19 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 20 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 21 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 22 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 23 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 24 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 25 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 26 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 27 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 28 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 29 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 30 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 31 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 32 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 33 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 34 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 35 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 36 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 37 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 38 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 39 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 40 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 41 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 42 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 43 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 44 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 45 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 46 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 47 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled drumset 48 #filled - #extension copydrumset 0 #filled +#extension copydrumset 0 #filled +drumset 49 #filled +#extension copydrumset 0 #filled +drumset 50 #filled +#extension copydrumset 0 #filled +drumset 51 #filled +#extension copydrumset 0 #filled +drumset 52 #filled +#extension copydrumset 0 #filled +drumset 53 #filled +#extension copydrumset 0 #filled +drumset 54 #filled +#extension copydrumset 0 #filled +drumset 55 #filled +#extension copydrumset 0 #filled +drumset 56 #filled +#extension copydrumset 0 #filled +drumset 57 #filled +#extension copydrumset 0 #filled +drumset 58 #filled +#extension copydrumset 0 #filled +drumset 59 #filled +#extension copydrumset 0 #filled +drumset 60 #filled +#extension copydrumset 0 #filled +drumset 61 #filled +#extension copydrumset 0 #filled +drumset 62 #filled +#extension copydrumset 0 #filled +drumset 63 #filled +#extension copydrumset 0 #filled +drumset 64 #filled +#extension copydrumset 0 #filled +drumset 65 #filled +#extension copydrumset 0 #filled +drumset 66 #filled +#extension copydrumset 0 #filled + +bank 1 #filled +#extension copybank 0 #filled +bank 2 #filled +#extension copybank 0 #filled +bank 3 #filled +#extension copybank 0 #filled +bank 4 #filled +#extension copybank 0 #filled +bank 5 #filled +#extension copybank 0 #filled From 24230a50d899d0eda21eb2be986bc0c8cb198c62 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Fri, 23 Dec 2016 11:36:35 -0200 Subject: [PATCH 09/16] Dynamic mode --- .gitignore | 3 ++ MusAnimLauncher.py | 11 +++--- MusAnimRenderer.py | 86 +++++++++++++++++++++++++++++----------------- README.md | 10 ++++-- batch.sh | 10 ++++-- pymusanim.sh | 9 ++--- 6 files changed, 81 insertions(+), 48 deletions(-) mode change 100644 => 100755 .gitignore mode change 100644 => 100755 README.md diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index b25c15b..e8cc575 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ *~ +*.pyc +midi/ +output/ diff --git a/MusAnimLauncher.py b/MusAnimLauncher.py index 12ea519..ad3990e 100755 --- a/MusAnimLauncher.py +++ b/MusAnimLauncher.py @@ -141,7 +141,7 @@ def main(): ] if len(sys.argv)<3: - print("Usage: python MusAnimLauncher.py input.mid outputDirectory") + print("Usage: python MusAnimLauncher.py input.mid outputDirectory [--dynamic]") sys.exit(1) return @@ -152,6 +152,8 @@ def main(): frames_dir = sys.argv[2]+os.sep os.makedirs(frames_dir) + + dynamic=len(sys.argv)>=4 and sys.argv[3]=='--dynamic' speed_map = [{'time': 0.0, 'speed': 4}] @@ -164,12 +166,7 @@ def main(): renderer=MusAnimRenderer() renderer.introduction=False - renderer.render(mid, frames_dir, tracks, - speed_map=speed_map, - dimensions=dimensions, - min_pitch=lexer.minPitch-PITCH_GRACE, - max_pitch=lexer.maxPitch+PITCH_GRACE, - fps=fps) + renderer.render(mid,frames_dir,tracks,speed_map=speed_map,dimensions=dimensions,min_pitch=lexer.minPitch-PITCH_GRACE,max_pitch=lexer.maxPitch+PITCH_GRACE,fps=fps,dynamicmode=dynamic) if __name__ == '__main__': main() diff --git a/MusAnimRenderer.py b/MusAnimRenderer.py index ffa82cb..aa87685 100755 --- a/MusAnimRenderer.py +++ b/MusAnimRenderer.py @@ -65,8 +65,7 @@ def blockify(self, midi_events): def add_block_info(self, blocks, tracks, fps, speed_map, dimensions, min_pitch, max_pitch): - """Adds essential information to each block dict in blocks, also returns - last_block_end to tell when animation is over""" + """Adds essential information to each block dict in blocks, also returns last_block_end to tell when animation is over""" # need: start_time (seconds), end_time (seconds), pitch, track_num for # each block last_block_end = 0 @@ -146,9 +145,7 @@ def add_block_info(self, blocks, tracks, fps, speed_map, dimensions, return blocks, last_block_end def calc_offset(self, speed_map, time_offset, fps): - """Calculates the x-offset of a block given its time offset and a speed - map. Needed for laying out blocks because of variable block speed in the - animation.""" + """Calculates the x-offset of a block given its time offset and a speed map. Needed for laying out blocks because of variable block speed in the animation.""" x_offset = 0 i = 0 # speed is a dict with a speed and a time when we switch to speed @@ -175,21 +172,29 @@ def get_speed(self, speed_map, time): return speed_map[i]['speed'] def draw_block_cairo(self, block, tracks, dimensions, cr, transparent=False): - if block['start_x'] < (dimensions[0] / 2) and (block['end_x'] > - (dimensions[0] / 2)): - color = tracks[block['track_num']]['high_color'] + middle=dimensions[0] / 2 + start=block['start_x'] + if start< middle and block['end_x'] > middle: + color = tracks[block['track_num']]['color' if self.dynamicmode else 'high_color'] self.first_highlight = True else: color = tracks[block['track_num']]['color'] + transparent=transparent or self.dynamicmode if transparent: - r, g, b = color - cr.set_source_rgba(r, g, b, 0.5) + alpha=0.5 + if self.dynamicmode: + alpha=block['end_x']#block position + alpha=2*(middle-alpha)#distance from middle + alpha=1-(alpha/middle)#percentage + alpha=alpha/3.0#force past blocks to be at most 33% opaque + cr.set_source_rgba(color[0], color[1], color[2], alpha) else: cr.set_source_rgb(*color) if block['shape'] == 'circle': - cr.arc(block['start_x'] + block['width']/2, block['top_y'] + block['width']/2, block['width']/2, 0, 2 * math.pi) + halfwidth=block['width']/2 + cr.arc(start + halfwidth, block['top_y'] + halfwidth, halfwidth, 0, 2 * math.pi) else: - cr.rectangle(block['start_x'], block['top_y'], block['x_length'], + cr.rectangle(start, block['top_y'], block['x_length'], block['width']) cr.fill() @@ -233,11 +238,9 @@ def draw_lyrics_cairo(self, block, tracks, dimensions, cr): do_render=1 introduction=True - def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_map, - dimensions=(width,height), fps=fps, min_pitch=min_pitch, max_pitch=max_pitch, first_frame=first_frame, - last_frame=last_frame, every_nth_frame=every_nth_frame, do_render=do_render): + def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_map,dimensions=(width,height), fps=fps, min_pitch=min_pitch, max_pitch=max_pitch, first_frame=first_frame,last_frame=last_frame, every_nth_frame=every_nth_frame, do_render=do_render,dynamicmode=False): + self.dynamicmode=dynamicmode """Render the animation!""" - print "Beginning render..." speed = speed_map[0]['speed'] if first_frame == None: @@ -252,6 +255,7 @@ def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_ma print "Blockifying midi..." blocks = self.blockify(midi_events) # convert into list of blocks + print str(len(blocks))+" blocks" for track in tracks: if 'color' in track: @@ -288,13 +292,12 @@ def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_ma print "Rendering frames..." # generate frames while there are blocks on the screen: - while last_block_end > (0 - speed): + while last_block_end > -speed: # code only for rendering blocks - if frame >= first_frame and frame <= last_frame and frame % every_nth_frame == 0: + if first_frame < frame and frame <= last_frame and frame % every_nth_frame == 0: # cairo setup stuff filename = frame_save_dir + ("frame%05i.png" % framefile) surface = cairo.ImageSurface(cairo.FORMAT_RGB24, *dimensions) - #surface = cairo.SVGSurface(filename, *dimensions) cr = cairo.Context(surface) cr.set_antialias(cairo.ANTIALIAS_GRAY) cr.select_font_face("Garamond", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) @@ -304,29 +307,31 @@ def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_ma cr.rectangle(0, 0, *dimensions) cr.fill() - # need to do two passes of drawing blocks, once in reverse order - # in full opacity, and a second time in ascending order in half- - # opacityto get fully-colored bars that blend together when - # overlapping + '''need to do two passes of drawing blocks, once in reverse order in full opacity, and a second time in ascending order in half-opacity to get fully-colored bars that blend together when overlapping''' # get list of blocks that are on screen on_screen_blocks = [block for block in blocks - if block['start_x'] < dimensions[0] and block['end_x'] > 0] - on_screen_layers = list(set([block['layer'] for block in on_screen_blocks])) + if block['start_x'] < dimensions[0]] - for layer in on_screen_layers: + for layer in set([block['layer'] for block in on_screen_blocks]): layer_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, *dimensions) layer_context = cairo.Context(layer_surface) in_layer_blocks = [block for block in on_screen_blocks if block['layer'] == layer] - + # do first drawing pass for block in in_layer_blocks: + block=self.makedynamic(block,dimensions) + if block==None: + continue self.draw_block_cairo(block, tracks, dimensions, layer_context) # do second drawing pass on_screen_blocks.reverse() for block in in_layer_blocks: + block=self.makedynamic(block,dimensions) + if block==None: + continue self.draw_block_cairo(block, tracks, dimensions, layer_context, transparent=True) cr.set_source_surface(layer_surface) @@ -338,7 +343,6 @@ def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_ma if ('lyrics' in block): self.draw_lyrics_cairo(block, tracks, dimensions, cr) - #cr.save() if self.introduction or self.first_highlight: framefile += 1 surface.write_to_png(filename) @@ -347,14 +351,19 @@ def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_ma frame += 1 # need to set speed speed = self.get_speed(speed_map, time) - for block in blocks: # move blocks to left + for block in list(blocks): # move blocks to left + end=block['end_x']-speed + if end<0: + '''trash blocks no longer needed. benchmark shows this causes a 2% increase in processing time in medium-sized files (~1000 blocks) but up to 15% redution in big files (~10000 blocks)''' + blocks.remove(block) + continue + block['end_x'] = end block['start_x'] -= speed - block['end_x'] -= speed if 'lyrics_end_x' in block: block['lyrics_end_x'] -= speed last_block_end -= speed # move video endpoint left as well - percent = int(min((original_end - last_block_end) * 100.0 - / original_end, 100)) + percent = min(int((original_end - last_block_end) * 100.0 + / original_end), 100) if percent != last_percent: print percent, "% done" last_percent = percent @@ -364,6 +373,19 @@ def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_ma print "Done!" + def makedynamic(self,block,dimensions): + if self.dynamicmode: + middle=dimensions[0] / 2 + if block['start_x'] > middle: + return None + if block['end_x'] > middle: + block=block.copy() + shorten=block['end_x']-(middle+1) + block['end_x']-=shorten + block['x_length']-=shorten + return block + return block + if __name__ == '__main__': print ("Sorry, I don't really do anything useful as an executable, see " "RunAnim.py for usage") \ No newline at end of file diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 3283761..949a7d5 --- a/README.md +++ b/README.md @@ -21,16 +21,22 @@ On Debian and Ubuntu you can fulfill these requirementes by running the followin The core of PyMusAnim is virtually unchanged, the only difference being that instead of having to create your own Python configuration files the new module MusAnimLauncher does that for you (while still being configurable). This module is automatically called from two Linux command line (bash) utilities: - ./pymusanim.sh [file.mid] [output directory name] + ./pymusanim.sh file.mid outputdirectory [--dynamic] Use this to create a video. For example: `pymusanim.sh mysong.mid mysong` will create a MPG file of `mysong.mid` inside the directory `mysong`. - ./batch.sh midisDirectory [threadsLimit] + ./batch.sh midisDirectory mode [threadsLimit] Use this one if you want to create several videos at once. Since PyMusAnim is single-threaded this will let you take advantage of a multi-core CPU if you have one (and you probably do). `midisDirectory` is the directory you have your MIDI files on and `threadsLimit` is an optional argument to explicitally set the number of threads to use (if not set the program will use all available processors). For each MIDI file a sub-directoy will be created inside the `output` folder, which will be created if it doesn't exist. For example: `batch.sh mymidis/` Remember to run all these commands from the project's root directory. +## Dynamic mode + +This fork adds a new rendering mode that can be activated by passing the argument --dynamic as the last argument to `MusAnimLauncher.py` or `pymusanim.sh`. Note that when running `batch.sh` you'll always need to inform which mode you're using (either `--classic` or `--dynamic`). + +This mode tries to mimic somewhat the "shapes mode" of the original Music Animation Machine and focuses more on enhancing what's being played at each moment than on having a wider view of the composition being played. + ## A note about PyMusAnim and MIDI files Unfortunately PyMusAnim is pretty bad at reading MIDI files in weird formats. I would go as far as to say that it's unable to read most MIDI files found on the web. The good news is that as long as the file is well formatted it will work perfectly. diff --git a/batch.sh b/batch.sh index 97ae0b6..3630451 100755 --- a/batch.sh +++ b/batch.sh @@ -5,7 +5,11 @@ OUT_DIR=output/ shopt -s nocaseglob trap 'kill 0' EXIT if [ ! $1 ]; then - echo "Usage: batch.sh midisDirectory [threadsLimit]" + echo "Usage: batch.sh midisDirectory mode [threadsLimit]" + exit 1 +fi +if [ ! $2 ]; then + echo "Please inform a mode: either --classic or --dynamic" exit 1 fi mkdir $OUT_DIR @@ -16,9 +20,9 @@ if [ ! $? == 0 ]; then fi for m in $1/*.mid*; do #call pymusanim if there is an idle thread name=`basename $m .${m#*.}` - nice -n19 ./pymusanim.sh $m $OUT_DIR$name &> $OUT_DIR${name}.log& + nice -n19 ./pymusanim.sh $m $OUT_DIR$name $2 &> $OUT_DIR${name}.log& echo "Starting $name" - threadsLimit=$2 + threadsLimit=$3 if [ ! $threadsLimit ]; then threadsLimit=`cat /proc/cpuinfo | grep processor | wc -l` fi diff --git a/pymusanim.sh b/pymusanim.sh index 1c56ebc..7214f12 100755 --- a/pymusanim.sh +++ b/pymusanim.sh @@ -4,8 +4,8 @@ function checktime() { now=`date +%s` } -if [ ! $? == 0 ]; then - echo "Usage: ./pymusanim.sh file.mid outputdirectory/" +if [ $# == 0 ]; then + echo "Usage: ./pymusanim.sh file.mid outputdirectory/ [--dynamic]" exit 1 fi name=`basename $1 .${1#*.}` @@ -16,9 +16,10 @@ fi checktime;allStart=$now python MusAnimLauncher.py $* +checktime;echo "Total time: $((($now-$allStart)/60)) minutes ($((($now-$allStart))) seconds)" cp $1 $2 timidity $1 -Ov -o $2/${name}.ogg cd $2 -ffmpeg -f image2 -i frame%5d.png -i ${name}.ogg -sameq ${name}.mpg #sameq is same_quant in upstream +ffmpeg -f image2 -i frame%5d.png -i ${name}.ogg -qscale 0 ${name}.mpg rm *.png -checktime;echo "Total time: $((($now-$allStart)/60)) minutes" +checktime;echo "Total time: $((($now-$allStart)/60)) minutes ($((($now-$allStart))) seconds)" From 1fecc718f5563956cea42515359457de89a0454d Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Fri, 23 Dec 2016 15:19:29 -0200 Subject: [PATCH 10/16] Adding replaceaudio tool --- README.md | 6 ++++++ pymusanim.sh | 5 ++++- replaceaudio.sh | 22 ++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100755 replaceaudio.sh diff --git a/README.md b/README.md index 949a7d5..7d4909a 100755 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ I have found that if I have trouble opening a MIDI file then I can use TuxGuitar I haven't tried but other programs with MIDI import and export features could potentially work for this as well. +## FreePats + +FreePats is the libre soundbank that is used by Timidity and other Linux MIDI software. Despite doing a great job of allowing you to play most MIDI files for free, it can be a bit quirky and unfortunately isn't updated very often. You can see some hacks around its shortcomings on the 'freepats' directory. If you happen to know of any others hacks or good alternative libre soundbanks, let me know! + ## Links Original PyMusAnim: https://github.com/zhanrnl/PyMusAnim @@ -59,6 +63,8 @@ Malinowski on YouTube https://www.youtube.com/user/smalin TuxGuitar: http://tuxguitar.herac.com.ar/ (also available via `sudo apt-get install tuxguitar` ) +FreePats http://freepats.zenvoid.org/ + ## To do * Improve support for reading all formats of MIDI files (this would be huge, if you think you can help please contact me!) diff --git a/pymusanim.sh b/pymusanim.sh index 7214f12..76da199 100755 --- a/pymusanim.sh +++ b/pymusanim.sh @@ -11,11 +11,14 @@ fi name=`basename $1 .${1#*.}` if [[ "$1" =~ \ |\' ]]; then echo "Found an illegal character in this path, please rename: $1" - exit + exit 2 fi checktime;allStart=$now python MusAnimLauncher.py $* +if [ $? -ne 0 ]; then + exit 3 +fi checktime;echo "Total time: $((($now-$allStart)/60)) minutes ($((($now-$allStart))) seconds)" cp $1 $2 timidity $1 -Ov -o $2/${name}.ogg diff --git a/replaceaudio.sh b/replaceaudio.sh new file mode 100755 index 0000000..cb35676 --- /dev/null +++ b/replaceaudio.sh @@ -0,0 +1,22 @@ +#!/bin/bash +function usage() { + echo "Usage: ./replaceaudio.sh directory/" + echo "" + echo "This utility takes a directory name and will use the following files" + echo "to create a new video with a given audio track:" + echo "- audio.mid (will be converted to ogg with timidity)" + echo "- video.mpg (will used as video input, current audio being ignored)" +} + +if [ $# == 0 ]; then + usage;exit 1 +fi +cd $1 +if [ ! -e "audio.mid" ] || [ ! -e "video.mpg" ]; then + usage;exit 2 +fi +timidity audio.mid -Ov -o audio.tmp.ogg +ffmpeg -i video.mpg -codec copy -an silent.tmp.mpg +ffmpeg -i silent.tmp.mpg -i audio.tmp.ogg -vcodec copy replaced.mpg +rm audio.tmp.ogg +rm silent.tmp.mpg From ee1485126901bd555234e43377557ae2a6354eee Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Sat, 24 Dec 2016 15:11:18 -0200 Subject: [PATCH 11/16] MuseScore support and better pymusanim.sh --- README.md | 19 +++++++++----- pymusanim.sh | 68 ++++++++++++++++++++++++++++++++++--------------- replaceaudio.sh | 17 ++++++++----- 3 files changed, 72 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 7d4909a..2607b31 100755 --- a/README.md +++ b/README.md @@ -11,11 +11,12 @@ The original Music Animaion Machine and it's open source clone, PyMusAnim, are a You need those external programs installed on your system for this fork to work: - * Python 2 - * Timidity - * FFMpeg + * Python 2 https://www.python.org/ + * Timidity http://timidity.sourceforge.net/ + * FFMpeg https://ffmpeg.org/ + * MuseScore (optional) https://musescore.com/ -On Debian and Ubuntu you can fulfill these requirementes by running the following command: `sudo apt-get install ffmpeg timidity python2.6` +On Debian and Ubuntu you can fulfill these requirementes by running the following command: `sudo apt-get install ffmpeg timidity python2.6 musescore` ## How it works @@ -37,7 +38,13 @@ This fork adds a new rendering mode that can be activated by passing the argumen This mode tries to mimic somewhat the "shapes mode" of the original Music Animation Machine and focuses more on enhancing what's being played at each moment than on having a wider view of the composition being played. -## A note about PyMusAnim and MIDI files +## MuseScore + +MuseScore is a great composition tool and offers a few featuress that work very well with PyMusAnim - namely being able to convert unsupported MIDI files into a supported format and also the ability to render them as audio files with higher quality than other tools. It is suggested that you install MuseScore in your system and, if you do then PyMusAnim will take full advantage of it when possible. Having it installed should help you automatically circumvent the issues detailed below. + +## Issues + +### A note about PyMusAnim and MIDI files Unfortunately PyMusAnim is pretty bad at reading MIDI files in weird formats. I would go as far as to say that it's unable to read most MIDI files found on the web. The good news is that as long as the file is well formatted it will work perfectly. @@ -47,7 +54,7 @@ I have found that if I have trouble opening a MIDI file then I can use TuxGuitar I haven't tried but other programs with MIDI import and export features could potentially work for this as well. -## FreePats +### FreePats FreePats is the libre soundbank that is used by Timidity and other Linux MIDI software. Despite doing a great job of allowing you to play most MIDI files for free, it can be a bit quirky and unfortunately isn't updated very often. You can see some hacks around its shortcomings on the 'freepats' directory. If you happen to know of any others hacks or good alternative libre soundbanks, let me know! diff --git a/pymusanim.sh b/pymusanim.sh index 76da199..0237a99 100755 --- a/pymusanim.sh +++ b/pymusanim.sh @@ -1,28 +1,56 @@ #!/bin/bash # given the same parameters as PyMusAnimLauncher, runs the whole process of creating a video -function checktime() { - now=`date +%s` +function checktime() { now=`date +%s`;} +function printtime(){ + checktime + timeelapsed=$(($now-$allStart)) + echo "Total time: $(($timeelapsed/60)) minutes ($timeelapsed seconds)" +} +function animate(){ + python MusAnimLauncher.py $* #first try + if [ $? -ne 0 ]; then + if ! [ $musescore ]; then exit 1; fi #first try failed + echo "Converting MIDI..." + converted="$2/musescore.mid" + #echo mscore "$1" -o $converted #convert mid->mid + mscore "$1" -o $converted #convert mid->mid 2> /dev/null + python MusAnimLauncher.py $converted $2 $3 #second try + if [ $? -ne 0 ]; then exit 1; fi #second try failed + fi +} +function record(){ + for midi in *.mid*; do #call pymusanim if there is an idle thread + if [ $timidity ]; then + me=`whoami` + while [ ps -u $me|grep timidity ]; do sleep 1; done #wait for other instances to finish + timidity $midi -Ov -o "$midi.timidity.ogg" + fi + if [ $musescore ] ; then + while [ ps -u $me|grep mscore ]; do sleep 1; done #wait for other instances to finish + mscore $midi -o "$midi.musescore.ogg" + fi + done +} +function convert(){ + ffmpeg -f image2 -i frame%5d.png -qscale 0 silent.tmp.mpg + for ogg in *.ogg; do #call pymusanim if there is an idle thread + ffmpeg -i silent.tmp.mpg -i $ogg -vcodec copy "$ogg.mpg" + done + rm *.png + rm silent.tmp.mpg } -if [ $# == 0 ]; then - echo "Usage: ./pymusanim.sh file.mid outputdirectory/ [--dynamic]" - exit 1 -fi -name=`basename $1 .${1#*.}` -if [[ "$1" =~ \ |\' ]]; then - echo "Found an illegal character in this path, please rename: $1" - exit 2 -fi +if [ $# == 0 ]; then echo "Usage: ./pymusanim.sh file.mid outputdirectory/ [--dynamic]"; exit 2; fi +if [ -e $2 ]; then echo 'Output folder already exists...'; exit 3; fi +#name=`basename $1 .${1#*.}`; if [[ "$1" =~ \ |\' ]]; then echo "Found an illegal character in this path, please rename: $1"; exit 4; fi +type mscore 2&>/dev/null;musescore=$? +type timidity 2&>/dev/null;timidity=$? checktime;allStart=$now -python MusAnimLauncher.py $* -if [ $? -ne 0 ]; then - exit 3 -fi -checktime;echo "Total time: $((($now-$allStart)/60)) minutes ($((($now-$allStart))) seconds)" +animate $* +printtime cp $1 $2 -timidity $1 -Ov -o $2/${name}.ogg cd $2 -ffmpeg -f image2 -i frame%5d.png -i ${name}.ogg -qscale 0 ${name}.mpg -rm *.png -checktime;echo "Total time: $((($now-$allStart)/60)) minutes ($((($now-$allStart))) seconds)" +record $* +convert $* +printtime diff --git a/replaceaudio.sh b/replaceaudio.sh index cb35676..942494c 100755 --- a/replaceaudio.sh +++ b/replaceaudio.sh @@ -5,18 +5,23 @@ function usage() { echo "This utility takes a directory name and will use the following files" echo "to create a new video with a given audio track:" echo "- audio.mid (will be converted to ogg with timidity)" - echo "- video.mpg (will used as video input, current audio being ignored)" + echo "- audio.ogg (will exists will not process audio.mid)" + echo "- video.mpg (will be used as video input, current audio being ignored)" } if [ $# == 0 ]; then usage;exit 1 fi -cd $1 -if [ ! -e "audio.mid" ] || [ ! -e "video.mpg" ]; then +cd "$1" +if [ ! -e "video.mpg" ]; then usage;exit 2 fi -timidity audio.mid -Ov -o audio.tmp.ogg +if [ ! -e "audio.ogg" ]; then + if [ ! -e "audio.mid" ]; then + usage;exit 2 + fi + timidity audio.mid -Ov -o audio.ogg +fi ffmpeg -i video.mpg -codec copy -an silent.tmp.mpg -ffmpeg -i silent.tmp.mpg -i audio.tmp.ogg -vcodec copy replaced.mpg -rm audio.tmp.ogg +ffmpeg -i silent.tmp.mpg -i audio.ogg -vcodec copy replaced.mpg rm silent.tmp.mpg From c51e8fa95d8a8bbd836b8f68de8686d182455388 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Sat, 24 Dec 2016 15:36:54 -0200 Subject: [PATCH 12/16] Moving render operations to a separate file --- pymusanim.sh | 28 +--------------------------- render.sh | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 27 deletions(-) create mode 100755 render.sh diff --git a/pymusanim.sh b/pymusanim.sh index 0237a99..22a141f 100755 --- a/pymusanim.sh +++ b/pymusanim.sh @@ -12,45 +12,19 @@ function animate(){ if ! [ $musescore ]; then exit 1; fi #first try failed echo "Converting MIDI..." converted="$2/musescore.mid" - #echo mscore "$1" -o $converted #convert mid->mid mscore "$1" -o $converted #convert mid->mid 2> /dev/null python MusAnimLauncher.py $converted $2 $3 #second try if [ $? -ne 0 ]; then exit 1; fi #second try failed fi } -function record(){ - for midi in *.mid*; do #call pymusanim if there is an idle thread - if [ $timidity ]; then - me=`whoami` - while [ ps -u $me|grep timidity ]; do sleep 1; done #wait for other instances to finish - timidity $midi -Ov -o "$midi.timidity.ogg" - fi - if [ $musescore ] ; then - while [ ps -u $me|grep mscore ]; do sleep 1; done #wait for other instances to finish - mscore $midi -o "$midi.musescore.ogg" - fi - done -} -function convert(){ - ffmpeg -f image2 -i frame%5d.png -qscale 0 silent.tmp.mpg - for ogg in *.ogg; do #call pymusanim if there is an idle thread - ffmpeg -i silent.tmp.mpg -i $ogg -vcodec copy "$ogg.mpg" - done - rm *.png - rm silent.tmp.mpg -} if [ $# == 0 ]; then echo "Usage: ./pymusanim.sh file.mid outputdirectory/ [--dynamic]"; exit 2; fi if [ -e $2 ]; then echo 'Output folder already exists...'; exit 3; fi -#name=`basename $1 .${1#*.}`; if [[ "$1" =~ \ |\' ]]; then echo "Found an illegal character in this path, please rename: $1"; exit 4; fi type mscore 2&>/dev/null;musescore=$? type timidity 2&>/dev/null;timidity=$? - checktime;allStart=$now animate $* printtime cp $1 $2 -cd $2 -record $* -convert $* +./render.sh $2 printtime diff --git a/render.sh b/render.sh new file mode 100755 index 0000000..30e5359 --- /dev/null +++ b/render.sh @@ -0,0 +1,32 @@ +#!/bin/bash +#responsible from generating the OGG files and final MPG from PyMusLauncher's output +#being a separate step allows you to do some tricks like adding a few midi files if anything goes wrong and running this step again +function record(){ + rm *.ogg* 2&>/dev/null + for midi in *.mid*; do #call pymusanim if there is an idle thread + echo $midi + if [ $timidity ]; then + me=`whoami` + while [ `ps -u $me|grep timidity|wc -l` != 0 ]; do sleep 1; done #wait for other instances to finish + timidity $midi -Ov -o "$midi.timidity.ogg" + fi + if [ $musescore ] ; then + while [ `ps -u $me|grep mscore|wc -l` != 0 ]; do sleep 1; done #wait for other instances to finish + mscore $midi -o "$midi.musescore.ogg" + fi + done +} +function merge(){ + if [ -e frame00000.png ] ; then ffmpeg -f image2 -i frame%5d.png -qscale 0 silent.mpg; fi #png -> mpg + for ogg in *.ogg; do #call pymusanim if there is an idle thread + ffmpeg -i silent.mpg -i $ogg -vcodec copy "$ogg.mpg" + done + rm *.png 2&>/dev/null +} + +type mscore 2&>/dev/null;musescore=$? +type timidity 2&>/dev/null;timidity=$? +if [ $# == 0 ]; then echo "Usage: ./render.sh partialdirectory/"; exit 2; fi +cd $1 +#record +merge From 1d1166da997d27f74d507eb381aae798487ef306 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Sat, 24 Dec 2016 18:28:42 -0200 Subject: [PATCH 13/16] fixes --- MusAnimLauncher.py | 3 ++- pymusanim.sh | 10 ++++++---- render.sh | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/MusAnimLauncher.py b/MusAnimLauncher.py index ad3990e..9460d1c 100755 --- a/MusAnimLauncher.py +++ b/MusAnimLauncher.py @@ -151,7 +151,8 @@ def main(): lexer.lex(mid) frames_dir = sys.argv[2]+os.sep - os.makedirs(frames_dir) + if not os.path.isdir(frames_dir): + os.makedirs(frames_dir) dynamic=len(sys.argv)>=4 and sys.argv[3]=='--dynamic' diff --git a/pymusanim.sh b/pymusanim.sh index 22a141f..b67a281 100755 --- a/pymusanim.sh +++ b/pymusanim.sh @@ -7,24 +7,26 @@ function printtime(){ echo "Total time: $(($timeelapsed/60)) minutes ($timeelapsed seconds)" } function animate(){ + echo python MusAnimLauncher.py $* #TODO python MusAnimLauncher.py $* #first try - if [ $? -ne 0 ]; then + mkdir $2 + cp $1 $2 + if ! [ -e "$2/frame00000.png" ]; then if ! [ $musescore ]; then exit 1; fi #first try failed echo "Converting MIDI..." converted="$2/musescore.mid" mscore "$1" -o $converted #convert mid->mid 2> /dev/null + echo python MusAnimLauncher.py $converted $2 $3 #TODO python MusAnimLauncher.py $converted $2 $3 #second try - if [ $? -ne 0 ]; then exit 1; fi #second try failed + if ! [ -e "$2/frame00000.png" ]; then exit 1; fi #second try failed fi } if [ $# == 0 ]; then echo "Usage: ./pymusanim.sh file.mid outputdirectory/ [--dynamic]"; exit 2; fi if [ -e $2 ]; then echo 'Output folder already exists...'; exit 3; fi type mscore 2&>/dev/null;musescore=$? -type timidity 2&>/dev/null;timidity=$? checktime;allStart=$now animate $* printtime -cp $1 $2 ./render.sh $2 printtime diff --git a/render.sh b/render.sh index 30e5359..98df40b 100755 --- a/render.sh +++ b/render.sh @@ -28,5 +28,5 @@ type mscore 2&>/dev/null;musescore=$? type timidity 2&>/dev/null;timidity=$? if [ $# == 0 ]; then echo "Usage: ./render.sh partialdirectory/"; exit 2; fi cd $1 -#record +record merge From 082c995623313df483ebd5efa2255840a10833de Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Sat, 9 Mar 2024 00:00:00 +0000 Subject: [PATCH 14/16] Python 3 suppert --- MusAnimLexer.py | 38 +++++++++++++++++++------------------- MusAnimRenderer.py | 40 ++++++++++++++++++++-------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/MusAnimLexer.py b/MusAnimLexer.py index f6d3f54..e086267 100755 --- a/MusAnimLexer.py +++ b/MusAnimLexer.py @@ -8,11 +8,11 @@ class MidiLexer: # beats, without making any pretenses about figuring out timing in seconds. # That has to be done later, once we have all the timing events sorted midi_events = [] - minPitch = sys.maxint - maxPitch = -sys.maxint-1 + minPitch = sys.maxsize + maxPitch = -sys.maxsize-1 - def debug(self,event): - print 'Unknown event: ' + bin(event) + " (" + hex(event) + ")" + def debug(self, event): + print('Unknown event: ' + bin(event) + " (" + hex(event) + ")") def get_v_time(self, data): """Picks off the variable-length time from a block of data and returns @@ -30,15 +30,15 @@ def get_v_time(self, data): return d_time, data[i+1:] def read_midi_event(self, track_data, time, track_num): - # have to read off vtime first! - d_time, track_data = self.get_v_time(track_data) + # have to read off vtime first! + d_time, track_data = self.get_v_time(track_data) time += d_time #print time - event=((ord(track_data[0]) & 0xF0) >> 4) + event=((ord(track_data[0]) & 0xF0) >> 4) if track_data[0] == '\xff': # event is meta event, we do nothing unless it's a tempo event - if ord(track_data[1]) == 0x51: + if ord(track_data[1]) == 0x51: # tempo event mpqn = ((ord(track_data[3]) << 16) + (ord(track_data[4]) << 8) + ord(track_data[5])) # microseconds per quarter note @@ -69,13 +69,13 @@ def read_midi_event(self, track_data, time, track_num): elif pitch == 1: mode = "pizz" else: - print("Unknown keyswitch: "+str(pitch)) + print(("Unknown keyswitch: "+str(pitch))) pitch=False - if pitch != False: - self.midi_events.append({'type': 'keyswitch', 'time': time, - 'track_num': track_num, 'mode': mode}) + if pitch != False: + self.midi_events.append({'type': 'keyswitch', 'time': time, + 'track_num': track_num, 'mode': mode}) else: - if pitch > self.maxPitch: + if pitch > self.maxPitch: self.maxPitch = pitch if pitch < self.minPitch: self.minPitch = pitch @@ -88,10 +88,10 @@ def read_midi_event(self, track_data, time, track_num): return track_data[3:], time #kek elif event == 0xF or event == 0xD: - self.debug(event) + self.debug(event) return track_data[2:], time else: - self.debug(event) + self.debug(event) return track_data[3:], time def lex(self, filename): @@ -106,7 +106,7 @@ def lex(self, filename): # open and read file f = open(filename, 'rb') - s = f.read() + s = f.read().decode('latin-1') # grab header header = s[0:14] @@ -123,18 +123,18 @@ def lex(self, filename): # parse midi events for a single track while len(track) > 0: # read off midi events and add to midi_events - track, time = self.read_midi_event(track, time, track_num) + track, time = self.read_midi_event(track, time, track_num) track_num += 1 # convert all times from ticks to beats, for convenience for event in self.midi_events: event['time'] = (event['time'] + 0.0) / self.ticks_per_quarter #960 - self.midi_events.sort(lambda a, b: cmp(a['time'], b['time'])) + self.midi_events.sort(key=lambda a: a['time']) return self.midi_events if __name__ == '__main__': lexer = MidiLexer() blocks = lexer.lex('multitrackmidi01.MID') - print blocks + print(blocks) diff --git a/MusAnimRenderer.py b/MusAnimRenderer.py index aa87685..60d9a5a 100755 --- a/MusAnimRenderer.py +++ b/MusAnimRenderer.py @@ -108,7 +108,7 @@ def add_block_info(self, blocks, tracks, fps, speed_map, dimensions, #block['start_x'] = round(block['start_x']) # sort by track_num so we get proper melisma length counting - blocks.sort(lambda a, b: cmp(a['track_num'], b['track_num'])) + blocks.sort(key=lambda a: a['track_num']) # can't add lyrics until we add in end_x for all blocks block_num = 0 @@ -140,7 +140,7 @@ def add_block_info(self, blocks, tracks, fps, speed_map, dimensions, block_num += 1 # go back to sorting by start time - blocks.sort(lambda a, b: cmp(a['start_time'], b['start_time'])) + blocks.sort(key=lambda a: a['start_time']) return blocks, last_block_end @@ -238,24 +238,24 @@ def draw_lyrics_cairo(self, block, tracks, dimensions, cr): do_render=1 introduction=True - def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_map,dimensions=(width,height), fps=fps, min_pitch=min_pitch, max_pitch=max_pitch, first_frame=first_frame,last_frame=last_frame, every_nth_frame=every_nth_frame, do_render=do_render,dynamicmode=False): + def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_map,dimensions=(width, height), fps=fps, min_pitch=min_pitch, max_pitch=max_pitch, first_frame=first_frame,last_frame=last_frame, every_nth_frame=every_nth_frame, do_render=do_render,dynamicmode=False): self.dynamicmode=dynamicmode """Render the animation!""" - print "Beginning render..." + print("Beginning render...") speed = speed_map[0]['speed'] if first_frame == None: first_frame = 0 if last_frame == None: last_frame = 10000000 # just a large number - print "Lexing midi..." + print("Lexing midi...") blocks = [] lexer = MidiLexer() midi_events = lexer.lex(input_midi_filename) - print "Blockifying midi..." + print("Blockifying midi...") blocks = self.blockify(midi_events) # convert into list of blocks - print str(len(blocks))+" blocks" + print(str(len(blocks))+" blocks") for track in tracks: if 'color' in track: @@ -277,7 +277,7 @@ def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_ma last_percent = -1 # sort by z-index descending - blocks.sort(lambda a, b: cmp(b['z-index'], a['z-index'])) + blocks.sort(key=lambda a: a['z-index'],reverse=True) # for naming image files: frame = 0 @@ -287,10 +287,10 @@ def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_ma time = -dimensions[0]/(2.0*fps*speed_map[0]['speed']) if not do_render: - print "Skipping render pass, Done!" + print("Skipping render pass, Done!") return - print "Rendering frames..." + print("Rendering frames...") # generate frames while there are blocks on the screen: while last_block_end > -speed: # code only for rendering blocks @@ -313,7 +313,7 @@ def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_ma on_screen_blocks = [block for block in blocks if block['start_x'] < dimensions[0]] - for layer in set([block['layer'] for block in on_screen_blocks]): + for layer in {block['layer'] for block in on_screen_blocks}: layer_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, *dimensions) layer_context = cairo.Context(layer_surface) @@ -321,7 +321,7 @@ def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_ma # do first drawing pass for block in in_layer_blocks: - block=self.makedynamic(block,dimensions) + block=self.makedynamic(block, dimensions) if block==None: continue self.draw_block_cairo(block, tracks, dimensions, layer_context) @@ -329,7 +329,7 @@ def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_ma # do second drawing pass on_screen_blocks.reverse() for block in in_layer_blocks: - block=self.makedynamic(block,dimensions) + block=self.makedynamic(block, dimensions) if block==None: continue self.draw_block_cairo(block, tracks, dimensions, layer_context, transparent=True) @@ -338,14 +338,14 @@ def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_ma cr.paint() # do lyrics pass, sort by start x so starts of words are on top - on_screen_blocks.sort(lambda a, b: cmp(a['start_x'], b['start_x'])) + on_screen_blocks.sort(key=lambda a: a['start_x']) for block in on_screen_blocks: if ('lyrics' in block): self.draw_lyrics_cairo(block, tracks, dimensions, cr) if self.introduction or self.first_highlight: - framefile += 1 - surface.write_to_png(filename) + framefile += 1 + surface.write_to_png(filename) # other code needed to advance animation frame += 1 @@ -365,15 +365,15 @@ def render(self, input_midi_filename, frame_save_dir, tracks, speed_map=speed_ma percent = min(int((original_end - last_block_end) * 100.0 / original_end), 100) if percent != last_percent: - print percent, "% done" + print(percent, "% done") last_percent = percent time += (1/fps) - print "Done!" + print("Done!") - def makedynamic(self,block,dimensions): + def makedynamic(self, block, dimensions): if self.dynamicmode: middle=dimensions[0] / 2 if block['start_x'] > middle: @@ -388,4 +388,4 @@ def makedynamic(self,block,dimensions): if __name__ == '__main__': print ("Sorry, I don't really do anything useful as an executable, see " - "RunAnim.py for usage") \ No newline at end of file + "RunAnim.py for usage") From cf08537d61ce1819d98886e01f00aa4f7ade8bc4 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Sat, 9 Mar 2024 00:00:00 +0000 Subject: [PATCH 15/16] Python 3 support for pymusanim.sh --- pymusanim.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pymusanim.sh b/pymusanim.sh index b67a281..737635a 100755 --- a/pymusanim.sh +++ b/pymusanim.sh @@ -1,14 +1,16 @@ #!/bin/bash # given the same parameters as PyMusAnimLauncher, runs the whole process of creating a video function checktime() { now=`date +%s`;} + function printtime(){ checktime timeelapsed=$(($now-$allStart)) echo "Total time: $(($timeelapsed/60)) minutes ($timeelapsed seconds)" } + function animate(){ - echo python MusAnimLauncher.py $* #TODO - python MusAnimLauncher.py $* #first try + echo python3 MusAnimLauncher.py $* #TODO + python3 MusAnimLauncher.py $* #first try mkdir $2 cp $1 $2 if ! [ -e "$2/frame00000.png" ]; then @@ -16,8 +18,8 @@ function animate(){ echo "Converting MIDI..." converted="$2/musescore.mid" mscore "$1" -o $converted #convert mid->mid 2> /dev/null - echo python MusAnimLauncher.py $converted $2 $3 #TODO - python MusAnimLauncher.py $converted $2 $3 #second try + echo python3 MusAnimLauncher.py $converted $2 $3 #TODO + python3 MusAnimLauncher.py $converted $2 $3 #second try if ! [ -e "$2/frame00000.png" ]; then exit 1; fi #second try failed fi } From 218a165ade4ab3ed891bb76f2d8b1b16a2b90f97 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Tue, 30 Jul 2024 00:00:00 +0000 Subject: [PATCH 16/16] Invert z-index... Better suited to my workflow. --- MusAnimLauncher.py | 243 ++++++++++++++++++++------------------------- 1 file changed, 110 insertions(+), 133 deletions(-) diff --git a/MusAnimLauncher.py b/MusAnimLauncher.py index 9460d1c..fcd2ddc 100755 --- a/MusAnimLauncher.py +++ b/MusAnimLauncher.py @@ -8,137 +8,115 @@ def main(): tracks = [ - { 'name': "track0", - 'color': (0.000, 0.996, 0.000), - 'width': TRACK_WIDTH, - 'z-index': 0 - }, - { 'name': "track1", - 'color': (0.996, 0.000, 0.000), - 'width': TRACK_WIDTH, - 'z-index': 1 - }, - { 'name': "track2", - 'color': (0.004, 0.996, 0.992), - 'width': TRACK_WIDTH, - 'z-index': 2 - }, - { 'name': "track3", - 'color': (0.996, 0.855, 0.398), - 'width': TRACK_WIDTH, - 'z-index': 3 - }, - { 'name': "track4", - 'color': (0.563, 0.980, 0.570), - 'width': TRACK_WIDTH, - 'z-index': 4 - }, - { 'name': "track5", - 'color': (0.000, 0.461, 0.996), - 'width': TRACK_WIDTH, - 'z-index': 5 - }, - { 'name': "track6", - 'color': (0.832, 0.996, 0.000), - 'width': TRACK_WIDTH, - 'z-index': 6 - }, - { 'name': "track7", - 'color': (0.996, 0.574, 0.492), - 'width': TRACK_WIDTH, - 'z-index': 7 - }, - { 'name': "track8", - 'color': (0.992, 0.535, 0.000), - 'width': TRACK_WIDTH, - 'z-index': 8 - }, - { 'name': "track9", - 'color': (0.520, 0.660, 0.000), - 'width': TRACK_WIDTH, - 'z-index': 9 - }, - { 'name': "track10", - 'color': (0.000, 0.680, 0.492), - 'width': TRACK_WIDTH, - 'z-index': 10 - }, - { 'name': "track11", - 'color': (0.738, 0.773, 0.996), - 'width': TRACK_WIDTH, - 'z-index': 11 - }, - { 'name': "track12", - 'color': (0.738, 0.824, 0.574), - 'width': TRACK_WIDTH, - 'z-index': 12 - }, - { 'name': "track13", - 'color': (0.000, 0.723, 0.090), - 'width': TRACK_WIDTH, - 'z-index': 13 - }, - { 'name': "track14", - 'color': (0.004, 0.813, 0.996), - 'width': TRACK_WIDTH, - 'z-index': 14 - }, - { 'name': "track15", - 'color': (0.566, 0.813, 0.793), - 'width': TRACK_WIDTH, - 'z-index': 15 - }, - { 'name': "track16", - 'color': (0.730, 0.531, 0.000), - 'width': TRACK_WIDTH, - 'z-index': 16 - }, - { 'name': "track17", - 'color': (0.867, 0.996, 0.453), - 'width': TRACK_WIDTH, - 'z-index': 17 - }, - { 'name': "track18", - 'color': (0.000, 0.996, 0.773), - 'width': TRACK_WIDTH, - 'z-index': 18 - }, - { 'name': "track19", - 'color': (0.996, 0.895, 0.008), - 'width': TRACK_WIDTH, - 'z-index': 19 - }, - { 'name': "track20", - 'color': (0.594, 0.996, 0.320), - 'width': TRACK_WIDTH, - 'z-index': 20 - }, - { 'name': "track21", - 'color': (0.000, 0.996, 0.469), - 'width': TRACK_WIDTH, - 'z-index': 21 - }, - { 'name': "track22", - 'color': (0.996, 0.430, 0.254), - 'width': TRACK_WIDTH, - 'z-index': 22 - }, - { 'name': "track23", - 'color': (0.645, 0.996, 0.820), - 'width': TRACK_WIDTH, - 'z-index': 23 - }, - { 'name': "track24", - 'color': (0.996, 0.691, 0.402), - 'width': TRACK_WIDTH, - 'z-index': 24 - }, - { 'name': "track25", - 'color': (0.000, 0.605, 0.996), - 'width': TRACK_WIDTH, - 'z-index': 25 - }, + { 'name': "track0", + 'color': (0.000, 0.996, 0.000), + 'width': TRACK_WIDTH, + }, + { 'name': "track1", + 'color': (0.996, 0.000, 0.000), + 'width': TRACK_WIDTH, + }, + { 'name': "track2", + 'color': (0.004, 0.996, 0.992), + 'width': TRACK_WIDTH, + }, + { 'name': "track3", + 'color': (0.996, 0.855, 0.398), + 'width': TRACK_WIDTH, + }, + { 'name': "track4", + 'color': (0.563, 0.980, 0.570), + 'width': TRACK_WIDTH, + }, + { 'name': "track5", + 'color': (0.000, 0.461, 0.996), + 'width': TRACK_WIDTH, + }, + { 'name': "track6", + 'color': (0.832, 0.996, 0.000), + 'width': TRACK_WIDTH, + }, + { 'name': "track7", + 'color': (0.996, 0.574, 0.492), + 'width': TRACK_WIDTH, + }, + { 'name': "track8", + 'color': (0.992, 0.535, 0.000), + 'width': TRACK_WIDTH, + }, + { 'name': "track9", + 'color': (0.520, 0.660, 0.000), + 'width': TRACK_WIDTH, + }, + { 'name': "track10", + 'color': (0.000, 0.680, 0.492), + 'width': TRACK_WIDTH, + }, + { 'name': "track11", + 'color': (0.738, 0.773, 0.996), + 'width': TRACK_WIDTH, + }, + { 'name': "track12", + 'color': (0.738, 0.824, 0.574), + 'width': TRACK_WIDTH, + }, + { 'name': "track13", + 'color': (0.000, 0.723, 0.090), + 'width': TRACK_WIDTH, + }, + { 'name': "track14", + 'color': (0.004, 0.813, 0.996), + 'width': TRACK_WIDTH, + }, + { 'name': "track15", + 'color': (0.566, 0.813, 0.793), + 'width': TRACK_WIDTH, + }, + { 'name': "track16", + 'color': (0.730, 0.531, 0.000), + 'width': TRACK_WIDTH, + }, + { 'name': "track17", + 'color': (0.867, 0.996, 0.453), + 'width': TRACK_WIDTH, + }, + { 'name': "track18", + 'color': (0.000, 0.996, 0.773), + 'width': TRACK_WIDTH, + }, + { 'name': "track19", + 'color': (0.996, 0.895, 0.008), + 'width': TRACK_WIDTH, + }, + { 'name': "track20", + 'color': (0.594, 0.996, 0.320), + 'width': TRACK_WIDTH, + }, + { 'name': "track21", + 'color': (0.000, 0.996, 0.469), + 'width': TRACK_WIDTH, + }, + { 'name': "track22", + 'color': (0.996, 0.430, 0.254), + 'width': TRACK_WIDTH, + }, + { 'name': "track23", + 'color': (0.645, 0.996, 0.820), + 'width': TRACK_WIDTH, + }, + { 'name': "track24", + 'color': (0.996, 0.691, 0.402), + 'width': TRACK_WIDTH, + }, + { 'name': "track25", + 'color': (0.000, 0.605, 0.996), + 'width': TRACK_WIDTH, + }, ] + + ntracks=len(tracks) + for i in range(ntracks): + tracks[i]['z-index']=ntracks-i if len(sys.argv)<3: print("Usage: python MusAnimLauncher.py input.mid outputDirectory [--dynamic]") @@ -158,12 +136,11 @@ def main(): speed_map = [{'time': 0.0, 'speed': 4}] + dimensions = 1920, 1080 + #dimensions = 720, 480 #dimensions = 426, 240 - dimensions = 720, 480 - #dimensions = 1920, 1080 fps = 25 - #fps = 29.97 renderer=MusAnimRenderer() renderer.introduction=False