瀏覽代碼

Initial commit.

master
kpl 4 年之前
當前提交
c3db8c8ab6
共有 6 個文件被更改,包括 1303 次插入0 次删除
  1. 256
    0
      AudioDevice.py
  2. 80
    0
      convert.py
  3. 522
    0
      p_ac.py
  4. 132
    0
      p_ac.yml
  5. 279
    0
      plot_seq.py
  6. 34
    0
      result.py

+ 256
- 0
AudioDevice.py 查看文件

@@ -0,0 +1,256 @@
1
+import pprint,queue
2
+import sounddevice as sd
3
+import soundfile as sf
4
+import numpy as np
5
+
6
+from result import Result
7
+
8
+class AudioDevice(object):
9
+    def __init__(self):
10
+        self.inputPortIdx  = None
11
+        self.outputPortIdx = None
12
+        self.recordFl      = False
13
+        self.sineFl        = False
14
+        self.inStream      = None
15
+        self.queue         = queue.Queue()
16
+        self.bufL          = []
17
+        self.bufIdx        = -1
18
+        self.srate         = 0
19
+        self.ch_cnt        = 1
20
+        
21
+
22
+    def setup( self, **kwargs  ):
23
+
24
+        res = Result()
25
+        
26
+        if kwargs is None:
27
+            return res
28
+
29
+        if 'inPortLabel' in kwargs:
30
+            res += self.select_port( True, kwargs['inPortLabel'] )
31
+            
32
+        if 'outPortLabel' in kwargs:
33
+            res += self.select_port( False, kwargs['outPortLabel'] )
34
+
35
+        return res
36
+    
37
+    def get_port_label( self, inFl ):
38
+
39
+        portL     = self.get_port_list(inFl)
40
+        portIdx   = self.inputPortIdx if inFl else self.outputPortIdx
41
+        portLabel = None
42
+
43
+        if portL and (portIdx is not None):
44
+            for port in portL:
45
+                if portIdx == port['index']:
46
+                    portLabel = port['label']
47
+                    break
48
+                
49
+        return portLabel
50
+        
51
+
52
+    def get_state( self ):
53
+
54
+        d= {
55
+            'recordFl':    self.inStream is not None and self.inStream.active,
56
+            'sineFl':      self.sineFl,
57
+            'inPortL':     [ d['label'] for d in self.get_port_list(True) ],
58
+            'outPortL':    [ d['label'] for d in self.get_port_list(False) ],
59
+            'inPortLabel':  self.get_port_label(True),
60
+            'outPortLabel': self.get_port_label(False)
61
+            }
62
+
63
+        return d
64
+    
65
+    def get_port_list( self, inFl ):
66
+
67
+        devLabelL = str(sd.query_devices()).split("\n")
68
+
69
+        portL = []
70
+        for i,dev in enumerate(sd.query_devices()):
71
+            isInputFl =  dev['max_input_channels']  > 0
72
+            isOutputFl = dev['max_output_channels'] > 0
73
+            if (inFl and isInputFl) or (not inFl and isOutputFl):
74
+                portL.append({ 'chN':dev['max_input_channels'], 'label':devLabelL[i].strip(), 'index':i } )
75
+
76
+        return portL
77
+    
78
+    def get_in_port_list( self ):
79
+        return get_port_list( True )
80
+
81
+    def get_out_port_list( self ):
82
+        return get_port_list( False )
83
+
84
+    def select_port( self, inFl, portLabel ):
85
+
86
+        res = Result()
87
+        
88
+        if inFl and self.inStream is not None and self.inStream.active:
89
+            return res.set_error("A new port may not be selected while it is active." % (portLabel))
90
+        
91
+        devLabelL = str(sd.query_devices()).split("\n")
92
+        foundFl   = False
93
+
94
+        if portLabel is None and len(devLabelL)>0:
95
+            portLabel = devLabelL[0]
96
+
97
+        portLabel = portLabel.strip()
98
+        N = len(portLabel)
99
+
100
+        # for each device
101
+        for i,label in enumerate(devLabelL):
102
+
103
+            label = label.strip()
104
+
105
+            # if the first N char's of this label match the requested port label
106
+            if len(label)>=N and label[0:N] == portLabel:
107
+                if inFl:
108
+                    self.inputPortIdx = i
109
+                else:
110
+                    self.outputPortIdx = i
111
+                foundFl = True
112
+
113
+                if inFl:
114
+                    dev = sd.query_devices()[i]
115
+                    self.srate = dev['default_samplerate']
116
+                    if self.inStream:
117
+                        self.inStream.close()
118
+
119
+                    self.inStream = sd.InputStream(samplerate=self.srate, device=self.inputPortIdx, channels=self.ch_cnt, callback=self._record_callback, dtype=np.dtype('float32'))
120
+
121
+
122
+                break
123
+
124
+        if not foundFl:
125
+            res.set_error("Unable to locate the audio port named: '%s'." % (portLabel))
126
+
127
+        return res
128
+
129
+    def record_enable( self, enableFl ):
130
+
131
+        # if the input stream has not already been configured
132
+        if enableFl and self.inStream is None:
133
+            return Result(None,'Recording cannot start because a recording input port has not been selected.')
134
+
135
+        if not enableFl and self.inStream is None:
136
+            return Result()
137
+
138
+        # if the input stream already matches the 'enableFl'.
139
+        if enableFl == self.inStream.active:
140
+            return Result()
141
+
142
+        # if we are starting recording
143
+        if enableFl:
144
+
145
+            try:
146
+                self.bufL = []
147
+                self.inStream.start()
148
+                
149
+            except Exception as ex:
150
+                return Result(None,'The recording input stream could not be started. Reason: %s' % str(ex))
151
+
152
+        else:
153
+            self.inStream.stop()
154
+
155
+        return Result()
156
+    
157
+    def play_buffer( self ):
158
+        if not self.playFl:
159
+            self.playFl = True
160
+
161
+    def play_end( self ):
162
+        if self.playFl:
163
+            self.playFl = False
164
+    
165
+    def write_buffer( self, fn=None ):
166
+
167
+        if fn is None:
168
+            fn = "temp.wav"
169
+
170
+        with sf.SoundFile(fn,'w',samplerate=int(self.srate), channels=int(self.ch_cnt)) as f:
171
+            for i,buf in enumerate(self.bufL):
172
+                N = buf.shape[0] if i<len(self.bufL)-1 else self.bufIdx
173
+                f.write(buf[0:N,])
174
+
175
+        return Result()
176
+
177
+    def buffer_sample_count( self ):
178
+        smpN = 0
179
+        for i in range(len(self.bufL)):
180
+            smpN += self.bufIdx if i == len(self.bufL)-1 else self.bufL[i].shape[0]
181
+
182
+        return Result(smpN)
183
+
184
+    def linear_buffer( self ):
185
+
186
+        smpN = self.buffer_sample_count()
187
+        bV   = np.zeros( (smpN,self.ch_cnt) )
188
+
189
+        bi = 0
190
+        for i in range(len(self.bufL)):
191
+          bufSmpN = self.bufIdx if i == len(self.bufL)-1 else self.bufL[i].shape[0]
192
+          bV[ bi:bi+bufSmpN, ] = self.bufL[i][0:bufSmpN]
193
+          bi += bufSmpN
194
+          
195
+        return Result(bV)
196
+            
197
+    
198
+    def _record_callback(self, v, frames, time, status):
199
+        """This is called (from a separate thread) for each audio block."""
200
+        # send audio to the app
201
+        self.queue.put(v.copy())
202
+
203
+    def tick( self, ms ):
204
+
205
+        try:
206
+            while True:
207
+
208
+                # get audio from the incoming audio thread
209
+                v = self.queue.get_nowait()
210
+
211
+                #print(type(v),len(v),np.result_type(v))
212
+                self._update_buffer(v)
213
+                
214
+        except queue.Empty: # queue was empty
215
+            pass
216
+
217
+        return Result()
218
+
219
+
220
+    def _update_buffer(self,v ):
221
+
222
+        # get the length of the last buffer in bufL
223
+        bufN = 0 if len(self.bufL)==0 else self.bufL[-1].shape[0] - self.bufIdx
224
+
225
+        # get the length of the incoming sample vector
226
+        vN   = v.shape[0]
227
+
228
+        # if there are more incoming samples than space in the buffer
229
+        if vN > bufN:
230
+
231
+            # store as much of the incoming vector as possible
232
+            if len(self.bufL) > 0:
233
+                self.bufL[-1][self.bufIdx:,:] = v[0:bufN,:]
234
+
235
+            vi  = bufN # increment the starting position of the src vector
236
+            vN -= bufN # decrement the count of samples that needs to be stored
237
+
238
+            # add an empty buffer to bufL[]
239
+            N = int(self.srate * 60) # add a one minute buffer to bufL
240
+            self.bufL.append( np.zeros( (N, self.ch_cnt), dtype=np.dtype('float32') ) )
241
+            self.bufIdx = 0
242
+
243
+            self.bufL[-1][self.bufIdx:vN,:] = v[vi:,]
244
+            self.bufIdx += vN
245
+
246
+        else:
247
+            self.bufL[-1][self.bufIdx:self.bufIdx+vN,:] = v
248
+            self.bufIdx += vN
249
+            
250
+
251
+
252
+if __name__ == "__main__":
253
+
254
+    ad = AudioDevice()
255
+    ad.get_port_list(True)
256
+    

