Преглед на файлове

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.
master
kpl преди 4 години
родител
ревизия
26b997811a
променени са 10 файла, в които са добавени 969 реда и са изтрити 67 реда
  1. 2
    0
      .gitignore
  2. 10
    8
      AudioDevice.py
  3. 209
    0
      keyboard.py
  4. 58
    24
      p_ac.py
  5. 7
    4
      p_ac.yml
  6. 79
    0
      plot_all_note_durations.ipynb
  7. 300
    0
      plot_note_analysis.py
  8. 49
    29
      plot_seq.py
  9. 179
    2
      rms_analysis.py
  10. 76
    0
      rt_note_analysis.py

+ 2
- 0
.gitignore Целия файл

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

+ 10
- 8
AudioDevice.py Целия файл

@@ -191,14 +191,16 @@ class AudioDevice(object):
191 191
     
192 192
     def linear_buffer( self ):
193 193
 
194
-        smpN = self.buffer_sample_count()
195
-        bV   = np.zeros( (smpN,self.ch_cnt) )
196
-
197
-        bi = 0
198
-        for i in range(len(self.bufL)):
199
-          bufSmpN = self.bufIdx if i == len(self.bufL)-1 else self.bufL[i].shape[0]
200
-          bV[ bi:bi+bufSmpN, ] = self.bufL[i][0:bufSmpN]
201
-          bi += bufSmpN
194
+        r = self.buffer_sample_count()
195
+        if r:
196
+            smpN = r.value
197
+            bV   = np.zeros( (smpN,self.ch_cnt) )
198
+
199
+            bi = 0
200
+            for i in range(len(self.bufL)):
201
+              bufSmpN = self.bufIdx if i == len(self.bufL)-1 else self.bufL[i].shape[0]
202
+              bV[ bi:bi+bufSmpN, ] = self.bufL[i][0:bufSmpN]
203
+              bi += bufSmpN
202 204
           
203 205
         return Result(bV)
204 206
             

+ 209
- 0
keyboard.py Целия файл

@@ -0,0 +1,209 @@
1
+import os,types,pickle
2
+import numpy as np
3
+from plot_seq     import form_final_pulse_list
4
+from rms_analysis import rms_analysis_main_all
5
+
6
+class Keyboard:
7
+    def __init__(self, cfg, audio, api):
8
+        self.cfg = cfg
9
+        self.audio = audio
10
+        self.api   = api
11
+        self.keyD = {}   # { midi_pitch: { pulseUsL, holdDutyPctL } }
12
+
13
+
14
+        self.noteDurMs = cfg.noteDurMs
15
+        self.pauseDurMs= cfg.pauseDurMs
16
+        
17
+        self.pitchL = None
18
+        
19
+        
20
+        self.pitch_idx = None    
21
+        self.pulse_idx = None
22
+        self.targetDb  = None
23
+        self.next_ms   = None
24
+        self.enableFl  = False
25
+        self.state     = None #"note_on" | "note_off"
26
+
27
+        self.rmsAnlD = None
28
+        #self._load( cfg.outDir, cfg.analysisArgs)
29
+        
30
+    def load( self, inDir, pitchL, analysisArgsD ):
31
+
32
+        self.keyD = {}
33
+        
34
+        inDir = os.path.expanduser(inDir)
35
+
36
+        finalPulseListCacheFn = analysisArgsD['finalPulseListCacheFn']
37
+        if os.path.isfile(finalPulseListCacheFn):
38
+            print("READING: final pulse list cache file: %s" % (finalPulseListCacheFn))
39
+            with open(finalPulseListCacheFn,'rb') as f:
40
+                self.keyD = pickle.load(f)
41
+        else:
42
+            dirL = os.listdir(inDir)
43
+        
44
+            for dirStr in dirL:
45
+                dirStr = os.path.normpath(os.path.join(inDir,dirStr))
46
+
47
+                if os.path.isdir(dirStr):
48
+                    pathL = dirStr.split(os.sep)
49
+
50
+                    midi_pitch = int( pathL[-1] )
51
+
52
+                    if midi_pitch in pitchL:
53
+                        print(dirStr,midi_pitch)
54
+                        pulseUsL,pulseDbL,holdDutyPctL = form_final_pulse_list( dirStr, midi_pitch, analysisArgsD )
55
+
56
+                        d = { 'pulseUsL':pulseUsL, 'holdDutyPctL':holdDutyPctL, 'lastDutyPct':0 }
57
+
58
+                        self.keyD[ midi_pitch ] = types.SimpleNamespace(**d)
59
+
60
+            with open(finalPulseListCacheFn,'wb') as f:
61
+                pickle.dump(self.keyD,f)
62
+                
63
+        print("Loading analysis ...")
64
+
65
+        cacheFn = analysisArgsD['rmsAnalysisCacheFn']
66
+        self.rmsAnlD = rms_analysis_main_all( inDir, cacheFn, **analysisArgsD['rmsAnalysisArgs'] )
67
+
68
+        print("Load DONE.")
69
+
70
+    def _get_duty_cycle_from_pulse_usec( self, pitch, pulseUsec ):
71
+
72
+        if pitch not in self.keyD:
73
+            print("Missing duty cycle.")
74
+            return None
75
+        
76
+        dutyPct = self.keyD[pitch].holdDutyPctL[0][1]
77
+        for refUsec,refDuty in  self.keyD[pitch].holdDutyPctL:
78
+            if pulseUsec < refUsec:
79
+                break
80
+            dutyPct = refDuty
81
+
82
+        return dutyPct
83
+        
84
+    def _get_pulse_and_duty_cycle_from_pulse_idx( self, pitch, pulse_idx ):
85
+        
86
+        pulseUsec = self.keyD[ pitch ].pulseUsL[ pulse_idx ]
87
+
88
+        dutyPct = self._get_duty_cycle_from_pulse_usec( pitch, pulseUsec )
89
+
90
+        return pulseUsec, dutyPct
91
+
92
+    def _get_pulse_and_duty_cycle_target_db( self, pitch, targetDb ):
93
+
94
+        r = self.rmsAnlD[pitch]
95
+
96
+        pulse_idx = np.argmin( np.abs(np.array(r.pkDbL) - targetDb) )
97
+
98
+        print("PULSE idx:",pulse_idx," db:", r.pkDbL[pulse_idx] )
99
+
100
+        pulseUsec = r.pkUsL[pulse_idx]
101
+        
102
+        dutyPct = self._get_duty_cycle_from_pulse_usec( pitch, pulseUsec )
103
+
104
+        return pulseUsec, dutyPct
105
+    
106
+    def _get_pulse_and_duty_cycle( self, pitch, pulse_idx, targetDb ):
107
+
108
+        if pulse_idx is not None:
109
+            return self._get_pulse_and_duty_cycle_from_pulse_idx(pitch,pulse_idx)
110
+        else:
111
+            return self._get_pulse_and_duty_cycle_target_db( pitch, targetDb )
112
+            
113
+            
114
+
115
+    def start( self, ms, pitchL, pulse_idx, targetDb=None ):
116
+
117
+        loadFl = True
118
+        
119
+        if self.pitchL is not None:
120
+            loadFl = False
121
+            for pitch in pitchL:
122
+                if pitch  not in self.pitchL:
123
+                    loadFl = True
124
+                    break
125
+
126
+        if loadFl:
127
+            self.load(self.cfg.outDir, pitchL, self.cfg.analysisArgs)
128
+        
129
+        self.pitchL  = pitchL
130
+        self.pitch_idx = 0
131
+        self.pulse_idx = pulse_idx
132
+        self.targetDb  = targetDb
133
+        self.state   = "note_on"
134
+        self.next_ms = ms
135
+        self.eventTimeL = [[0,0]  for _ in range(len(pitchL))] # initialize the event time                 
136
+        self.audio.record_enable(True)   # start recording audio        
137
+        self.tick(ms)                    # play the first note
138
+
139
+    def repeat( self, ms, pulse_idx, targetDb=None ):
140
+        self.start( ms, self.pitchL, pulse_idx, targetDb )
141
+                    
142
+    def stop( self, ms ):
143
+        self._send_note_off()
144
+        self.audio.record_enable(False)  # stop recording audio            
145
+        self.state = None          # disable this sequencer
146
+        
147
+
148
+    def tick( self, ms ):
149
+        #self.audio.tick(ms)
150
+        
151
+        # if next event time has arrived
152
+        if self.state is not None and  ms >= self.next_ms:
153
+
154
+            # if waiting to turn note on
155
+            if self.state == 'note_on':
156
+                self._note_on(ms)
157
+        
158
+            # if waiting to turn a note off
159
+            elif self.state == 'note_off':
160
+                self._note_off(ms)                
161
+                self.pitch_idx += 1
162
+                
163
+                # if all notes have been played
164
+                if self.pitch_idx >= len(self.pitchL):
165
+                    self.stop(ms)
166
+                    
167
+            else:                
168
+                assert(0)
169
+
170
+    def _note_on( self, ms ):
171
+
172
+        self.eventTimeL[ self.pitch_idx ][0] = self.audio.buffer_sample_ms().value
173
+        self.next_ms = ms + self.noteDurMs
174
+        self.state = 'note_off'
175
+
176
+        pitch                = self.pitchL[ self.pitch_idx ]
177
+        pulse_usec, dutyPct  = self._get_pulse_and_duty_cycle( pitch, self.pulse_idx, self.targetDb )
178
+
179
+        if pulse_usec is not None and dutyPct is not None:
180
+            self._set_pwm_duty( pitch, dutyPct )
181
+            self.api.note_on_us( pitch, pulse_usec )
182
+
183
+        pulse_idx = 0 if self.pulse_idx is None else self.pulse_idx
184
+        targetDb =  0 if self.targetDb  is None else self.targetDb
185
+        dutyPct  =  0 if dutyPct is None else dutyPct
186
+        print("note-on: %i %i  %4.1f %8.1f %i" % (pitch, pulse_idx, targetDb, pulse_usec, dutyPct))
187
+
188
+    def _set_pwm_duty( self, pitch, dutyPct ):
189
+
190
+        if self.keyD[pitch].lastDutyPct != dutyPct:
191
+            self.keyD[pitch].lastDutyPct = dutyPct
192
+            self.api.set_pwm_duty( pitch, dutyPct )
193
+
194
+    def _note_off( self, ms ):
195
+        self.eventTimeL[ self.pitch_idx ][1] = self.audio.buffer_sample_ms().value
196
+        self.next_ms = ms + self.pauseDurMs
197
+        self.state   = 'note_on'
198
+
199
+        #begTimeMs = self.eventTimeL[ self.pulse_idx ][0]
200
+        #endTimeMs = self.eventTimeL[ self.pulse_idx ][1]
201
+        #self.rtAnalyzer.analyze_note( self.audio, self.pitchL[0], begTimeMs, endTimeMs, self.cfg.analysisArgs['rmsAnalysisArgs'] )
202
+        
203
+        self._send_note_off()
204
+        
205
+    def _send_note_off( self ):
206
+        for pitch in self.pitchL:
207
+            self.api.note_off( pitch )
208
+            #print("note-off:",pitch,self.pulse_idx)
209
+

