AudioDevice.py : fixed linear_buffer()

p_ac.py : changed dutyCyclePct to a list, added real-timen note analysis and multi-pitch tests.
Added keyboard.py, rt_note_analysis.py, plot_note_analysis.py, plot_all_note_durations.ipynb.
This commit is contained in:
kpl 2019-11-18 11:44:47 -05:00
parent 919ecadf8d
commit 26b997811a
10 changed files with 969 additions and 67 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.pyc
.ipynb_checkpoints

View File

@ -191,7 +191,9 @@ class AudioDevice(object):
def linear_buffer( self ):
smpN = self.buffer_sample_count()
r = self.buffer_sample_count()
if r:
smpN = r.value
bV = np.zeros( (smpN,self.ch_cnt) )
bi = 0

209
keyboard.py Normal file
View File

@ -0,0 +1,209 @@
import os,types,pickle
import numpy as np
from plot_seq import form_final_pulse_list
from rms_analysis import rms_analysis_main_all
class Keyboard:
def __init__(self, cfg, audio, api):
self.cfg = cfg
self.audio = audio
self.api = api
self.keyD = {} # { midi_pitch: { pulseUsL, holdDutyPctL } }
self.noteDurMs = cfg.noteDurMs
self.pauseDurMs= cfg.pauseDurMs
self.pitchL = None
self.pitch_idx = None
self.pulse_idx = None
self.targetDb = None
self.next_ms = None
self.enableFl = False
self.state = None #"note_on" | "note_off"
self.rmsAnlD = None
#self._load( cfg.outDir, cfg.analysisArgs)
def load( self, inDir, pitchL, analysisArgsD ):
self.keyD = {}
inDir = os.path.expanduser(inDir)
finalPulseListCacheFn = analysisArgsD['finalPulseListCacheFn']
if os.path.isfile(finalPulseListCacheFn):
print("READING: final pulse list cache file: %s" % (finalPulseListCacheFn))
with open(finalPulseListCacheFn,'rb') as f:
self.keyD = pickle.load(f)
else:
dirL = os.listdir(inDir)
for dirStr in dirL:
dirStr = os.path.normpath(os.path.join(inDir,dirStr))
if os.path.isdir(dirStr):
pathL = dirStr.split(os.sep)
midi_pitch = int( pathL[-1] )
if midi_pitch in pitchL:
print(dirStr,midi_pitch)
pulseUsL,pulseDbL,holdDutyPctL = form_final_pulse_list( dirStr, midi_pitch, analysisArgsD )
d = { 'pulseUsL':pulseUsL, 'holdDutyPctL':holdDutyPctL, 'lastDutyPct':0 }
self.keyD[ midi_pitch ] = types.SimpleNamespace(**d)
with open(finalPulseListCacheFn,'wb') as f:
pickle.dump(self.keyD,f)
print("Loading analysis ...")
cacheFn = analysisArgsD['rmsAnalysisCacheFn']
self.rmsAnlD = rms_analysis_main_all( inDir, cacheFn, **analysisArgsD['rmsAnalysisArgs'] )
print("Load DONE.")
def _get_duty_cycle_from_pulse_usec( self, pitch, pulseUsec ):
if pitch not in self.keyD:
print("Missing duty cycle.")
return None
dutyPct = self.keyD[pitch].holdDutyPctL[0][1]
for refUsec,refDuty in self.keyD[pitch].holdDutyPctL:
if pulseUsec < refUsec:
break
dutyPct = refDuty
return dutyPct
def _get_pulse_and_duty_cycle_from_pulse_idx( self, pitch, pulse_idx ):
pulseUsec = self.keyD[ pitch ].pulseUsL[ pulse_idx ]
dutyPct = self._get_duty_cycle_from_pulse_usec( pitch, pulseUsec )
return pulseUsec, dutyPct
def _get_pulse_and_duty_cycle_target_db( self, pitch, targetDb ):
r = self.rmsAnlD[pitch]
pulse_idx = np.argmin( np.abs(np.array(r.pkDbL) - targetDb) )
print("PULSE idx:",pulse_idx," db:", r.pkDbL[pulse_idx] )
pulseUsec = r.pkUsL[pulse_idx]
dutyPct = self._get_duty_cycle_from_pulse_usec( pitch, pulseUsec )
return pulseUsec, dutyPct
def _get_pulse_and_duty_cycle( self, pitch, pulse_idx, targetDb ):
if pulse_idx is not None:
return self._get_pulse_and_duty_cycle_from_pulse_idx(pitch,pulse_idx)
else:
return self._get_pulse_and_duty_cycle_target_db( pitch, targetDb )
def start( self, ms, pitchL, pulse_idx, targetDb=None ):
loadFl = True
if self.pitchL is not None:
loadFl = False
for pitch in pitchL:
if pitch not in self.pitchL:
loadFl = True
break
if loadFl:
self.load(self.cfg.outDir, pitchL, self.cfg.analysisArgs)
self.pitchL = pitchL
self.pitch_idx = 0
self.pulse_idx = pulse_idx
self.targetDb = targetDb
self.state = "note_on"
self.next_ms = ms
self.eventTimeL = [[0,0] for _ in range(len(pitchL))] # initialize the event time
self.audio.record_enable(True) # start recording audio
self.tick(ms) # play the first note
def repeat( self, ms, pulse_idx, targetDb=None ):
self.start( ms, self.pitchL, pulse_idx, targetDb )
def stop( self, ms ):
self._send_note_off()
self.audio.record_enable(False) # stop recording audio
self.state = None # disable this sequencer
def tick( self, ms ):
#self.audio.tick(ms)
# if next event time has arrived
if self.state is not None and ms >= self.next_ms:
# if waiting to turn note on
if self.state == 'note_on':
self._note_on(ms)
# if waiting to turn a note off
elif self.state == 'note_off':
self._note_off(ms)
self.pitch_idx += 1
# if all notes have been played
if self.pitch_idx >= len(self.pitchL):
self.stop(ms)
else:
assert(0)
def _note_on( self, ms ):
self.eventTimeL[ self.pitch_idx ][0] = self.audio.buffer_sample_ms().value
self.next_ms = ms + self.noteDurMs
self.state = 'note_off'
pitch = self.pitchL[ self.pitch_idx ]
pulse_usec, dutyPct = self._get_pulse_and_duty_cycle( pitch, self.pulse_idx, self.targetDb )
if pulse_usec is not None and dutyPct is not None:
self._set_pwm_duty( pitch, dutyPct )
self.api.note_on_us( pitch, pulse_usec )
pulse_idx = 0 if self.pulse_idx is None else self.pulse_idx
targetDb = 0 if self.targetDb is None else self.targetDb
dutyPct = 0 if dutyPct is None else dutyPct
print("note-on: %i %i %4.1f %8.1f %i" % (pitch, pulse_idx, targetDb, pulse_usec, dutyPct))
def _set_pwm_duty( self, pitch, dutyPct ):
if self.keyD[pitch].lastDutyPct != dutyPct:
self.keyD[pitch].lastDutyPct = dutyPct
self.api.set_pwm_duty( pitch, dutyPct )
def _note_off( self, ms ):
self.eventTimeL[ self.pitch_idx ][1] = self.audio.buffer_sample_ms().value
self.next_ms = ms + self.pauseDurMs
self.state = 'note_on'
#begTimeMs = self.eventTimeL[ self.pulse_idx ][0]
#endTimeMs = self.eventTimeL[ self.pulse_idx ][1]
#self.rtAnalyzer.analyze_note( self.audio, self.pitchL[0], begTimeMs, endTimeMs, self.cfg.analysisArgs['rmsAnalysisArgs'] )
self._send_note_off()
def _send_note_off( self ):
for pitch in self.pitchL:
self.api.note_off( pitch )
#print("note-off:",pitch,self.pulse_idx)