+ 80
- 0
convert.py 查看文件

@@ -0,0 +1,80 @@
1
+import os,json,pickle,csv
2
+from shutil import copyfile
3
+
4
+def event_times( eventTimeFn ):
5
+
6
+    eventL = []
7
+    
8
+    with open(eventTimeFn,"r") as f:
9
+
10
+        rdr = csv.reader(f)
11
+
12
+        for row in rdr:
13
+            if row[0] == 'start':
14
+                beginMs = int(row[1])
15
+            elif row[0] == 'key_down':
16
+                key_downMs = int(row[1]) - beginMs
17
+            elif row[0] == 'key_up':
18
+                key_upMs = row[1]
19
+
20
+                eventL.append( [ key_downMs, key_downMs+1000 ] )
21
+
22
+    return eventL
23
+
24
+def pulse_lengths( pulseLenFn ):
25
+
26
+    with open(pulseLenFn,'rb') as f:
27
+        d = pickle.load(f)
28
+        msL = d['msL']
29
+        # note: first posn in table is a multiplier
30
+        return [ msL[i]*msL[0] for i in  range(1,len(msL))]
31
+
32
+
33
+def convert( inDir, outDir ):
34
+
35
+    if not os.path.isdir(outDir):
36
+        os.mkdir(outDir)
37
+
38
+    for dirStr in os.listdir(inDir):
39
+
40
+        idir = os.path.join( inDir, dirStr )
41
+        
42
+        if os.path.isdir(idir):
43
+
44
+            eventTimeFn = os.path.join( idir, "labels_0.csv" )
45
+
46
+            eventTimeL = event_times(eventTimeFn)
47
+            
48
+            pulseTimeFn = os.path.join( idir, "table_0.pickle")
49
+
50
+            pulseUsL = pulse_lengths( pulseTimeFn )
51
+
52
+            pitch = idir.split("/")[-1]
53
+
54
+            
55
+            d = {
56
+                "pulseUsL":pulseUsL,
57
+                "pitchL":[ pitch ],
58
+                "noteDurMs":1000,
59
+                "pauseDurMs":0,
60
+                "holdDutyPct":50,
61
+                "eventTimeL":eventTimeL,
62
+                "beginMs":0
63
+            }
64
+
65
+            odir = os.path.join( outDir, pitch )
66
+            if not os.path.isdir(odir):
67
+                os.mkdir(odir)
68
+
69
+            with open(os.path.join( odir, "seq.json" ),"w") as f:
70
+                f.write(json.dumps( d ))
71
+
72
+            copyfile( os.path.join(idir,"audio_0.wav"), os.path.join(odir,"audio.wav"))
73
+                
74
+
75
+if __name__ == "__main__":
76
+    inDir = "/home/kevin/temp/picadae_ac_2/full_map"
77
+    outDir = "/home/kevin/temp/p_ac_3_cvt"
78
+
79
+    convert( inDir, outDir )
80
+    

+ 522
- 0
p_ac.py 查看文件