+ 58
- 24
p_ac.py Целия файл

@@ -10,11 +10,14 @@ from result       import Result
10 10
 from common       import parse_yaml_cfg
11 11
 from plot_seq     import form_resample_pulse_time_list
12 12
 from plot_seq     import form_final_pulse_list
13
+from rt_note_analysis import RT_Analyzer
14
+from keyboard         import Keyboard
13 15
 
14 16
 class AttackPulseSeq:
15 17
     """ Sequence a fixed chord over a list of attack pulse lengths."""
16 18
     
17
-    def __init__(self, audio, api, noteDurMs=1000, pauseDurMs=1000, holdDutyPctL=[(0,50)] ):
19
+    def __init__(self, cfg, audio, api, noteDurMs=1000, pauseDurMs=1000 ):
20
+        self.cfg         = cfg
18 21
         self.audio       = audio
19 22
         self.api         = api
20 23
         self.outDir      = None           # directory to write audio file and results
@@ -22,7 +25,7 @@ class AttackPulseSeq:
22 25
         self.pulseUsL    = []             # one onset pulse length in microseconds per sequence element
23 26
         self.noteDurMs   = noteDurMs      # duration of each chord in milliseconds
24 27
         self.pauseDurMs  = pauseDurMs     # duration between end of previous note and start of next
25
-        self.holdDutyPctL= holdDutyPctL    # hold voltage duty cycle table [ (minPulseSeqUsec,dutyCyclePct) ]
28
+        self.holdDutyPctL= None           # hold voltage duty cycle table [ (minPulseSeqUsec,dutyCyclePct) ]
26 29
         
27 30
         self.pulse_idx            = 0     # Index of next pulse 
28 31
         self.state                = None  # 'note_on','note_off'
@@ -31,12 +34,13 @@ class AttackPulseSeq:
31 34
         self.eventTimeL           = []    # Onset/offset time of each note [ [onset_ms,offset_ms] ] (used to locate the note in the audio file)
32 35
         self.beginMs              = 0
33 36
         self.playOnlyFl           = False
37
+        self.rtAnalyzer           = RT_Analyzer()
34 38
 
35
-    def start( self, ms, outDir, pitchL, pulseUsL, playOnlyFl=False ):
39
+    def start( self, ms, outDir, pitchL, pulseUsL, holdDutyPctL, playOnlyFl=False ):
36 40
         self.outDir     = outDir         # directory to write audio file and results
37 41
         self.pitchL     = pitchL         # chord to play
38 42
         self.pulseUsL   = pulseUsL       # one onset pulse length in microseconds per sequence element
39
-        
43
+        self.holdDutyPctL = holdDutyPctL
40 44
         self.pulse_idx  = 0
41 45
         self.state      = 'note_on'
42 46
         self.prevHoldDutyPct = None
@@ -49,15 +53,16 @@ class AttackPulseSeq:
49 53
         #    self.api.set_pwm_duty( pitch, self.holdDutyPct )
50 54
         #    print("set PWM:%i"%(self.holdDutyPct))
51 55
 
52
-        if not playOnlyFl:
53
-            self.audio.record_enable(True)   # start recording audio
56
+        # kpl if not playOnlyFl:
57
+        self.audio.record_enable(True)   # start recording audio
58
+        
54 59
         self.tick(ms)                    # play the first note
55 60
         
56 61
     def stop(self, ms):