66
p_ac.py
View File

@ -10,11 +10,14 @@ 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
from rt_note_analysis import RT_Analyzer
from keyboard import Keyboard
class AttackPulseSeq:
""" Sequence a fixed chord over a list of attack pulse lengths."""
def __init__(self, audio, api, noteDurMs=1000, pauseDurMs=1000, holdDutyPctL=[(0,50)] ):
def __init__(self, cfg, audio, api, noteDurMs=1000, pauseDurMs=1000 ):
self.cfg = cfg
self.audio = audio
self.api = api
self.outDir = None # directory to write audio file and results
@ -22,7 +25,7 @@ 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.holdDutyPctL= holdDutyPctL # hold voltage duty cycle table [ (minPulseSeqUsec,dutyCyclePct) ]
self.holdDutyPctL= None # hold voltage duty cycle table [ (minPulseSeqUsec,dutyCyclePct) ]
self.pulse_idx = 0 # Index of next pulse
self.state = None # 'note_on','note_off'
@ -31,12 +34,13 @@ class AttackPulseSeq:
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
self.rtAnalyzer = RT_Analyzer()
def start( self, ms, outDir, pitchL, pulseUsL, playOnlyFl=False ):
def start( self, ms, outDir, pitchL, pulseUsL, holdDutyPctL, playOnlyFl=False ):
self.outDir = outDir # directory to write audio file and results
self.pitchL = pitchL # chord to play
self.pulseUsL = pulseUsL # one onset pulse length in microseconds per sequence element
self.holdDutyPctL = holdDutyPctL
self.pulse_idx = 0
self.state = 'note_on'
self.prevHoldDutyPct = None
@ -49,14 +53,15 @@ class AttackPulseSeq:
# self.api.set_pwm_duty( pitch, self.holdDutyPct )
# print("set PWM:%i"%(self.holdDutyPct))
if not playOnlyFl:
# kpl if not playOnlyFl:
self.audio.record_enable(True) # start recording audio
self.tick(ms) # play the first note
def stop(self, ms):
self._send_note_off() # be sure that all notes are actually turn-off
if not self.playOnlyFl:
# kpl if not self.playOnlyFl:
self.audio.record_enable(False) # stop recording audio
self._disable() # disable this sequencer
@ -69,8 +74,6 @@ class AttackPulseSeq:
def tick(self, ms):
self.audio.tick(ms)
# if next event time has arrived
if self.is_enabled() and ms >= self.next_ms:
@ -120,20 +123,25 @@ class AttackPulseSeq:
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)
print("note-on:",pitch, self.pulse_idx, pulse_usec)
def _note_off( self, ms ):
self.eventTimeL[ self.pulse_idx ][1] = self.audio.buffer_sample_ms().value
self.next_ms = ms + self.pauseDurMs
self.state = 'note_on'
if self.playOnlyFl:
begTimeMs = self.eventTimeL[ self.pulse_idx ][0]
endTimeMs = self.eventTimeL[ self.pulse_idx ][1]
self.rtAnalyzer.analyze_note( self.audio, self.pitchL[0], begTimeMs, endTimeMs, self.cfg.analysisArgs['rmsAnalysisArgs'] )
self._send_note_off()
def _send_note_off( self ):
for pitch in self.pitchL:
self.api.note_off( pitch )
print("note-off:",pitch,self.pulse_idx)
#print("note-off:",pitch,self.pulse_idx)
def _disable(self):
self.state = None
@ -167,7 +175,7 @@ class AttackPulseSeq:
class CalibrateKeys:
def __init__(self, cfg, audioDev, api):
self.cfg = cfg
self.seq = AttackPulseSeq( audioDev, api, noteDurMs=cfg.noteDurMs, pauseDurMs=cfg.pauseDurMs, holdDutyPctL=cfg.holdDutyPctL )
self.seq = AttackPulseSeq( cfg, audioDev, api, noteDurMs=cfg.noteDurMs, pauseDurMs=cfg.pauseDurMs )
self.pulseUsL = None
self.chordL = None
@ -233,8 +241,10 @@ class CalibrateKeys:
if outDir_id != 0:
self.pulseUsL,_,_ = form_resample_pulse_time_list( outDir, self.cfg.analysisArgs )
holdDutyPctL = self.cfg.holdDutyPctL
if playOnlyFl:
self.pulseUsL,_ = form_final_pulse_list( outDir, pitchL[0], self.cfg.analysisArgs, take_id=None )
self.pulseUsL,_,holdDutyPctL = form_final_pulse_list( outDir, pitchL[0], self.cfg.analysisArgs, take_id=None )
noteN = cfg.analysisArgs['auditionNoteN']
self.pulseUsL = [ self.pulseUsL[ int(round(i*126.0/(noteN-1)))] for i in range(noteN) ]
@ -246,7 +256,7 @@ class CalibrateKeys:
os.mkdir(outDir)
# start the sequencer
self.seq.start( ms, outDir, pitchL, self.pulseUsL, playOnlyFl )
self.seq.start( ms, outDir, pitchL, self.pulseUsL, holdDutyPctL, playOnlyFl )
def _calc_next_out_dir_id( self, outDir ):
@ -265,6 +275,7 @@ class App:
self.audioDev = None
self.api = None
self.calibrate = None
self.keyboard = None
def setup( self, cfg ):
self.cfg = cfg
@ -294,12 +305,18 @@ class App:
self.calibrate = CalibrateKeys( cfg, self.audioDev, self.api )
self.keyboard = Keyboard( cfg, self.audioDev, self.api )
return res
def tick( self, ms ):
self.audioDev.tick(ms)
if self.calibrate:
self.calibrate.tick(ms)
if self.keyboard:
self.keyboard.tick(ms)
def audio_dev_list( self, ms ):
portL = self.audioDev.get_port_list( True )
@ -315,8 +332,23 @@ class App:
chordL = [ [pitch] for pitch in range(pitchRangeL[0], pitchRangeL[1]+1)]
self.calibrate.start( ms, chordL, cfg.full_pulseL, playOnlyFl=True )
def keyboard_start_pulse_idx( self, ms, argL ):
pitchL = [ pitch for pitch in range(argL[0], argL[1]+1)]
self.keyboard.start( ms, pitchL, argL[2], None )
def keyboard_repeat_pulse_idx( self, ms, argL ):
self.keyboard.repeat( ms, argL[0], None )
def keyboard_start_target_db( self, ms, argL ):
pitchL = [ pitch for pitch in range(argL[0], argL[1]+1)]
self.keyboard.start( ms, pitchL, None, argL[2] )
def keyboard_repeat_target_db( self, ms, argL ):
self.keyboard.repeat( ms, None, argL[0] )
def calibrate_keys_stop( self, ms ):
self.calibrate.stop(ms)
self.keyboard.stop(ms)
def quit( self, ms ):
if self.api:
@ -440,7 +472,11 @@ class Shell:
'a':{ "func":"audio_dev_list", "minN":0, "maxN":0, "help":"List the audio devices."},
'c':{ "func":"calibrate_keys_start", "minN":1, "maxN":2, "help":"Calibrate a range of keys. "},
's':{ "func":"calibrate_keys_stop", "minN":0, "maxN":0, "help":"Stop key calibration"},
'p':{ "func":"play_keys_start", "minN":1, "maxN":2, "help":"Play current calibration"}
'p':{ "func":"play_keys_start", "minN":1, "maxN":2, "help":"Play current calibration"},
'k':{ "func":"keyboard_start_pulse_idx", "minN":3, "maxN":3, "help":"Play pulse index across keyboard"},
'r':{ "func":"keyboard_repeat_pulse_idx", "minN":1, "maxN":1, "help":"Repeat pulse index across keyboard with new pulse_idx"},
'K':{ "func":"keyboard_start_target_db", "minN":3, "maxN":3, "help":"Play db across keyboard"},
'R':{ "func":"keyboard_repeat_target_db", "minN":1, "maxN":1, "help":"Repeat db across keyboard with new pulse_idx"},
}
def _help( self, _=None ):
@ -515,11 +551,9 @@ class Shell:
# tokenize the command
tokL = s.split(' ')
# execute the command
result = self._exec_cmd( tokL )
# if this is the 'quit' command
if tokL[0] == 'q':
break