@@ -0,0 +1,522 @@
1
+import sys,os,argparse,yaml,types,logging,select,time,json
2
+from datetime import datetime
3
+
4
+import multiprocessing
5
+from multiprocessing import Process, Pipe
6
+
7
+from picadae_api  import Picadae
8
+from AudioDevice  import AudioDevice
9
+from result       import Result
10
+
11
+class AttackPulseSeq:
12
+    """ Sequence a fixed chord over a list of attack pulse lengths."""
13
+    
14
+    def __init__(self, audio, api, noteDurMs=1000, pauseDurMs=1000, holdDutyPct=50 ):
15
+        self.audio       = audio
16
+        self.api         = api
17
+        self.outDir      = None           # directory to write audio file and results
18
+        self.pitchL      = None           # chord to play
19
+        self.pulseUsL    = []             # one onset pulse length in microseconds per sequence element
20
+        self.noteDurMs   = noteDurMs      # duration of each chord in milliseconds
21
+        self.pauseDurMs  = pauseDurMs     # duration between end of previous note and start of next
22
+        self.holdDutyPct = holdDutyPct    # hold voltage duty cycle as a percentage (0-100)
23
+        
24
+        self.pulse_idx            = 0     # Index of next pulse 
25
+        self.state                = None  # 'note_on','note_off'
26
+        self.next_ms              = 0     # Time of next event (note-on or note_off)
27
+        self.eventTimeL           = []    # Onset/offset time of each note [ [onset_ms,offset_ms] ]
28
+        self.beginMs              = 0
29
+
30
+    def start( self, ms, outDir, pitchL, pulseUsL ):
31
+        self.outDir     = outDir         # directory to write audio file and results
32
+        self.pitchL     = pitchL         # chord to play
33
+        self.pulseUsL   = pulseUsL       # one onset pulse length in microseconds per sequence element
34
+        
35
+        self.pulse_idx  = 0
36
+        self.state      = 'note_on'
37
+        self.next_ms    = ms + 500       # wait for 500ms to play the first note (this will guarantee that there is some empty space in the audio file before the first note)
38
+        self.eventTimeL = [[0,0]] * len(pulseUsL) # initialize the event time         
39
+        self.beginMs    = ms
40
+        self.audio.record_enable(True)   # start recording audio
41
+        self.tick(ms)                    # play the first note
42
+        
43
+    def stop(self, ms):
44
+        self._send_note_off() # be sure that all notes are actually turn-off
45
+        self.audio.record_enable(False)  # stop recording audio
46
+        self._disable()          # disable this sequencer
47
+        self._write()            # write the results
48
+
49
+    def is_enabled(self):
50
+        return self.state is not None
51
+    
52
+    def tick(self, ms):
53
+
54
+        self.audio.tick(ms)
55
+        
56
+        # if next event time has arrived
57
+        if self.is_enabled() and  ms >= self.next_ms:
58
+
59
+            # if waiting to turn note on
60
+            if self.state == 'note_on':
61
+                self._note_on(ms)
62
+        
63
+            # if waiting to turn a note off
64
+            elif self.state == 'note_off':
65
+                self._note_off(ms)                
66
+                self.pulse_idx += 1
67
+                
68
+                # if all notes have been played
69
+                if self.pulse_idx >= len(self.pulseUsL):
70
+                    self.stop(ms)
71
+                    
72
+            else:                
73
+                assert(0)
74
+                    
75
+        
76
+    def _note_on( self, ms ):
77
+
78
+        self.eventTimeL[ self.pulse_idx ][0] = ms - self.beginMs
79
+        self.next_ms = ms + self.noteDurMs
80
+        self.state = 'note_off'
81
+
82
+        for pitch in self.pitchL:
83
+            self.api.note_on_us( pitch, int(self.pulseUsL[ self.pulse_idx ]) )
84
+            print("note-on:",pitch,self.pulse_idx)
85
+
86
+    def _note_off( self, ms ):
87
+        self.eventTimeL[ self.pulse_idx ][1] = ms - self.beginMs
88
+        self.next_ms = ms + self.pauseDurMs
89
+        self.state   = 'note_on'
90
+        
91
+        self._send_note_off()        
92
+
93
+    
94
+    def _send_note_off( self ):
95
+        for pitch in self.pitchL:
96
+            self.api.note_off( pitch )
97
+            print("note-off:",pitch,self.pulse_idx)
98
+
99
+    def _disable(self):
100
+        self.state = None
101
+        
102
+            
103
+    def _write( self ):
104
+
105
+        d = {
106
+            "pulseUsL":self.pulseUsL,
107
+            "pitchL":self.pitchL,
108
+            "noteDurMs":self.noteDurMs,
109
+            "pauseDurMs":self.pauseDurMs,
110
+            "holdDutyPct":self.holdDutyPct,
111
+            "eventTimeL":self.eventTimeL,
112
+            "beginMs":self.beginMs
113
+            }
114
+
115
+        print("Writing: ", self.outDir )
116
+
117
+        outDir = os.path.expanduser(self.outDir)
118
+
119
+        if  not os.path.isdir(outDir):
120
+            os.mkdir(outDir)
121
+        
122
+        with open(os.path.join( outDir, "seq.json" ),"w") as f:
123
+            f.write(json.dumps( d ))
124
+
125
+        self.audio.write_buffer( os.path.join( outDir, "audio.wav" ) )
126
+        
127
+            
128
+class CalibrateKeys:
129
+    def __init__(self, cfg, audioDev, api):
130
+        self.cfg      = cfg
131
+        self.seq      = AttackPulseSeq(  audioDev, api, noteDurMs=1000, pauseDurMs=1000, holdDutyPct=50 )
132
+        
133
+        self.label     = None
134
+        self.pulseUsL  = None
135
+        self.chordL   = None
136
+        self.pitch_idx = -1
137
+
138
+        
139
+    def start( self, ms, label, chordL, pulseUsL ):
140
+        if len(chordL) > 0:
141
+            self.label     = label
142
+            self.pulseUsL  = pulseUsL
143
+            self.chordL   = chordL
144
+            self.pitch_idx = -1
145
+            self._start_next_chord( ms )
146
+        
147
+        
148
+    def stop( self, ms ):
149
+        self.pitch_idx = -1
150
+        self.seq.stop(ms)
151
+
152
+    def is_enabled( self ):
153
+        return self.pitch_idx >= 0
154
+
155
+    def tick( self, ms ):
156
+        if self.is_enabled():
157
+            self.seq.tick(ms)
158
+
159
+            # if the sequencer is done playing 
160
+            if not self.seq.is_enabled():
161
+                self._start_next_chord( ms ) # ... else start the next sequence
162
+
163
+        return None
164
+
165
+    def _start_next_chord( self, ms ):
166
+
167
+        self.pitch_idx += 1
168
+
169
+        # if the last chord in chordL has been played ...
170
+        if self.pitch_idx >= len(self.chordL):
171
+            self.stop(ms)  # ... then we are done
172
+        else:
173
+
174
+            pitchL = self.chordL[ self.pitch_idx ]
175
+            
176
+            # be sure that the base directory exists
177
+            outDir = os.path.expanduser( cfg.outDir )
178
+            if not os.path.isdir( outDir ):
179
+                os.mkdir( outDir )
180
+
181
+            # form the output directory as "<label>_<pitch0>_<pitch1> ... "
182
+            dirStr = self.label + "_" + "_".join([ str(pitch) for pitch in pitchL ])
183
+
184
+            outDir = os.path.join(outDir, dirStr )
185
+
186
+            # start the sequencer
187
+            self.seq.start( ms, outDir, pitchL, self.pulseUsL )
188
+        
189
+    
190
+
191
+# This is the main application API it is running in a child process.
192
+class App:
193
+    def __init__(self ):
194
+        self.cfg       = None
195
+        self.audioDev  = None
196
+        self.api       = None
197
+        self.calibrate = None
198
+        
199
+    def setup( self, cfg ):
200
+        self.cfg = cfg
201
+
202
+        self.audioDev = AudioDevice()
203
+
204
+        #
205
+        # TODO: unify the result error handling
206
+        # (the API and the audio device return two diferent 'Result' types
207
+        #
208
+        
209
+        res = self.audioDev.setup(**cfg.audio)
210
+
211
+        if res:
212
+            self.api = Picadae( key_mapL=cfg.key_mapL)
213
+
214
+            # wait for the letter 'a' to come back from the serial port
215
+            api_res = self.api.wait_for_serial_sync(timeoutMs=cfg.serial_sync_timeout_ms)
216
+
217
+            # did the serial port sync fail?
218
+            if not api_res:
219
+                res.set_error("Serial port sync failed.")
220
+            else:
221
+                print("Serial port sync'ed")
222
+
223
+                self.calibrate = CalibrateKeys( cfg, self.audioDev, self.api )
224
+
225
+    
226
+        return res
227
+
228
+    def tick( self, ms ):
229
+        if self.calibrate:
230
+            self.calibrate.tick(ms)
231
+
232
+    def audio_dev_list( self, ms ):
233
+        portL = self.audioDev.get_port_list( True )
234
+
235
+        for port in portL:
236
+            print("chs:%4i label:%s" % (port['chN'],port['label']))
237
+
238
+    def calibrate_keys_start( self, ms, pitchRangeL ):
239
+        chordL = [ [pitch]  for pitch in range(pitchRangeL[0], pitchRangeL[1]+1)]
240
+        self.calibrate.start(  ms, "full", chordL, cfg.full_pulseL )
241
+
242
+    def calibrate_keys_stop( self, ms ):
243
+        self.calibrate.stop(ms)
244
+
245
+    def quit( self, ms ):
246
+        if self.api:
247
+            self.api.close()
248
+    
249
+
250
+def _send_error( pipe, res ):
251
+    if res is None:
252
+        return
253
+    
254
+    if res.msg:
255
+        pipe.send( [{"type":"error", 'value':res.msg}] )
256
+
257
+def _send_error_msg( pipe, msg ):
258
+    _send_error( pipe, Result(None,msg))
259
+
260
+def _send_quit( pipe ):
261
+    pipe.send( [{ 'type':'quit' }] )
262
+            
263
+# This is the application engine async. process loop
264
+def app_event_loop_func( pipe, cfg ):
265
+
266
+    multiprocessing.get_logger().info("App Proc Started.")
267
+
268
+    # create the asynchronous application object
269
+    app = App()
270
+    
271
+    res = app.setup(cfg)
272
+
273
+    # if the app did not initialize successfully
274
+    if not res:
275
+        _send_error( pipe, res )
276
+        _send_quit(pipe)
277
+        return
278
+
279
+    dt0 = datetime.now()
280
+
281
+    ms = 0
282
+
283
+    while True:
284
+        
285
+        # have any message arrived from the parent process?
286
+        if pipe.poll():
287
+
288
+            msg = None
289
+            try:
290
+                msg = pipe.recv()
291
+            except EOFError:
292
+                return
293
+
294
+            if not hasattr(app,msg.type):
295
+                _send_error_msg( pipe, "Unknown message type:'%s'." % (msg.type) )
296
+            else:
297
+
298
+                # get the command handler function in 'app'
299
+                func = getattr(app,msg.type)
300
+
301
+                ms  = int(round( (datetime.now() - dt0).total_seconds() * 1000.0) )
302
+                
303
+                # call the command handler
304
+                if msg.value:
305
+                    res = func( ms, msg.value )
306
+                else:
307
+                    res = func( ms )
308
+
309
+                # handle any errors returned from the commands
310
+                _send_error( pipe, res )
311
+
312
+            # if a 'quit' msg was recived then break out of the loop
313
+            if msg.type == 'quit':
314
+                _send_quit(pipe)
315
+                break
316
+
317
+
318
+        # give some time to the system
319
+        time.sleep(0.1)
320
+        
321
+        # calc the tick() time stamp
322
+        ms  = int(round( (datetime.now() - dt0).total_seconds() * 1000.0) )
323
+
324
+        # tick the app
325
+        app.tick( ms )
326
+        
327
+
328
+class AppProcess(Process):
329
+    def __init__(self,cfg):
330
+        self.parent_end, child_end = Pipe()
331
+        super(AppProcess, self).__init__(target=app_event_loop_func,name="AppProcess",args=(child_end,cfg))
332
+        self.doneFl    = False
333
+        
334
+
335
+    def send(self, d):
336
+        # This function is called by the parent process to send an arbitrary msg to the App process
337
+        self.parent_end.send( types.SimpleNamespace(**d) )
338
+        return None
339
+        
340
+    def recv(self):
341
+        # This function is called by the parent process to receive lists of child messages.
342
+        
343
+        msgL = None
344
+        if not self.doneFl and self.parent_end.poll():
345
+            
346
+            msgL = self.parent_end.recv()
347
+
348
+            for msg in msgL:
349
+                if msg['type'] == 'quit':
350
+                    self.doneFl = True
351
+            
352
+        return msgL
353
+    
354
+    def isdone(self):
355
+        return self.doneFl
356
+        
357
+
358
+class Shell:
359
+    def __init__( self, cfg ):
360
+        self.appProc = None
361
+        self.parseD  = {
362
+            'q':{ "func":'quit',                  "minN":0,  "maxN":0, "help":"quit"},
363
+            '?':{ "func":"_help",                 "minN":0,  "maxN":0, "help":"Print usage text."},
364
+            'a':{ "func":"audio_dev_list",        "minN":0,  "maxN":0, "help":"List the audio devices."},
365
+            'c':{ "func":"calibrate_keys_start",  "minN":1,  "maxN":2, "help":"Calibrate a range of keys."},
366
+            's':{ "func":"calibrate_keys_stop",   "minN":0,  "maxN":0, "help":"Stop key calibration"}
367
+            }
368
+
369
+    def _help( self, _=None ):
370
+        for k,d in self.parseD.items():
371
+            s = "{} = {}".format( k, d['help'] )
372
+            print(s)
373
+        return None
374
+
375
+
376
+    def _syntaxError( self, msg ):
377
+        return Result(None,"Syntax Error: " + msg )
378
+    
379
+    def _exec_cmd( self, tokL ):
380
+
381
+        if len(tokL) <= 0:
382
+            return None
383
+
384
+        opcode = tokL[0]
385
+        
386
+        if opcode not in self.parseD:
387
+            return self._syntaxError("Unknown opcode: '{}'.".format(opcode))
388
+
389
+        d = self.parseD[ opcode ]
390
+
391
+        func_name = d['func']
392
+        func      = None
393
+
394
+        # find the function associated with this command
395
+        if hasattr(self, func_name ):
396
+            func = getattr(self, func_name )
397
+
398
+        try:
399
+            # convert the parameter list into integers
400
+            argL = [ int(tokL[i]) for i in range(1,len(tokL)) ]
401
+        except:
402
+            return self._syntaxError("Unable to create integer arguments.")
403
+
404
+        # validate the count of command args
405
+        if  d['minN'] != -1 and (d['minN'] > len(argL) or len(argL) > d['maxN']):                
406
+            return self._syntaxError("Argument count mismatch. {} is out of range:{} to {}".format(len(argL),d['minN'],d['maxN']))
407
+
408
+
409
+        # call the command function
410
+        if func:
411
+            result = func(*argL)
412
+        else:
413
+            result = self.appProc.send( { 'type':func_name, 'value':argL } )
414
+
415
+        return result
416
+
417
+
418
+    
419
+    def run( self ):
420
+
421
+        # create the API object
422
+        self.appProc = AppProcess(cfg)
423
+
424
+        self.appProc.start()
425
+            
426
+        print("'q'=quit '?'=help")
427
+        time_out_secs = 1
428
+
429
+        # this is the shell main loop
430
+        while True:
431
+
432
+            # wait for keyboard activity
433
+            i, o, e = select.select( [sys.stdin], [], [], time_out_secs )
434
+
435
+            if i:
436
+                # read the command
437
+                s = sys.stdin.readline().strip() 
438
+
439
+                # tokenize the command
440
+                tokL = s.split(' ')
441
+
442
+
443
+                # execute the command
444
+                result = self._exec_cmd( tokL )
445
+
446
+                        
447
+                # if this is the 'quit' command
448
+                if tokL[0] == 'q':
449
+                    break
450
+
451
+            # check for msg's from the async application process
452
+            if self._handle_app_msgs( self.appProc.recv() ):
453
+                break
454
+
455
+
456
+        # wait for the appProc to complete                    
457
+        while not self.appProc.isdone():
458
+            self.appProc.recv()  # drain the AppProc() as it shutdown
459
+            time.sleep(0.1)
460
+        
461
+
462
+    def _handle_app_msgs( self, msgL ):
463
+        quitAppFl = False
464
+        if msgL:
465
+            for msg in msgL:
466
+                if msg:
467
+                    if msg['type'] == 'error':
468
+                        print("Error: {}".format(msg['value']))
469
+
470
+                    elif msg['type'] == 'quit':
471
+                        quitAppFl = True
472
+                    else:
473
+                       print(msg) 
474
+
475
+        return quitAppFl
476
+
477
+def parse_args():
478
+    """Parse the command line arguments."""
479
+    
480
+    descStr  = """Picadae auto-calibrate."""
481
+    logL     = ['debug','info','warning','error','critical']
482
+    
483
+    ap = argparse.ArgumentParser(description=descStr)
484
+
485
+
486
+    ap.add_argument("-c","--config",                     default="cfg/p_ac.yml",  help="YAML configuration file.")
487
+    ap.add_argument("-l","--log_level",choices=logL,     default="warning",             help="Set logging level: debug,info,warning,error,critical. Default:warning")
488
+
489
+    return ap.parse_args()
490
+    
491
+            
492
+def parse_yaml_cfg( fn ):
493
+    """Parse the YAML configuration file."""
494
+    
495
+    cfg  = None
496
+    
497
+    with open(fn,"r") as f:
498
+        cfgD = yaml.load(f, Loader=yaml.FullLoader)
499
+
500
+        cfg = types.SimpleNamespace(**cfgD['p_ac'])
501
+
502
+    return cfg
503
+
504
+
505
+    
506
+
507
+    
508
+if __name__ == "__main__":
509
+    
510
+    logging.basicConfig()
511
+
512
+    #mplog = multiprocessing.log_to_stderr()
513
+    #mplog.setLevel(logging.INFO)
514
+    
515
+
516
+    args = parse_args()
517
+
518
+    cfg = parse_yaml_cfg(args.config)
519
+
520
+    shell = Shell(cfg)
521
+
522
+    shell.run()