57 62
         self._send_note_off() # be sure that all notes are actually turn-off
58 63
         
59
-        if not self.playOnlyFl:
60
-            self.audio.record_enable(False)  # stop recording audio
64
+        # kpl if not self.playOnlyFl:
65
+        self.audio.record_enable(False)  # stop recording audio
61 66
             
62 67
         self._disable()          # disable this sequencer
63 68
         
@@ -69,8 +74,6 @@ class AttackPulseSeq:
69 74
     
70 75
     def tick(self, ms):
71 76
 
72
-        self.audio.tick(ms)
73
-        
74 77
         # if next event time has arrived
75 78
         if self.is_enabled() and  ms >= self.next_ms:
76 79
 
@@ -120,12 +123,17 @@ class AttackPulseSeq:
120 123
             pulse_usec = int(self.pulseUsL[ self.pulse_idx ])
121 124
             self._set_duty_cycle( pitch, pulse_usec )
122 125
             self.api.note_on_us( pitch, pulse_usec )
123
-            print("note-on:",pitch,self.pulse_idx)
126
+            print("note-on:",pitch, self.pulse_idx, pulse_usec)
124 127
 
125 128
     def _note_off( self, ms ):
126 129
         self.eventTimeL[ self.pulse_idx ][1] = self.audio.buffer_sample_ms().value
127 130
         self.next_ms = ms + self.pauseDurMs
128 131
         self.state   = 'note_on'
132
+
133
+        if self.playOnlyFl:
134
+            begTimeMs = self.eventTimeL[ self.pulse_idx ][0]
135
+            endTimeMs = self.eventTimeL[ self.pulse_idx ][1]
136
+            self.rtAnalyzer.analyze_note( self.audio, self.pitchL[0], begTimeMs, endTimeMs, self.cfg.analysisArgs['rmsAnalysisArgs'] )
129 137
         
130 138
         self._send_note_off()        
131 139
 
@@ -133,7 +141,7 @@ class AttackPulseSeq:
133 141
     def _send_note_off( self ):
134 142
         for pitch in self.pitchL:
135 143
             self.api.note_off( pitch )
136
-            print("note-off:",pitch,self.pulse_idx)
144
+            #print("note-off:",pitch,self.pulse_idx)
137 145
 
138 146
     def _disable(self):
139 147
         self.state = None
@@ -167,7 +175,7 @@ class AttackPulseSeq:
167 175
 class CalibrateKeys:
168 176
     def __init__(self, cfg, audioDev, api):
169 177
         self.cfg      = cfg
170
-        self.seq      = AttackPulseSeq(  audioDev, api, noteDurMs=cfg.noteDurMs, pauseDurMs=cfg.pauseDurMs, holdDutyPctL=cfg.holdDutyPctL )
178
+        self.seq      = AttackPulseSeq(  cfg, audioDev, api, noteDurMs=cfg.noteDurMs, pauseDurMs=cfg.pauseDurMs )
171 179
         
172 180
         self.pulseUsL  = None
173 181
         self.chordL   = None
@@ -233,8 +241,10 @@ class CalibrateKeys:
233 241
             if outDir_id != 0:
234 242
                 self.pulseUsL,_,_ = form_resample_pulse_time_list( outDir, self.cfg.analysisArgs )
235 243
 
244
+            holdDutyPctL = self.cfg.holdDutyPctL
245
+            
236 246
             if playOnlyFl:
237
-                self.pulseUsL,_ = form_final_pulse_list( outDir,  pitchL[0],  self.cfg.analysisArgs, take_id=None )
247
+                self.pulseUsL,_,holdDutyPctL = form_final_pulse_list( outDir,  pitchL[0],  self.cfg.analysisArgs, take_id=None )
238 248
 
239 249
                 noteN = cfg.analysisArgs['auditionNoteN']
240 250
                 self.pulseUsL   = [ self.pulseUsL[ int(round(i*126.0/(noteN-1)))] for i in range(noteN) ]
@@ -246,7 +256,7 @@ class CalibrateKeys:
246 256
                     os.mkdir(outDir)
247 257
 
248 258
             # start the sequencer
249
-            self.seq.start( ms, outDir, pitchL, self.pulseUsL, playOnlyFl )
259
+            self.seq.start( ms, outDir, pitchL, self.pulseUsL, holdDutyPctL, playOnlyFl )
250 260
         
251 261
 
252 262
     def _calc_next_out_dir_id( self, outDir ):
@@ -265,6 +275,7 @@ class App:
265 275
         self.audioDev  = None
266 276
         self.api       = None
267 277
         self.calibrate = None
278
+        self.keyboard  = None
268 279
         
269 280
     def setup( self, cfg ):
270 281
         self.cfg = cfg
@@ -294,12 +305,18 @@ class App:
294 305
 
295 306
                 self.calibrate = CalibrateKeys( cfg, self.audioDev, self.api )
296 307
 
308
+                self.keyboard  = Keyboard( cfg, self.audioDev, self.api )
297 309
     
298 310
         return res
299 311
 
300 312
     def tick( self, ms ):
313
+        
314
+        self.audioDev.tick(ms)
315
+        
301 316
         if self.calibrate:
302 317
             self.calibrate.tick(ms)
318
+        if self.keyboard:
319
+            self.keyboard.tick(ms)
303 320
 
304 321
     def audio_dev_list( self, ms ):
305 322
         portL = self.audioDev.get_port_list( True )
@@ -315,9 +332,24 @@ class App:
315 332
         chordL = [ [pitch]  for pitch in range(pitchRangeL[0], pitchRangeL[1]+1)]
316 333
         self.calibrate.start(  ms, chordL, cfg.full_pulseL, playOnlyFl=True )
317 334
 
335
+    def keyboard_start_pulse_idx( self, ms, argL ):
336
+        pitchL = [ pitch  for pitch in range(argL[0], argL[1]+1)]        
337
+        self.keyboard.start(  ms, pitchL, argL[2], None )
338
+
339
+    def keyboard_repeat_pulse_idx( self, ms, argL ):
340
+        self.keyboard.repeat(  ms, argL[0], None )
341
+        
342
+    def keyboard_start_target_db( self, ms, argL ):
343
+        pitchL = [ pitch  for pitch in range(argL[0], argL[1]+1)]        
344
+        self.keyboard.start(  ms, pitchL, None, argL[2] )
345
+
346
+    def keyboard_repeat_target_db( self, ms, argL ):
347
+        self.keyboard.repeat(  ms, None, argL[0] )
348
+
318 349
     def calibrate_keys_stop( self, ms ):
319 350
         self.calibrate.stop(ms)
320
-
351
+        self.keyboard.stop(ms)
352
+        
321 353
     def quit( self, ms ):
322 354
         if self.api:
323 355
             self.api.close()
@@ -435,12 +467,16 @@ class Shell:
435 467
     def __init__( self, cfg ):
436 468
         self.appProc = None