View File

@ -21,7 +21,7 @@
outDir: "~/temp/p_ac_3c",
noteDurMs: 1000,
pauseDurMs: 1000,
holdDutyPctL: [ [0,50], [17000,65] ],
holdDutyPctL: [ [0,50], [22000,55] ],
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],
@ -35,9 +35,9 @@
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: [ 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_pulse7L: [ 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 ],
@ -56,7 +56,10 @@
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: 50, # minimum distance between sample points in microseconds
auditionNoteN: 19 # count of notes to play for audition
auditionNoteN: 19, # count of notes to play for audition
finalPulseListCacheFn: "/home/kevin/temp/final_pulse_list_cache.pickle",
rmsAnalysisCacheFn: "/home/kevin/temp/rms_analysis_cache.pickle"
},
key_mapL: [

File diff suppressed because one or more lines are too long

300
plot_note_analysis.py Normal file
View File

@ -0,0 +1,300 @@
import os,sys,pickle
import numpy as np
import matplotlib.pyplot as plt
import matplotlib._color_data as mcd
from matplotlib.pyplot import figure
from rms_analysis import rms_analysis_main
from rms_analysis import note_stats
from rms_analysis import key_info_dictionary
from rms_analysis import select_first_stable_note_by_delta_db
from rms_analysis import select_first_stable_note_by_dur
def do_plot(r, statsL ):
fig,ax = plt.subplots()
x = [ i / r.rms_srate for i in range(len(r.tdRmsDbV)) ]
ax.plot( x,r.tdRmsDbV, color="blue" )
x = [ i / r.rms_srate for i in range(len(r.rmsDbV)) ]
ax.plot( x,r.rmsDbV, color="green")
ymx = np.max(r.tdRmsDbV)
ymn = np.min(r.tdRmsDbV)
for r in statsL:
x = r.pkSmpSec
ax.axvline(x,ymax=ymx,ymin=ymn)
ax.text(x,r.pkDb+1,"%i ms" % r.durMs)
ax.text(x,r.pkDb+2,"%4.1f dB" % r.pkDb)
ax.text(x,r.pkDb+3,"%i us" % r.pulse_us)
if hasattr(r,"MIN"):
ax.plot(x,r.pkDb,marker="*",color="red")
plt.show()
def select_min_note( rr, statsL, minDurMs=600, minDb=8, contextSecs=10 ):
sel_note_r = None
for r in statsL:
if r.pkDb > minDb and r.durMs > minDurMs:
sel_note_r = r
break
#print(rr.tdRmsDbV.shape,rr.rmsDbV.shape)
if sel_note_r is None:
print("ERROR: No min note found.")
else:
#print(sel_note_r)
bi = max(0, int(round(sel_note_r.pkSmpSec * rr.rms_srate - contextSecs*rr.rms_srate)))
ei = min(rr.tdRmsDbV.shape[0],int(round(sel_note_r.pkSmpSec * rr.rms_srate + contextSecs*rr.rms_srate)))
rr.tdRmsDbV = rr.tdRmsDbV[bi:ei]
rr.rmsDbV = rr.rmsDbV[bi:ei]
offsSec = bi / rr.rms_srate
sL = []
for r in statsL:
begSmpIdx = int(round(r.begSmpSec * rr.rms_srate))
endSmpIdx = int(round(r.endSmpSec * rr.rms_srate))
if begSmpIdx > bi and endSmpIdx < ei:
r0 = r
r0.begSmpSec = r.begSmpSec - offsSec
r0.endSmpSec = r.endSmpSec - offsSec
r0.pkSmpSec = r.pkSmpSec - offsSec
if r.begSmpSec == sel_note_r.begSmpSec:
setattr(r0,"MIN",True)
sL.append(r0)
return rr,sL
def plot_note_analysis( inDir ):
rmsWndMs=300
rmsHopMs=30
dbRefWndMs=500
harmCandN=5
harmN=3
durDecayPct = 50
path = os.path.normpath(inDir)
pathL = inDir.split(os.sep)
take_id = int(pathL[-1])
midi_pitch = int(pathL[-2])
r = rms_analysis_main( inDir, midi_pitch, rmsWndMs=rmsWndMs, rmsHopMs=rmsHopMs, dbRefWndMs=dbRefWndMs, harmCandN=harmCandN, harmN=harmN, durDecayPct=durDecayPct )
r,statsL = select_min_note(r,r.statsL)
do_plot(r,statsL)
def plot_note_analysis_dir( inDir, dirL ):
for folder in dirL:
path = os.path.join(inDir,str(folder),"0")
plot_note_analysis( path )
def get_all_note_durations( inDir, cacheFn ):
folderL = os.listdir( inDir )
yL = []
for folder in folderL:
takeId = 0
rmsWndMs=300
rmsHopMs=30
dbRefWndMs=500
harmCandN=5
harmN=3
durDecayPct = 40
path = os.path.normpath(inDir)
midi_pitch = int( folder )
takePath = os.path.join(inDir,folder,str(takeId))
if os.path.isfile(os.path.join(takePath,'seq.json')):
print(midi_pitch)
r = rms_analysis_main( takePath, midi_pitch, rmsWndMs=rmsWndMs, rmsHopMs=rmsHopMs, dbRefWndMs=dbRefWndMs, harmCandN=harmCandN, harmN=harmN, durDecayPct=durDecayPct )
xL = []
for i,sr in enumerate(r.statsL):
xL.append((r.pkUsL[i],sr.durMs,sr.pkDb,sr.quality))
yL.append((midi_pitch,xL))
with open(cacheFn,"wb") as f:
pickle.dump(yL,f)
def plot_all_note_durations( cacheFn, pitchL=None, axN=12, yamlCfgFn=None, minDurMs=800, maxPulseUs=None ):
keyMapD = None
if yamlCfgFn is not None:
keyMapD = key_info_dictionary( keyMapL=None, yamlCfgFn=yamlCfgFn)
fig,axL = plt.subplots(axN,1)
fig.set_size_inches(18.5, 10.5*axN)
#cL = list(mcd.CSS4_COLORS.values())
cL = ['black','brown','orangered','saddlebrown','peru','olivedrab','lightgreen','springgreen','cadetblue','slategray','royalblue','navy','darkviolet','deeppink','crimson']
yD=[]
with open(cacheFn,"rb") as f:
yD = dict(pickle.load(f))
cn = 3 #min(1,len(cL)//len(yD))
ci = 0
i = 0
for midi_pitch in pitchL:
if (pitchL is not None and midi_pitch not in pitchL) or midi_pitch not in yD:
continue
xL = yD[midi_pitch]
pkUsL,durMsL,pkDbL,qualityL = tuple(zip(*xL))
if maxPulseUs is not None:
pkUsL = np.array(pkUsL)
pkUsL = pkUsL[ pkUsL < maxPulseUs ]
durMsL = durMsL[0:len(pkUsL)]
pkDbL = pkDbL[0:len(pkUsL)]
qualityL = qualityL[0:len(pkUsL)]
axi = i//(len(pitchL)//axN)
if keyMapD is None:
legendLabel = str(midi_pitch)
else:
legendLabel = getattr(keyMapD[midi_pitch],'type') + " " + getattr(keyMapD[midi_pitch],'class') + str(midi_pitch)
axL[axi].plot(pkUsL,durMsL,color=cL[ci],label=legendLabel)
# plot the quietest stable note
if minDurMs is not None:
sni = select_first_stable_note_by_dur( durMsL, minDurMs )
if sni is not None:
axL[axi].plot(pkUsL[sni],durMsL[sni],marker=".",color='red')
axL[axi].text(pkUsL[sni],durMsL[sni] + 50,"%4.1f" % pkDbL[sni])
sni = select_first_stable_note_by_delta_db( pkDbL, pkUsL )
if sni is not None:
axL[axi].plot(pkUsL[sni],durMsL[sni],marker=".",color='blue')
axL[axi].text(pkUsL[sni],durMsL[sni] + 50,"%4.1f" % pkDbL[sni])
ci += cn
if ci >= len(cL):
ci = ci - len(cL)
axL[axi].legend()
i+=1
plt.show()
def plot_quiet_note_db( cacheFn, yamlCfgFn, minDurMs=700 ):
keyMapD = key_info_dictionary( keyMapL=None, yamlCfgFn=yamlCfgFn)
yD=[]
with open(cacheFn,"rb") as f:
yD = dict(pickle.load(f))
dbL = []
for midi_pitch in range(24,108):
pk0Db = 0
pk1Db = 0
minDb = 0
if midi_pitch in yD:
xL = yD[midi_pitch]
pkUsL,durMsL,pkDbL,qualityL = tuple(zip(*xL))
# plot the quietest stable note
sni = select_first_stable_note_by_dur( durMsL, minDurMs )
if sni is not None:
pk0Db = pkDbL[sni]
sni = select_first_stable_note_by_delta_db( pkDbL, pkUsL )
if sni is not None:
pk1Db = pkDbL[sni]
minDb = min(pk0Db,pk1Db)
dbL.append( (midi_pitch, minDb, pk0Db,pk1Db) )
fig,ax = plt.subplots()
pitchL,minDbL,pk0DbL,pk1DbL = tuple(zip(*dbL))
ax.plot( pitchL, pk0DbL, label="dur" )
ax.plot( pitchL, pk1DbL, label="delta" )
#ax.plot( pitchL, minDbL, label="min" )
for i,pitch in enumerate(pitchL):
ax.text( pitch, pk0DbL[i]+1, "%i %s" % (pitch,getattr(keyMapD[pitch],'type')))
ax.axhline( np.mean(minDbL), label="mean", color="blue" )
ax.axhline( np.median(minDbL), label="median", color="green" )
ax.legend()
plt.show()
if __name__ == "__main__":
#inDir = sys.argv[1]
#plot_note_analysis( inDir )
pitchL = [ 30,31,32,33,34,35 ]
pitchL = [ 70,71,72,73,74,75 ]
#plot_note_analysis_dir( "/home/kevin/temp/p_ac_3c",pitchL)
durFn = "/home/kevin/temp/cache_note_dur.pickle"
#get_all_note_durations("/home/kevin/temp/p_ac_3c",durFn)
#plot_all_note_durations(durFn, np.arange(45,55),2,"p_ac.yml",800,20000)
plot_quiet_note_db(durFn,"p_ac.yml")

View File

@ -3,6 +3,8 @@ import matplotlib.pyplot as plt
import numpy as np
from common import parse_yaml_cfg
from rms_analysis import rms_analysis_main
from rms_analysis import select_first_stable_note_by_delta_db
from rms_analysis import select_first_stable_note_by_dur
def is_nanV( xV ):
@ -29,29 +31,6 @@ def form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ):
# append the midi pitch to the input directory
#inDir = os.path.join( inDir, "%i" % (midi_pitch))
if False:
# determine the take id if none was given
if take_id is None:
take_id = _find_max_take_id( inDir )
inDir = os.path.join(inDir,"%i" % (take_id))
assert( os.path.isdir(inDir))
# analyze the requested take audio
r = rms_analysis_main( inDir, midi_pitch, **analysisArgsD['rmsAnalysisArgs'] )
pkL = []
# store the peaks in pkL[ (db,us) ]
for db,us in zip(r.pkDbL,r.pkUsL):
pkL.append( (db,us) )
# sort the peaks on increasing attack pulse microseconds
pkL = sorted( pkL, key= lambda x: x[1] )
# split pkL
pkDbL,pkUsL = tuple(zip(*pkL))
dirL = os.listdir(inDir)
pkL = []
@ -123,7 +102,7 @@ def form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ):
# print("Multi-value pulse locations were found during velocity table formation: ",multValL)
pass
return pulseUsL,pulseDbL
return pulseUsL,pulseDbL,r.holdDutyPctL
@ -132,7 +111,7 @@ def merge_close_sample_points( pkDbUsL, minSampleDistanceUs ):
avg0Us = np.mean(np.diff([ x[1] for x in pkDbUsL ]))
n0 = len(pkDbUsL)
while True:
while True and n0>0:
us0 = None
db0 = None
@ -262,7 +241,7 @@ def plot_resample_pulse_times( inDir, analysisArgsD ):
newPulseUsL, rmsDbV, pulseUsL = form_resample_pulse_time_list( inDir, analysisArgsD )
midi_pitch = int( inDir.split("/")[-1] )
velTblUsL,velTblDbL = form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None )
velTblUsL,velTblDbL,_ = form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None )
fig,ax = plt.subplots()
@ -432,8 +411,14 @@ def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ):
# print beg/end boundaries
for i,(begMs, endMs) in enumerate(r.eventTimeL):
pkSec = r.pkIdxL[i] / r.rms_srate
endSec = pkSec + r.statsL[i].durMs / 1000.0
ax.axvline( x=begMs/1000.0, color="green")
ax.axvline( x=endMs/1000.0, color="red")
ax.axvline( x=pkSec, color="black")
ax.axvline( x=endSec, color="black")
ax.text(begMs/1000.0, 20.0, str(i) )
@ -449,9 +434,11 @@ def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ):
return r
def do_td_plot( inDir, analysisArgs ):
fig,axL = plt.subplots(2,1)
fig,axL = plt.subplots(3,1)
fig.set_size_inches(18.5, 10.5, forward=True)
id = int(inDir.split("/")[-1])
@ -459,7 +446,40 @@ def do_td_plot( inDir, analysisArgs ):
r = td_plot(axL[0],inDir,midi_pitch,id,analysisArgs)
axL[1].plot( r.pkUsL, r.pkDbL, marker='.' )
qualityV = np.array([ x.quality for x in r.statsL ]) * np.max(r.pkDbL)
durMsV = np.array([ x.durMs for x in r.statsL ])
avgV = np.array([ x.durAvgDb for x in r.statsL ])
#durMsV[ durMsV < 400 ] = 0
#durMsV = durMsV * np.max(r.pkDbL)/np.max(durMsV)
#durMsV = durMsV / 100.0
dV = np.diff(r.pkDbL) / r.pkDbL[1:]
axL[1].plot( r.pkUsL, r.pkDbL, marker='.',label="pkDb" )
axL[1].plot( r.pkUsL, qualityV, marker='.',label="quality" )
axL[1].plot( r.pkUsL, avgV, marker='.',label="avgDb" )
#axL[2].plot( r.pkUsL, durMsV, marker='.' )
axL[2].plot( r.pkUsL[1:], dV, marker='.',label='d')
axL[2].set_ylim([-1,1])
axL[1].legend()
sni = select_first_stable_note_by_dur( durMsV )
if sni is not None:
axL[1].plot( r.pkUsL[sni], r.pkDbL[sni], marker='*', color='red')
sni = select_first_stable_note_by_delta_db( r.pkDbL )
if sni is not None:
axL[2].plot( r.pkUsL[sni], dV[sni-1], marker='*', color='red')
for i,s in enumerate(r.statsL):
axL[1].text( r.pkUsL[i], r.pkDbL[i] + 1, "%i" % (i))
for i in range(1,len(r.pkUsL)):
axL[2].text( r.pkUsL[i], dV[i-1], "%i" % (i))
plt.show()