+ 132
- 0
p_ac.yml 查看文件

@@ -0,0 +1,132 @@
1
+{
2
+  p_ac: {
3
+
4
+
5
+    # Audio device setup
6
+    audio: {
7
+      inPortLabel: "5 USB Audio CODEC:", #"HDA Intel PCH: CS4208", # "5 USB Audio CODEC:", #"5 USB Sound Device",
8
+      outPortLabel: ,
9
+      },
10
+
11
+    
12
+    # Picadae API args
13
+    serial_dev: "/dev/ttyACM0",
14
+    serial_baud: 38400,
15
+    i2c_base_addr: 21,
16
+    prescaler_usec: 16,
17
+    serial_sync_timeout_ms: 10000,
18
+
19
+
20
+    # MeasureSeq args
21
+    outDir: "~/temp/p_ac3",
22
+    noteDurMs: 1000,
23
+    pauseDurMs: 1000,
24
+    holdDutyPct: 50,
25
+
26
+    #full_pulseL: [ 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
+    full_pulseL: [ 18000, 22000, 26000, 30000, 34000, 40000],
28
+    
29
+     key_mapL: [
30
+
31
+       { index: 0, board: 1, ch:  1, type: 'wB', midi: 21, class: 'A' },
32
+       { index: 1, board: 1, ch:  2, type: 'Bl', midi: 22, class: 'A#' },
33
+       { index: 2, board: 1, ch:  3, type: 'wF', midi: 23, class: 'B' },
34
+       { index: 3, board: 1, ch:  4, type: 'wB', midi: 24, class: 'C' },
35
+       { index: 4, board: 1, ch:  5, type: 'Bl', midi: 25, class: 'C#' },
36
+       { index: 5, board: 1, ch:  6, type: 'wF', midi: 26, class: 'D' },
37
+       { index: 6, board: 1, ch:  7, type: 'Bl', midi: 27, class: 'D#' },
38
+       { index: 7, board: 1, ch:  8, type: 'wB', midi: 28, class: 'E' },
39
+       { index: 8, board: 1, ch:  9, type: 'wF', midi: 29, class: 'F' },
40
+       { index: 9, board: 1, ch: 10, type: 'Bl', midi: 30, class: 'F#' },
41
+       { index: 10, board: 1, ch: 11, type: 'wB', midi: 31, class: 'G' },
42
+
43
+       { index: 11, board: 2, ch:  1, type: 'Bl', midi: 32, class: 'G#' },
44
+       { index: 12, board: 2, ch:  2, type: 'wF', midi: 33, class: 'A' },
45
+       { index: 13, board: 2, ch:  3, type: 'Bl', midi: 34, class: 'A#' },
46
+       { index: 14, board: 2, ch:  4, type: 'wB', midi: 35, class: 'B' },
47
+       { index: 15, board: 2, ch:  5, type: 'wF', midi: 36, class: 'C' },
48
+       { index: 16, board: 2, ch:  6, type: 'Bl', midi: 37, class: 'C#' },
49
+       { index: 17, board: 2, ch:  7, type: 'wB', midi: 38, class: 'D' },
50
+       { index: 18, board: 2, ch:  8, type: 'Bl', midi: 39, class: 'D#' },
51
+       { index: 19, board: 2, ch:  9, type: 'wF', midi: 40, class: 'E' },
52
+       { index: 20, board: 2, ch: 10, type: 'wB', midi: 41, class: 'F' },
53
+       { index: 21, board: 2, ch: 11, type: 'Bl', midi: 42, class: 'F#' },
54
+
55
+       { index: 22, board: 3, ch:  1, type: 'wF', midi: 43, class: 'G' },
56
+       { index: 23, board: 3, ch:  2, type: 'Bl', midi: 44, class: 'G#' },
57
+       { index: 24, board: 3, ch:  3, type: 'wB', midi: 45, class: 'A' },
58
+       { index: 25, board: 3, ch:  4, type: 'Bl', midi: 46, class: 'A#' },
59
+       { index: 26, board: 3, ch:  5, type: 'wF', midi: 47, class: 'B' },
60
+       { index: 27, board: 3, ch:  6, type: 'wB', midi: 48, class: 'C' },
61
+       { index: 28, board: 3, ch:  7, type: 'Bl', midi: 49, class: 'C#' },
62
+       { index: 29, board: 3, ch:  8, type: 'wF', midi: 50, class: 'D' },
63
+       { index: 30, board: 3, ch:  9, type: 'Bl', midi: 51, class: 'D#' },
64
+       { index: 31, board: 3, ch: 10, type: 'wB', midi: 52, class: 'E' },
65
+       { index: 32, board: 3, ch: 11, type: 'wF', midi: 53, class: 'F' },
66
+
67
+       { index: 33, board: 4, ch:  1, type: 'Bl', midi: 54, class: 'F#' },
68
+       { index: 34, board: 4, ch:  2, type: 'wB', midi: 55, class: 'G' },
69
+       { index: 35, board: 4, ch:  3, type: 'Bl', midi: 56, class: 'G#' },
70
+       { index: 36, board: 4, ch:  4, type: 'wF', midi: 57, class: 'A' },
71
+       { index: 37, board: 4, ch:  5, type: 'Bl', midi: 58, class: 'A#' },
72
+       { index: 38, board: 4, ch:  6, type: 'wB', midi: 59, class: 'B' },
73
+       { index: 39, board: 4, ch:  7, type: 'wF', midi: 60, class: 'C' },
74
+       { index: 40, board: 4, ch:  8, type: 'Bl', midi: 61, class: 'C#' },
75
+       { index: 41, board: 4, ch:  9, type: 'wB', midi: 62, class: 'D' },
76
+       { index: 42, board: 4, ch: 10, type: 'Bl', midi: 63, class: 'D#' },
77
+       { index: 43, board: 4, ch: 11, type: 'wF', midi: 64, class: 'E' },
78
+
79
+       { index: 44, board: 5, ch:  1, type: 'wB', midi: 65, class: 'F' },
80
+       { index: 45, board: 5, ch:  2, type: 'Bl', midi: 66, class: 'F#' },
81
+       { index: 46, board: 5, ch:  3, type: 'wF', midi: 67, class: 'G' },
82
+       { index: 47, board: 5, ch:  4, type: 'Bl', midi: 68, class: 'G#' },
83
+       { index: 48, board: 5, ch:  5, type: 'wB', midi: 69, class: 'A' },
84
+       { index: 49, board: 5, ch:  6, type: 'Bl', midi: 70, class: 'A#' },
85
+       { index: 50, board: 5, ch:  7, type: 'wF', midi: 71, class: 'B' },
86
+       { index: 51, board: 5, ch:  8, type: 'wB', midi: 72, class: 'C' },
87
+       { index: 52, board: 5, ch:  9, type: 'Bl', midi: 73, class: 'C#' },
88
+       { index: 53, board: 5, ch: 10, type: 'wF', midi: 74, class: 'D' },
89
+       { index: 54, board: 5, ch: 11, type: 'Bl', midi: 75, class: 'D#' },
90
+
91
+       { index: 55, board: 6, ch:  1, type: 'wB', midi: 76, class: 'E' },
92
+       { index: 56, board: 6, ch:  2, type: 'wF', midi: 77, class: 'F' },
93
+       { index: 57, board: 6, ch:  3, type: 'Bl', midi: 78, class: 'F#' },
94
+       { index: 58, board: 6, ch:  4, type: 'wB', midi: 79, class: 'G' },
95
+       { index: 59, board: 6, ch:  5, type: 'Bl', midi: 80, class: 'G#' },
96
+       { index: 60, board: 6, ch:  6, type: 'wF', midi: 81, class: 'A' },
97
+       { index: 61, board: 6, ch:  7, type: 'Bl', midi: 82, class: 'A#' },
98
+       { index: 62, board: 6, ch:  8, type: 'wB', midi: 83, class: 'B' },
99
+       { index: 63, board: 6, ch:  9, type: 'wF', midi: 84, class: 'C' },
100
+       { index: 64, board: 6, ch: 10, type: 'Bl', midi: 85, class: 'C#' },
101
+       { index: 65, board: 6, ch: 11, type: 'wB', midi: 86, class: 'D' },
102
+
103
+       { index: 66, board: 6, ch:  1, type: 'Bl', midi: 87, class: 'D#' },
104
+       { index: 67, board: 6, ch:  2, type: 'wF', midi: 88, class: 'E' },
105
+       { index: 68, board: 6, ch:  3, type: 'wB', midi: 89, class: 'F' },
106
+       { index: 69, board: 6, ch:  4, type: 'Bl', midi: 90, class: 'F#' },
107
+       { index: 70, board: 6, ch:  5, type: 'wF', midi: 91, class: 'G' },
108
+       { index: 71, board: 6, ch:  6, type: 'Bl', midi: 92, class: 'G#' },
109
+       { index: 72, board: 6, ch:  7, type: 'wB', midi: 93, class: 'A' },
110
+       { index: 73, board: 6, ch:  8, type: 'Bl', midi: 94, class: 'A#' },
111
+       { index: 74, board: 6, ch:  9, type: 'wF', midi: 95, class: 'B' },
112
+       { index: 75, board: 6, ch: 10, type: 'wB', midi: 96, class: 'C' },
113
+       { index: 76, board: 6, ch: 11, type: 'Bl', midi: 97, class: 'C#' },
114
+       
115
+       { index: 77, board: 7, ch:  1, type: 'wF', midi: 98,  class: 'D' },
116
+       { index: 78, board: 7, ch:  2, type: 'Bl', midi: 99,  class: 'D#' },
117
+       { index: 79, board: 7, ch:  3, type: 'wB', midi: 100, class: 'E' },
118
+       { index: 80, board: 7, ch:  4, type: 'wF', midi: 101, class: 'F' },
119
+       { index: 81, board: 7, ch:  5, type: 'Bl', midi: 102, class: 'F#' },
120
+       { index: 82, board: 7, ch:  6, type: 'wB', midi: 103, class: 'G' },
121
+       { index: 83, board: 7, ch:  7, type: 'Bl', midi: 104, class: 'G#' },
122
+       { index: 84, board: 7, ch:  8, type: 'wF', midi: 105, class: 'A' },
123
+       { index: 85, board: 7, ch:  9, type: 'Bl', midi: 106, class: 'A#' },
124
+       { index: 86, board: 7, ch: 10, type: 'wB', midi: 107, class: 'B' },
125
+       { index: 87, board: 7, ch: 11, type: 'wF', midi: 108, class: 'C' },
126
+
127
+       ]
128
+      
129
+    
130
+    
131
+    }
132
+}

