Added multiple PWM values per note.

Misc. changes to plot_seq.py
This commit is contained in:
kpl 2019-11-09 11:13:34 -05:00
parent f69c71c498
commit 919ecadf8d
4 changed files with 114 additions and 52 deletions

62
p_ac.py
View File

@ -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()

View File

@ -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: [

View File

@ -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 )

View File

@ -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,