def test_percussive_samples(): mod = load_file(TEST_PATH / 'androidr.mod') assert percussive_samples(mod) == {2, 3, 4} mod = load_file(TEST_PATH / 'big_blunts.mod') assert percussive_samples(mod) == {17, 21} mod = load_file(TEST_PATH / 'boner.mod') assert percussive_samples(mod) == {1, 2, 3, 6} mod = load_file(TEST_PATH / 'lambada.mod') assert percussive_samples(mod) == {2, 4, 5, 6} mod = load_file(TEST_PATH / 'mist-eek.mod') assert percussive_samples(mod) == {10, 11, 12} # This is a difficult one mod = load_file(TEST_PATH / 'zodiak_-_gasp.mod') assert percussive_samples(mod) == {3, 4} # Percussive instruments are repeating... mod = load_file(TEST_PATH / 'alfrdchi_endofgame1.mod') assert percussive_samples(mod) == {1, 2, 3, 4, 5, 6, 8} # 1 and 2 are chord samples incorrectly classified as drums. mod = load_file(TEST_PATH / 'afro_afro.mod') assert percussive_samples(mod) == {1, 2, 5, 6, 7, 8}
def test_subsongs(): mod = load_file(TEST_PATH / 'beast2-ingame-st.mod') subsongs = list(linearize_subsongs(mod, 1)) orders = [o for (o, _) in subsongs] assert len(orders) == 6 assert orders[0] == [0, 1, 2, 3, 1, 2, 3] assert orders[2] == [14] mod = load_file(TEST_PATH / 'satanic.mod') subsongs = list(linearize_subsongs(mod, 1)) orders = [o for (o, _) in subsongs] assert len(orders) == 114 mod = load_file(TEST_PATH / 'entity.mod') subsongs = list(linearize_subsongs(mod, 1)) assert len(subsongs) == 2
def main(): args = docopt(__doc__, version='MIDI file generator 1.0') SP.enabled = args['--verbose'] # Parse mod_file = args['<mod>'] programs = parse_programs(args['--programs']) mod_file = Path(mod_file) midi_mapping = args['--midi-mapping'] if midi_mapping != 'auto': with open(midi_mapping, 'r') as f: midi_mapping = load(f) midi_mapping = {int(k): v for (k, v) in midi_mapping.items()} mod = load_file(mod_file) subsongs = linearize_subsongs(mod, 1) volumes = [header.volume for header in mod.sample_headers] for idx, (_, rows) in enumerate(subsongs): notes = rows_to_mod_notes(rows, volumes) if midi_mapping == 'auto': props = sample_props(mod, notes) samples = [(sample_idx, props.is_percussive, props.note_duration) for (sample_idx, props) in props.items()] midi_mapping = assign_instruments(samples, programs) midi_file = 'test-%02d.mid' % idx notes_to_midi_file(notes, midi_file, midi_mapping)
def extract_mod_file_samples(mod_file): mod = load_file(mod_file) samples = load_samples(mod) name_prefix = mod_file.stem for idx, sample in enumerate(samples): fname = '%s-%02d.wav' % (name_prefix, idx + 1) write_sample(sample, fname)
def mod_file_to_patterns(mod_file): SP.print(str(mod_file)) try: mod = load_file(mod_file) except PowerPackerModule: return [] rows = linearize_rows(mod) volumes = [header.volume for header in mod.sample_headers] notes = rows_to_mod_notes(rows, volumes) percussive = {s for (s, p) in sample_props(mod, notes) if p.is_percussive} return [pattern_to_matrix(pat, percussive) for pat in mod.patterns]
def main(): parser = ArgumentParser(description='Module stripper') parser.add_argument('input', type=FileType('rb')) parser.add_argument('output', type=FileType('wb')) parser.add_argument('--samples', help='Samples to keep (default: all)') parser.add_argument('--pattern-table', help='Pattern table (default: existing)') parser.add_argument('--channels', help='Channels to keep (default: all)') parser.add_argument('--info', help='Print module information', action='store_true') args = parser.parse_args() args.input.close() args.output.close() mod = load_file(args.input.name) sp = StructuredPrinter(args.info) # Parse sample indices sample_indices = list(range(1, 32)) if args.samples: sample_indices = parse_comma_list(args.samples) # Parse channel indices col_indices = list(range(4)) if args.channels: col_indices = [c - 1 for c in parse_comma_list(args.channels)] # Print pattern table pattern_table = [mod.pattern_table[i] for i in range(mod.n_orders)] s = ' '.join(map(str, pattern_table)) sp.print('Input pattern table: %s', s) if args.pattern_table: # Parse pattern indices pattern_indices = parse_comma_list(args.pattern_table) # Install new pattern table update_pattern_table(mod, pattern_indices) # Strip effects for pattern in mod.patterns: for i in range(4): strip_column(pattern.rows, i, sample_indices, col_indices) sp.header('Output patterns') for idx, pattern in enumerate(mod.patterns): sp.header('Pattern', '%2d', idx) for row in pattern.rows: sp.print(row_to_string(row)) sp.leave() sp.leave() save_file(args.output.name, mod)
def mod_file_to_piano_roll(file_path): SP.header('PARSING %s' % str(file_path)) try: mod = load_file(file_path) except PowerPackerModule: SP.print('PowerPacker module.') return None rows = linearize_rows(mod) volumes = [header.volume for header in mod.sample_headers] notes = rows_to_mod_notes(rows, volumes) props = sample_props(mod, notes) mat = notes_to_matrix(notes, props, len(rows)) SP.leave() return mat
def mod_file_to_codes_w_progress(i, n, file_path, code_type): SP.header('[ %4d / %4d ] PARSING %s' % (i, n, file_path)) try: mod = load_file(file_path) except UnsupportedModule as e: SP.print('Unsupported module format.') SP.leave() err_arg = e.args[0] if e.args else e.__class__.__name__ yield False, 0, (ERR_PARSE_ERROR, err_arg) return code_mod = CODE_MODULES[code_type] subsongs = list(linearize_subsongs(mod, 1)) volumes = [header.volume for header in mod.sample_headers] parsed_subsongs = [] for idx, (order, rows) in enumerate(subsongs): SP.header('SUBSONG %d' % idx) notes = rows_to_mod_notes(rows, volumes) percussion = guess_percussive_instruments(mod, notes) if notes: fmt = '%d rows, %d ms/row, percussion %s, %d notes' args = (len(rows), notes[0].time_ms, set(percussion), len(notes)) SP.print(fmt % args) err = training_error(notes, percussion) if err: yield False, idx, err else: pitches = {n.pitch_idx for n in notes if n.sample_idx not in percussion} min_pitch = min(pitches, default = 0) # Subtract min pitch for n in notes: n.pitch_idx -= min_pitch code = list(code_mod.to_code(notes, percussion)) if code_mod.is_transposable(): codes = code_mod.code_transpositions(code) else: codes = [code] fmt = '%d transpositions of length %d' SP.print(fmt % (len(codes), len(code))) yield True, idx, codes SP.leave() SP.leave()
def main(): parser = ArgumentParser(description='Sample synthesizer and player') parser.add_argument('module', type=FileType('rb')) parser.add_argument('--samples', required=True, help='Indices of samples to play') parser.add_argument('--period', required=True, help='Sample period') parser.add_argument('--volume', type=int) args = parser.parse_args() args.module.close() mod = load_file(args.module.name) samples = load_samples(mod) init_player(SAMPLE_RATE) sample_indices = [int(s) - 1 for s in args.samples.split(',')] note_idx = notestr_to_idx(args.period) freq = FREQS[note_idx] for sample_idx in sample_indices: header = mod.sample_headers[sample_idx] name = header.name volume = args.volume if not args.volume: volume = header.volume fine_tune = header.fine_tune sample = samples[sample_idx] length = header.size * 2 repeat_from = header.repeat_from repeat_len = 0 if header.repeat_len < 2 else header.repeat_len print(f'*** Sample "{name}" (#{sample_idx + 1}) ***') print(f'Volume : {volume}') print(f'Fine tune : {fine_tune}') print(f'Length : {length}') print(f'Repeat from: {repeat_from}') print(f'Repeat len : {repeat_len}') if volume == 0: volume = 64 play_sample_at_freq(sample, freq, volume) sleep(0.5)
def main(): args = docopt(__doc__, version='MIDI file generator 1.0') SP.enabled = args['--verbose'] file_path = args['<mod>'] mod = load_file(file_path) rows = list(linearize_subsongs(mod, 1))[0][1] n_rows = len(rows) sample_headers = mod.sample_headers volumes = [header.volume for header in mod.sample_headers] notes = rows_to_mod_notes(rows, volumes) props = sample_props(mod, notes) mel_notes = {n for n in notes if not props[n.sample_idx].is_percussive} perc_notes = {n for n in notes if props[n.sample_idx].is_percussive} pitches = {n.pitch_idx for n in mel_notes} n_unique_mel_notes = len(pitches) pitch_range = max(pitches) - min(pitches) header = [ '#', 'MC freq', 'Notes', 'Uniq', 'PCs', 'Longest rep', 'Size', 'Dur', 'Repeat pct', 'Max ringout', 'Perc?' ] row_fmt = [ '%2d', '%.2f', '%3d', '%2d', '%2d', '%3d', '%5d', '%2d', '%.2f', '%.2f', lambda x: 'T' if x else 'F' ] # Make a table rows = [(sample, ) + p for (sample, p) in props.items()] print_term_table(row_fmt, rows, header, 'rrrrrrrrrrc') n_chords, n_diss_chords = dissonant_chords(mel_notes) diss_frac = n_diss_chords / n_chords if n_chords else 0.0 header = ['Item', 'Value'] rows = [['Rows', n_rows], ['Melodic notes', len(mel_notes)], ['Percussive notes', len(perc_notes)], ['Unique melodic notes', n_unique_mel_notes], ['Pitch range', pitch_range], ['Chords', n_chords], ['Chord dissonance', '%.2f' % diss_frac]] print_term_table(['%s', '%s'], rows, ['Key', 'Value'], 'lr')
def convert_to_midi(code_type, mod_file): code_mod = CODE_MODULES[code_type] mod = load_file(mod_file) subsongs = linearize_subsongs(mod, 1) volumes = [header.volume for header in mod.sample_headers] for idx, (_, rows) in enumerate(subsongs): notes = rows_to_mod_notes(rows, volumes) percussion = guess_percussive_instruments(mod, notes) pitches = {n.pitch_idx for n in notes if n.sample_idx not in percussion} min_pitch = min(pitches, default = 0) for n in notes: n.pitch_idx -= min_pitch code = list(code_mod.to_code(notes, percussion)) fmt = '%d notes, %d rows, %d tokens, %d ms/row, percussion %s' args = (len(notes), len(rows), len(code), notes[0].time_ms if notes else - 1, set(percussion)) SP.print(fmt % args) row_time = code_mod.estimate_row_time(code) notes = code_mod.to_notes(code, row_time) fname = Path('test-%02d.mid' % idx) notes_to_audio_file(notes, fname, CODE_MIDI_MAPPING, False)
def test_rows_to_string(): mod = load_file(TEST_PATH / 'entity.mod') str = rows_to_string(mod.patterns[0].rows) assert len(str) == 64 * (10 * 4 + 7) - 1 assert mod.patterns[0].rows[0][0].period == 509
def test_weird_cells(): mod = load_file(TEST_PATH / 'drive_faster.mod') volumes = [header.volume for header in mod.sample_headers] notes = column_to_mod_notes(mod.patterns[0].rows, 1, volumes) assert len(notes) == 32
def test_pattern_jump(): mod = load_file(TEST_PATH / 'wax-rmx.mod') subsongs = list(linearize_subsongs(mod, 1)) assert len(subsongs) == 1 assert len(subsongs[0][1]) == 2640
def test_load_samples(): mod = load_file(TEST_PATH / 'entity.mod') samples = load_samples(mod) assert samples[13].repeat_len == 0
def test_broken_mod(): mod = load_file(TEST_PATH / 'operation_wolf-wolf31.mod') volumes = [header.volume for header in mod.sample_headers] notes = column_to_mod_notes(mod.patterns[1].rows, 3, volumes)
vol_idx = sample_idx - 1 if not 0 <= vol_idx < len(volumes): fmt = 'Sample %d out of bounds at cell %4d:%d. MOD bug?' SP.print(fmt % (sample_idx, row_idx, col_idx)) continue vol = mod_note_volume(volumes[vol_idx], cell) pitch_idx = period_to_idx(period) assert 0 <= pitch_idx < 60 note = ModNote(row_idx, col_idx, sample_idx, pitch_idx, vol, time_ms) notes.append(note) # Add durations for n1, n2 in zip(notes, notes[1:]): n1.duration = n2.row_idx - n1.row_idx if notes: notes[-1].duration = len(rows) - notes[-1].row_idx return notes def rows_to_mod_notes(rows, volumes): return flatten([column_to_mod_notes(rows, i, volumes) for i in range(4)]) if __name__ == '__main__': from sys import argv from musicgen.parser import load_file mod = load_file(argv[1]) for play_order, rows in linearize_subsongs(mod, 1): print(play_order)
def test_load_stk_module(): mod = load_file(TEST_PATH / '3ddance.mod') assert mod.n_orders == 28
def test_sample_length(): mod = load_file(TEST_PATH / 'his_hirsute_ant.mod') assert len(mod.samples[0].bytes) == 0x23ca
def test_loading_truncated_module(): mod = load_file(TEST_PATH / 'after-the-rain.mod') assert len(mod.samples[8].bytes) == 7990
def test_weird_magic(): # This mod has the signature "M&K!" mod = load_file(TEST_PATH / 'im_a_hedgehog.mod') assert mod.n_orders == 13
def test_protracker_15_sample_module(): mod = load_file(TEST_PATH / 'am-fm_-_0ldsk00l_w1z4rd.mod') for i in range(15, 31): assert len(mod.samples[i].bytes) == 0
def test_loading_tricky_mods(): mod = load_file(TEST_PATH / 'pachabel.mod') assert len(mod.samples[2].bytes) == 0x1ee1c
def main(): args = docopt(__doc__, version='MOD Melody Extractor 1.0') # Argument parsing SP.enabled = args['--verbose'] input_file = args['<input-mod>'] output_file = args['<output-mod>'] max_distance = int(args['--max-distance']) min_length = int(args['--min-length']) min_unique = int(args['--min-unique']) max_repeat = int(args['--max-repeat']) mu_factor = float(args['--mu-factor']) mu_threshold = int(args['--mu-threshold']) trailer = int(args['--trailer']) transpose = not args['--no-transpose'] # Load mod mod = load_file(input_file) rows = linearize_rows(mod) # Extract and filter melodies melodies = flatten( extract_sample_groups(rows, col_idx, max_distance, mu_factor, mu_threshold) for col_idx in range(4)) melodies = [ melody for (melody, msg) in melodies if is_melody(melody, min_length, min_unique, max_repeat) ] if transpose: melodies = [move_to_c(melody) for melody in melodies] melodies = [ remove_ending_silence(melody) for melody in filter_duplicate_melodies(melodies) ] SP.header('%d MELODIES' % len(melodies)) for melody in melodies: for cell in melody: SP.print(cell_to_string(cell)) SP.print('') SP.leave() melodies = [add_trailer(melody, trailer) for melody in melodies] if not melodies: fmt = 'Sorry, found no melodies in "%s"!' print(fmt % args.input_module.name) exit(1) cells = flatten(melodies) rows = [[c, ZERO_CELL, ZERO_CELL, ZERO_CELL] for c in cells] patterns = list(rows_to_patterns(rows)) n_patterns = len(patterns) pattern_table = list(range(n_patterns)) + [0] * (128 - n_patterns) mod_out = dict(title=mod.title, sample_headers=mod.sample_headers, n_orders=n_patterns, restart_pos=0, pattern_table=bytearray(pattern_table), initials='M.K.'.encode('utf-8'), patterns=patterns, samples=mod.samples) save_file(output_file, mod_out)