437 469
         self.parseD  = {
438
-            'q':{ "func":'quit',                  "minN":0,  "maxN":0, "help":"quit"},
439
-            '?':{ "func":"_help",                 "minN":0,  "maxN":0, "help":"Print usage text."},
440
-            'a':{ "func":"audio_dev_list",        "minN":0,  "maxN":0, "help":"List the audio devices."},
441
-            'c':{ "func":"calibrate_keys_start",  "minN":1,  "maxN":2, "help":"Calibrate a range of keys. "},
442
-            's':{ "func":"calibrate_keys_stop",   "minN":0,  "maxN":0, "help":"Stop key calibration"},
443
-            'p':{ "func":"play_keys_start",       "minN":1,  "maxN":2, "help":"Play current calibration"}
470
+            'q':{ "func":'quit',                      "minN":0,  "maxN":0, "help":"quit"},
471
+            '?':{ "func":"_help",                     "minN":0,  "maxN":0, "help":"Print usage text."},
472
+            'a':{ "func":"audio_dev_list",            "minN":0,  "maxN":0, "help":"List the audio devices."},
473
+            'c':{ "func":"calibrate_keys_start",      "minN":1,  "maxN":2, "help":"Calibrate a range of keys. "},
474
+            's':{ "func":"calibrate_keys_stop",       "minN":0,  "maxN":0, "help":"Stop key calibration"},
475
+            'p':{ "func":"play_keys_start",           "minN":1,  "maxN":2, "help":"Play current calibration"},
476
+            'k':{ "func":"keyboard_start_pulse_idx",  "minN":3,  "maxN":3, "help":"Play pulse index across keyboard"},
477
+            'r':{ "func":"keyboard_repeat_pulse_idx", "minN":1,  "maxN":1, "help":"Repeat pulse index across keyboard with new pulse_idx"},
478
+            'K':{ "func":"keyboard_start_target_db",  "minN":3,  "maxN":3, "help":"Play db across keyboard"},
479
+            'R':{ "func":"keyboard_repeat_target_db", "minN":1,  "maxN":1, "help":"Repeat db across keyboard with new pulse_idx"},
444 480
             }
445 481
 
446 482
     def _help( self, _=None ):
@@ -515,10 +551,8 @@ class Shell:
515 551
                 # tokenize the command
516 552
                 tokL = s.split(' ')
517 553
 
518
-
519 554
                 # execute the command
520 555
                 result = self._exec_cmd( tokL )
521
-
522 556
                         
523 557
                 # if this is the 'quit' command
524 558
                 if tokL[0] == 'q':

+ 7
- 4
p_ac.yml Целия файл

@@ -21,7 +21,7 @@
21 21
     outDir: "~/temp/p_ac_3c",
22 22
     noteDurMs: 1000,
23 23
     pauseDurMs: 1000,
24
-    holdDutyPctL: [ [0,50], [17000,65] ],
24
+    holdDutyPctL: [ [0,50], [22000,55] ],
25 25
 
26 26
     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],
27 27
     full_pulse1L: [  10000, 11000, 12000, 13000, 14000, 15000, 16000, 17000, 18000, 20000, 22000, 24000, 26000, 30000, 32000, 34000, 36000, 40000],
@@ -35,9 +35,9 @@
35 35
 
36 36
     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 ],    
37 37
 
38
-    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 ],    
38
+    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 ],    
39 39
 
40
-    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 ],    
40
+    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 ],    
41 41
 
42 42
     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 ],    
43 43
     
@@ -56,7 +56,10 @@
56 56
       maxDeltaDb: 1.5,  # maximum db change between volume samples (changes greater than this will trigger resampling)
57 57
       samplesPerDb: 4,   # count of samples per dB to resample ranges whose range is less than maxDeltaDb
58 58
       minSampleDistUs: 50, # minimum distance between sample points in microseconds
59
-      auditionNoteN: 19     # count of notes to play for audition
59
+      auditionNoteN: 19,    # count of notes to play for audition
60
+
61
+      finalPulseListCacheFn: "/home/kevin/temp/final_pulse_list_cache.pickle",
62
+      rmsAnalysisCacheFn: "/home/kevin/temp/rms_analysis_cache.pickle"
60 63
       },
61 64
     