View File

@ -1,7 +1,8 @@
import os,types,json
import os,types,json,pickle
from scipy.io import wavfile
from scipy.signal import stft
import numpy as np
from common import parse_yaml_cfg
def calc_harm_bins( srate, binHz, midiPitch, harmN ):
@ -92,6 +93,8 @@ def audio_harm_rms( srate, xV, rmsWndMs, hopMs, dbRefWndMs, midiPitch, harmCandN
binHz = srate / wndSmpN
#print( "STFT:", rmsWndMs, hopMs, wndSmpN, hopSmpN, wndSmpN-hopSmpN )
f,t,xM = stft( xV, fs=srate, window="hann", nperseg=wndSmpN, noverlap=wndSmpN-hopSmpN, return_onesided=True )
harmLBinL,harmMBinL,harmUBinL = calc_harm_bins( srate, binHz, midiPitch, harmCandN )
@ -115,6 +118,119 @@ def audio_harm_rms( srate, xV, rmsWndMs, hopMs, dbRefWndMs, midiPitch, harmCandN
rmsV = rms_to_db( rmsV, rms_srate, dbRefWndMs )
return rmsV, rms_srate, binHz
def measure_duration_ms( rmsV, rms_srate, peak_idx, end_idx, decay_pct ):
"""
Calcuate the time it takes for a note to decay from the peak at
rmsV[peak_idx] dB to 'decay_pct' percent of the peak value.
"""
pkRmsDb = rmsV[ peak_idx ]
# calc the note turn-off (offset) db as a percentage of the peak amplitude
offsetRmsDb = pkRmsDb * decay_pct / 100.0
# calc the sample index where the note is off
offset_idx = peak_idx + np.argmin( np.abs(rmsV[peak_idx:end_idx] - offsetRmsDb) )
# calc the duration of the note
dur_ms = int(round((offset_idx - peak_idx) * 1000.0 / rms_srate))
#print(pkRmsDb, offsetRmsDb, peak_idx, offset_idx, end_idx, dur_ms, rms_srate)
return dur_ms
def select_first_stable_note_by_dur( durMsL, minDurMs=800 ):
first_stable_idx = None
for i,durMs in enumerate(durMsL):
if durMs > minDurMs and first_stable_idx is None:
first_stable_idx = i
else:
if durMs < minDurMs:
first_stable_idx = None
return first_stable_idx
def select_first_stable_note_by_delta_db_1( pkDbL, pkUsL, maxPulseUs=0.1 ):
wndN = 5
aL = []
dV = np.diff(pkDbL) / pkDbL[1:]
for ei in range(wndN,len(pkDbL)):
xV = dV[ei-wndN:ei]
avg = np.mean(np.abs(xV))
aL.append(avg)
k = np.argmin(np.abs(np.array(pkUsL) - maxPulseUs))
print(aL)
print(k)
for i in range(k,0,-1):
if aL[i] > maxDeltaDb:
return i + 1
return None
def select_first_stable_note_by_delta_db( pkDbL, pkUsL=None, maxPulseUs=0.1 ):
wndN = 5
dV = np.diff(pkDbL) / pkDbL[1:]
for ei in range(wndN,len(pkDbL)):
xV = dV[ei-wndN:ei]
avg = np.mean(np.abs(xV))
if avg < .1:
return (ei-wndN)+1
return None
def note_stats( r, decay_pct=50.0, extraDurSearchMs=500 ):
statsL = []
srate = r.rms_srate
qmax = 0
for i,(begSmpMs, endSmpMs) in enumerate(r.eventTimeL):
begSmpIdx = int(round(srate * begSmpMs / 1000.0))
endSmpIdx = int(round(srate * (endSmpMs + extraDurSearchMs) / 1000.0))
pkSmpIdx = r.pkIdxL[i]
durMs = measure_duration_ms( r.rmsDbV, srate, pkSmpIdx, endSmpIdx, decay_pct )
bi = pkSmpIdx
ei = pkSmpIdx + int(round(durMs * srate / 1000.0))
#bi = begSmpIdx
#ei = endSmpIdx
qualityCoeff = np.sum(r.rmsDbV[bi:ei]) + np.sum(r.tdRmsDbV[bi:ei])
if qualityCoeff > qmax:
qmax = qualityCoeff
durAvgDb = (np.mean(r.rmsDbV[bi:ei]) + np.mean(r.tdRmsDbV[bi:ei]))/2.0
statsL.append( types.SimpleNamespace(**{'begSmpSec':begSmpIdx/srate,'endSmpSec':endSmpIdx/srate,'pkSmpSec':pkSmpIdx/srate,'durMs':durMs, 'pkDb':r.pkDbL[i], 'pulse_us':r.pkUsL[i], 'quality':qualityCoeff, 'durAvgDb':durAvgDb }))
for i,r in enumerate(statsL):
statsL[i].quality /= qmax
return statsL
def locate_peak_indexes( xV, xV_srate, eventMsL ):
@ -130,10 +246,21 @@ def locate_peak_indexes( xV, xV_srate, eventMsL ):
return pkIdxL
def key_info_dictionary( keyMapL=None, yamlCfgFn=None):
if yamlCfgFn is not None:
cfg = parse_yaml_cfg(yamlCfgFn)
keyMapL = cfg.key_mapL
kmD = {}
for d in keyMapL:
kmD[ d['midi'] ] = types.SimpleNamespace(**d)
return kmD
def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=500, harmCandN=5, harmN=3 ):
def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=500, harmCandN=5, harmN=3, durDecayPct=40 ):
seqFn = os.path.join( inDir, "seq.json")
audioFn = os.path.join( inDir, "audio.wav")
@ -154,6 +281,15 @@ def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=
pkIdxL = locate_peak_indexes( rmsDbV, rms_srate, r['eventTimeL'] )
holdDutyPctL = None
if 'holdDutyPct' in r:
holdDutyPctL = [ (0, r['holdDutyPct']) ]
else:
holdDutyPctL = r['holdDutyPctL']
r = types.SimpleNamespace(**{
"audio_srate":srate,
"tdRmsDbV": tdRmsDbV,
@ -166,7 +302,48 @@ def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=
#"min_pk_idx":min_pk_idx,
#"max_pk_idx":max_pk_idx,
"eventTimeL":r['eventTimeL'],
"holdDutyPctL":holdDutyPctL,
'pkDbL': [ rmsDbV[ i ] for i in pkIdxL ],
'pkUsL':r['pulseUsL'] })
statsL = note_stats(r,durDecayPct)
setattr(r,"statsL", statsL )
return r
def rms_analysis_main_all( inDir, cacheFn, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=500, harmCandN=5, harmN=3, durDecayPct=40 ):
if os.path.isfile(cacheFn):
print("READING analysis cache file: %s" % (cacheFn))
with open(cacheFn,"rb") as f:
rD = pickle.load(f)
return rD
folderL = os.listdir(inDir)
rD = {}
for folder in folderL:
pathL = folder.split(os.sep)
midi_pitch = int(pathL[-1])
print(midi_pitch)
path = os.path.join(inDir,folder,'0')
if os.path.isdir(path) and os.path.isfile(os.path.join(os.path.join(path,"seq.json"))):
r = rms_analysis_main( path, midi_pitch, rmsWndMs=rmsWndMs, rmsHopMs=rmsHopMs, dbRefWndMs=dbRefWndMs, harmCandN=harmCandN, harmN=harmN, durDecayPct=durDecayPct )
rD[ midi_pitch ] = r
with open(cacheFn,"wb") as f:
pickle.dump(rD,f)
return rD

