diff --git a/README.md b/README.md index 2dfe3e2..c2acee4 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ SampleScanner is a command-line tool to turn MIDI instruments (usually hardware) ## Installation -Requires a working `python` (version 2.7), `pip`, and `ffmpeg` to be installed on the system. +Requires a working `python` (version 3.10), `pip`, and `ffmpeg` to be installed on the system. ``` git clone git@github.com:psobot/SampleScanner @@ -29,7 +29,7 @@ pip install -r requirements.txt ## How to run -Run `./samplescanner -h` for a full argument listing: +Run `./samplescanner.py -h` for a full argument listing: ```contentsof usage: samplescanner [-h] [--cc-before [CC_BEFORE [CC_BEFORE ...]]] diff --git a/lib/audio_helpers.py b/lib/audio_helpers.py index a7f8247..f7e81be 100644 --- a/lib/audio_helpers.py +++ b/lib/audio_helpers.py @@ -1,12 +1,12 @@ import time import numpy -from utils import note_name, percent_to_db -from record import record -from constants import CLIPPING_THRESHOLD, \ +from .utils import note_name, percent_to_db +from .record import record +from .consts import CLIPPING_THRESHOLD, \ CLIPPING_CHECK_NOTE, \ EXIT_ON_CLIPPING, \ SAMPLE_RATE -from midi_helpers import all_notes_off, CHANNEL_OFFSET +from .midi_helpers import all_notes_off, CHANNEL_OFFSET def generate_sample( @@ -46,7 +46,7 @@ def on_time_up(): def sample_threshold_from_noise_floor(bit_depth, audio_interface_name): time.sleep(1) - print "Sampling noise floor..." + print("Sampling noise floor...") sample_width, data, release_time = record( limit=2.0, after_start=None, @@ -60,9 +60,9 @@ def sample_threshold_from_noise_floor(bit_depth, audio_interface_name): numpy.amax(numpy.absolute(data)) / float(2 ** (bit_depth - 1)) ) - print "Noise floor has volume %8.8f dBFS" % percent_to_db(noise_floor) + print("Noise floor has volume %8.8f dBFS" % percent_to_db(noise_floor)) threshold = noise_floor * 1.1 - print "Setting threshold to %8.8f dBFS" % percent_to_db(threshold) + print("Setting threshold to %8.8f dBFS" % percent_to_db(threshold)) return threshold @@ -74,9 +74,9 @@ def check_for_clipping( audio_interface_name, ): time.sleep(1) - print "Checking for clipping and balance on note %s..." % ( + print("Checking for clipping and balance on note %s..." % ( note_name(CLIPPING_CHECK_NOTE) - ) + )) sample_width, data, release_time = generate_sample( limit=2.0, @@ -99,14 +99,14 @@ def check_for_clipping( ) # All notes off, but like, a lot, again - for _ in xrange(0, 2): + for _ in range(0, 2): all_notes_off(midiout, midi_channel) - print "Maximum volume is around %8.8f dBFS" % percent_to_db(max_volume) + print("Maximum volume is around %8.8f dBFS" % percent_to_db(max_volume)) if max_volume >= CLIPPING_THRESHOLD: - print "Clipping detected (%2.2f dBFS >= %2.2f dBFS) at max volume!" % ( + print("Clipping detected (%2.2f dBFS >= %2.2f dBFS) at max volume!" % ( percent_to_db(max_volume), percent_to_db(CLIPPING_THRESHOLD) - ) + )) if EXIT_ON_CLIPPING: raise ValueError("Clipping detected at max volume!") diff --git a/lib/constants.py b/lib/consts.py similarity index 100% rename from lib/constants.py rename to lib/consts.py diff --git a/lib/deflac.py b/lib/deflac.py index 3de6b4b..1e84ea3 100644 --- a/lib/deflac.py +++ b/lib/deflac.py @@ -9,7 +9,7 @@ from wavio import read_wave_file from utils import normalized from record import RATE, save_to_file -from constants import bit_depth +from consts import bit_depth def full_path(sfzfile, filename): diff --git a/lib/flacize.py b/lib/flacize.py index 967d209..ab5e8c1 100644 --- a/lib/flacize.py +++ b/lib/flacize.py @@ -5,9 +5,10 @@ import argparse import subprocess from tqdm import tqdm -from sfzparser import SFZFile, Group -from wavio import read_wave_file -from utils import group_by_attr, note_name + +from .sfzparser import SFZFile, Group +from .wavio import read_wave_file +from .utils import group_by_attr, note_name def full_path(sfzfile, filename): @@ -66,7 +67,7 @@ def flacize_after_sampling( for key, key_regions in group_by_attr(group.regions, [ 'key', 'pitch_keycenter' - ]).iteritems()], []) + ]).items()], []) new_groups.append(Group(group.attributes, output)) with open(sfzfile + '.flac.sfz', 'w') as file: @@ -77,7 +78,7 @@ def flacize_after_sampling( try: os.unlink(path) except OSError as e: - print "Could not unlink path: %s: %s" % (path, e) + print("Could not unlink path: %s: %s" % (path, e)) ANTI_CLICK_OFFSET = 3 @@ -129,7 +130,7 @@ def concat_samples(regions, path, name=None): note_name(key))) for key, regions in tqdm(group_by_attr(group.regions, - 'key').iteritems())], []) - print group.just_group() + 'key').items())], []) + print(group.just_group()) for region in output: - print region + print(region) diff --git a/lib/group_velcurves.py b/lib/group_velcurves.py index cff6a95..88c516a 100644 --- a/lib/group_velcurves.py +++ b/lib/group_velcurves.py @@ -18,12 +18,12 @@ def should_group_key(key): def group_by_pitch(regions): - for key, regions in group_by_attr(regions, 'key').iteritems(): + for key, regions in group_by_attr(regions, 'key').items(): # Group together all amp_velcurve_* and key params. yield Group(dict([ (key, value) for region in regions - for key, value in region.attributes.iteritems() + for key, value in region.attributes.items() if should_group_key(key) ] + DEFAULT_ATTRIBUTES.items()), [ region.without_attributes(should_group_key) for region in regions diff --git a/lib/loop.py b/lib/loop.py index 155d65b..023798b 100644 --- a/lib/loop.py +++ b/lib/loop.py @@ -1,8 +1,9 @@ import sys import numpy from tqdm import tqdm -from truncate import read_wave_file -from audio_helpers import fundamental_frequency + +from .truncate import read_wave_file +from .audio_helpers import fundamental_frequency QUANTIZE_FACTOR = 8 @@ -12,20 +13,20 @@ def compare_windows(window_a, window_b): def slide_window(file, period, start_at=0, end_before=0): - for power in reversed(xrange(7, 10)): + for power in reversed(range(7, 10)): multiple = 2 ** power window_size = int(period * multiple) # Uncomment this to search from the start_at value to the end_before # rather than just through one window's length # end_range = len(file) - (window_size * 2) - end_before end_range = start_at + window_size - for i in xrange(start_at, end_range): + for i in range(start_at, end_range): yield power, i, window_size def window_match(file): period = (1.0 / fundamental_frequency(file, 1)) * 2 - print period, 'period in samples' + print(period, 'period in samples') winner = None @@ -48,14 +49,14 @@ def window_match(file): i, abs(file[i] - file[window_start]) ) - print 'new winner', winner + print('new winner', winner) lowest_difference, winning_window_size, winning_index, gap = winner - print "Best loop match:", lowest_difference - print "window size", winning_window_size - print "winning index", winning_index - print "winning gap", gap + print("Best loop match:", lowest_difference) + print("window size", winning_window_size) + print("winning index", winning_index) + print("winning gap", gap) return winning_index, winning_window_size @@ -71,7 +72,7 @@ def find_similar_sample_index( ): reference_slope = slope_at_index(file, reference_index) > 0 best_match = None - search_range = xrange( + search_range = range( search_around_index - search_size, search_around_index + search_size ) @@ -93,12 +94,12 @@ def find_similar_sample_index( def zero_crossing_match(file): period = (1.0 / fundamental_frequency(file, 1)) * 2 - print period, 'period in samples' + print(period, 'period in samples') period_multiple = 64 period = period * period_multiple - for i in reversed(xrange(2 * len(file) / 3, 5 * len(file) / 6)): + for i in reversed(range(2 * len(file) / 3, 5 * len(file) / 6)): if file[i] >= 0 and file[i + 1] < 0 and \ file[int(i + period)] >= 0 and \ file[int(i + 1 + period)] < 0 and \ @@ -131,7 +132,13 @@ def fast_autocorrelate(x): f = numpy.fft.fft(xp) p = numpy.absolute(numpy.power(f, 2)) pi = numpy.fft.ifft(p) - result = numpy.real(pi)[:x.size / 2] / numpy.sum(numpy.power(xp, 2)) + + index = int(x.size / 2) + top = numpy.real(pi)[:index] + bottom = numpy.sum(numpy.power(xp, 2)) + result = top / bottom + + return result @@ -164,7 +171,7 @@ def find_loop_from_autocorrelation( min_loop_width_in_seconds=0.2, sample_rate=48000 ): - search_start /= 2 + search_start = int(search_start/2) max_autocorrelation_peak_width = int( min_loop_width_in_seconds * sample_rate ) @@ -234,7 +241,7 @@ def process(aif, sample_rate=48000): file = file[0] - print 'start, end', loop_start, loop_end + print('start, end', loop_start, loop_end) plt.plot(file[loop_start:loop_end]) plt.plot(file[loop_end:loop_start + (2 * loop_size)]) diff --git a/lib/midi_helpers.py b/lib/midi_helpers.py index 23fe634..e2217ce 100644 --- a/lib/midi_helpers.py +++ b/lib/midi_helpers.py @@ -1,7 +1,6 @@ import time import rtmidi - CHANNEL_OFFSET = 0x90 - 1 CC_CHANNEL_OFFSET = 0xB0 - 1 @@ -37,6 +36,7 @@ def all_notes_off(midiout, midi_channel): def open_midi_port(midi_port_name): midiout = rtmidi.MidiOut() ports = midiout.get_ports() + for i, port_name in enumerate(ports): if not midi_port_name or midi_port_name.lower() in port_name.lower(): midiout.open_port(i) @@ -49,7 +49,7 @@ def open_midi_port(midi_port_name): def set_program_number(midiout, midi_channel, program_number): if program_number is not None: - print "Sending program change to program %d..." % program_number + print("Sending program change to program %d..." % program_number) # Bank change (fine) to (program_number / 128) midiout.send_message([ CC_CHANNEL_OFFSET + midi_channel, @@ -64,7 +64,7 @@ def set_program_number(midiout, midi_channel, program_number): ]) # All notes off, but like, a lot - for _ in xrange(0, 2): + for _ in range(0, 2): all_notes_off(midiout, midi_channel) time.sleep(0.5) diff --git a/lib/quantize.py b/lib/quantize.py index 28f363d..0d29829 100644 --- a/lib/quantize.py +++ b/lib/quantize.py @@ -25,7 +25,7 @@ def quantize_pitch(regions, pitch_levels=25): # a dict of sample_pitch -> [lokey, hikey, pitch_keycenter] pitchmapping = {} - for key in xrange( + for key in range( lowestkey + (pitch_skip / 2), highestkey + 1 + (pitch_skip / 2), pitch_skip): @@ -35,7 +35,7 @@ def quantize_pitch(regions, pitch_levels=25): 'hikey': key + (pitch_skip / 2) - (0 if evenly_divided else 1), } - for key, regions in group_by_attr(regions, 'key').iteritems(): + for key, regions in group_by_attr(regions, 'key').items(): if int(key) in pitchmapping: for region in regions: region.attributes.update(pitchmapping[int(key)]) @@ -55,7 +55,7 @@ def quantize_velocity(regions, velocity_levels=5): # a dict of sample_pitch -> [lokey, hikey, pitch_keycenter] pitchmapping = {} - for key in xrange( + for key in range( lowestkey + (pitch_skip / 2), highestkey + 1 + (pitch_skip / 2), pitch_skip): @@ -65,7 +65,7 @@ def quantize_velocity(regions, velocity_levels=5): 'hikey': key + (pitch_skip / 2) - (0 if evenly_divided else 1), } - for key, regions in group_by_attr(regions, 'key').iteritems(): + for key, regions in group_by_attr(regions, 'key').items(): if int(key) in pitchmapping: for region in regions: region.attributes.update(pitchmapping[int(key)]) diff --git a/lib/record.py b/lib/record.py index 298876e..c7b64d9 100644 --- a/lib/record.py +++ b/lib/record.py @@ -1,8 +1,9 @@ import sys import numpy from struct import pack -from constants import bit_depth, NUMPY_DTYPE, SAMPLE_RATE -from utils import percent_to_db, dbfs_as_percent + +from .consts import bit_depth, NUMPY_DTYPE, SAMPLE_RATE +from .utils import percent_to_db, dbfs_as_percent import pyaudio import wave @@ -41,7 +42,7 @@ def get_input_device_index(py_audio, audio_interface_name=None): input_interface_names = get_input_device_names(py_audio, info) if audio_interface_name: - for index, name in input_interface_names.iteritems(): + for index, name in input_interface_names.items(): if audio_interface_name.lower() in name.lower(): return index else: @@ -55,8 +56,7 @@ def get_input_device_name_by_index(audio_interface_index): py_audio = pyaudio.PyAudio() info = py_audio.get_host_api_info_by_index(0) input_interface_names = get_input_device_names(py_audio, info) - - for index, name in input_interface_names.iteritems(): + for index, name in input_interface_names.items(): if index == audio_interface_index: return name else: @@ -68,7 +68,7 @@ def get_input_device_name_by_index(audio_interface_index): def list_input_devices(device_names): lines = [] - for index, name in sorted(device_names.iteritems()): + for index, name in sorted(device_names.items()): lines.append(u"{:3d}. {}".format(index, name)) return u"\n".join(lines).encode("ascii", "ignore") @@ -256,7 +256,7 @@ def save_to_file(path, sample_width, data, sample_rate=SAMPLE_RATE): flattened = numpy.asarray(data.flatten('F'), dtype=NUMPY_DTYPE) write_chunk_size = 512 - for chunk_start in xrange(0, len(flattened), write_chunk_size): + for chunk_start in range(0, len(flattened), write_chunk_size): chunk = flattened[chunk_start:chunk_start + write_chunk_size] packstring = '<' + ('h' * len(chunk)) wf.writeframes(pack(packstring, *chunk)) @@ -264,5 +264,5 @@ def save_to_file(path, sample_width, data, sample_rate=SAMPLE_RATE): if __name__ == '__main__': - print record_to_file('./demo.wav', sys.argv[1] if sys.argv[1] else None) + print(record_to_file('./demo.wav', sys.argv[1] if sys.argv[1] else None)) print("done - result written to demo.wav") diff --git a/lib/send_notes.py b/lib/send_notes.py index dded31f..5378181 100644 --- a/lib/send_notes.py +++ b/lib/send_notes.py @@ -1,23 +1,24 @@ import os import time from tqdm import tqdm -from record import save_to_file, get_input_device_name_by_index -from sfzparser import SFZFile, Region -from pitch import compute_zones, Zone -from utils import trim_data, \ + +from .record import save_to_file, get_input_device_name_by_index +from .sfzparser import SFZFile, Region +from .pitch import compute_zones, Zone +from .utils import trim_data, \ note_name, \ first_non_none, \ warn_on_clipping -from constants import bit_depth, SAMPLE_RATE -from volume_leveler import level_volume -from flacize import flacize_after_sampling -from loop import find_loop_points -from midi_helpers import Midi, all_notes_off, \ +from .consts import bit_depth, SAMPLE_RATE +from .volume_leveler import level_volume +from .flacize import flacize_after_sampling +from .loop import find_loop_points +from .midi_helpers import Midi, all_notes_off, \ open_midi_port, \ open_midi_port_by_index, \ set_program_number, \ CHANNEL_OFFSET -from audio_helpers import sample_threshold_from_noise_floor, \ +from .audio_helpers import sample_threshold_from_noise_floor, \ generate_sample, \ check_for_clipping @@ -244,7 +245,7 @@ def sample_program( ) time.sleep(PORTAMENTO_PRESAMPLE_WAIT) - for attempt in xrange(0, MAX_ATTEMPTS): + for attempt in range(0, MAX_ATTEMPTS): try: region = generate_and_save_sample( limit=limit, diff --git a/lib/sfzparser.py b/lib/sfzparser.py index 4e44b4f..7c469ef 100644 --- a/lib/sfzparser.py +++ b/lib/sfzparser.py @@ -64,7 +64,7 @@ def flattened_regions(self): def just_group(self): return "\n".join( [""] + - ['%s=%s' % (k, v) for k, v in self.attributes.iteritems()] + ['%s=%s' % (k, v) for k, v in self.attributes.items()] ) def __repr__(self): @@ -91,7 +91,7 @@ def __repr__(self): def __str__(self): return "\n".join( [""] + - ['%s=%s' % (k, v) for k, v in self.attributes.iteritems()] + ['%s=%s' % (k, v) for k, v in self.attributes.items()] ) def exists(self, root=None): @@ -103,7 +103,7 @@ def exists(self, root=None): def without_attributes(self, discard=lambda x: False): return Region(dict([ (k, v) - for k, v in self.attributes.iteritems() + for k, v in self.attributes.items() if not discard(k) ])) @@ -111,7 +111,7 @@ def merge(self, other_attrs): return Region(dict( (k, v) for d in [self.attributes, other_attrs] - for k, v in d.iteritems() + for k, v in d.items() )) @@ -125,4 +125,4 @@ def merge(self, other_attrs): for fn in args.files: file = SFZFile(open(fn).read()) for group in file.groups: - print group + print(group) diff --git a/lib/starts_with_click.py b/lib/starts_with_click.py index f83a0af..9ed42aa 100644 --- a/lib/starts_with_click.py +++ b/lib/starts_with_click.py @@ -1,6 +1,6 @@ import sys from wavio import read_wave_file -from constants import bit_depth +from consts import bit_depth default_threshold_samples = (0.001 * float(2 ** (bit_depth - 1))) diff --git a/lib/truncate.py b/lib/truncate.py index 73884bd..9e7182b 100644 --- a/lib/truncate.py +++ b/lib/truncate.py @@ -1,13 +1,14 @@ import sys -from wavio import read_wave_file -from utils import start_of, end_of + +from .wavio import read_wave_file +from .utils import start_of, end_of def chop(aif): file = read_wave_file(aif) start, end = min([start_of(chan) for chan in file]), \ max([end_of(chan) for chan in file]) - print aif, start, end, float(end) / len(file[0]) + print(aif, start, end, float(end) / len(file[0])) # outfile = aif + '.chopped.aif' # r = wave.open(aif, 'rb') diff --git a/lib/utils.py b/lib/utils.py index 65ed281..b49fd7f 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -3,7 +3,8 @@ import math from numpy import inf -from constants import default_silence_threshold, bit_depth +from .consts import default_silence_threshold, bit_depth + from collections import defaultdict diff --git a/lib/volume_leveler.py b/lib/volume_leveler.py index 293803f..c5edff9 100644 --- a/lib/volume_leveler.py +++ b/lib/volume_leveler.py @@ -1,18 +1,22 @@ import numpy import argparse -from constants import bit_depth -from sfzparser import SFZFile, Group -from wavio import read_wave_file -from utils import group_by_attr -from flacize import full_path -from itertools import tee, izip + +from .consts import bit_depth +from .sfzparser import SFZFile, Group +from .wavio import read_wave_file +from .utils import group_by_attr +from .flacize import full_path + +#from itertools import tee, izip +from itertools import tee def pairwise(iterable): "s -> (s0,s1), (s1,s2), (s2, s3), ..." a, b = tee(iterable) next(b, None) - return izip(a, b) + #return izip(a, b) + return zip(a, b) def max_amp(filename): @@ -23,12 +27,14 @@ def peak_rms(data, window_size=480, limits=960): index = max([numpy.argmax(channel) for channel in data]) maxlimit = max([len(channel) for channel in data]) max_so_far = 0 - for i in xrange( + for i in range( max(index - limits, (window_size / 2)), min(index + limits, maxlimit - (window_size / 2)) ): for channel in data: - window = channel[i - (window_size / 2):i + (window_size / 2)] + index1 = int(i - (window_size / 2)) + index2 = int(i + (window_size / 2)) + window = channel[index1:index2] if len(window) == 0: raise Exception("Cannot take mean of empty slice! Channel " "size %d, index %d, window size %d" % ( @@ -69,8 +75,8 @@ def level_volume(regions, dirname): ) ) except ZeroDivisionError: - print "Got ZeroDivisionError with high sample path: %s" % \ - high.attributes['sample'] + print("Got ZeroDivisionError with high sample path: %s" % \ + high.attributes['sample']) raise for attr in REMOVE_ATTRS: if attr in high.attributes: @@ -105,5 +111,5 @@ def level_volume(regions, dirname): for filename in args.files: sfz = SFZFile(open(filename).read()) regions = sum([group.regions for group in sfz.groups], []) - for key, regions in group_by_attr(regions, 'key').iteritems(): - print level_volume(regions) + for key, regions in group_by_attr(regions, 'key').items(): + print(level_volume(regions)) diff --git a/lib/wavio.py b/lib/wavio.py index d8f78ff..76b0582 100644 --- a/lib/wavio.py +++ b/lib/wavio.py @@ -3,7 +3,7 @@ import numpy import subprocess -from constants import NUMPY_DTYPE +from .consts import NUMPY_DTYPE def read_flac_file(filename, use_numpy=False): @@ -35,8 +35,8 @@ def read_wave_file(filename, use_numpy=False): else: return [ a[i::w.getnchannels()] - for i in xrange(w.getnchannels()) + for i in range(w.getnchannels()) ] except wave.Error: - print "Could not open %s" % filename + print("Could not open %s" % filename) raise diff --git a/samplescanner b/samplescanner deleted file mode 120000 index c020662..0000000 --- a/samplescanner +++ /dev/null @@ -1 +0,0 @@ -record.py \ No newline at end of file diff --git a/record.py b/samplescanner.py similarity index 100% rename from record.py rename to samplescanner.py