62 65
      key_mapL: [

+ 79
- 0
plot_all_note_durations.ipynb
Файловите разлики са ограничени, защото са твърде много
Целия файл


+ 300
- 0
plot_note_analysis.py Целия файл

@@ -0,0 +1,300 @@
1
+import os,sys,pickle
2
+import numpy as np
3
+
4
+import matplotlib.pyplot as plt
5
+import matplotlib._color_data as mcd
6
+from matplotlib.pyplot import figure
7
+
8
+from rms_analysis import rms_analysis_main 
9
+from rms_analysis import note_stats
10
+from rms_analysis import key_info_dictionary
11
+from rms_analysis import select_first_stable_note_by_delta_db
12
+from rms_analysis import select_first_stable_note_by_dur
13
+
14
+
15
+def do_plot(r, statsL ):
16
+
17
+    fig,ax = plt.subplots()
18
+
19
+    x = [ i / r.rms_srate for i in range(len(r.tdRmsDbV)) ]
20
+    ax.plot( x,r.tdRmsDbV, color="blue" )
21
+    
22
+    x = [ i / r.rms_srate for i in range(len(r.rmsDbV)) ]
23
+    ax.plot( x,r.rmsDbV, color="green")
24
+
25
+    ymx = np.max(r.tdRmsDbV)
26
+    ymn = np.min(r.tdRmsDbV)
27
+    
28
+    for r in statsL:
29
+        x = r.pkSmpSec
30
+        ax.axvline(x,ymax=ymx,ymin=ymn)
31
+        ax.text(x,r.pkDb+1,"%i ms" % r.durMs)
32
+        ax.text(x,r.pkDb+2,"%4.1f dB" % r.pkDb)
33
+        ax.text(x,r.pkDb+3,"%i us" % r.pulse_us)
34
+
35
+        if hasattr(r,"MIN"):
36
+            ax.plot(x,r.pkDb,marker="*",color="red")
37
+        
38
+    plt.show()
39
+
40
+
41
+def select_min_note( rr, statsL, minDurMs=600, minDb=8, contextSecs=10 ):
42
+
43
+    sel_note_r = None
44
+    
45
+    for r in statsL:
46
+
47
+        if r.pkDb > minDb and r.durMs > minDurMs:
48
+            sel_note_r = r
49
+            break
50
+
51
+
52
+    #print(rr.tdRmsDbV.shape,rr.rmsDbV.shape)
53
+    
54
+    if sel_note_r is None:
55
+        print("ERROR: No min note found.")
56
+    else:
57
+        #print(sel_note_r)
58
+    
59
+        bi = max(0,                   int(round(sel_note_r.pkSmpSec * rr.rms_srate - contextSecs*rr.rms_srate)))
60
+        ei = min(rr.tdRmsDbV.shape[0],int(round(sel_note_r.pkSmpSec * rr.rms_srate + contextSecs*rr.rms_srate)))
61
+
62
+        
63
+        
64
+        rr.tdRmsDbV = rr.tdRmsDbV[bi:ei]    
65
+        rr.rmsDbV   = rr.rmsDbV[bi:ei]    
66
+        offsSec     = bi / rr.rms_srate
67
+        
68
+        sL = []
69
+        for r in statsL:
70
+            begSmpIdx = int(round(r.begSmpSec * rr.rms_srate))
71
+            endSmpIdx = int(round(r.endSmpSec * rr.rms_srate))
72
+
73
+            if begSmpIdx > bi and endSmpIdx < ei:
74
+                r0 = r
75
+                r0.begSmpSec = r.begSmpSec - offsSec
76
+                r0.endSmpSec = r.endSmpSec - offsSec
77
+                r0.pkSmpSec  = r.pkSmpSec  - offsSec
78
+
79
+                if r.begSmpSec == sel_note_r.begSmpSec:
80
+                    setattr(r0,"MIN",True)
81
+                sL.append(r0)
82
+
83
+
84
+    return rr,sL
85
+        
86
+
87
+def plot_note_analysis( inDir ):
88
+
89
+    rmsWndMs=300
90
+    rmsHopMs=30
91
+    dbRefWndMs=500
92
+    harmCandN=5
93
+    harmN=3
94
+    durDecayPct = 50
95
+    
96
+    path = os.path.normpath(inDir)
97
+    
98
+    pathL = inDir.split(os.sep)
99
+    take_id    = int(pathL[-1])
100
+    midi_pitch = int(pathL[-2])
101
+
102
+    r = rms_analysis_main( inDir, midi_pitch, rmsWndMs=rmsWndMs, rmsHopMs=rmsHopMs, dbRefWndMs=dbRefWndMs, harmCandN=harmCandN, harmN=harmN, durDecayPct=durDecayPct )
103
+
104
+    r,statsL = select_min_note(r,r.statsL)
105
+    
106
+    do_plot(r,statsL)
107
+    
108
+
109
+def plot_note_analysis_dir( inDir, dirL ):
110
+
111
+
112
+    for folder in dirL:
113
+
114
+        path = os.path.join(inDir,str(folder),"0")
115
+
116
+        plot_note_analysis( path )
117
+        
118
+        
119
+
120
+def get_all_note_durations( inDir, cacheFn ):
121
+
122
+    folderL = os.listdir( inDir )
123
+
124
+    yL = []
125
+    for folder in folderL:
126
+
127
+        takeId = 0
128
+        rmsWndMs=300
129
+        rmsHopMs=30
130
+        dbRefWndMs=500
131
+        harmCandN=5
132
+        harmN=3
133
+        durDecayPct = 40
134
+    
135
+        path = os.path.normpath(inDir)
136
+    
137
+        midi_pitch = int( folder )
138
+        takePath = os.path.join(inDir,folder,str(takeId))
139
+
140
+        if os.path.isfile(os.path.join(takePath,'seq.json')):
141
+            print(midi_pitch)
142
+            r = rms_analysis_main( takePath, midi_pitch, rmsWndMs=rmsWndMs, rmsHopMs=rmsHopMs, dbRefWndMs=dbRefWndMs, harmCandN=harmCandN, harmN=harmN, durDecayPct=durDecayPct )
143
+
144
+            xL = []
145
+            for i,sr in enumerate(r.statsL):
146
+                xL.append((r.pkUsL[i],sr.durMs,sr.pkDb,sr.quality))
147
+
148
+            yL.append((midi_pitch,xL))
149
+
150
+    with open(cacheFn,"wb") as f:
151
+        pickle.dump(yL,f)
152
+
153
+
154
+    
155
+def plot_all_note_durations( cacheFn, pitchL=None, axN=12, yamlCfgFn=None, minDurMs=800, maxPulseUs=None ):
156
+
157
+    keyMapD = None
158
+    if yamlCfgFn is not None:
159
+        keyMapD = key_info_dictionary( keyMapL=None, yamlCfgFn=yamlCfgFn)
160
+        
161
+    fig,axL = plt.subplots(axN,1)
162
+    fig.set_size_inches(18.5, 10.5*axN)
163
+    
164
+    #cL =  list(mcd.CSS4_COLORS.values())
165
+    cL =  ['black','brown','orangered','saddlebrown','peru','olivedrab','lightgreen','springgreen','cadetblue','slategray','royalblue','navy','darkviolet','deeppink','crimson']
166
+
167
+    
168
+    yD=[]
169
+    with open(cacheFn,"rb") as f:
170
+        yD = dict(pickle.load(f))
171
+        
172
+
173
+    cn = 3 #min(1,len(cL)//len(yD))
174
+
175
+    ci = 0
176
+    i = 0
177
+    for midi_pitch in pitchL:
178
+
179
+        if (pitchL is not None and midi_pitch not in pitchL) or midi_pitch not in yD:
180
+            continue
181
+
182
+        
183
+        xL = yD[midi_pitch]
184
+
185
+        pkUsL,durMsL,pkDbL,qualityL = tuple(zip(*xL))
186
+
187
+        if maxPulseUs is not None:
188
+            pkUsL = np.array(pkUsL)
189
+            pkUsL    = pkUsL[ pkUsL < maxPulseUs ]
190
+            durMsL   = durMsL[0:len(pkUsL)]
191
+            pkDbL    = pkDbL[0:len(pkUsL)]
192
+            qualityL = qualityL[0:len(pkUsL)]
193
+
194
+        axi = i//(len(pitchL)//axN)
195
+
196
+        if keyMapD is None:
197
+            legendLabel = str(midi_pitch)
198
+        else:
199
+            legendLabel = getattr(keyMapD[midi_pitch],'type') + " " + getattr(keyMapD[midi_pitch],'class') + str(midi_pitch)
200
+            
201
+        axL[axi].plot(pkUsL,durMsL,color=cL[ci],label=legendLabel)
202
+
203
+        # plot the quietest stable note
204
+        if minDurMs is not None:
205
+            sni = select_first_stable_note_by_dur( durMsL, minDurMs )
206
+
207
+            if sni is not None:
208
+                axL[axi].plot(pkUsL[sni],durMsL[sni],marker=".",color='red')
209
+                axL[axi].text(pkUsL[sni],durMsL[sni] + 50,"%4.1f" % pkDbL[sni])
210
+
211
+        sni = select_first_stable_note_by_delta_db( pkDbL, pkUsL )
212
+        
213
+        if sni is not None:
214
+            axL[axi].plot(pkUsL[sni],durMsL[sni],marker=".",color='blue')
215
+            axL[axi].text(pkUsL[sni],durMsL[sni] + 50,"%4.1f" % pkDbL[sni])
216
+                
217
+        ci += cn
218
+        if ci >= len(cL):
219
+            ci = ci - len(cL)
220
+
221
+        axL[axi].legend()
222
+        i+=1
223
+
224
+        
225
+    plt.show()
226
+
227
+def plot_quiet_note_db( cacheFn, yamlCfgFn, minDurMs=700 ):
228
+    
229
+    keyMapD = key_info_dictionary( keyMapL=None, yamlCfgFn=yamlCfgFn)
230
+    
231
+    yD=[]
232
+    with open(cacheFn,"rb") as f:
233
+        yD = dict(pickle.load(f))
234
+
235
+    dbL = []
236
+        
237
+    for midi_pitch in range(24,108):
238
+
239
+        pk0Db = 0
240
+        pk1Db = 0
241
+        minDb = 0
242
+        if midi_pitch in yD:
243
+
244
+            xL = yD[midi_pitch]
245
+
246
+            pkUsL,durMsL,pkDbL,qualityL = tuple(zip(*xL))
247
+
248
+            # plot the quietest stable note
249
+            sni = select_first_stable_note_by_dur( durMsL, minDurMs )
250
+            if sni is not None:
251
+                pk0Db = pkDbL[sni]
252
+
253
+            sni = select_first_stable_note_by_delta_db( pkDbL, pkUsL )
254
+            if sni is not None:
255
+                pk1Db = pkDbL[sni]
256
+            
257
+            minDb = min(pk0Db,pk1Db)
258
+            
259
+        dbL.append( (midi_pitch, minDb, pk0Db,pk1Db) )
260
+
261
+
262
+    fig,ax = plt.subplots()
263
+
264
+    pitchL,minDbL,pk0DbL,pk1DbL = tuple(zip(*dbL))
265
+
266
+        
267
+    
268
+    ax.plot( pitchL, pk0DbL, label="dur" )
269
+    ax.plot( pitchL, pk1DbL, label="delta" )
270
+    #ax.plot( pitchL, minDbL, label="min" )
271
+
272
+    for i,pitch in enumerate(pitchL):
273
+        ax.text( pitch, pk0DbL[i]+1, "%i %s" % (pitch,getattr(keyMapD[pitch],'type')))
274
+
275
+    ax.axhline( np.mean(minDbL), label="mean", color="blue" )
276
+    ax.axhline( np.median(minDbL), label="median", color="green" )
277
+    
278
+    ax.legend()
279
+    plt.show()
280
+    
281
+    
282
+        
283
+
284
+        
285
+if __name__ == "__main__":
286
+
287
+    #inDir = sys.argv[1]
288
+    
289
+    #plot_note_analysis( inDir )
290
+
291
+    pitchL = [ 30,31,32,33,34,35 ]
292
+    pitchL = [ 70,71,72,73,74,75 ]
293
+    
294
+    #plot_note_analysis_dir( "/home/kevin/temp/p_ac_3c",pitchL)
295
+
296
+    durFn = "/home/kevin/temp/cache_note_dur.pickle"
297
+    #get_all_note_durations("/home/kevin/temp/p_ac_3c",durFn)
298
+    #plot_all_note_durations(durFn, np.arange(45,55),2,"p_ac.yml",800,20000)
299
+
300
+    plot_quiet_note_db(durFn,"p_ac.yml")

+ 49
- 29
plot_seq.py Целия файл

@@ -3,6 +3,8 @@ import matplotlib.pyplot as plt
3 3
 import numpy as np
4 4
 from common import parse_yaml_cfg
5 5
 from rms_analysis import rms_analysis_main
6
+from rms_analysis import select_first_stable_note_by_delta_db
7
+from rms_analysis import select_first_stable_note_by_dur
6 8
 
7 9
 def is_nanV( xV ):
8 10
     
@@ -29,29 +31,6 @@ def form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ):
29 31
     # append the midi pitch to the input directory