76
rt_note_analysis.py Normal file
View File

@ -0,0 +1,76 @@
import types
import numpy as np
from rms_analysis import audio_harm_rms
from rms_analysis import audio_rms
from rms_analysis import locate_peak_indexes
from rms_analysis import measure_duration_ms
class RT_Analyzer:
def __init__(self):
self.td_dur_ms = 0
self.td_db = 0
self.hm_dur_ms = 0
self.hm_db = 0
def analyze_note( self, audioDev, midi_pitch, begTimeMs, endTimeMs, anlArgD ):
td_dur_ms = 0
td_db = 0
hm_dur_ms = 0
hm_db = 0
decay_pct = 50.0
result = audioDev.linear_buffer()
if result:
sigV = result.value
anlArgs = types.SimpleNamespace(**anlArgD)
rmsDbV, rms_srate, binHz = audio_harm_rms( audioDev.srate, np.squeeze(sigV), anlArgs.rmsWndMs, anlArgs.rmsHopMs, anlArgs.dbRefWndMs, midi_pitch, anlArgs.harmCandN, anlArgs.harmN )
pkIdxL = locate_peak_indexes( rmsDbV, rms_srate, [( begTimeMs, endTimeMs)] )
if len(pkIdxL) > 0:
end_idx = int(round(endTimeMs * rms_srate / 1000.0))
if end_idx > pkIdxL[0]:
hm_dur_ms = measure_duration_ms( rmsDbV, rms_srate, pkIdxL[0], end_idx, decay_pct )
hm_db = rmsDbV[ pkIdxL[0] ]
tdRmsDbV, rms0_srate = audio_rms( audioDev.srate, np.squeeze(sigV), anlArgs.rmsWndMs, anlArgs.rmsHopMs, anlArgs.dbRefWndMs )
tdPkIdxL = locate_peak_indexes( tdRmsDbV, rms0_srate, [( begTimeMs, endTimeMs)] )
if len(tdPkIdxL):
end_idx = int(round(endTimeMs * rms0_srate / 1000.0))
if end_idx > tdPkIdxL[0]:
td_dur_ms = measure_duration_ms( tdRmsDbV, rms0_srate, tdPkIdxL[0], end_idx, decay_pct )
td_db = tdRmsDbV[ tdPkIdxL[0] ]
td_d_ms = td_dur_ms - self.td_dur_ms
td_d_db = td_db - self.td_db
hm_d_ms = hm_dur_ms - self.hm_dur_ms
hm_d_db = hm_db - self.hm_db
print("DUR: %5.2f %5.2f d:%5.2f %5.2f dB | %i %i d:%i %i ms" % (hm_db, td_db, hm_d_db, td_d_db, hm_dur_ms, td_dur_ms, hm_d_ms, td_d_ms) )
self.td_dur_ms = td_dur_ms
self.td_db = td_db
self.hm_dur_ms = hm_dur_ms
self.hm_db = hm_db