diff --git a/p_ac.py b/p_ac.py index 7b3dcb2..f1d54a3 100644 --- a/p_ac.py +++ b/p_ac.py @@ -9,11 +9,12 @@ from AudioDevice import AudioDevice from result import Result from common import parse_yaml_cfg from plot_seq import form_resample_pulse_time_list +from plot_seq import form_final_pulse_list class AttackPulseSeq: """ Sequence a fixed chord over a list of attack pulse lengths.""" - def __init__(self, audio, api, noteDurMs=1000, pauseDurMs=1000, holdDutyPct=50 ): + def __init__(self, audio, api, noteDurMs=1000, pauseDurMs=1000, holdDutyPctL=[(0,50)] ): self.audio = audio self.api = api self.outDir = None # directory to write audio file and results @@ -21,12 +22,13 @@ class AttackPulseSeq: self.pulseUsL = [] # one onset pulse length in microseconds per sequence element self.noteDurMs = noteDurMs # duration of each chord in milliseconds self.pauseDurMs = pauseDurMs # duration between end of previous note and start of next - self.holdDutyPct = holdDutyPct # hold voltage duty cycle as a percentage (0-100) + self.holdDutyPctL= holdDutyPctL # hold voltage duty cycle table [ (minPulseSeqUsec,dutyCyclePct) ] self.pulse_idx = 0 # Index of next pulse self.state = None # 'note_on','note_off' + self.prevHoldDutyPct = None self.next_ms = 0 # Time of next event (note-on or note_off) - self.eventTimeL = [] # Onset/offset time of each note [ [onset_ms,offset_ms] ] + self.eventTimeL = [] # Onset/offset time of each note [ [onset_ms,offset_ms] ] (used to locate the note in the audio file) self.beginMs = 0 self.playOnlyFl = False @@ -37,13 +39,15 @@ class AttackPulseSeq: self.pulse_idx = 0 self.state = 'note_on' + self.prevHoldDutyPct = None self.next_ms = ms + 500 # wait for 500ms to play the first note (this will guarantee that there is some empty space in the audio file before the first note) self.eventTimeL = [[0,0] for _ in range(len(pulseUsL))] # initialize the event time self.beginMs = ms self.playOnlyFl = playOnlyFl - for pitch in pitchL: - self.api.set_pwm( pitch, self.holdDutyPct ) + #for pitch in pitchL: + # self.api.set_pwm_duty( pitch, self.holdDutyPct ) + # print("set PWM:%i"%(self.holdDutyPct)) if not playOnlyFl: self.audio.record_enable(True) # start recording audio @@ -86,20 +90,39 @@ class AttackPulseSeq: else: assert(0) - + + def _get_duty_cycle( self, pulseUsec ): + dutyPct = self.holdDutyPctL[0][1] + for refUsec,refDuty in self.holdDutyPctL: + if pulseUsec < refUsec: + break + dutyPct = refDuty + + return dutyPct + + def _set_duty_cycle( self, pitch, pulseUsec ): + + dutyPct = self._get_duty_cycle( pulseUsec ) + + if dutyPct != self.prevHoldDutyPct: + self.api.set_pwm_duty( pitch, dutyPct ) + print("Hold Duty:",dutyPct) + + self.prevHoldDutyPct = dutyPct + def _note_on( self, ms ): - #self.eventTimeL[ self.pulse_idx ][0] = ms - self.beginMs self.eventTimeL[ self.pulse_idx ][0] = self.audio.buffer_sample_ms().value self.next_ms = ms + self.noteDurMs self.state = 'note_off' for pitch in self.pitchL: - self.api.note_on_us( pitch, int(self.pulseUsL[ self.pulse_idx ]) ) + pulse_usec = int(self.pulseUsL[ self.pulse_idx ]) + self._set_duty_cycle( pitch, pulse_usec ) + self.api.note_on_us( pitch, pulse_usec ) print("note-on:",pitch,self.pulse_idx) def _note_off( self, ms ): - #self.eventTimeL[ self.pulse_idx ][1] = ms - self.beginMs self.eventTimeL[ self.pulse_idx ][1] = self.audio.buffer_sample_ms().value self.next_ms = ms + self.pauseDurMs self.state = 'note_on' @@ -123,7 +146,7 @@ class AttackPulseSeq: "pitchL":self.pitchL, "noteDurMs":self.noteDurMs, "pauseDurMs":self.pauseDurMs, - "holdDutyPct":self.holdDutyPct, + "holdDutyPctL":self.holdDutyPctL, "eventTimeL":self.eventTimeL, "beginMs":self.beginMs } @@ -144,7 +167,7 @@ class AttackPulseSeq: class CalibrateKeys: def __init__(self, cfg, audioDev, api): self.cfg = cfg - self.seq = AttackPulseSeq( audioDev, api, noteDurMs=1000, pauseDurMs=1000, holdDutyPct=50 ) + self.seq = AttackPulseSeq( audioDev, api, noteDurMs=cfg.noteDurMs, pauseDurMs=cfg.pauseDurMs, holdDutyPctL=cfg.holdDutyPctL ) self.pulseUsL = None self.chordL = None @@ -198,14 +221,14 @@ class CalibrateKeys: outDir = os.path.join(outDir, dirStr ) - print(outDir) if not os.path.isdir(outDir): os.mkdir(outDir) - # get the next available output directory id outDir_id = self._calc_next_out_dir_id( outDir ) + print(outDir_id,outDir) + # if this is not the first time this note has been sampled then get the resample locations if outDir_id != 0: self.pulseUsL,_,_ = form_resample_pulse_time_list( outDir, self.cfg.analysisArgs ) @@ -213,11 +236,14 @@ class CalibrateKeys: if playOnlyFl: self.pulseUsL,_ = form_final_pulse_list( outDir, pitchL[0], self.cfg.analysisArgs, take_id=None ) - - outDir = os.path.join( outDir, str(outDir_id) ) + noteN = cfg.analysisArgs['auditionNoteN'] + self.pulseUsL = [ self.pulseUsL[ int(round(i*126.0/(noteN-1)))] for i in range(noteN) ] - if not os.path.isdir(outDir): - os.mkdir(outDir) + else: + outDir = os.path.join( outDir, str(outDir_id) ) + + if not os.path.isdir(outDir): + os.mkdir(outDir) # start the sequencer self.seq.start( ms, outDir, pitchL, self.pulseUsL, playOnlyFl ) @@ -533,7 +559,7 @@ def parse_args(): ap = argparse.ArgumentParser(description=descStr) - ap.add_argument("-c","--config", default="cfg/p_ac.yml", help="YAML configuration file.") + ap.add_argument("-c","--config", default="p_ac.yml", help="YAML configuration file.") ap.add_argument("-l","--log_level",choices=logL, default="warning", help="Set logging level: debug,info,warning,error,critical. Default:warning") return ap.parse_args() diff --git a/p_ac.yml b/p_ac.yml index a4ff0d5..0f2e86c 100644 --- a/p_ac.yml +++ b/p_ac.yml @@ -18,15 +18,29 @@ # MeasureSeq args - outDir: "~/temp/p_ac_3", + outDir: "~/temp/p_ac_3c", noteDurMs: 1000, pauseDurMs: 1000, - holdDutyPct: 50, + holdDutyPctL: [ [0,50], [17000,65] ], full_pulse0L: [ 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500, 7000, 8000, 9000, 10000, 12000, 14000, 18000, 22000, 26000, 30000, 34000, 40000], full_pulse1L: [ 10000, 11000, 12000, 13000, 14000, 15000, 16000, 17000, 18000, 20000, 22000, 24000, 26000, 30000, 32000, 34000, 36000, 40000], - full_pulseL: [ 10000, 10500, 11000, 11500, 12000, 12500, 13000, 13500, 14000, 14500, 15000, 15500, 16000, 16500, 17000, 17500, 18000, 18500, 20000, 22000, 24000, 26000, 30000, 32000, 34000, 36000, 40000], + full_pulse2L: [ 10000, 10500, 11000, 11500, 12000, 12500, 13000, 13500, 14000, 14500, 15000, 15500, 16000, 16500, 17000, 17500, 18000, 18500, 20000, 22000, 24000, 26000, 30000, 32000, 34000, 36000, 40000], + full_pulse3L: [ 10000, 10125, 10250, 10500, 10625, 10750, 10875, 11000, 11125, 11250, 11500, 11625, 11750, 11875, 12000, 12125, 12250, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13500, 13625, 13750, 13875, 14000, 14125, 14250, 14500, 14625, 14750, 14875, 15000, 15500, 16000, 16500, 17000, 17500, 18000, 18500, 20000, 22000, 24000, 26000, 30000, 32000, 34000, 36000, 40000], + + full_pulse4L: [ 8000, 8125, 8250, 8375, 8500, 8625, 8750, 8875, 9000, 9125, 9250, 9375, 9500, 9625, 9750, 9875, 10000, 10125, 10250, 10375, 10500, 10625, 10750, 10875, 11000, 11125, 11250, 11375, 11500, 11625, 11750, 11875, 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13375, 13500, 13625, 13750, 13875, 14000, 14125, 14375, 14250, 14500, 14625, 14750, 14875, 15000, 15250, 15375, 15500, 15750, 16000, 16250, 16500, 16750, 17000, 17250, 17500, 17750, 18000, 18250, 18500, 18750, 20000, 21000, 22000, 23000, 24000, 25000, 26000, 27000, 28000, 30000, 32000, 34000, 36000, 40000], + + full_pulse5L: [ 10000, 10125, 10250, 10375, 10500, 10625, 10750, 10875, 11000, 11125, 11250, 11375, 11500, 11625, 11750, 11875, 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13375, 13500, 13625, 13750, 13875, 14000, 14125, 14250, 14375, 14500, 14625, 14750, 14875, 15000, 15250, 15375, 15500, 15750, 16000, 16250, 16500, 16750, 17000, 17250, 17500, 17750, 18000, 18250, 18500, 18750, 20000, 21000, 22000, 23000, 24000, 25000, 26000, 27000, 28000, 30000, 32000, 34000, 36000, 40000], + + full_pulse6L: [ 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13375, 13500, 13625, 13750, 13875, 14000, 14125, 14250, 14375, 14500, 14625, 14750, 14875, 15000, 15250, 15375, 15500, 15750, 16000, 16250, 16500, 16750, 17000, 17250, 17500, 17750, 18000, 18250, 18500, 18750, 19000, 19500, 20000, 20500, 21000, 21500, 22000, 22500, 23000, 23500, 24000, 24500, 25000, 25500, 26000, 26500, 27000, 27500, 28000, 28500, 29000, 30000, 31000, 32000, 33000, 34000, 35000, 36000, 37000, 38000, 39000, 40000 ], + + full_pulse7L: [ 11000, 11125, 11250, 11375, 11500, 11625, 11750, 11875, 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13375, 13500, 13625, 13750, 13875, 14000, 14125, 14250, 14375, 14500, 14625, 14750, 14875, 15000, 15250, 15375, 15500, 15750, 16000, 16250, 16500, 16750, 17000, 17250, 17500, 17750, 18000, 18250, 18500, 18750, 19000, 19500, 20000, 20500, 21000, 21500, 22000, 22500, 23000, 23500, 24000, 24500, 25000, 25500, 26000, 26500, 27000, 27500, 28000, 28500, 29000, 30000, 31000, 32000, 33000, 34000, 35000, 36000, 37000, 38000, 39000, 40000 ], + + full_pulseL: [ 10000, 10050, 10100, 10150, 10200, 10250, 10300, 10350, 10400, 10450, 10500, 10550, 10600, 10650, 10700, 10750, 10800, 10850, 10900, 10950, 11000, 11125, 11250, 11375, 11500, 11625, 11750, 11875, 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13375, 13500, 13625, 13750, 13875, 14000, 14125, 14250, 14375, 14500, 14625, 14750, 14875, 15000, 15250, 15375, 15500, 15750, 16000, 16250, 16500, 16750, 17000, 17250, 17500, 17750, 18000, 18250, 18500, 18750, 19000, 19500, 20000, 20500, 21000, 21500, 22000, 22500, 23000, 23500, 24000, 24500, 25000, 25500, 26000, 26500, 27000, 27500, 28000, 28500, 29000, 30000, 31000, 32000, 33000, 34000, 35000, 36000, 37000, 38000, 39000, 40000 ], + + full_pulse9L: [ 8750, 8800, 8850, 8900, 8950, 9000, 9050, 9100, 9150, 9200, 9250, 9300, 9350, 9400, 9450,9500, 9550, 9600, 9650, 9700, 9750, 9800, 9850, 9900, 9950, 10000, 10050, 10100, 10150, 10200, 10250, 10300, 10350, 10400, 10450, 10500, 10550, 10600, 10650, 10700, 10750, 10800, 10850, 10900, 10950, 11000, 11125, 11250, 11375, 11500, 11625, 11750, 11875, 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13125, 13250, 13375, 13500, 13625, 13750, 13875, 14000, 14125, 14250, 14375, 14500, 14625, 14750, 14875, 15000, 15250, 15375, 15500, 15750, 16000, 16250, 16500, 16750, 17000, 17250, 17500, 17750, 18000, 18250, 18500, 18750, 19000, 19500, 20000, 20500, 21000, 21500, 22000, 22500, 23000, 23500, 24000, 24500, 25000, 25500, 26000, 26500, 27000, 27500, 28000, 28500, 29000, 30000, 31000, 32000, 33000, 34000, 35000, 36000, 37000, 38000, 39000, 40000 ], + # RMS analysis args analysisArgs: { rmsAnalysisArgs: { @@ -37,11 +51,12 @@ harmN: 3, # count of harmonics to use to calculate harmonic based RMS analysis }, - minAttkDb: 5.0, # threshold of silence level - maxDbOffset: 0.5, # travel down the from the max. note level by at most this amount to locate the max. peak - maxDeltaDb: 2.0, # maximum db change between volume samples (changes greater than this will trigger resampling) + minAttkDb: 7.0, # threshold of silence level + maxDbOffset: 0.25, # travel down the from the max. note level by at most this amount to locate the max. peak + maxDeltaDb: 1.5, # maximum db change between volume samples (changes greater than this will trigger resampling) samplesPerDb: 4, # count of samples per dB to resample ranges whose range is less than maxDeltaDb - minSampleDistUs: 500 # minimum distance between sample points in microseconds + minSampleDistUs: 50, # minimum distance between sample points in microseconds + auditionNoteN: 19 # count of notes to play for audition }, key_mapL: [ diff --git a/plot_seq.py b/plot_seq.py index 8bb9ab8..faef268 100644 --- a/plot_seq.py +++ b/plot_seq.py @@ -120,7 +120,8 @@ def form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ): multValL.append((out_idx,multi_value_count)) if len(multValL) > 0: - print("Multi-value pulse locations were found during velocity table formation: ",multValL) + # print("Multi-value pulse locations were found during velocity table formation: ",multValL) + pass return pulseUsL,pulseDbL @@ -158,8 +159,18 @@ def merge_close_sample_points( pkDbUsL, minSampleDistanceUs ): return pkDbUsL +def _calc_resample_points( dPkDb, pkUs0, pkUs1, samplePerDb, minSampleDistUs ): -def calc_resample_ranges( pkDbL, pkUsL, min_pk_idx, max_pk_idx, maxDeltaDb, samplePerDb ): + dPkUs = pkUs1 - pkUs0 + sampleCnt = max(int(round(abs(dPkDb) * samplePerDb)),samplePerDb) + dUs = max(int(round(dPkUs/sampleCnt)),minSampleDistUs) + sampleCnt = int(round(dPkUs/dUs)) + dUs = int(round(dPkUs/sampleCnt)) + usL = [ pkUs0 + dUs*j for j in range(sampleCnt+1)] + + return usL + +def calc_resample_ranges( pkDbL, pkUsL, min_pk_idx, max_pk_idx, maxDeltaDb, samplePerDb, minSampleDistUs ): if min_pk_idx == 0: print("No silent notes were generated. Decrease the minimum peak level or the hold voltage.") @@ -180,16 +191,13 @@ def calc_resample_ranges( pkDbL, pkUsL, min_pk_idx, max_pk_idx, maxDeltaDb, samp # it is below the previous max peak if d > maxDeltaDb or d <= 0 or pkDbL[i] < refPkDb: - sampleCnt = max(int(round(abs(d) * samplePerDb)),samplePerDb) - dUs = int(round((pkUsL[i] - pkUsL[i-1])/sampleCnt)) - usL = [ pkUsL[i-1] + dUs*j for j in range(sampleCnt)] - - if i + 1 < len(pkDbL): + usL = _calc_resample_points( d, pkUsL[i-1], pkUsL[i], samplePerDb, minSampleDistUs ) + + if d <= 0 and i + 1 < len(pkDbL): d = pkDbL[i+1] - pkDbL[i] - - sampleCnt = max(int(round(abs(d) * samplePerDb)),samplePerDb) - dUs = int(round((pkUsL[i+1] - pkUsL[i])/sampleCnt)) - usL += [ pkUsL[i] + dUs*j for j in range(sampleCnt)] + + usL += _calc_resample_points( d, pkUsL[i-1], pkUsL[i], samplePerDb, minSampleDistUs ) + if pkDbL[i] > refPkDb: refPkDb = pkDbL[i] @@ -239,10 +247,12 @@ def form_resample_pulse_time_list( inDir, analysisArgsD ): min_pk_idx, max_pk_idx = find_min_max_peak_index( pkDbL, analysisArgsD['minAttkDb'], analysisArgsD['maxDbOffset'] ) # estimate the microsecond locations to resample - resampleUsSet = calc_resample_ranges( pkDbL, pkUsL, min_pk_idx, max_pk_idx, analysisArgsD['maxDeltaDb'], analysisArgsD['samplesPerDb'] ) + resampleUsSet = calc_resample_ranges( pkDbL, pkUsL, min_pk_idx, max_pk_idx, analysisArgsD['maxDeltaDb'], analysisArgsD['samplesPerDb'], analysisArgsD['minSampleDistUs'] ) resampleUsL = sorted( list(resampleUsSet) ) + #print(resampleUsL) + return resampleUsL, pkDbL, pkUsL @@ -282,8 +292,8 @@ def find_min_max_peak_index( pkDbL, minDb, maxDbOffs ): for i in range( max_i, 0, -1 ): # if this peak is within maxDbOffs of the loudest then choose this one instead - if maxDb - yV[i] < maxDbOffs: - max_i = i + #if maxDb - yV[i] < maxDbOffs: + # max_i = i # if this peak is less than minDb then the previous note is the min note if yV[i] < minDb: @@ -291,7 +301,10 @@ def find_min_max_peak_index( pkDbL, minDb, maxDbOffs ): min_i = i - assert( min_i < max_i ) + if min_i >= max_i: + min_i = 0 + max_i = len(pkDbL)-1 + if min_i == 0: print("No silent notes were generated. Decrease the minimum peak level or the hold voltage.") @@ -404,9 +417,9 @@ def plot_spectral_ranges( inDir, pitchL, rmsWndMs=300, rmsHopMs=30, harmN=5, dbR def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ): r = rms_analysis_main( inDir, midi_pitch, **analysisArgsD['rmsAnalysisArgs'] ) - - min_pk_idx, max_pk_idx = find_min_max_peak_index( r.pkDbL, analysisArgsD['minAttkDb'], analysisArgsD['maxDbOffset'] ) + min_pk_idx, max_pk_idx = find_min_max_peak_index( r.pkDbL, analysisArgsD['minAttkDb'], analysisArgsD['maxDbOffset'] ) + skipPkIdxL = find_skip_peaks( r.rmsDbV, r.pkIdxL, min_pk_idx, max_pk_idx ) jmpPkIdxL = find_out_of_range_peaks( r.rmsDbV, r.pkIdxL, min_pk_idx, max_pk_idx, analysisArgsD['maxDeltaDb'] ) @@ -423,7 +436,7 @@ def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ): ax.axvline( x=endMs/1000.0, color="red") ax.text(begMs/1000.0, 20.0, str(i) ) - return + # plot peak markers for i,pki in enumerate(r.pkIdxL): marker = 4 if i==min_pk_idx or i==max_pk_idx else 5 @@ -434,18 +447,19 @@ def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ): ax.plot( [pki / r.rms_srate], [ r.rmsDbV[pki] ], marker=6, color="blue") - + return r def do_td_plot( inDir, analysisArgs ): - fig,ax = plt.subplots() + fig,axL = plt.subplots(2,1) fig.set_size_inches(18.5, 10.5, forward=True) - id = int(inDir.split("/")[-1]) midi_pitch = int(inDir.split("/")[-2]) - td_plot(ax,inDir,midi_pitch,id,analysisArgs) + r = td_plot(axL[0],inDir,midi_pitch,id,analysisArgs) + + axL[1].plot( r.pkUsL, r.pkDbL, marker='.' ) plt.show() @@ -469,14 +483,17 @@ if __name__ == "__main__": inDir = sys.argv[1] cfgFn = sys.argv[2] + take_id = None if len(sys.argv)<4 else sys.argv[3] cfg = parse_yaml_cfg( cfgFn ) - - #do_td_plot(inDir,cfg.analysisArgs) - #o_td_multi_plot(inDir,cfg.analysisArgs) + if take_id is not None: + inDir = os.path.join(inDir,take_id) + do_td_plot(inDir,cfg.analysisArgs) + else: + #do_td_multi_plot(inDir,cfg.analysisArgs) - #plot_spectral_ranges( inDir, [ 24, 36, 48, 60, 72, 84, 96, 104] ) + #plot_spectral_ranges( inDir, [ 24, 36, 48, 60, 72, 84, 96, 104] ) - plot_resample_pulse_times( inDir, cfg.analysisArgs ) + plot_resample_pulse_times( inDir, cfg.analysisArgs ) diff --git a/rms_analysis.py b/rms_analysis.py index 49ec4ff..b77c351 100644 --- a/rms_analysis.py +++ b/rms_analysis.py @@ -148,6 +148,8 @@ def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs= tdRmsDbV, rms0_srate = audio_rms( srate, sigV, rmsWndMs, rmsHopMs, dbRefWndMs ) + tdPkIdxL = locate_peak_indexes( tdRmsDbV, rms0_srate, r['eventTimeL']) + rmsDbV, rms_srate, binHz = audio_harm_rms( srate, sigV, rmsWndMs, rmsHopMs, dbRefWndMs, midi_pitch, harmCandN, harmN ) pkIdxL = locate_peak_indexes( rmsDbV, rms_srate, r['eventTimeL'] ) @@ -155,6 +157,8 @@ def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs= r = types.SimpleNamespace(**{ "audio_srate":srate, "tdRmsDbV": tdRmsDbV, + "tdPkIdxL": tdPkIdxL, + "tdPkDbL": [ tdRmsDbV[i] for i in tdPkIdxL ], "binHz": binHz, "rmsDbV":rmsDbV, "rms_srate":rms_srate,