+ 279
- 0
plot_seq.py 查看文件

@@ -0,0 +1,279 @@
1
+import os, sys, json
2
+from scipy.io import wavfile
3
+from scipy.signal import stft
4
+import matplotlib.pyplot as plt
5
+import numpy as np
6
+
7
+def is_nanV( xV ):
8
+    
9
+    for i in range(xV.shape[0]):
10
+        if np.isnan( xV[i] ):
11
+            return True
12
+        
13
+    return False
14
+        
15
+def calc_harm_bins( srate, binHz, midiPitch, harmN ):
16
+
17
+    semi_tone     = 1.0/12
18
+    quarter_tone  = 1.0/24
19
+    eigth_tone    = 1.0/48
20
+    band_width_st = 3.0/48  # 3/8 tone
21
+    
22
+    fundHz     = (13.75 * pow(2.0,(-9.0/12.0))) * pow(2.0,(midiPitch / 12))
23
+    fund_l_binL   = [int(round(fundHz * pow(2.0,-band_width_st) * i/binHz)) for i in range(1,harmN+1)]
24
+    fund_m_binL   = [int(round(fundHz *         i/binHz)) for i in range(1,harmN+1)]
25
+    fund_u_binL   = [int(round(fundHz * pow(2.0, band_width_st) * i/binHz)) for i in range(1,harmN+1)]
26
+
27
+    for i in range(len(fund_m_binL)):
28
+        if fund_l_binL[i] >= fund_m_binL[i] and fund_l_binL[i] > 0:
29
+            fund_l_binL[i] = fund_m_binL[i] - 1
30
+
31
+        if fund_u_binL[i] <= fund_m_binL[i] and fund_u_binL[i] < len(fund_u_binL)-1:
32
+            fund_u_binL[i] = fund_m_binL[i] + 1
33
+    
34
+    return fund_l_binL, fund_m_binL, fund_u_binL
35
+    
36
+
37
+
38
+def audio_rms( srate, xV, rmsWndMs, hopMs  ):
39
+
40
+    wndSmpN = int(round( rmsWndMs * srate / 1000.0))
41
+    hopSmpN = int(round( hopMs    * srate / 1000.0))
42
+
43
+    xN   = xV.shape[0]
44
+    yN   = int(((xN - wndSmpN) / hopSmpN) + 1)
45
+    assert( yN > 0)
46
+    yV   = np.zeros( (yN, ) )
47
+
48
+    assert( wndSmpN > 1 )
49
+
50
+    i = 0
51
+    j = 0
52
+    while i < xN and j < yN:
53
+
54
+        if i == 0:
55
+            yV[j] = np.sqrt(xV[0]*xV[0])
56
+        elif i < wndSmpN:
57
+            yV[j] = np.sqrt( np.mean( xV[0:i] * xV[0:i] ) )
58
+        else:
59
+            yV[j] = np.sqrt( np.mean( xV[i-wndSmpN:i] * xV[i-wndSmpN:i] ) )
60
+
61
+        i += hopSmpN
62
+        j += 1
63
+        
64
+    return yV, srate / hopSmpN
65
+
66
+def audio_db_rms( srate, xV, rmsWndMs, hopMs, dbRefWndMs ):
67
+
68
+    rmsV, rms_srate = audio_rms( srate, xV, rmsWndMs, hopMs )
69
+    dbWndN = int(round(dbRefWndMs * rms_srate / 1000.0))
70
+    dbRef = ref = np.mean(rmsV[0:dbWndN])
71
+    return 20.0 * np.log10( rmsV / dbRef ), rms_srate
72
+
73
+
74
+
75
+def audio_stft_rms( srate, xV, rmsWndMs, hopMs, spectrumIdx ):
76
+    
77
+    wndSmpN = int(round( rmsWndMs * srate / 1000.0))
78
+    hopSmpN = int(round( hopMs    * srate / 1000.0))
79
+    binHz   = srate / wndSmpN
80
+    
81
+    f,t,xM = stft( xV, fs=srate, window="hann", nperseg=wndSmpN, noverlap=wndSmpN-hopSmpN, return_onesided=True )
82
+
83
+    specHopIdx = int(round( spectrumIdx ))    
84
+    specV = np.sqrt(np.abs(xM[:, specHopIdx ]))
85
+    
86
+    mV = np.zeros((xM.shape[1]))
87
+
88
+    for i in range(xM.shape[1]):
89
+        mV[i] = np.max(np.sqrt(np.abs(xM[:,i])))
90
+
91
+    return mV, srate / hopSmpN, specV, specHopIdx, binHz
92
+
93
+def audio_stft_db_rms( srate, xV, rmsWndMs, hopMs, dbRefWndMs, spectrumIdx ):
94
+    rmsV, rms_srate, specV, specHopIdx, binHz = audio_stft_rms( srate, xV, rmsWndMs, hopMs, spectrumIdx )
95
+    
96
+    dbWndN = int(round(dbRefWndMs * rms_srate / 1000.0))
97
+    dbRef = ref = np.mean(rmsV[0:dbWndN])
98
+    rmsDbV = 20.0 * np.log10( rmsV / dbRef )
99
+
100
+    return rmsDbV, rms_srate, specV, specHopIdx, binHz
101
+
102
+def audio_harm_rms( srate, xV, rmsWndMs, hopMs, midiPitch, harmCandN, harmN  ):
103
+
104
+    wndSmpN   = int(round( rmsWndMs * srate / 1000.0))
105
+    hopSmpN   = int(round( hopMs    * srate / 1000.0))
106
+
107
+    binHz   = srate / wndSmpN
108
+    
109
+    f,t,xM = stft( xV, fs=srate, window="hann", nperseg=wndSmpN, noverlap=wndSmpN-hopSmpN, return_onesided=True )
110
+
111
+    harmLBinL,harmMBinL,harmUBinL = calc_harm_bins( srate, binHz, midiPitch, harmCandN )
112
+    
113
+    rmsV = np.zeros((xM.shape[1],))
114
+
115
+
116
+    for i in range(xM.shape[1]):
117
+        mV = np.sqrt(np.abs(xM[:,i]))
118
+
119
+        pV = np.zeros((len(harmLBinL,)))
120
+        
121
+        for j,(b0i,b1i) in enumerate(zip( harmLBinL, harmUBinL )):
122
+            pV[j] = np.max(mV[b0i:b1i])
123
+
124
+        rmsV[i] = np.mean( sorted(pV)[-harmN:] )
125
+            
126
+            
127
+        
128
+
129
+    return rmsV, srate / hopSmpN, binHz
130
+
131
+
132
+    
133
+def audio_harm_db_rms( srate, xV, rmsWndMs, hopMs, dbRefWndMs, midiPitch, harmCandN, harmN  ):
134
+    
135
+    rmsV, rms_srate, binHz = audio_harm_rms( srate, xV, rmsWndMs, hopMs, midiPitch, harmCandN, harmN )
136
+    
137
+    dbWndN = int(round(dbRefWndMs * rms_srate / 1000.0))
138
+    dbRef = ref = np.mean(rmsV[0:dbWndN])
139
+    rmsDbV = 20.0 * np.log10( rmsV / dbRef )
140
+
141
+    return rmsDbV, rms_srate, binHz
142
+    
143
+    
144
+               
145
+def locate_peak_indexes( xV, xV_srate, eventMsL ):
146
+
147
+    pkIdxL = []
148
+    for begMs, endMs in eventMsL:
149
+
150
+        begSmpIdx = int(begMs * xV_srate / 1000.0)
151
+        endSmpIdx = int(endMs * xV_srate / 1000.0)
152
+
153
+        pkIdxL.append( begSmpIdx + np.argmax( xV[begSmpIdx:endSmpIdx] ) )
154
+
155
+    return pkIdxL
156
+
157
+
158
+def plot_spectrum( ax, srate, binHz, specV, midiPitch, harmN ):
159
+
160
+    binN      = specV.shape[0]
161
+    harmLBinL,harmMBinL,harmUBinL = calc_harm_bins( srate, binHz, midiPitch, harmN )
162
+
163
+    fundHz      = harmMBinL[0] * binHz
164
+    maxPlotHz   = fundHz * (harmN+1)
165
+    maxPlotBinN = int(round(maxPlotHz/binHz))
166
+    
167
+    hzV = np.arange(binN) * (srate/(binN*2))
168
+    
169
+    specV = 20.0 * np.log10(specV) 
170
+    
171
+    ax.plot(hzV[0:maxPlotBinN], specV[0:maxPlotBinN] )
172
+
173
+    for h0,h1,h2 in zip(harmLBinL,harmMBinL,harmUBinL):
174
+        ax.axvline( x=h0 * binHz, color="blue")
175
+        ax.axvline( x=h1 * binHz, color="black")
176
+        ax.axvline( x=h2 * binHz, color="blue")
177
+
178
+    ax.set_ylabel(str(midiPitch))
179
+        
180
+def plot_spectral_ranges( inDir, pitchL, rmsWndMs=300, rmsHopMs=30, harmN=5, dbRefWndMs=500 ):
181
+
182
+    plotN = len(pitchL)
183
+    fig,axL = plt.subplots(plotN,1)
184
+
185
+    for plot_idx,midiPitch in enumerate(pitchL):
186
+
187
+        # get the audio and meta-data file names
188
+        seqFn   = os.path.join( inDir, str(midiPitch), "seq.json")
189
+        audioFn = os.path.join( inDir, str(midiPitch), "audio.wav")
190
+
191
+        # read the meta data object
192
+        with open( seqFn, "rb") as f:
193
+            r = json.load(f)
194
+
195
+        # read the audio file
196
+        srate, signalM  = wavfile.read(audioFn)
197
+        sigV  = signalM / float(0x7fff)
198
+
199
+        # calc. the RMS envelope in the time domain
200
+        rms0DbV, rms0_srate = audio_db_rms( srate, sigV, rmsWndMs, rmsHopMs, dbRefWndMs )
201
+
202
+        # locate the sample index of the peak of each note attack 
203
+        pkIdx0L = locate_peak_indexes( rms0DbV, rms0_srate, r['eventTimeL'] )
204
+
205
+        # select the 7th to last note for spectrum measurement
206
+
207
+        #
208
+        # TODO: come up with a better way to select the note to measure
209
+        #
210
+        spectrumSmpIdx = pkIdx0L[ len(pkIdx0L) - 7 ]
211
+
212
+
213
+        # calc. the RMS envelope by taking the max spectral peak in each STFT window 
214
+        rmsDbV, rms_srate, specV, specHopIdx, binHz = audio_stft_db_rms( srate, sigV, rmsWndMs, rmsHopMs, dbRefWndMs, spectrumSmpIdx)
215
+
216
+        # specV[] is the spectrum of the note at spectrumSmpIdx
217
+
218
+        # plot the spectrum and the harmonic selection ranges
219
+        plot_spectrum( axL[plot_idx], srate, binHz, specV, midiPitch, harmN )
220
+    plt.show()
221
+
222
+        
223
+        
224
+def do_td_plot( inDir ):
225
+
226
+    rmsWndMs = 300
227
+    rmsHopMs = 30
228
+    dbRefWndMs = 500
229
+    harmCandN = 5
230
+    harmN     = 3
231
+    
232
+    seqFn = os.path.join( inDir, "seq.json")
233
+    audioFn = os.path.join( inDir, "audio.wav")
234
+    midiPitch = int(inDir.split("/")[-1])
235
+    
236
+
237
+    with open( seqFn, "rb") as f:
238
+        r = json.load(f)
239
+    
240
+    
241
+    srate, signalM  = wavfile.read(audioFn)
242
+    sigV  = signalM / float(0x7fff)
243
+        
244
+    rms0DbV, rms0_srate = audio_db_rms( srate, sigV, rmsWndMs, rmsHopMs, dbRefWndMs )
245
+
246
+    rmsDbV, rms_srate, binHz = audio_harm_db_rms( srate, sigV, rmsWndMs, rmsHopMs, dbRefWndMs, midiPitch, harmCandN, harmN  )
247
+    
248
+    pkIdxL = locate_peak_indexes( rmsDbV, rms_srate, r['eventTimeL'] )
249
+        
250
+    fig,ax = plt.subplots()
251
+    fig.set_size_inches(18.5, 10.5, forward=True)
252
+
253
+    secV = np.arange(0,len(rmsDbV)) / rms_srate
254
+    
255
+    ax.plot( secV, rmsDbV )
256
+    ax.plot( np.arange(0,len(rms0DbV)) / rms0_srate, rms0DbV, color="black" )
257
+
258
+    for begMs, endMs in r['eventTimeL']:
259
+        ax.axvline( x=begMs/1000.0, color="green")
260
+        ax.axvline( x=endMs/1000.0, color="red")
261
+
262
+        
263
+    for i,pki in enumerate(pkIdxL):
264
+        ax.plot( [pki / rms_srate], [ rmsDbV[pki] ], marker='.', color="black")
265
+
266
+    plt.show()
267
+
268
+
269
+    
270
+
271
+
272
+
273
+if __name__ == "__main__":
274
+
275
+    inDir = sys.argv[1]
276
+
277
+    do_td_plot(inDir)
278
+
279
+    #plot_spectral_ranges( inDir, [ 24, 36, 48, 60, 72, 84, 96, 104] )

+ 34
- 0
result.py 查看文件

@@ -0,0 +1,34 @@
1
+import json
2
+
3
+class Result(object):
4
+    def __init__( self, value=None, msg=None ):
5
+        self.value = value
6
+        self.msg   = msg
7
+        self.resultL = []
8
+
9
+    def set_error( self, msg ):
10
+        if self.msg is None:
11
+            self.msg = ""
12
+            
13
+        self.msg += " " + msg
14
+
15
+    def print(self):
16
+        if value:
17
+            print(value)
18
+        if msg:
19
+            print(msg)
20
+            
21
+        if resultL:
22
+            print(resultL)
23
+
24
+    def __bool__( self ):
25
+        return self.msg is  None
26
+
27
+    def __iadd__( self, b ):
28
+        if self.value is None and self.msg is None:
29
+            self = b
30
+        else:
31
+            self.resultL.append(b)
32
+
33
+        return self
34
+        

Loading…
取消
儲存