From 5f123f5f728fa4998469660a210cea968d841981 Mon Sep 17 00:00:00 2001 From: kevin Date: Sun, 7 Jul 2024 13:36:42 -0400 Subject: [PATCH] libcw/py : Initial commit. --- py/dsp_recorder_plot.py | 38 ++++++ py/gen_midi_csv.py | 103 +++++++++++++++ py/sample_looper.py | 282 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 423 insertions(+) create mode 100644 py/dsp_recorder_plot.py create mode 100644 py/gen_midi_csv.py create mode 100644 py/sample_looper.py diff --git a/py/dsp_recorder_plot.py b/py/dsp_recorder_plot.py new file mode 100644 index 0000000..0ea062c --- /dev/null +++ b/py/dsp_recorder_plot.py @@ -0,0 +1,38 @@ +# Plot the output of a libcw:cwDspTransform.cpp: 'recorder' object. + +import sys,os,json + +import matplotlib.pyplot as plt + +def plot_file( fname ): + + r = None + with open(fname,"r") as f: + r = json.load(f) + + idx = 0 + while True: + + label = "{}".format(idx) + + if label not in r: + break + + plt.plot(r[label]) + + idx += 1 + + plt.show() + + + +if __name__ == "__main__": + + fname = os.path.expanduser("~/temp/temp_1.json") + + if len(sys.argv) > 1: + fname = sys.argv[1] + + plot_file( fname ) + + diff --git a/py/gen_midi_csv.py b/py/gen_midi_csv.py new file mode 100644 index 0000000..caf8078 --- /dev/null +++ b/py/gen_midi_csv.py @@ -0,0 +1,103 @@ +import csv,os + +def gen_sample_midi_csv(pitch,velA,note_on_sec,note_off_sec): + + markA = [] + msgA = [] + tpqn = 1260 + bpm = 60 + + ticks_per_sec = tpqn * bpm / 60.0 + ticks_per_note_on = ticks_per_sec * note_on_sec + ticks_per_note_off = ticks_per_sec * note_off_sec + uid = 0 + dticks = 0 + cur_sec = 0; + + r = { 'uid':len(msgA), + 'tpQN':tpqn, + 'bpm':bpm, + 'dticks':0, + 'ch':None, + 'status':None, + 'd0':None, + 'd1':None } + + msgA.append(r); + + for vel in velA: + + ch = 0 + status = 0x90 + + + r = { 'uid':len(msgA), + 'tpQN':None, + 'bpm':None, + 'dticks':dticks, + 'ch':ch, + 'status':status, + 'd0':pitch, + 'd1':vel } + + msgA.append(r) + + dticks = ticks_per_note_on + + r = { 'uid':len(msgA), + 'tpQN':None, + 'bpm':None, + 'dticks':dticks, + 'ch':ch, + 'status':status, + 'd0':pitch, + 'd1':0 } + + msgA.append(r) + + dticks = ticks_per_note_off + + markA.append( (cur_sec, cur_sec+note_on_sec, vel) ) + + cur_sec += note_on_sec + note_off_sec + + return msgA,markA + +def write_file( fname, msgA ): + + fieldnames = list(msgA[0].keys()) + + with open(fname,"w") as f: + wtr = csv.DictWriter(f, fieldnames=fieldnames) + + wtr.writeheader() + + for m in msgA: + wtr.writerow(m) + +def write_marker_file(fname, markA ): + + with open(fname,"w") as f: + for beg_sec,end_sec,vel in markA: + f.write(f"{beg_sec}\t{end_sec}\t{vel}\n") + + + +if __name__ == "__main__": + + out_dir = "/home/kevin/temp/wt" + note_on_sec = 1.0 + note_off_sec = 0.5 + pitch = 60 + velA = [1,8,15,22,29,36,42,49,56,63,70,77,84,91,98,105,112,119,126] + + msgA,markA = gen_sample_midi_csv(60,velA,note_on_sec,note_off_sec) + + midi_csv_fname = os.path.join(out_dir,f"{pitch}_sample.csv") + mark_fname = os.path.join(out_dir,f"{pitch}_marker.txt") + + print(midi_csv_fname) + print(mark_fname) + + write_file(midi_csv_fname,msgA) + write_marker_file(mark_fname,markA) diff --git a/py/sample_looper.py b/py/sample_looper.py new file mode 100644 index 0000000..e881f98 --- /dev/null +++ b/py/sample_looper.py @@ -0,0 +1,282 @@ +import math +import json +import wave as w +import array +import types +import matplotlib.pyplot as plt +import numpy as np + +def parse_marker_file( marker_fname ): + + markL = [] + with open(marker_fname) as f: + for line in f: + tokL = line.split("\t"); + + assert( len(tokL) == 3 ) + + markL.append( ( float(tokL[0]), float(tokL[1]), tokL[2] ) ) + + return markL + +def parse_audio_file( audio_fname ): + + with w.open(audio_fname,"rb") as f: + print(f"ch:{f.getnchannels()} bits:{f.getsampwidth()} srate:{f.getframerate()} frms:{f.getnframes()}") + + srate = f.getframerate() + frmN = f.getnframes() + data_bytes = f.readframes(frmN) + smpM = np.array(array.array('i',data_bytes)) + + + smpM = np.reshape(smpM,(frmN,2)) + + if 0: + fig, ax = plt.subplots(1,1) + + + + + ax.plot(smpM[0:48000*10,1]) + + plt.show() + + return smpM,srate + +def plot_overlap( xV, bi, ei, wndN, smp_per_cycle, title ): + + fig, ax = plt.subplots(1,1) + + x0 = [ i for i in range(bi-wndN,bi+wndN) ] + x1 = [ x-x0[0] for x in x0 ] + ax.plot(x1,xV[x0]) + + x0 = [ i for i in range(ei-wndN,ei+wndN) ] + x1 = [ x-x0[0] for x in x0 ] + ax.plot(x1,xV[x0]) + + plt.title(title) + + plt.show() + +def sign(x): + return x<0 + +def find_zero_crossing( xV, si, inc ): + + while si > 0: + + if sign(xV[si-1]) != sign(xV[si]): + break; + si += inc + + return si + +def meas_fit(xV,ei,bi,wndN): + + v0 = xV[ei-wndN:ei+wndN]/0x7fffffff + v1 = xV[bi-wndN:bi+wndN]/0x7fffffff + + dv = (v1-v0) * (v1-v0) + return np.mean(dv) + +def find_loop_points( xV, bsi, esi, smp_per_cycle, wndN, est_N ): + + ei = find_zero_crossing(xV,esi,-1) + + min_d = None + min_bi = None + bi = bsi + for i in range(0,est_N): + + bi = find_zero_crossing(xV,bi,1) + + d = meas_fit(xV,ei,bi,wndN) + + #print(i,bi,d) + + if min_bi is None or d < min_d: + min_d = d + min_bi = bi + + bi += int(wndN/2) #smp_per_cycle + + + return min_bi, ei, min_d + +def process_all_samples_0( markL, smpM, srate, args ): + + wtL = [] + + fund_hz = 13.75 * math.pow(2,(-9.0/12.0)) * math.pow(2.0,(args.midi_pitch / 12.0)) + + end_offs_smp_idx = int(args.end_offset_ms * srate / 1000) + loop_dur_smp = int(args.loop_dur_ms * srate / 1000) + smp_per_cycle = int(srate / fund_hz) + + print(f"Hz:{fund_hz} smp/cycle:{smp_per_cycle}") + + for beg_sec,end_sec,vel_label in markL: + for ch_idx in range(0,smpM.shape[1]): + beg_smp_idx = int(beg_sec * srate) + end_smp_idx = int(end_sec * srate) + + eli = end_smp_idx - end_offs_smp_idx + bli = eli - loop_dur_smp + + #print(beg_smp_idx,bli,eli,end_smp_idx) + + xV = smpM[:,ch_idx] + wndN = int(smp_per_cycle/3) + bi,ei,cost = find_loop_points(xV,bli,eli,smp_per_cycle,wndN,args.guess_cnt) + + plot_title = f"vel:{vel_label} ch:{ch_idx} cost:{math.log(cost):.2f}" + #plot_overlap(xV,bi,ei,wndN,smp_per_cycle,plot_title) + + wtL.append( { + "pitch":args.midi_pitch, + "vel": int(vel_label), + "cost":cost, + "ch_idx":ch_idx, + "beg_smp_idx":beg_smp_idx, + "end_smp_idx":end_smp_idx, + "beg_loop_idx":bi, + "end_loop_idx":ei }) + + return wtL + + +def process_all_samples( markL, smpM, srate, args ): + + wtL = [] + + fund_hz = 13.75 * math.pow(2,(-9.0/12.0)) * math.pow(2.0,(args.midi_pitch / 12.0)) + + end_offs_smp_idx = int(args.end_offset_ms * srate / 1000) + loop_dur_smp = int(args.loop_dur_ms * srate / 1000) + smp_per_cycle = int(srate / fund_hz) + + print(f"Hz:{fund_hz} smp/cycle:{smp_per_cycle}") + + for beg_sec,end_sec,vel_label in markL: + + beg_smp_idx = int(beg_sec * srate) + end_smp_idx = int(end_sec * srate) + + r = { + "instr":"piano", + "pitch":args.midi_pitch, + "vel": int(vel_label), + "beg_smp_idx":beg_smp_idx, + "end_smp_idx":None, + "chL": [] + } + + eli = end_smp_idx - end_offs_smp_idx + bli = eli - loop_dur_smp + + esi = beg_smp_idx; + for ch_idx in range(0,smpM.shape[1]): + + xV = smpM[:,ch_idx] + wndN = int(smp_per_cycle/3) + bi,ei,cost = find_loop_points(xV,bli,eli,smp_per_cycle,wndN,args.guess_cnt) + + plot_title = f"vel:{vel_label} ch:{ch_idx} cost:{math.log(cost):.2f}" + #plot_overlap(xV,bi,ei,wndN,smp_per_cycle,plot_title) + + r["chL"].append({ + "ch_idx":ch_idx, + "segL":[] }) + + + r["chL"][ch_idx]["segL"].append({ + "cost":0, + "bsi":beg_smp_idx, + "esi":bi }) + + r["chL"][ch_idx]["segL"].append({ + "cost":cost, + "bsi":bi, + "esi":ei }) + + esi = max(esi,ei) + + r['end_smp_idx'] = esi + wtL.append(r) + + return wtL + + +def write_loop_label_file_0( fname, wtL, srate ): + + with open(fname,"w") as f: + for r in wtL: + beg_sec = r['beg_smp_idx'] / srate + end_sec = r['end_smp_idx'] / srate + # f.write(f"{beg_sec}\t{end_sec}\t{r['vel_label']}\n") + + beg_sec = r['beg_loop_idx'] / srate + end_sec = r['end_loop_idx'] / srate + cost = math.log(r['cost']) + label = f"ch:{r['ch_idx']} {cost:.2f}" + f.write(f"{beg_sec}\t{end_sec}\t{label}\n") + +def write_loop_label_file( fname, wtL, srate ): + + with open(fname,"w") as f: + for r in wtL: + for cr in r['chL']: + for sr in cr['segL']: + beg_sec = sr['bsi'] / srate + end_sec = sr['esi'] / srate + if sr['cost']!=0: + cost = math.log(sr['cost']) + label = f"ch:{cr['ch_idx']} {cost:.2f}" + f.write(f"{r['vel']} {beg_sec}\t{end_sec}\t{label}\n") + + +def write_wt_file( fname, audio_fname, wtL, srate ): + + r = { + "audio_fname":audio_fname, + "srate":srate, + "wt":wtL + } + + + + + + with open(fname,"w") as f: + json.dump(r,f); + + +if __name__ == "__main__": + + audio_fname = "/home/kevin/temp/wt/wav/60_samples.wav" + marker_fname = "/home/kevin/temp/wt/60_marker.txt" + loop_marker_fname = "/home/kevin/temp/wt/60_loop_mark.txt" + wt_fname = "/home/kevin/temp/wt/bank/60_wt.json" + midi_pitch = 60 + + args = { + 'end_offset_ms':100, + 'loop_dur_ms':100, + 'midi_pitch':midi_pitch, + 'guess_cnt':40 + + } + + args = types.SimpleNamespace(**args) + markL = parse_marker_file(marker_fname) + smpM,srate = parse_audio_file(audio_fname) + chN = smpM.shape[1] + wtL = process_all_samples(markL,smpM,srate,args) + + write_loop_label_file(loop_marker_fname, wtL, srate) + + write_wt_file(wt_fname,audio_fname, wtL,srate) + +