README.md,doc/*.png : Updates to verify plotting and add example plots to README.md

This commit is contained in:
kevin 2020-11-25 14:44:19 -05:00
parent c017cafe51
commit ee3c41fe70
14 changed files with 490 additions and 6222 deletions

View File

@ -34,12 +34,51 @@ python calibrate_plot.py ~/temp/p_ac_3_oa/60/2 p_ac.yml 60
![Plot Seq 1](doc/plot_seq_0.png) ![Plot Seq 1](doc/do_td_plot.png)
`python plot_seq.py ~/temp/p_ac_3_od p_ac.yml 60 10`
`python plot_seq.py ~/temp/p_ac_3_od/60 p_ac.yml 12` `do_td_plot(inDir,cfg.analysisArgs, pitch, take_id )`
![Multi Plot 1](doc/multi_plot.png) ![Multi Plot 1](doc/multi_plot.png)
`python plot_seq.py p_ac.yml ~/temp/p_ac_3_od td_multi_plot 60 3 60 4 60 5`
plot_seq.py `do_td_multi_plot(inDir,cfg.analysisArgs,[(36,4), (48,2)] )` plot_seq.py `do_td_multi_plot(inDir,cfg.analysisArgs,[(36,4), (48,2)] )`
![Spectral Ranges](doc/plot_spectral_ranges.png)
`python plot_seq.py p_ac.yml ~/temp/p_ac_3_od plot_spectral_ranges 60 3 60 4`
![Multi Usec dB](doc/us_db.png)
`python plot_seq_1.py p_ac.yml ~/temp/p_ac_3_od us_db 84`
![Usec dB Spread](doc/us_db_map.png)
`python plot_seq_1.py p_ac.yml ~/temp/p_ac_3_od us_db_map 84 72`
![Min Max](doc/min_max_db.png)
`python plot_seq_1.py p_ac.yml ~/temp/p_ac_3_od min_max 36 48 60 72 84`
![Min Max 2](doc/min_max_db_2.png)
The last number in the list is the 'takeId'
`python plot_seq_1.py p_ac.yml ~/temp/p_ac_3_od min_max_2 36 48 60 72 84 2`
`python plot_seq_1.py p_ac.yml ~/temp/p_ac_3_od manual_db`
Interpolate across the min and max db values to form the min/max curves for the complete
set of keys. The anchor points for the curves are taken from cfg record
manuMinD,manualAnchorPitchMinDbL, and manualMaxDbL.
````
# select the event (takeId, eventIdx) to use to represent the min value for each pitch
manualMinD: {
36: [2, 10],
48: [2, 10],
60: [2, 10],
72: [2, 10],
84: [2, 10]
},
# leave 60 out of the min anchor point list
manualAnchorPitchMinDbL: [ 36,48,72,84 ],
manualAnchorPitchMaxDbL: [ 36,48,60,72,84 ],
````

BIN
do_td_plot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
doc/do_td_plot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
doc/manual_db.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
doc/min_max_db_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 83 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
doc/us_db_map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

156
p_ac.yml
View File

@ -3,8 +3,8 @@
# Audio device setup # Audio device setup
audio_off: { audio: {
inPortLabel: "5 USB Audio CODEC:", #"HDA Intel PCH: CS4208", # "5 USB Audio CODEC:", #"5 USB Sound Device", inPortLabel: "8 USB Audio CODEC:", #"HDA Intel PCH: CS4208", # "5 USB Audio CODEC:", #"5 USB Sound Device",
outPortLabel: , outPortLabel: ,
}, },
@ -12,10 +12,10 @@
inMonitorFl: False, inMonitorFl: False,
outMonitorFl: False, outMonitorFl: False,
throughFl: False, throughFl: False,
#inPortLabel: "Fastlane:Fastlane MIDI A", inPortLabel: "Fastlane:Fastlane MIDI A",
#outPortLabel: "Fastlane:Fastlane MIDI A" outPortLabel: "Fastlane:Fastlane MIDI A"
inPortLabel: "picadae:picadae MIDI 1", #inPortLabel: "picadae:picadae MIDI 1",
outPortLabel: "picadae:picadae MIDI 1" #outPortLabel: "picadae:picadae MIDI 1"
}, },
# Picadae API args # Picadae API args
@ -23,18 +23,19 @@
serial_baud: 38400, serial_baud: 38400,
i2c_base_addr: 21, i2c_base_addr: 21,
prescaler_usec: 16, prescaler_usec: 16,
pwm_div: 5,
serial_sync_timeout_ms: 10000, serial_sync_timeout_ms: 10000,
# MeasureSeq args # MeasureSeq args
outDir: "~/temp/p_ac_3g", outDir: "~/temp/p_ac_3_oe",
noteDurMs: 500, noteDurMs: 500,
pauseDurMs: 500, pauseDurMs: 500,
reversePulseListFl: True, reversePulseListFl: True,
useFullPulseListFl: True, useFullPulseListFl: True,
maxSilentNoteCount: 4, maxSilentNoteCount: 4,
silentNoteMaxPulseUs: 15000, silentNoteMaxPulseUs: 15000,
silentNoteMinDurMs: 250, silentNoteMinDurMs: 180, #250,
# Midi file player # Midi file player
midiFileFn: "/home/kevin/media/audio/midi/txt/round4.txt", midiFileFn: "/home/kevin/media/audio/midi/txt/round4.txt",
@ -57,8 +58,8 @@
full_pulse8L: [ 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_pulse8L: [ 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 was the last pulse list used on Matt's piano
full_pulseL: [11000, 11075, 11150, 11225, 11300, 11375, 11450, 11525, 11600,11675, 11750, 11825, 11900, 11975, 12050, 12125, 12200, 12275,12350, 12425, 12500, 12575, 12650, 12725, 12800, 12875, 12950, 13025, 13100, 13175, 13250, 13325, 13400, 13475, 13550, 13625, 13700, 13775, 13850, 13925, 14000, 14075, 14150, 14225, 14300, 14375, 14450, 14525, 14600, 14675, 14750, 14825, 14900, 14975], full_pulse9L: [11000, 11075, 11150, 11225, 11300, 11375, 11450, 11525, 11600,11675, 11750, 11825, 11900, 11975, 12050, 12125, 12200, 12275,12350, 12425, 12500, 12575, 12650, 12725, 12800, 12875, 12950, 13025, 13100, 13175, 13250, 13325, 13400, 13475, 13550, 13625, 13700, 13775, 13850, 13925, 14000, 14075, 14150, 14225, 14300, 14375, 14450, 14525, 14600, 14675, 14750, 14825, 14900, 14975],
full_pulse10L: [ 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 ], full_pulse10L: [ 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 ],
@ -66,6 +67,35 @@
full_pulse12L: [ 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 ], full_pulse12L: [ 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 ],
# pulse lists below this line were developed on Steinway D (11/14/20)
# 60
full_pulse13L: [8000, 8250, 8500, 8750, 9000, 9250, 9500, 9750, 10000, 10250, 10500, 10750, 11000, 11250, 11500, 11750, 12000, 12250, 12500, 12750, 13000, 13500, 14000, 14500, 15000, 15500, 16000, 16500, 17000, 17500, 18000, 18500, 19000, 19500, 20000 ],
full_pulse14L: [ 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, 12250, 12500, 12750, 13000, 13500, 14000, 14500, 15000, 15500, 16000, 16500, 17000, 17500, 18000, 18500, 19000, 19500, 20000, 20500, 21000, 21500, 22000 ],
full_pulse15L: [ 8000, 9000,10000, 11000, 12000, 13000, 14000, 15000, 16000, 17000, 18000, 19000, 20000 ],
full_pulse16L: [ 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, 12250, 12500, 12750, 13000, 13500, 14000, 14500, 15000, 15500, 16000, 16500, 17000, 17500, 18000, 18500, 19000, 19500, 20000, 20500, 21000, 21500, 22000 ],
full_pulse17L: [ 8100, 8150, 8200, 8250, 8300, 8350, 8400, 8450, 8500, 8550, 8600, 8650, 8700, 8750, 8800, 8850, 8900, 8950, 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, 12250, 12500, 12750, 13000, 13500, 14000, 14500, 15000, 15500, 16000, 16500, 17000, 17500, 18000, 18500, 19000, 19500, 20000, 20500, 21000, 21500, 22000 ],
full_pulse18L: [ 11500, 11625, 11750, 11875, 12000, 12250, 12500, 12750, 13000, 13500, 14000, 14500, 15000, 15500 ],
full_pulse19L: [ 11875, 12000, 12125, 12250, 12375, 12500, 12625, 12750, 12875, 13000, 13250, 13500, 13750, 14000, 14500, 15000, 15500, 16000, 16500 ],
# pulse lists below this line were developed on Steinway D w/ new firmware (11/21/20)
full_pulse20L: [ 1000, 1250, 1500, 1750, 2000, 2250, 2500, 2750, 3000, 3250, 3500, 3750, 4000, 4250, 4500, 4750, 5000, 5250, 5500, 5750, 6000, 6250, 6500, 6750, 7000, 7250, 7500, 7750, 8000, 8250, 8500, 8750, 9000, 9250, 9500, 9750, 10000, 10250, 10500, 10750, 11000, 11250, 11500, 11750, 12000, 12250, 12500, 12750, 13000, 13500, 14000, 14500, 15000, 15500 ],
# 60,72
full_pulse21L: [ 1000, 1250, 1500, 1750, 2000, 2250, 2500, 2750, 3000, 3250, 3500, 3750, 4000, 4500, 5000, 5500, 6000, 6500, 7000, 7500, 8000, 8500, 9000, 10000, 11000, 12000, 13000, 14000, 15000, 16000, 17000, 18000, 19000 ],
# 48,36,85
full_pulse22L: [ 3500, 3750, 4000, 4250, 4500, 4750, 5000, 5250, 5500, 5750, 6000, 6250, 6500, 6750, 7000, 7500, 8000, 8500, 9000, 9500, 10000, 10500, 11000, 12000, 12500, 13000, 14000, 15000, 16000, 17000, 18000, 19000 ],
# 84
full_pulseL: [ 2000, 2250, 2500,2750, 3000, 3250, 3500, 3750, 4000, 4250, 4500, 4750, 5000, 5250, 5500, 5750, 6000, 6250, 6500, 6750, 7000, 7500, 8000, 8500, 9000, 9500, 10000, 10500, 11000, 12000, 12500, 13000, 14000, 15000, 16000 ],
# RMS analysis args # RMS analysis args
analysisArgs: { analysisArgs: {
rmsAnalysisArgs: { rmsAnalysisArgs: {
@ -77,11 +107,12 @@
durDecayPct: 40, # percent drop in RMS to indicate the end of a note durDecayPct: 40, # percent drop in RMS to indicate the end of a note
}, },
resampleMinDb: 7.0, # note's less than this will be skipped resampleMinDb: -5.0, # note's less than this will be skipped
resampleNoiseLimitPct: 5.0, # resampleNoiseLimitPct: 5.0, #
resampleMinDurMs: 800, # notes's whose duration is less than this will be skipped resampleMinDurMs: 150, # notes's whose duration is less than this will be skipped
minAttkDb: 7.0, # threshold of silence level useLastTakeOnlyFl: True,
minAttkDb: -5.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 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) 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 samplesPerDb: 4, # count of samples per dB to resample ranges whose range is less than maxDeltaDb
@ -93,6 +124,18 @@
}, },
manualMinD: { manualMinD: {
36: [2, 10],
48: [2, 10],
60: [2, 10],
72: [2, 10],
84: [2, 10]
},
manualAnchorPitchMinDbL: [ 36,48,72,84 ],
manualAnchorPitchMaxDbL: [ 36,48,60,72,84 ],
manualMinD_0: {
23: [2, 24], 23: [2, 24],
24: [2, 18], 24: [2, 18],
25: [2, 41], 25: [2, 41],
@ -175,8 +218,8 @@
}, },
manualAnchorPitchMinDbL: [ 23, 27, 31, 34, 44, 51, 61, 70, 74, 81, 87, 93, 96, 101 ], manualAnchorPitchMinDbL_0: [ 23, 27, 31, 34, 44, 51, 61, 70, 74, 81, 87, 93, 96, 101 ],
manualAnchorPitchMaxDbL: [ 23, 32, 49, 57, 67, 76, 83, 93, 99, 101 ], manualAnchorPitchMaxDbL_0: [ 23, 32, 49, 57, 67, 76, 83, 93, 99, 101 ],
calibrateArgs: { calibrateArgs: {
@ -213,6 +256,89 @@
dbSrcLabel: 'hm', # source of the db measurement 'td' (time-domain) or 'hm' (harmonic) dbSrcLabel: 'hm', # source of the db measurement 'td' (time-domain) or 'hm' (harmonic)
holdDutyPctD: { holdDutyPctD: {
23: [[0, 40]],
24: [[0, 40]],
25: [[0, 40]],
26: [[0, 40]],
27: [[0, 40]],
28: [[0, 40]],
29: [[0, 40]],
30: [[0, 40]],
31: [[0, 40]],
32: [[0, 40]],
33: [[0, 40]],
34: [[0, 40]],
35: [[0, 40]],
36: [[0, 45]],
37: [[0, 40]],
38: [[0, 40]],
39: [[0, 40]],
40: [[0, 40]],
41: [[0, 40]],
42: [[0, 40]],
43: [[0, 40]],
44: [[0, 40]],
45: [[0, 40]],
46: [[0, 40]],
47: [[0, 40]],
48: [[0, 40]],
49: [[0, 40]],
50: [[0, 40]],
51: [[0, 40]],
52: [[0, 40]],
53: [[0, 40]],
54: [[0, 40]],
55: [[0, 40]],
56: [[0, 40]],
57: [[0, 40]],
58: [[0, 40]],
59: [[0, 40]],
60: [[0, 50]],
61: [[0, 43]],
62: [[0, 43]],
63: [[0, 43]],
64: [[0, 40],[14000, 45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
65: [[0, 99]],
66: [[0, 40]],
67: [[0, 40],[14000, 45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
68: [[0, 40],[14000, 45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
69: [[0, 40],[14000, 45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
70: [[0, 40],[14000, 45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
71: [[0, 40],[14000, 45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
72: [[0, 45],[11000, 65] ],
73: [[0, 45],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
74: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
75: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
76: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
77: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
78: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
79: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
80: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
81: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
82: [[0, 40],[14000,45],[15000,50],[16000,55],[17000,60],[18000,65],[19000,55]],
83: [[0, 42]],
84: [[0, 42],[10000,50],[11000,60]],
85: [[0, 42],[9000,45],[10000,50],[11000,60]],
86: [[0, 40]],
87: [[0, 40]],
88: [[0, 40]],
89: [[0, 40]],
91: [[0, 40]],
92: [[0, 40]],
93: [[0, 40]],
94: [[0, 40]],
95: [[0, 40]],
96: [[0, 40]],
97: [[0, 40]],
98: [[0, 40]],
99: [[0, 40]],
100: [[0, 40]],
101: [[0, 40]],
106: [[0, 40]]
},
# Final for Matt's piano
holdDutyPct0D: {
23: [[0, 70]], 23: [[0, 70]],
24: [[0, 75]], 24: [[0, 75]],
25: [[0, 70]], 25: [[0, 70]],

View File

@ -1,14 +1,22 @@
##| Copyright: (C) 2019-2020 Kevin Larke <contact AT larke DOT org> ##| Copyright: (C) 2019-2020 Kevin Larke <contact AT larke DOT org>
##| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file. ##| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file.
import os, sys import os, sys,json
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
from scipy.io import wavfile
from common import parse_yaml_cfg from common import parse_yaml_cfg
from rms_analysis import rms_analysis_main 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_delta_db
from rms_analysis import select_first_stable_note_by_dur from rms_analysis import select_first_stable_note_by_dur
from rms_analysis import samples_to_linear_residual from rms_analysis import samples_to_linear_residual
import rms_analysis as ra
#from rms_analysis import audio_rms
#from rms_analysis import locate_peak_indexes
#from rms_analysis import audio_stft_rms
#from rms_analysis import calc_harm_bins
def is_nanV( xV ): def is_nanV( xV ):
for i in range(xV.shape[0]): for i in range(xV.shape[0]):
@ -31,13 +39,16 @@ def _find_max_take_id( inDir ):
def form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ): def form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ):
# append the midi pitch to the input directory # append the midi pitch to the input directory
#inDir = os.path.join( inDir, "%i" % (midi_pitch)) inDir = os.path.join( inDir, str(midi_pitch))
dirL = os.listdir(inDir) dirL = os.listdir(inDir)
pkL = [] pkL = []
maxTakeNumber = 0
# for each take in this directory # for each take in this directory
for idir in dirL: for idir in dirL:
@ -47,6 +58,14 @@ def form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ):
if not os.path.isfile(os.path.join( inDir,idir, "seq.json")): if not os.path.isfile(os.path.join( inDir,idir, "seq.json")):
continue continue
if analysisArgsD['useLastTakeOnlyFl']:
if take_number > maxTakeNumber:
pkL.clear()
maxTakeNumber = take_number
else:
continue
# analyze this takes audio and locate the note peaks # analyze this takes audio and locate the note peaks
r = rms_analysis_main( os.path.join(inDir,idir), midi_pitch, **analysisArgsD['rmsAnalysisArgs'] ) r = rms_analysis_main( os.path.join(inDir,idir), midi_pitch, **analysisArgsD['rmsAnalysisArgs'] )
@ -68,6 +87,8 @@ def form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ):
# locate the first and last note # locate the first and last note
min_pk_idx, max_pk_idx = find_min_max_peak_index( pkDbL, analysisArgsD['minAttkDb'], analysisArgsD['maxDbOffset'] ) min_pk_idx, max_pk_idx = find_min_max_peak_index( pkDbL, analysisArgsD['minAttkDb'], analysisArgsD['maxDbOffset'] )
print("MIN MAX:",min_pk_idx,pkUsL[min_pk_idx],max_pk_idx,pkUsL[max_pk_idx])
db1 = pkDbL[ max_pk_idx ] db1 = pkDbL[ max_pk_idx ]
db0 = pkDbL[ min_pk_idx ] db0 = pkDbL[ min_pk_idx ]
@ -195,14 +216,12 @@ def calc_resample_ranges( pkDbL, pkUsL, min_pk_idx, max_pk_idx, maxDeltaDb, samp
def form_resample_pulse_time_list( inDir, analysisArgsD ): def form_resample_pulse_time_list( inDir, midi_pitch, analysisArgsD ):
"""" This function merges all available data from previous takes to form """" This function merges all available data from previous takes to form
a new list of pulse times to sample. a new list of pulse times to sample.
""" """
# the last folder is always the midi pitch of the note under analysis inDir = os.path.join( inDir, str(midi_pitch) )
midi_pitch = int( inDir.split("/")[-1] )
dirL = os.listdir(inDir) dirL = os.listdir(inDir)
pkL = [] pkL = []
@ -248,11 +267,10 @@ def plot_curve( ax, pulseUsL, rmsDbV ):
ax.plot( pulseUsL, func(pulseUsL), color='red') ax.plot( pulseUsL, func(pulseUsL), color='red')
def plot_resample_pulse_times_0( inDir, analysisArgsD ): def plot_resample_pulse_times_0( inDir, analysisArgsD, midi_pitch, printDir="" ):
newPulseUsL, rmsDbV, pulseUsL = form_resample_pulse_time_list( inDir, analysisArgsD ) newPulseUsL, rmsDbV, pulseUsL = form_resample_pulse_time_list( inDir, midi_pitch, 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() fig,ax = plt.subplots()
@ -262,16 +280,18 @@ def plot_resample_pulse_times_0( inDir, analysisArgsD ):
for us in newPulseUsL: for us in newPulseUsL:
ax.axvline( x = us ) ax.axvline( x = us )
ax.plot(velTblUsL,velTblDbL,marker='.',linestyle='None') print(len(velTblUsL))
ax.plot(velTblUsL,velTblDbL,marker='.',linestyle='None',color='red')
if printDir:
plt.savefig(os.path.join(printDir,"plot_resample_times_0.png"),format="png")
plt.show() plt.show()
def plot_resample_pulse_times( inDir, analysisArgsD ): def plot_resample_pulse_times( inDir, analysisArgsD, midi_pitch, printDir="" ):
newPulseUsL, rmsDbV, pulseUsL = form_resample_pulse_time_list( inDir, analysisArgsD ) newPulseUsL, rmsDbV, pulseUsL = form_resample_pulse_time_list( inDir, midi_pitch, 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,axL = plt.subplots(2,1,gridspec_kw={'height_ratios': [2, 1]}) fig,axL = plt.subplots(2,1,gridspec_kw={'height_ratios': [2, 1]})
@ -290,6 +310,10 @@ def plot_resample_pulse_times( inDir, analysisArgsD ):
axL[1].axhline(1.0,color='black') axL[1].axhline(1.0,color='black')
axL[1].plot(pulseUsL,np.abs(scoreV * 100.0 / rmsDbV)) axL[1].plot(pulseUsL,np.abs(scoreV * 100.0 / rmsDbV))
axL[1].set_ylim((0.0,50)) axL[1].set_ylim((0.0,50))
if printDir:
plt.savefig(os.path.join(printDir,"plot_resample_times.png"),format="png")
plt.show() plt.show()
@ -369,7 +393,7 @@ def plot_spectrum( ax, srate, binHz, specV, midiPitch, harmN ):
""" Plot a single spectrum, 'specV' and the harmonic peak location boundaries.""" """ Plot a single spectrum, 'specV' and the harmonic peak location boundaries."""
binN = specV.shape[0] binN = specV.shape[0]
harmLBinL,harmMBinL,harmUBinL = calc_harm_bins( srate, binHz, midiPitch, harmN ) harmLBinL,harmMBinL,harmUBinL = ra.calc_harm_bins( srate, binHz, midiPitch, harmN )
fundHz = harmMBinL[0] * binHz fundHz = harmMBinL[0] * binHz
maxPlotHz = fundHz * (harmN+1) maxPlotHz = fundHz * (harmN+1)
@ -386,19 +410,19 @@ def plot_spectrum( ax, srate, binHz, specV, midiPitch, harmN ):
ax.axvline( x=h1 * binHz, color="black") ax.axvline( x=h1 * binHz, color="black")
ax.axvline( x=h2 * binHz, color="blue") ax.axvline( x=h2 * binHz, color="blue")
ax.set_ylabel(str(midiPitch)) ax.set_ylabel("dB : %i " % (midiPitch))
def plot_spectral_ranges( inDir, pitchL, rmsWndMs=300, rmsHopMs=30, harmN=5, dbLinRef=0.001 ): def plot_spectral_ranges( inDir, pitchTakeL, rmsWndMs=300, rmsHopMs=30, harmN=5, dbLinRef=0.001, printDir="" ):
""" Plot the spectrum from one note (7th from last) in each attack pulse length sequence referred to by pitchL.""" """ Plot the spectrum from one note (7th from last) in each attack pulse length sequence referred to by pitchTakeL."""
plotN = len(pitchL) plotN = len(pitchTakeL)
fig,axL = plt.subplots(plotN,1) fig,axL = plt.subplots(plotN,1)
for plot_idx,midiPitch in enumerate(pitchL): for plot_idx,(midiPitch,takeId) in enumerate(pitchTakeL):
# get the audio and meta-data file names # get the audio and meta-data file names
seqFn = os.path.join( inDir, str(midiPitch), "seq.json") seqFn = os.path.join( inDir, str(midiPitch), str(takeId), "seq.json")
audioFn = os.path.join( inDir, str(midiPitch), "audio.wav") audioFn = os.path.join( inDir, str(midiPitch), str(takeId), "audio.wav")
# read the meta data object # read the meta data object
with open( seqFn, "rb") as f: with open( seqFn, "rb") as f:
@ -406,13 +430,20 @@ def plot_spectral_ranges( inDir, pitchL, rmsWndMs=300, rmsHopMs=30, harmN=5, dbL
# read the audio file # read the audio file
srate, signalM = wavfile.read(audioFn) srate, signalM = wavfile.read(audioFn)
# convert the audio signal vector to contain only the first (left) channel
if len(signalM.shape)>1:
signalM = signalM[:,0].squeeze()
sigV = signalM / float(0x7fff) sigV = signalM / float(0x7fff)
# calc. the RMS envelope in the time domain # calc. the RMS envelope in the time domain
rms0DbV, rms0_srate = audio_rms( srate, sigV, rmsWndMs, rmsHopMs, dbLinRef ) rms0DbV, rms0_srate = ra.audio_rms( srate, sigV, rmsWndMs, rmsHopMs, dbLinRef )
# locate the sample index of the peak of each note attack # locate the sample index of the peak of each note attack
pkIdx0L = locate_peak_indexes( rms0DbV, rms0_srate, r['eventTimeL'] ) pkIdx0L = ra.locate_peak_indexes( rms0DbV, rms0_srate, r['eventTimeL'] )
# select the 7th to last note for spectrum measurement # select the 7th to last note for spectrum measurement
@ -423,33 +454,47 @@ def plot_spectral_ranges( inDir, pitchL, rmsWndMs=300, rmsHopMs=30, harmN=5, dbL
# calc. the RMS envelope by taking the max spectral peak in each STFT window # calc. the RMS envelope by taking the max spectral peak in each STFT window
rmsDbV, rms_srate, specV, specHopIdx, binHz = audio_stft_rms( srate, sigV, rmsWndMs, rmsHopMs, dbLinRef, spectrumSmpIdx) rmsDbV, rms_srate, specV, specHopIdx, binHz = ra.audio_stft_rms( srate, sigV, rmsWndMs, rmsHopMs, dbLinRef, spectrumSmpIdx)
# specV[] is the spectrum of the note at spectrumSmpIdx # specV[] is the spectrum of the note at spectrumSmpIdx
# plot the spectrum and the harmonic selection ranges # plot the spectrum and the harmonic selection ranges
plot_spectrum( axL[plot_idx], srate, binHz, specV, midiPitch, harmN ) plot_spectrum( axL[plot_idx], srate, binHz, specV, midiPitch, harmN )
axL[-1].set_xlabel("Hertz")
if printDir:
plt.savefig(os.path.join(printDir,"plot_spectral_ranges.png"),format="png")
plt.show() plt.show()
def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ): def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ):
#
r = rms_analysis_main( inDir, midi_pitch, **analysisArgsD['rmsAnalysisArgs'] ) r = rms_analysis_main( inDir, midi_pitch, **analysisArgsD['rmsAnalysisArgs'] )
# find min/max peak in the sequence
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'] )
# find ranges of the sequence which should be skipped because they are noisy or unreliable
skipPkIdxL = find_skip_peaks( r.rmsDbV, r.pkIdxL, min_pk_idx, max_pk_idx ) skipPkIdxL = find_skip_peaks( r.rmsDbV, r.pkIdxL, min_pk_idx, max_pk_idx )
# find peaks whose difference to surrounding peaks is greater than 'maxDeltaDb'.
jmpPkIdxL = find_out_of_range_peaks( r.rmsDbV, r.pkIdxL, min_pk_idx, max_pk_idx, analysisArgsD['maxDeltaDb'] ) jmpPkIdxL = find_out_of_range_peaks( r.rmsDbV, r.pkIdxL, min_pk_idx, max_pk_idx, analysisArgsD['maxDeltaDb'] )
secV = np.arange(0,len(r.rmsDbV)) / r.rms_srate secV = np.arange(0,len(r.rmsDbV)) / r.rms_srate
ax.plot( secV, r.rmsDbV ) # plot the harmonic RMS signal
ax.plot( np.arange(0,len(r.tdRmsDbV)) / r.rms_srate, r.tdRmsDbV, color="black" ) ax.plot( secV, r.rmsDbV, color='blue', label="Harmonic" )
# plot the time-domain RMS signal
ax.plot( np.arange(0,len(r.tdRmsDbV)) / r.rms_srate, r.tdRmsDbV, color="black", label="TD" )
# print beg/end boundaries # print note beg/end/peak boundaries
for i,(begMs, endMs) in enumerate(r.eventTimeL): for i,(begMs, endMs) in enumerate(r.eventTimeL):
pkSec = r.pkIdxL[i] / r.rms_srate pkSec = r.pkIdxL[i] / r.rms_srate
@ -472,37 +517,44 @@ def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ):
ax.plot( [pki / r.rms_srate], [ r.rmsDbV[pki] ], marker=6, color="blue") ax.plot( [pki / r.rms_srate], [ r.rmsDbV[pki] ], marker=6, color="blue")
ax.legend();
ax.set_ylabel("dB");
return r return r
def do_td_plot( inDir, analysisArgs ): def do_td_plot( inDir, analysisArgs, midi_pitch, takeId, printDir="" ):
fig,axL = plt.subplots(3,1) fig,axL = plt.subplots(3,1)
fig.set_size_inches(18.5, 10.5, forward=True) fig.set_size_inches(18.5, 10.5, forward=True)
id = int(inDir.split("/")[-1]) # parse the file name
midi_pitch = int(inDir.split("/")[-2]) inDir = os.path.join(inDir,str(midi_pitch),str(takeId));
r = td_plot(axL[0],inDir,midi_pitch,id,analysisArgs) # plot the time domain signal
r = td_plot(axL[0],inDir,midi_pitch,takeId,analysisArgs)
qualityV = np.array([ x.quality for x in r.statsL ]) * np.max(r.pkDbL) qualityV = np.array([ x.quality for x in r.statsL ]) * np.max(r.pkDbL)
durMsV = np.array([ x.durMs for x in r.statsL ]) durMsV = np.array([ x.durMs for x in r.statsL ])
avgV = np.array([ x.durAvgDb 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:] dV = np.diff(r.pkDbL) / r.pkDbL[1:]
axL[1].plot( r.pkUsL, r.pkDbL, marker='.',label="pkDb" ) axL[1].plot( r.pkUsL, r.pkDbL, marker='.', label="harmonic" )
axL[1].plot( r.pkUsL, qualityV, marker='.',label="quality" ) #axL[1].plot( r.pkUsL, qualityV, marker='.', label="quality" )
axL[1].plot( r.pkUsL, avgV, marker='.',label="avgDb" ) axL[1].plot( r.pkUsL, avgV, marker='.', label="harm-td avg" )
#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() axL[1].legend()
axL[1].set_ylabel("dB");
#axL[2].plot( r.pkUsL, durMsV, marker='.' )
axL[2].plot( r.pkUsL[1:], dV, marker='.',label='delta')
axL[2].set_ylim([-1,1])
axL[2].legend()
axL[2].set_ylabel("dB");
axL[2].set_xlabel("Microseconds")
sni = select_first_stable_note_by_dur( durMsV ) sni = select_first_stable_note_by_dur( durMsV )
@ -520,40 +572,62 @@ def do_td_plot( inDir, analysisArgs ):
for i in range(1,len(r.pkUsL)): for i in range(1,len(r.pkUsL)):
axL[2].text( r.pkUsL[i], dV[i-1], "%i" % (i)) axL[2].text( r.pkUsL[i], dV[i-1], "%i" % (i))
if printDir:
plt.savefig(os.path.join(printDir,"do_td_plot.png"),format="png")
plt.show() plt.show()
def do_td_multi_plot( inDir, analysisArgs ): def do_td_multi_plot( inDir, analysisArgs, pitchTakeL, printPlotFl=False ):
midi_pitch = int(inDir.split("/")[-1]) #midi_pitch = int(inDir.split("/")[-1])
dirL = os.listdir(inDir) fig,axL = plt.subplots(len(pitchTakeL),1)
fig,axL = plt.subplots(len(dirL),1)
for id,(idir,ax) in enumerate(zip(dirL,axL)): for id,((pitch,takeId),ax) in enumerate(zip(pitchTakeL,axL)):
td_plot(ax, os.path.join(inDir,str(id)), midi_pitch, id, analysisArgs ) td_plot(ax, os.path.join(inDir,str(pitch),str(takeId)), pitch, takeId, analysisArgs )
ax.set_xlabel("Seconds")
if printDir:
plt.savefig(os.path.join(printDir,"multi_plot.png"),format="png")
plt.show() plt.show()
if __name__ == "__main__": if __name__ == "__main__":
inDir = sys.argv[1] printDir = os.path.expanduser("~/src/picadae_ac_3/doc")
cfgFn = sys.argv[2] cfgFn = sys.argv[1]
take_id = None if len(sys.argv)<4 else sys.argv[3] inDir = sys.argv[2]
mode = sys.argv[3]
cfg = parse_yaml_cfg( cfgFn ) cfg = parse_yaml_cfg( cfgFn )
if take_id is not None: if mode == "td_plot":
inDir = os.path.join(inDir,take_id)
do_td_plot(inDir,cfg.analysisArgs) # python plot_seq.py p_ac.yml ~/temp/p_ac_3_od td_plot 60 10
pitch = int(sys.argv[4])
take_id = int(sys.argv[5])
do_td_plot(inDir,cfg.analysisArgs, pitch, take_id, printDir )
elif mode == "td_multi_plot" or mode == 'plot_spectral_ranges':
pitchTakeIdL = []
for i in range(4,len(sys.argv),2):
pitchTakeIdL.append( (int(sys.argv[i]), int(sys.argv[i+1])) )
if mode == "td_multi_plot":
# python plot_seq.py p_ac.yml ~/temp/p_ac_3_od td_multi_plot 36 2 48 3 60 4
do_td_multi_plot(inDir, cfg.analysisArgs, pitchTakeIdL, printDir )
else: else:
#do_td_multi_plot(inDir,cfg.analysisArgs) # python plot_seq.py p_ac.yml ~/temp/p_ac_3_od plot_spectral_ranges 36 2 48 3 60 4
plot_spectral_ranges( inDir, pitchTakeIdL, printDir=printDir )
#plot_spectral_ranges( inDir, [ 24, 36, 48, 60, 72, 84, 96, 104] ) elif mode == "resample_pulse_times":
plot_resample_pulse_times( inDir, cfg.analysisArgs ) # python plot_seq.py p_ac.yml ~/temp/p_ac_3_od resample_pulse_times 60
pitch = int(sys.argv[4])
plot_resample_pulse_times( inDir, cfg.analysisArgs, pitch, printDir )
else:
print("Unknown plot mode:%s" % (mode))

View File

@ -14,6 +14,7 @@ def fit_to_reference( pkL, refTakeId ):
dur_outL = [] dur_outL = []
tid_outL = [] tid_outL = []
dbL,usL,durMsL,takeIdL = tuple(zip(*pkL)) dbL,usL,durMsL,takeIdL = tuple(zip(*pkL))
us_refL,db_refL,dur_refL = zip(*[(usL[i],dbL[i],durMsL[i]) for i in range(len(usL)) if takeIdL[i]==refTakeId]) us_refL,db_refL,dur_refL = zip(*[(usL[i],dbL[i],durMsL[i]) for i in range(len(usL)) if takeIdL[i]==refTakeId])
@ -27,6 +28,8 @@ def fit_to_reference( pkL, refTakeId ):
db_outL += db0L db_outL += db0L
else: else:
db1V = elbow.fit_points_to_reference(us0L,db0L,us_refL,db_refL) db1V = elbow.fit_points_to_reference(us0L,db0L,us_refL,db_refL)
if db1V is not None:
db_outL += db1V.tolist() db_outL += db1V.tolist()
us_outL += us0L us_outL += us0L
@ -44,11 +47,17 @@ def get_merged_pulse_db_measurements( inDir, midi_pitch, analysisArgsD ):
pkL = [] pkL = []
refTakeId = None
usRefL = None usRefL = None
dbRefL = None dbRefL = None
# for each take in this directory # for each take in this directory
for take_number in range(len(takeDirL)): for take_folder in takeDirL:
take_number = int(take_folder)
if refTakeId is None:
refTakeId = take_number
# analyze this takes audio and locate the note peaks # analyze this takes audio and locate the note peaks
r = rms_analysis.rms_analysis_main( os.path.join(inDir,str(take_number)), midi_pitch, **analysisArgsD ) r = rms_analysis.rms_analysis_main( os.path.join(inDir,str(take_number)), midi_pitch, **analysisArgsD )
@ -57,7 +66,18 @@ def get_merged_pulse_db_measurements( inDir, midi_pitch, analysisArgsD ):
for db,us,stats in zip(r.pkDbL,r.pkUsL,r.statsL): for db,us,stats in zip(r.pkDbL,r.pkUsL,r.statsL):
pkL.append( (db,us,stats.durMs,take_number) ) pkL.append( (db,us,stats.durMs,take_number) )
pkL = fit_to_reference( pkL, 0 )
pkUsL = []
pkDbL = []
durMsL = []
takeIdL = []
holdDutyPctL = []
if refTakeId is None:
print("No valid data files at %s pitch:%i" % (inDir,midi_pitch))
else:
pkL = fit_to_reference( pkL, refTakeId )
# sort the peaks on increasing attack pulse microseconds # sort the peaks on increasing attack pulse microseconds
pkL = sorted( pkL, key= lambda x: x[1] ) pkL = sorted( pkL, key= lambda x: x[1] )
@ -220,6 +240,7 @@ def get_resample_points_wrap( inDir, midi_pitch, analysisArgsD ):
def plot_us_db_curves( ax, inDir, keyMapD, midi_pitch, analysisArgsD, plotResamplePointsFl=False, plotTakesFl=True, usMax=None ): def plot_us_db_curves( ax, inDir, keyMapD, midi_pitch, analysisArgsD, plotResamplePointsFl=False, plotTakesFl=True, usMax=None ):
usL, dbL, durMsL, takeIdL, holdDutyPctL = get_merged_pulse_db_measurements( inDir, midi_pitch, analysisArgsD['rmsAnalysisArgs'] ) usL, dbL, durMsL, takeIdL, holdDutyPctL = get_merged_pulse_db_measurements( inDir, midi_pitch, analysisArgsD['rmsAnalysisArgs'] )
reUsL, reDbL, noiseL, resampleL, skipL, firstAudibleIdx, firstNonSkipIdx = get_resample_points( usL, dbL, durMsL, takeIdL, analysisArgsD['resampleMinDurMs'], analysisArgsD['resampleMinDb'], analysisArgsD['resampleNoiseLimitPct'] ) reUsL, reDbL, noiseL, resampleL, skipL, firstAudibleIdx, firstNonSkipIdx = get_resample_points( usL, dbL, durMsL, takeIdL, analysisArgsD['resampleMinDurMs'], analysisArgsD['resampleMinDb'], analysisArgsD['resampleNoiseLimitPct'] )
# plot first audible and non-skip position # plot first audible and non-skip position
@ -303,7 +324,7 @@ def plot_us_db_curves( ax, inDir, keyMapD, midi_pitch, analysisArgsD, plotResamp
ax.set_ylabel( "%i %s %s" % (midi_pitch, keyMapD[midi_pitch]['type'],keyMapD[midi_pitch]['class'])) ax.set_ylabel( "%i %s %s" % (midi_pitch, keyMapD[midi_pitch]['type'],keyMapD[midi_pitch]['class']))
def plot_us_db_curves_main( inDir, cfg, pitchL, plotTakesFl=True, usMax=None ): def plot_us_db_curves_main( inDir, cfg, pitchL, plotTakesFl=True, usMax=None, printDir="" ):
analysisArgsD = cfg.analysisArgs analysisArgsD = cfg.analysisArgs
keyMapD = { d['midi']:d for d in cfg.key_mapL } keyMapD = { d['midi']:d for d in cfg.key_mapL }
@ -311,6 +332,7 @@ def plot_us_db_curves_main( inDir, cfg, pitchL, plotTakesFl=True, usMax=None ):
fig,axL = plt.subplots(axN,1,sharex=True) fig,axL = plt.subplots(axN,1,sharex=True)
if axN == 1: if axN == 1:
axL = [axL] axL = [axL]
fig.set_size_inches(18.5, 10.5*axN) fig.set_size_inches(18.5, 10.5*axN)
for ax,midi_pitch in zip(axL,pitchL): for ax,midi_pitch in zip(axL,pitchL):
@ -319,6 +341,9 @@ def plot_us_db_curves_main( inDir, cfg, pitchL, plotTakesFl=True, usMax=None ):
if plotTakesFl: if plotTakesFl:
plt.legend() plt.legend()
if printDir:
plt.savefig(os.path.join(printDir,"us_db.png"),format="png")
plt.show() plt.show()
def plot_all_noise_curves( inDir, cfg, pitchL=None ): def plot_all_noise_curves( inDir, cfg, pitchL=None ):
@ -332,16 +357,16 @@ def plot_all_noise_curves( inDir, cfg, pitchL=None ):
for midi_pitch in pitchL: for midi_pitch in pitchL:
print(midi_pitch)
usL, dbL, durMsL, takeIdL, holdDutyPctL = get_merged_pulse_db_measurements( inDir, midi_pitch, cfg.analysisArgs['rmsAnalysisArgs'] ) usL, dbL, durMsL, takeIdL, holdDutyPctL = get_merged_pulse_db_measurements( inDir, midi_pitch, cfg.analysisArgs['rmsAnalysisArgs'] )
scoreV = np.abs( rms_analysis.samples_to_linear_residual( usL, dbL) * 100.0 / dbL ) scoreV = np.abs( rms_analysis.samples_to_linear_residual( usL, dbL) * 100.0 / dbL )
minDurMs = cfg.analysisArgs['resampleMinDurMs'] minDurMs = cfg.analysisArgs['resampleMinDurMs']
minDb = cfg.analysisArgs['resampleMinDb'], minDb = cfg.analysisArgs['resampleMinDb']
noiseLimitPct = cfg.analysisArgs['resampleNoiseLimitPct'] noiseLimitPct = cfg.analysisArgs['resampleNoiseLimitPct']
skipIdxL, firstAudibleIdx, firstNonSkipIdx = get_dur_skip_indexes( durMsL, dbL, scoreV.tolist(), takeIdL, minDurMs, minDb, noiseLimitPct ) skipIdxL, firstAudibleIdx, firstNonSkipIdx = get_dur_skip_indexes( durMsL, dbL, scoreV.tolist(), takeIdL, minDurMs, minDb, noiseLimitPct )
@ -364,18 +389,22 @@ def plot_all_noise_curves( inDir, cfg, pitchL=None ):
plt.legend() plt.legend()
plt.show() plt.show()
def plot_min_max_2_db( inDir, cfg, pitchL=None, takeId=2 ): def plot_min_max_2_db( inDir, cfg, pitchL=None, takeId=2, printDir=None ):
pitchFolderL = os.listdir(inDir) pitchFolderL = os.listdir(inDir)
print(pitchL)
if pitchL is None: if pitchL is None:
pitchL = [ int( int(pitchFolder) ) for pitchFolder in pitchFolderL ] pitchL = [ int( int(pitchFolder) ) for pitchFolder in pitchFolderL ]
print(pitchL)
okL = [] okL = []
outPitchL = [] outPitchL = []
minDbL = [] minDbL = []
maxDbL = [] maxDbL = []
for midi_pitch in pitchL: for midi_pitch in pitchL:
print(midi_pitch) print(midi_pitch)
@ -384,12 +413,9 @@ def plot_min_max_2_db( inDir, cfg, pitchL=None, takeId=2 ):
okL.append(False) okL.append(False)
takeId = len(set(takeIdL))-1
db_maxL = sorted(dbL) db_maxL = sorted(dbL)
maxDbL.append( np.mean(db_maxL[-5:]) ) maxDbL.append( np.mean(db_maxL[-5:]) )
usL,dbL = zip(*[(usL[i],dbL[i]) for i in range(len(usL)) if takeIdL[i]==takeId ]) usL,dbL = zip(*[(usL[i],dbL[i]) for i in range(len(usL)) if takeIdL[i]==takeId ])
if len(set(takeIdL)) == 3: if len(set(takeIdL)) == 3:
@ -415,9 +441,13 @@ def plot_min_max_2_db( inDir, cfg, pitchL=None, takeId=2 ):
ax.text( pitch, max_db, "%i %s %s" % (pitch, keyMapD[pitch]['type'],keyMapD[pitch]['class']), color=c) ax.text( pitch, max_db, "%i %s %s" % (pitch, keyMapD[pitch]['type'],keyMapD[pitch]['class']), color=c)
if printDir:
plt.savefig(os.path.join(printDir,"min_max_db_2.png"),format="png")
plt.show() plt.show()
def plot_min_db_manual( inDir, cfg ): def plot_min_db_manual( inDir, cfg, printDir=None ):
pitchL = list(cfg.manualMinD.keys()) pitchL = list(cfg.manualMinD.keys())
@ -466,6 +496,8 @@ def plot_min_db_manual( inDir, cfg ):
# Form the complete set of min/max db levels for each pitch by interpolating the # Form the complete set of min/max db levels for each pitch by interpolating the
# db values between the manually selected anchor points. # db values between the manually selected anchor points.
interpMinDbL = np.interp( pitchL, cfg.manualAnchorPitchMinDbL, anchorMinDbL ) interpMinDbL = np.interp( pitchL, cfg.manualAnchorPitchMinDbL, anchorMinDbL )
@ -493,13 +525,12 @@ def plot_min_db_manual( inDir, cfg ):
if printDir:
plt.savefig(os.path.join(printDir,"manual_db.png"),format="png")
plt.show() plt.show()
def plot_min_max_db( inDir, cfg, pitchL=None ): def plot_min_max_db( inDir, cfg, pitchL=None, printDir=None ):
pitchFolderL = os.listdir(inDir) pitchFolderL = os.listdir(inDir)
@ -518,7 +549,7 @@ def plot_min_max_db( inDir, cfg, pitchL=None ):
scoreV = np.abs( rms_analysis.samples_to_linear_residual( usL, dbL) * 100.0 / dbL ) scoreV = np.abs( rms_analysis.samples_to_linear_residual( usL, dbL) * 100.0 / dbL )
minDurMs = cfg.analysisArgs['resampleMinDurMs'] minDurMs = cfg.analysisArgs['resampleMinDurMs']
minDb = cfg.analysisArgs['resampleMinDb'], minDb = cfg.analysisArgs['resampleMinDb']
noiseLimitPct = cfg.analysisArgs['resampleNoiseLimitPct'] noiseLimitPct = cfg.analysisArgs['resampleNoiseLimitPct']
skipIdxL, firstAudibleIdx, firstNonSkipIdx = get_dur_skip_indexes( durMsL, dbL, takeIdL, scoreV.tolist(), minDurMs, minDb, noiseLimitPct ) skipIdxL, firstAudibleIdx, firstNonSkipIdx = get_dur_skip_indexes( durMsL, dbL, takeIdL, scoreV.tolist(), minDurMs, minDb, noiseLimitPct )
@ -548,6 +579,9 @@ def plot_min_max_db( inDir, cfg, pitchL=None ):
ax.text( pitch, db, "%i %s %s" % (pitch, keyMapD[pitch]['type'],keyMapD[pitch]['class'])) ax.text( pitch, db, "%i %s %s" % (pitch, keyMapD[pitch]['type'],keyMapD[pitch]['class']))
if printDir:
plt.savefig(os.path.join(printDir,"min_max_db.png"),format="png")
plt.show() plt.show()
@ -575,7 +609,7 @@ def estimate_us_to_db_map( inDir, cfg, minMapDb=16.0, maxMapDb=26.0, incrMapDb=0
scoreV = np.abs( rms_analysis.samples_to_linear_residual( usL, dbL) * 100.0 / dbL ) scoreV = np.abs( rms_analysis.samples_to_linear_residual( usL, dbL) * 100.0 / dbL )
minDurMs = cfg.analysisArgs['resampleMinDurMs'] minDurMs = cfg.analysisArgs['resampleMinDurMs']
minDb = cfg.analysisArgs['resampleMinDb'], minDb = cfg.analysisArgs['resampleMinDb']
noiseLimitPct = cfg.analysisArgs['resampleNoiseLimitPct'] noiseLimitPct = cfg.analysisArgs['resampleNoiseLimitPct']
# get the set of samples that are not valid (too short, too quiet, too noisy) # get the set of samples that are not valid (too short, too quiet, too noisy)
@ -633,7 +667,7 @@ def estimate_us_to_db_map( inDir, cfg, minMapDb=16.0, maxMapDb=26.0, incrMapDb=0
return mapD, list(dbS) return mapD, list(dbS)
def plot_us_to_db_map( inDir, cfg, minMapDb=16.0, maxMapDb=26.0, incrMapDb=1.0, pitchL=None ): def plot_us_to_db_map( inDir, cfg, minMapDb=16.0, maxMapDb=26.0, incrMapDb=1.0, pitchL=None, printDir=None ):
fig,ax = plt.subplots() fig,ax = plt.subplots()
@ -644,6 +678,8 @@ def plot_us_to_db_map( inDir, cfg, minMapDb=16.0, maxMapDb=26.0, incrMapDb=1.0,
u_dL = [ (d['us_avg'],d['us_cls'],d['db_avg'],d['us_std'],d['us_min'],d['us_max'],d['db_std']) for loDb, d in dbD.items() if d['us_avg'] != 0 ] u_dL = [ (d['us_avg'],d['us_cls'],d['db_avg'],d['us_std'],d['us_min'],d['us_max'],d['db_std']) for loDb, d in dbD.items() if d['us_avg'] != 0 ]
if u_dL:
# get the us/db lists for this pitch # get the us/db lists for this pitch
usL,uscL,dbL,ussL,usnL,usxL,dbsL = zip(*u_dL) usL,uscL,dbL,ussL,usnL,usxL,dbsL = zip(*u_dL)
@ -660,6 +696,10 @@ def plot_us_to_db_map( inDir, cfg, minMapDb=16.0, maxMapDb=26.0, incrMapDb=1.0,
plt.legend() plt.legend()
if printDir:
plt.savefig(os.path.join(printDir,"us_db_map.png"),format="png")
plt.show() plt.show()
def report_take_ids( inDir ): def report_take_ids( inDir ):
@ -760,9 +800,11 @@ def gen_vel_map( inDir, cfg, minMaxDbFn, dynLevelN, cacheFn ):
if __name__ == "__main__": if __name__ == "__main__":
inDir = sys.argv[1] printDir =os.path.expanduser( "~/src/picadae_ac_3/doc")
cfgFn = sys.argv[2] cfgFn = sys.argv[1]
inDir = sys.argv[2]
mode = sys.argv[3] mode = sys.argv[3]
if len(sys.argv) <= 4: if len(sys.argv) <= 4:
pitchL = None pitchL = None
else: else:
@ -771,21 +813,23 @@ if __name__ == "__main__":
cfg = parse_yaml_cfg( cfgFn ) cfg = parse_yaml_cfg( cfgFn )
if mode == 'us_db': if mode == 'us_db':
plot_us_db_curves_main( inDir, cfg, pitchL, plotTakesFl=True,usMax=None ) plot_us_db_curves_main( inDir, cfg, pitchL, plotTakesFl=True,usMax=None, printDir=printDir )
elif mode == 'noise': elif mode == 'noise':
plot_all_noise_curves( inDir, cfg, pitchL ) plot_all_noise_curves( inDir, cfg, pitchL )
elif mode == 'min_max': elif mode == 'min_max':
plot_min_max_db( inDir, cfg, pitchL ) plot_min_max_db( inDir, cfg, pitchL, printDir=printDir )
elif mode == 'min_max_2': elif mode == 'min_max_2':
plot_min_max_2_db( inDir, cfg, pitchL ) takeId = pitchL[-1]
del pitchL[-1]
plot_min_max_2_db( inDir, cfg, pitchL, takeId=takeId, printDir=printDir )
elif mode == 'us_db_map': elif mode == 'us_db_map':
plot_us_to_db_map( inDir, cfg, pitchL=pitchL ) plot_us_to_db_map( inDir, cfg, pitchL=pitchL, printDir=printDir )
elif mode == 'audacity': elif mode == 'audacity':
rms_analysis.write_audacity_label_files( inDir, cfg.analysisArgs['rmsAnalysisArgs'] ) rms_analysis.write_audacity_label_files( inDir, cfg.analysisArgs['rmsAnalysisArgs'] )
elif mode == 'rpt_take_ids': elif mode == 'rpt_take_ids':
report_take_ids( inDir ) report_take_ids( inDir )
elif mode == 'manual_db': elif mode == 'manual_db':
plot_min_db_manual( inDir, cfg ) plot_min_db_manual( inDir, cfg, printDir=printDir )
elif mode == 'gen_vel_map': elif mode == 'gen_vel_map':
gen_vel_map( inDir, cfg, "minInterpDb.json", 9, "cache_us_db.json" ) gen_vel_map( inDir, cfg, "minInterpDb.json", 9, "cache_us_db.json" )
elif mode == 'cache_us_db': elif mode == 'cache_us_db':

View File

@ -201,19 +201,22 @@ def select_first_stable_note_by_delta_db( pkDbL, pkUsL=None, maxPulseUs=0.1 ):
return None return None
def note_stats( r, decay_pct=50.0, extraDurSearchMs=500 ): def note_stats( r, decay_pct=50.0, extraDurSearchMs=500 ):
""" Collect some statistics and markers for each note in the sequence. """
statsL = [] statsL = []
srate = r.rms_srate srate = r.rms_srate
qmax = 0 qmax = 0
for i,(begSmpMs, endSmpMs) in enumerate(r.eventTimeL): for i,(begSmpMs, endSmpMs) in enumerate(r.eventTimeL):
begSmpIdx = int(round(srate * begSmpMs / 1000.0)) begSmpIdx = int(round(srate * begSmpMs / 1000.0))
endSmpIdx = int(round(srate * (endSmpMs + extraDurSearchMs) / 1000.0)) endSmpIdx = int(round(srate * (endSmpMs + extraDurSearchMs) / 1000.0))
pkSmpIdx = r.pkIdxL[i] pkSmpIdx = r.pkIdxL[i]
durMs = measure_duration_ms( r.rmsDbV, srate, pkSmpIdx, endSmpIdx, decay_pct ) durMs = measure_duration_ms( r.rmsDbV, srate, pkSmpIdx, endSmpIdx, decay_pct )
bi = pkSmpIdx bi = pkSmpIdx
@ -232,7 +235,15 @@ def note_stats( r, decay_pct=50.0, extraDurSearchMs=500 ):
hmRmsDb_u = 0.0 if ei >= len(r.rmsDbV) else np.mean(r.rmsDbV[bi:ei]) hmRmsDb_u = 0.0 if ei >= len(r.rmsDbV) else np.mean(r.rmsDbV[bi:ei])
durAvgDb = (hmRmsDb_u + tdRmsDb_u)/2.0 durAvgDb = (hmRmsDb_u + tdRmsDb_u)/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 })) 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): for i,r in enumerate(statsL):
statsL[i].quality = 0 if qmax <= 0 else statsL[i].quality / qmax statsL[i].quality = 0 if qmax <= 0 else statsL[i].quality / qmax
@ -243,15 +254,19 @@ def note_stats( r, decay_pct=50.0, extraDurSearchMs=500 ):
def locate_peak_indexes( xV, xV_srate, eventMsL ): def locate_peak_indexes( xV, xV_srate, eventMsL, audioFn ):
pkIdxL = [] pkIdxL = []
for begMs, endMs in eventMsL: for i, (begMs, endMs) in enumerate(eventMsL):
begSmpIdx = int(begMs * xV_srate / 1000.0) begSmpIdx = int(begMs * xV_srate / 1000.0)
endSmpIdx = int(endMs * xV_srate / 1000.0) endSmpIdx = int(endMs * xV_srate / 1000.0)
if endSmpIdx != begSmpIdx:
pkIdxL.append( begSmpIdx + np.argmax( xV[begSmpIdx:endSmpIdx] ) ) pkIdxL.append( begSmpIdx + np.argmax( xV[begSmpIdx:endSmpIdx] ) )
else:
print("Missing peak %i : begween beg:%i ms end:%i ms : %s" % (i, begMs, endMs, audioFn ))
pkIdxL.append( None )
return pkIdxL return pkIdxL
@ -304,6 +319,11 @@ def rms_analyze_one_rt_note_wrap( audioDev, annBegMs, annEndMs, midi_pitch, note
sigV = buf_result.value sigV = buf_result.value
if len(sigV.shape) > 1:
sigV = sigV[:,0].squeeze()
# get the annotated begin and end of the note as sample indexes into sigV # get the annotated begin and end of the note as sample indexes into sigV
bi = int(round(annBegMs * audioDev.srate / 1000)) bi = int(round(annBegMs * audioDev.srate / 1000))
ei = int(round(annEndMs * audioDev.srate / 1000)) ei = int(round(annEndMs * audioDev.srate / 1000))
@ -399,47 +419,72 @@ def calibrate_recording_analysis( inDir ):
def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbLinRef=0.001, harmCandN=5, harmN=3, durDecayPct=40 ): def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbLinRef=0.001, harmCandN=5, harmN=3, durDecayPct=40 ):
# form the audio and meta data file names
seqFn = os.path.join( inDir, "seq.json") seqFn = os.path.join( inDir, "seq.json")
audioFn = os.path.join( inDir, "audio.wav") audioFn = os.path.join( inDir, "audio.wav")
# read the meta data file
with open( seqFn, "rb") as f: with open( seqFn, "rb") as f:
r = json.load(f) r = json.load(f)
# rad the auido file
srate, signalM = wavfile.read(audioFn) srate, signalM = wavfile.read(audioFn)
# convert the audio signal vector to contain only the first (left) channel
if len(signalM.shape)>1:
signalM = signalM[:,0].squeeze()
# convert the auido file to floats in range [-1.0 to 1.0]
sigV = signalM / float(0x7fff) sigV = signalM / float(0x7fff)
# calc. the RMS signal
tdRmsDbV, rms0_srate = audio_rms( srate, sigV, rmsWndMs, rmsHopMs, dbLinRef ) tdRmsDbV, rms0_srate = audio_rms( srate, sigV, rmsWndMs, rmsHopMs, dbLinRef )
tdPkIdxL = locate_peak_indexes( tdRmsDbV, rms0_srate, r['eventTimeL']) # locate the peaks in the RMS signal
tdPkIdxL = locate_peak_indexes( tdRmsDbV, rms0_srate, r['eventTimeL'], audioFn )
# sum the first harmN harmonics to form a envelope of the audio signal (this is an alternative to the RMS signal)
rmsDbV, rms_srate, binHz = audio_harm_rms( srate, sigV, rmsWndMs, rmsHopMs, dbLinRef, midi_pitch, harmCandN, harmN ) rmsDbV, rms_srate, binHz = audio_harm_rms( srate, sigV, rmsWndMs, rmsHopMs, dbLinRef, midi_pitch, harmCandN, harmN )
pkIdxL = locate_peak_indexes( rmsDbV, rms_srate, r['eventTimeL'] ) # locate the peaks in the harmonic sum signal
pkIdxL = locate_peak_indexes( rmsDbV, rms_srate, r['eventTimeL'], audioFn )
# form the 'holdDutyPctlL' hold duty cycle transition point list
holdDutyPctL = None holdDutyPctL = None
if 'holdDutyPct' in r: if 'holdDutyPct' in r:
holdDutyPctL = [ (0, r['holdDutyPct']) ] holdDutyPctL = [ (0, r['holdDutyPct']) ]
else: else:
holdDutyPctL = r['holdDutyPctL'] holdDutyPctL = r['holdDutyPctL']
eventN = len(r['eventTimeL'])
assert( len(tdPkIdxL) == eventN and len(pkIdxL) == eventN )
# filter out all missing events that have no peak
flL = [ (tdPkIdxL[i] is not None) and (pkIdxL[i] is not None) for i in range(eventN) ]
eventTimeL = [ r['eventTimeL'][i] for i in range(eventN) if flL[i] ]
tdPkDbL = [ tdRmsDbV[tdPkIdxL[i]] for i in range(eventN) if flL[i] ]
pkDbL = [ rmsDbV[ pkIdxL[i]] for i in range(eventN) if flL[i] ]
#pkUsL = [ r['pulseUsL'][pkIdxL[i]] for i in range(eventN) if flL[i] ]
tdPkIdxL = [ i for i in tdPkIdxL if i is not None ]
pkIdxL = [ i for i in pkIdxL if i is not None ]
# form the result record
r = types.SimpleNamespace(**{ r = types.SimpleNamespace(**{
"audio_srate":srate, "audio_srate":srate,
"eventTimeMsL":r['eventTimeL'], "eventTimeMsL": eventTimeL, #r['eventTimeL'],
"tdRmsDbV": tdRmsDbV, "tdRmsDbV": tdRmsDbV,
"tdPkIdxL": tdPkIdxL, "tdPkIdxL": tdPkIdxL,
"tdPkDbL": [ tdRmsDbV[i] for i in tdPkIdxL ], "tdPkDbL": tdPkDbL, # [ tdRmsDbV[i] for i in tdPkIdxL ],
"binHz": binHz, "binHz": binHz,
"rmsDbV":rmsDbV, "rmsDbV": rmsDbV,
"rms_srate":rms_srate, "rms_srate":rms_srate,
"pkIdxL":pkIdxL, # pkIdxL[ len(pulsUsL) ] - indexes into rmsDbV[] of peaks "pkIdxL":pkIdxL, # pkIdxL[ len(pulsUsL) ] - indexes into rmsDbV[] of peaks
"eventTimeL":r['eventTimeL'], "eventTimeL": eventTimeL, #r['eventTimeL'],
"holdDutyPctL":holdDutyPctL, "holdDutyPctL":holdDutyPctL,
'pkDbL': [ rmsDbV[ i ] for i in pkIdxL ], 'pkDbL': pkDbL, # [ rmsDbV[ i ] for i in pkIdxL ],
'pkUsL':r['pulseUsL'] }) 'pkUsL': r['pulseUsL'] }) # r['pulseUsL']
#
statsL = note_stats(r,durDecayPct) statsL = note_stats(r,durDecayPct)
setattr(r,"statsL", statsL ) setattr(r,"statsL", statsL )