30 32
     #inDir = os.path.join( inDir, "%i" % (midi_pitch))
31 33
 
32
-    if False:
33
-        # determine the take id if none was given
34
-        if take_id is None:
35
-            take_id = _find_max_take_id( inDir )
36
-
37
-        inDir = os.path.join(inDir,"%i" % (take_id))
38
-
39
-        assert( os.path.isdir(inDir))
40
-
41
-        # analyze the requested take audio
42
-        r = rms_analysis_main( inDir, midi_pitch, **analysisArgsD['rmsAnalysisArgs'] )
43
-
44
-        pkL = []
45
-        # store the peaks in pkL[ (db,us) ]
46
-        for db,us in zip(r.pkDbL,r.pkUsL):
47
-            pkL.append( (db,us) )
48
-
49
-        # sort the peaks on increasing attack pulse microseconds
50
-        pkL = sorted( pkL, key= lambda x: x[1] )
51
-
52
-        # split pkL 
53
-        pkDbL,pkUsL = tuple(zip(*pkL))
54
-
55 34
     dirL =  os.listdir(inDir)
56 35
 
57 36
     pkL = []
@@ -123,7 +102,7 @@ def form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None ):
123 102
         # print("Multi-value pulse locations were found during velocity table formation: ",multValL)
124 103
         pass
125 104
 
126
-    return pulseUsL,pulseDbL
105
+    return pulseUsL,pulseDbL,r.holdDutyPctL
127 106
     
128 107
 
129 108
 
@@ -132,7 +111,7 @@ def merge_close_sample_points( pkDbUsL, minSampleDistanceUs ):
132 111
     avg0Us = np.mean(np.diff([ x[1] for x in pkDbUsL ]))
133 112
     n0 = len(pkDbUsL)
134 113
     
135
-    while True:
114
+    while True and n0>0:
136 115
         us0 = None
137 116
         db0 = None
138 117
         
@@ -262,7 +241,7 @@ def plot_resample_pulse_times( inDir, analysisArgsD ):
262 241
     newPulseUsL, rmsDbV, pulseUsL = form_resample_pulse_time_list( inDir, analysisArgsD )
263 242
 
264 243
     midi_pitch = int( inDir.split("/")[-1] )
265
-    velTblUsL,velTblDbL = form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None )
244
+    velTblUsL,velTblDbL,_ = form_final_pulse_list( inDir, midi_pitch, analysisArgsD, take_id=None )
266 245
     
267 246
     fig,ax = plt.subplots()
268 247
 
@@ -432,8 +411,14 @@ def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ):
432 411
     
433 412
     # print beg/end boundaries
434 413
     for i,(begMs, endMs) in enumerate(r.eventTimeL):
414
+
415
+        pkSec = r.pkIdxL[i] / r.rms_srate
416
+        endSec = pkSec + r.statsL[i].durMs / 1000.0
417
+        
435 418
         ax.axvline( x=begMs/1000.0, color="green")
436 419
         ax.axvline( x=endMs/1000.0, color="red")
420
+        ax.axvline( x=pkSec,        color="black")
421
+        ax.axvline( x=endSec,       color="black")
437 422
         ax.text(begMs/1000.0, 20.0, str(i) )
438 423
 
439 424
 
@@ -448,10 +433,12 @@ def td_plot( ax, inDir, midi_pitch, id, analysisArgsD ):
448 433
 
449 434
 
450 435
     return r
451
-    
436
+
437
+
438
+
452 439
 def do_td_plot( inDir, analysisArgs ):
453 440
     
454
-    fig,axL = plt.subplots(2,1)
441
+    fig,axL = plt.subplots(3,1)
455 442
     fig.set_size_inches(18.5, 10.5, forward=True)
456 443
 
457 444
     id         = int(inDir.split("/")[-1])
@@ -459,8 +446,41 @@ def do_td_plot( inDir, analysisArgs ):
459 446
 
460 447
     r = td_plot(axL[0],inDir,midi_pitch,id,analysisArgs)
461 448
 
462
-    axL[1].plot( r.pkUsL, r.pkDbL, marker='.' )
449
+    qualityV = np.array([ x.quality  for x in r.statsL ]) * np.max(r.pkDbL)
450
+    durMsV   = np.array([ x.durMs    for x in r.statsL ])
451
+    avgV     = np.array([ x.durAvgDb for x in r.statsL ])
452
+    
453
+    #durMsV[ durMsV < 400 ] = 0
454
+    #durMsV = durMsV * np.max(r.pkDbL)/np.max(durMsV)
455
+    #durMsV  = durMsV / 100.0
456
+
457
+    dV = np.diff(r.pkDbL) / r.pkDbL[1:] 
458
+    
459
+    axL[1].plot( r.pkUsL, r.pkDbL, marker='.',label="pkDb" )
460
+    axL[1].plot( r.pkUsL, qualityV, marker='.',label="quality" )
461
+    axL[1].plot( r.pkUsL, avgV,     marker='.',label="avgDb" )
462
+    #axL[2].plot( r.pkUsL, durMsV,   marker='.' )
463
+    axL[2].plot( r.pkUsL[1:], dV,       marker='.',label='d')
464
+    axL[2].set_ylim([-1,1])
465
+    axL[1].legend()
466
+
467
+
468
+    sni = select_first_stable_note_by_dur( durMsV )
469
+    if sni is not None:
470
+        axL[1].plot( r.pkUsL[sni], r.pkDbL[sni], marker='*', color='red')
471
+        
472
+    sni = select_first_stable_note_by_delta_db( r.pkDbL )
473
+    if sni is not None:
474
+        axL[2].plot( r.pkUsL[sni], dV[sni-1], marker='*', color='red')
475
+    
463 476
 
477
+    for i,s in enumerate(r.statsL):
478
+        axL[1].text( r.pkUsL[i], r.pkDbL[i] + 1, "%i" % (i))
479
+
480
+    for i in range(1,len(r.pkUsL)):
481
+        axL[2].text( r.pkUsL[i],  dV[i-1], "%i" % (i))
482
+        
483
+    
464 484
     plt.show()
465 485
 
466 486
 def do_td_multi_plot( inDir, analysisArgs ):

+ 179
- 2
rms_analysis.py Целия файл

@@ -1,7 +1,8 @@
1
-import os,types,json
1
+import os,types,json,pickle
2 2
 from scipy.io import wavfile
3 3
 from scipy.signal import stft
4 4
 import numpy as np
5
+from common import parse_yaml_cfg
5 6
 
6 7
 
7 8
 def calc_harm_bins( srate, binHz, midiPitch, harmN ):
@@ -91,6 +92,8 @@ def audio_harm_rms( srate, xV, rmsWndMs, hopMs, dbRefWndMs, midiPitch, harmCandN
91 92
     hopSmpN   = int(round( hopMs    * srate / 1000.0))
92 93
 
93 94
     binHz   = srate / wndSmpN
95
+
96
+    #print( "STFT:", rmsWndMs, hopMs, wndSmpN, hopSmpN, wndSmpN-hopSmpN )
94 97
     
95 98
     f,t,xM = stft( xV, fs=srate, window="hann", nperseg=wndSmpN, noverlap=wndSmpN-hopSmpN, return_onesided=True )
96 99
 
@@ -115,7 +118,120 @@ def audio_harm_rms( srate, xV, rmsWndMs, hopMs, dbRefWndMs, midiPitch, harmCandN
115 118
     rmsV = rms_to_db( rmsV, rms_srate, dbRefWndMs )
116 119
     return rmsV, rms_srate, binHz
117 120
     
121
+def measure_duration_ms( rmsV, rms_srate, peak_idx, end_idx, decay_pct ):
122
+    """
123
+    Calcuate the time it takes for a note to decay from the peak at
124
+    rmsV[peak_idx] dB to 'decay_pct' percent of the peak value.
125
+    """
126
+    
127
+    pkRmsDb     = rmsV[ peak_idx ]
128
+
129
+    # calc the note turn-off (offset) db as a percentage of the peak amplitude
130
+    offsetRmsDb = pkRmsDb * decay_pct / 100.0
131
+
132
+    # calc the sample index where the note is off
133
+    offset_idx  = peak_idx + np.argmin( np.abs(rmsV[peak_idx:end_idx] - offsetRmsDb) )
134
+
135
+    
136
+    # calc the duration of the note
137
+    dur_ms =  int(round((offset_idx - peak_idx) * 1000.0 / rms_srate))
138
+
139
+    #print(pkRmsDb, offsetRmsDb, peak_idx, offset_idx, end_idx, dur_ms, rms_srate)
140
+    
141
+    return dur_ms
142
+
143
+
144
+def select_first_stable_note_by_dur( durMsL, minDurMs=800 ):
145
+
146
+    first_stable_idx = None
147
+    for i,durMs in enumerate(durMsL):
148
+        if durMs > minDurMs and first_stable_idx is None:
149
+            first_stable_idx = i
150
+        else:
151
+            if durMs < minDurMs:
152
+                first_stable_idx = None
153
+                
154
+    return first_stable_idx
155
+
156
+def select_first_stable_note_by_delta_db_1( pkDbL, pkUsL, maxPulseUs=0.1 ):
157
+
158
+    wndN = 5
159
+    aL = []
160
+    dV = np.diff(pkDbL) / pkDbL[1:]
161
+    
162
+    for ei in range(wndN,len(pkDbL)):
163
+        xV =  dV[ei-wndN:ei]
164
+        avg = np.mean(np.abs(xV))
165
+        aL.append(avg)
166
+
167
+    k = np.argmin(np.abs(np.array(pkUsL) - maxPulseUs))
168
+
169
+    print(aL)
170
+    print(k)
171
+
172
+    
173
+    for i in range(k,0,-1):
174
+        if aL[i] > maxDeltaDb:
175
+            return i + 1
176
+
177
+    return None
178
+    
179
+    
180
+def select_first_stable_note_by_delta_db( pkDbL, pkUsL=None, maxPulseUs=0.1 ):
181
+
182
+    wndN = 5
183
+    
184
+    dV = np.diff(pkDbL) / pkDbL[1:]
185
+
186
+    
187
+    for ei in range(wndN,len(pkDbL)):
188
+        xV =  dV[ei-wndN:ei]
189
+        avg = np.mean(np.abs(xV))
190
+
191
+        if avg < .1:
192
+            return (ei-wndN)+1
193
+
194
+
195
+    return None
196
+
197
+def note_stats( r, decay_pct=50.0, extraDurSearchMs=500 ):
198
+    
199
+    statsL = []
200
+
201
+    srate = r.rms_srate
202
+
203
+    qmax = 0
118 204
     
205
+    for i,(begSmpMs, endSmpMs) in enumerate(r.eventTimeL):
206
+
207
+        begSmpIdx = int(round(srate * begSmpMs / 1000.0))
208
+        endSmpIdx = int(round(srate * (endSmpMs + extraDurSearchMs) / 1000.0))
209
+        pkSmpIdx  = r.pkIdxL[i]
210
+
211
+        durMs = measure_duration_ms( r.rmsDbV, srate, pkSmpIdx, endSmpIdx, decay_pct )
212
+
213
+        bi = pkSmpIdx
214
+        ei = pkSmpIdx + int(round(durMs * srate / 1000.0))
215
+
216
+        #bi = begSmpIdx
217
+        #ei = endSmpIdx
218
+
219
+        qualityCoeff =  np.sum(r.rmsDbV[bi:ei]) + np.sum(r.tdRmsDbV[bi:ei])
220
+        if qualityCoeff > qmax:
221
+            qmax = qualityCoeff
222
+
223
+        durAvgDb = (np.mean(r.rmsDbV[bi:ei]) + np.mean(r.tdRmsDbV[bi:ei]))/2.0
224
+
225
+        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 }))
226
+
227
+    for i,r in enumerate(statsL):
228
+        statsL[i].quality /= qmax
229
+                           
230
+    
231
+    return statsL
232
+        
233
+        
234
+        
119 235
                
120 236
 def locate_peak_indexes( xV, xV_srate, eventMsL ):
121 237
 
@@ -130,10 +246,21 @@ def locate_peak_indexes( xV, xV_srate, eventMsL ):
130 246
     return pkIdxL
131 247
 
132 248
 
249
+def key_info_dictionary( keyMapL=None, yamlCfgFn=None):
133 250
 
251
+    if yamlCfgFn is not None:
252
+        cfg = parse_yaml_cfg(yamlCfgFn)
134 253
 
254
+        keyMapL = cfg.key_mapL
135 255
 
136
-def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=500, harmCandN=5, harmN=3 ):
256
+    kmD = {}
257
+    for d in keyMapL:
258
+        kmD[ d['midi'] ] = types.SimpleNamespace(**d)
259
+
260
+    return kmD
261
+
262
+
263
+def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=500, harmCandN=5, harmN=3, durDecayPct=40 ):
137 264
 
138 265
     seqFn     = os.path.join( inDir, "seq.json")
139 266
     audioFn   = os.path.join( inDir, "audio.wav")
@@ -154,6 +281,15 @@ def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=
154 281
     
155 282
     pkIdxL = locate_peak_indexes( rmsDbV, rms_srate, r['eventTimeL'] )
156 283
 
284
+    
285
+
286
+    holdDutyPctL = None
287
+    if 'holdDutyPct' in r:
288
+        holdDutyPctL = [ (0, r['holdDutyPct']) ]
289
+    else:
290
+        holdDutyPctL = r['holdDutyPctL']
291
+
292
+
157 293
     r = types.SimpleNamespace(**{
158 294
         "audio_srate":srate,
159 295
         "tdRmsDbV": tdRmsDbV,
@@ -166,7 +302,48 @@ def rms_analysis_main( inDir, midi_pitch, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=
166 302
         #"min_pk_idx":min_pk_idx,
167 303
         #"max_pk_idx":max_pk_idx,
168 304
         "eventTimeL":r['eventTimeL'],
305
+        "holdDutyPctL":holdDutyPctL,
169 306
         'pkDbL': [ rmsDbV[ i ] for i in pkIdxL ],
170 307
         'pkUsL':r['pulseUsL'] })
171 308
 
309
+
310
+    statsL = note_stats(r,durDecayPct)
311
+
312
+    setattr(r,"statsL",   statsL )
313
+
172 314
     return r
315
+
316
+
317
+def rms_analysis_main_all( inDir, cacheFn, rmsWndMs=300, rmsHopMs=30, dbRefWndMs=500, harmCandN=5, harmN=3, durDecayPct=40 ):
318
+
319
+    if os.path.isfile(cacheFn):
320
+        print("READING analysis cache file: %s" % (cacheFn))
321
+        with open(cacheFn,"rb") as f:
322
+            rD = pickle.load(f)
323
+            return rD
324
+    
325
+    
326
+    folderL = os.listdir(inDir)
327
+
328
+    rD = {}
329
+
330
+    for folder in folderL:
331
+
332
+        pathL = folder.split(os.sep)
333
+
334
+        midi_pitch = int(pathL[-1])
335
+
336
+        print(midi_pitch)
337
+
338
+        path = os.path.join(inDir,folder,'0')
339
+
340
+        if os.path.isdir(path) and os.path.isfile(os.path.join(os.path.join(path,"seq.json"))):
341
+            r = rms_analysis_main( path, midi_pitch, rmsWndMs=rmsWndMs, rmsHopMs=rmsHopMs, dbRefWndMs=dbRefWndMs, harmCandN=harmCandN, harmN=harmN, durDecayPct=durDecayPct )
342
+
343
+            rD[ midi_pitch ] = r
344
+
345
+
346
+    with open(cacheFn,"wb") as f:
347
+        pickle.dump(rD,f)
348
+        
349
+    return rD

+ 76
- 0
rt_note_analysis.py Целия файл

@@ -0,0 +1,76 @@
1
+import types
2
+
3
+import numpy as np
4
+
5
+from rms_analysis import audio_harm_rms
6
+from rms_analysis import audio_rms
7
+from rms_analysis import locate_peak_indexes
8
+from rms_analysis import measure_duration_ms
9
+
10
+
11
+class RT_Analyzer:
12
+    def __init__(self):
13
+        self.td_dur_ms = 0
14
+        self.td_db     = 0
15
+        self.hm_dur_ms = 0
16
+        self.hm_db     = 0
17
+    
18
+    def analyze_note( self, audioDev, midi_pitch, begTimeMs, endTimeMs, anlArgD ):
19
+        td_dur_ms = 0
20
+        td_db     = 0
21
+        hm_dur_ms = 0
22
+        hm_db     = 0
23
+
24
+
25
+        decay_pct = 50.0
26
+
27
+        result = audioDev.linear_buffer()
28
+
29
+        if result:
30
+
31
+
32
+            sigV = result.value
33
+
34
+            anlArgs = types.SimpleNamespace(**anlArgD)
35
+
36
+            rmsDbV, rms_srate, binHz = audio_harm_rms( audioDev.srate, np.squeeze(sigV), anlArgs.rmsWndMs, anlArgs.rmsHopMs, anlArgs.dbRefWndMs, midi_pitch, anlArgs.harmCandN, anlArgs.harmN  )
37
+
38
+            pkIdxL = locate_peak_indexes( rmsDbV, rms_srate, [( begTimeMs, endTimeMs)] )
39
+
40
+            if len(pkIdxL) > 0:
41
+
42
+                end_idx = int(round(endTimeMs * rms_srate / 1000.0))
43
+
44
+                if end_idx > pkIdxL[0]:
45
+                    hm_dur_ms = measure_duration_ms( rmsDbV, rms_srate, pkIdxL[0], end_idx, decay_pct )
46
+
47
+                hm_db = rmsDbV[ pkIdxL[0] ]
48
+
49
+            tdRmsDbV, rms0_srate = audio_rms( audioDev.srate, np.squeeze(sigV), anlArgs.rmsWndMs, anlArgs.rmsHopMs, anlArgs.dbRefWndMs )
50
+
51
+            tdPkIdxL = locate_peak_indexes( tdRmsDbV, rms0_srate,  [( begTimeMs, endTimeMs)] )
52
+
53
+            if len(tdPkIdxL):
54
+
55
+                end_idx = int(round(endTimeMs * rms0_srate / 1000.0))
56
+
57
+                if end_idx > tdPkIdxL[0]:
58
+                    td_dur_ms = measure_duration_ms( tdRmsDbV, rms0_srate, tdPkIdxL[0], end_idx, decay_pct )
59
+
60
+                td_db = tdRmsDbV[ tdPkIdxL[0] ]
61
+
62
+
63
+            td_d_ms = td_dur_ms - self.td_dur_ms
64
+            td_d_db = td_db     - self.td_db
65
+            hm_d_ms = hm_dur_ms - self.hm_dur_ms
66
+            hm_d_db = hm_db     - self.hm_db
67
+                
68
+                
69
+            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) )
70
+
71
+            self.td_dur_ms = td_dur_ms
72
+            self.td_db     = td_db
73
+            self.hm_dur_ms = hm_dur_ms
74
+            self.hm_db     = hm_db
75
+
76
+

Loading…
Отказ
Запис