]>
Commit | Line | Data |
---|---|---|
1 | ## | |
2 | ## This file is part of the libsigrokdecode project. | |
3 | ## | |
4 | ## Copyright (C) 2015 Petteri Aimonen <jpa@sigrok.mail.kapsi.fi> | |
5 | ## | |
6 | ## This program is free software; you can redistribute it and/or modify | |
7 | ## it under the terms of the GNU General Public License as published by | |
8 | ## the Free Software Foundation; either version 2 of the License, or | |
9 | ## (at your option) any later version. | |
10 | ## | |
11 | ## This program is distributed in the hope that it will be useful, | |
12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | ## GNU General Public License for more details. | |
15 | ## | |
16 | ## You should have received a copy of the GNU General Public License | |
17 | ## along with this program; if not, see <http://www.gnu.org/licenses/>. | |
18 | ## | |
19 | ||
20 | import sigrokdecode as srd | |
21 | import subprocess | |
22 | import re | |
23 | ||
24 | # See ETMv3 Signal Protocol table 7-11: 'Encoding of Exception[8:0]'. | |
25 | exc_names = [ | |
26 | 'No exception', 'IRQ1', 'IRQ2', 'IRQ3', 'IRQ4', 'IRQ5', 'IRQ6', 'IRQ7', | |
27 | 'IRQ0', 'UsageFault', 'NMI', 'SVC', 'DebugMon', 'MemManage', 'PendSV', | |
28 | 'SysTick', 'Reserved', 'Reset', 'BusFault', 'Reserved', 'Reserved' | |
29 | ] | |
30 | ||
31 | for i in range(8, 496): | |
32 | exc_names.append('IRQ%d' % i) | |
33 | ||
34 | def parse_varint(bytes): | |
35 | '''Parse an integer where the top bit is the continuation bit. | |
36 | Returns value and number of parsed bytes.''' | |
37 | v = 0 | |
38 | for i, b in enumerate(bytes): | |
39 | v |= (b & 0x7F) << (i * 7) | |
40 | if b & 0x80 == 0: | |
41 | return v, i+1 | |
42 | return v, len(bytes) | |
43 | ||
44 | def parse_uint(bytes): | |
45 | '''Parse little-endian integer.''' | |
46 | v = 0 | |
47 | for i, b in enumerate(bytes): | |
48 | v |= b << (i * 8) | |
49 | return v | |
50 | ||
51 | def parse_exc_info(bytes): | |
52 | '''Parse exception information bytes from a branch packet.''' | |
53 | if len(bytes) < 1: | |
54 | return None | |
55 | ||
56 | excv, exclen = parse_varint(bytes) | |
57 | if bytes[exclen - 1] & 0x80 != 0x00: | |
58 | return None # Exception info not complete. | |
59 | ||
60 | if exclen == 2 and excv & (1 << 13): | |
61 | # Exception byte 1 was skipped, fix up the decoding. | |
62 | excv = (excv & 0x7F) | ((excv & 0x3F80) << 7) | |
63 | ||
64 | ns = excv & 1 | |
65 | exc = ((excv >> 1) & 0x0F) | ((excv >> 7) & 0x1F0) | |
66 | cancel = (excv >> 5) & 1 | |
67 | altisa = (excv >> 6) & 1 | |
68 | hyp = (excv >> 12) & 1 | |
69 | resume = (excv >> 14) & 0x0F | |
70 | return (ns, exc, cancel, altisa, hyp, resume) | |
71 | ||
72 | def parse_branch_addr(bytes, ref_addr, cpu_state, branch_enc): | |
73 | '''Parse encoded branch address. | |
74 | Returns addr, addrlen, cpu_state, exc_info. | |
75 | Returns None if packet is not yet complete''' | |
76 | ||
77 | addr, addrlen = parse_varint(bytes) | |
78 | ||
79 | if bytes[addrlen-1] & 0x80 != 0x00: | |
80 | return None # Branch address not complete. | |
81 | ||
82 | addr_bits = 7 * addrlen | |
83 | ||
84 | have_exc_info = False | |
85 | if branch_enc == 'original': | |
86 | if addrlen == 5 and bytes[4] & 0x40: | |
87 | have_exc_info = True | |
88 | elif branch_enc == 'alternative': | |
89 | addr_bits -= 1 # Top bit of address indicates exc_info. | |
90 | if addrlen >= 2 and addr & (1 << addr_bits): | |
91 | have_exc_info = True | |
92 | addr &= ~(1 << addr_bits) | |
93 | ||
94 | exc_info = None | |
95 | if have_exc_info: | |
96 | exc_info = parse_exc_info(bytes[addrlen:]) | |
97 | if exc_info is None: | |
98 | return None # Exception info not complete. | |
99 | ||
100 | if addrlen == 5: | |
101 | # Possible change in CPU state. | |
102 | if bytes[4] & 0xB8 == 0x08: | |
103 | cpu_state = 'arm' | |
104 | elif bytes[4] & 0xB0 == 0x10: | |
105 | cpu_state = 'thumb' | |
106 | elif bytes[4] & 0xA0 == 0x20: | |
107 | cpu_state = 'jazelle' | |
108 | else: | |
109 | raise NotImplementedError('Unhandled branch byte 4: 0x%02x' % bytes[4]) | |
110 | ||
111 | # Shift the address according to current CPU state. | |
112 | if cpu_state == 'arm': | |
113 | addr = (addr & 0xFFFFFFFE) << 1 | |
114 | addr_bits += 1 | |
115 | elif cpu_state == 'thumb': | |
116 | addr = addr & 0xFFFFFFFE | |
117 | elif cpu_state == 'jazelle': | |
118 | addr = (addr & 0xFFFFFFFFE) >> 1 | |
119 | addr_bits -= 1 | |
120 | else: | |
121 | raise NotImplementedError('Unhandled state: ' + cpu_state) | |
122 | ||
123 | # If the address wasn't full, fill in with the previous address. | |
124 | if addrlen < 5: | |
125 | addr |= ref_addr & (0xFFFFFFFF << addr_bits) | |
126 | ||
127 | return addr, addrlen, cpu_state, exc_info | |
128 | ||
129 | class Decoder(srd.Decoder): | |
130 | api_version = 2 | |
131 | id = 'arm_etmv3' | |
132 | name = 'ARM ETMv3' | |
133 | longname = 'ARM Embedded Trace Macroblock' | |
134 | desc = 'Decode ETM instruction trace packets.' | |
135 | license = 'gplv2+' | |
136 | inputs = ['uart'] | |
137 | outputs = ['arm_etmv3'] | |
138 | annotations = ( | |
139 | ('trace', 'Trace info'), | |
140 | ('branch', 'Branches'), | |
141 | ('exception', 'Exceptions'), | |
142 | ('execution', 'Instruction execution'), | |
143 | ('data', 'Data access'), | |
144 | ('pc', 'Program counter'), | |
145 | ('instr_e', 'Executed instructions'), | |
146 | ('instr_n', 'Not executed instructions'), | |
147 | ('source', 'Source code'), | |
148 | ('location', 'Current location'), | |
149 | ('function', 'Current function'), | |
150 | ) | |
151 | annotation_rows = ( | |
152 | ('trace', 'Trace info', (0,)), | |
153 | ('flow', 'Code flow', (1, 2, 3,)), | |
154 | ('data', 'Data access', (4,)), | |
155 | ('pc', 'Program counter', (5,)), | |
156 | ('instruction', 'Instructions', (6, 7,)), | |
157 | ('source', 'Source code', (8,)), | |
158 | ('location', 'Current location', (9,)), | |
159 | ('function', 'Current function', (10,)), | |
160 | ) | |
161 | options = ( | |
162 | {'id': 'objdump', 'desc': 'objdump path', | |
163 | 'default': 'arm-none-eabi-objdump'}, | |
164 | {'id': 'objdump_opts', 'desc': 'objdump options', | |
165 | 'default': '-lSC'}, | |
166 | {'id': 'elffile', 'desc': '.elf path', | |
167 | 'default': ''}, | |
168 | {'id': 'branch_enc', 'desc': 'Branch encoding', | |
169 | 'default': 'alternative', 'values': ('alternative', 'original')}, | |
170 | ) | |
171 | ||
172 | def __init__(self): | |
173 | self.buf = [] | |
174 | self.syncbuf = [] | |
175 | self.prevsample = 0 | |
176 | self.last_branch = 0 | |
177 | self.cpu_state = 'arm' | |
178 | self.current_pc = 0 | |
179 | self.current_loc = None | |
180 | self.current_func = None | |
181 | self.next_instr_lookup = {} | |
182 | self.file_lookup = {} | |
183 | self.func_lookup = {} | |
184 | self.disasm_lookup = {} | |
185 | self.source_lookup = {} | |
186 | ||
187 | def start(self): | |
188 | self.out_ann = self.register(srd.OUTPUT_ANN) | |
189 | self.load_objdump() | |
190 | ||
191 | def load_objdump(self): | |
192 | '''Parse disassembly obtained from objdump into two tables: | |
193 | next_instr_lookup: Find the next PC addr from current PC. | |
194 | disasm_lookup: Find the instruction text from current PC. | |
195 | source_lookup: Find the source code line from current PC. | |
196 | ''' | |
197 | if not (self.options['objdump'] and self.options['elffile']): | |
198 | return | |
199 | ||
200 | opts = [self.options['objdump']] | |
201 | opts += self.options['objdump_opts'].split() | |
202 | opts += [self.options['elffile']] | |
203 | ||
204 | try: | |
205 | disasm = subprocess.check_output(opts) | |
206 | except subprocess.CalledProcessError: | |
207 | return | |
208 | ||
209 | disasm = disasm.decode('utf-8', 'replace') | |
210 | ||
211 | instpat = re.compile('\s*([0-9a-fA-F]+):\t+([0-9a-fA-F ]+)\t+([a-zA-Z][^;]+)\s*;?.*') | |
212 | branchpat = re.compile('(b|bl|b..|bl..|cbnz|cbz)(?:\.[wn])?\s+(?:r[0-9]+,\s*)?([0-9a-fA-F]+)') | |
213 | filepat = re.compile('[^\s]+[/\\\\]([a-zA-Z0-9._-]+:[0-9]+)(?:\s.*)?') | |
214 | funcpat = re.compile('[0-9a-fA-F]+\s*<([^>]+)>:.*') | |
215 | ||
216 | prev_src = '' | |
217 | prev_file = '' | |
218 | prev_func = '' | |
219 | ||
220 | for line in disasm.split('\n'): | |
221 | m = instpat.match(line) | |
222 | if m: | |
223 | addr = int(m.group(1), 16) | |
224 | raw = m.group(2) | |
225 | disas = m.group(3).strip().replace('\t', ' ') | |
226 | self.disasm_lookup[addr] = disas | |
227 | self.source_lookup[addr] = prev_src | |
228 | self.file_lookup[addr] = prev_file | |
229 | self.func_lookup[addr] = prev_func | |
230 | ||
231 | # Next address in direct sequence. | |
232 | ilen = len(raw.replace(' ', '')) // 2 | |
233 | next_n = addr + ilen | |
234 | ||
235 | # Next address if branch is taken. | |
236 | bm = branchpat.match(disas) | |
237 | if bm: | |
238 | next_e = int(bm.group(2), 16) | |
239 | else: | |
240 | next_e = next_n | |
241 | ||
242 | self.next_instr_lookup[addr] = (next_n, next_e) | |
243 | else: | |
244 | m = funcpat.match(line) | |
245 | if m: | |
246 | prev_func = m.group(1) | |
247 | prev_src = None | |
248 | else: | |
249 | m = filepat.match(line) | |
250 | if m: | |
251 | prev_file = m.group(1) | |
252 | prev_src = None | |
253 | else: | |
254 | prev_src = line.strip() | |
255 | ||
256 | def flush_current_loc(self): | |
257 | if self.current_loc is not None: | |
258 | ss, es, loc, src = self.current_loc | |
259 | if loc: | |
260 | self.put(ss, es, self.out_ann, [9, [loc]]) | |
261 | if src: | |
262 | self.put(ss, es, self.out_ann, [8, [src]]) | |
263 | self.current_loc = None | |
264 | ||
265 | def flush_current_func(self): | |
266 | if self.current_func is not None: | |
267 | ss, es, func = self.current_func | |
268 | if func: | |
269 | self.put(ss, es, self.out_ann, [10, [func]]) | |
270 | self.current_func = None | |
271 | ||
272 | def instructions_executed(self, exec_status): | |
273 | '''Advance program counter based on executed instructions. | |
274 | Argument is a list of False for not executed and True for executed | |
275 | instructions. | |
276 | ''' | |
277 | ||
278 | if len(exec_status) == 0: | |
279 | return | |
280 | ||
281 | tdelta = max(1, (self.prevsample - self.startsample) / len(exec_status)) | |
282 | ||
283 | for i, exec_status in enumerate(exec_status): | |
284 | pc = self.current_pc | |
285 | default_next = pc + 2 if self.cpu_state == 'thumb' else pc + 4 | |
286 | target_n, target_e = self.next_instr_lookup.get(pc, (default_next, default_next)) | |
287 | ss = self.startsample + round(tdelta * i) | |
288 | es = self.startsample + round(tdelta * (i+1)) | |
289 | ||
290 | self.put(ss, es, self.out_ann, | |
291 | [5, ['PC 0x%08x' % pc, '0x%08x' % pc, '%08x' % pc]]) | |
292 | ||
293 | new_loc = self.file_lookup.get(pc) | |
294 | new_src = self.source_lookup.get(pc) | |
295 | new_dis = self.disasm_lookup.get(pc) | |
296 | new_func = self.func_lookup.get(pc) | |
297 | ||
298 | # Report source line only when it changes. | |
299 | if self.current_loc is not None: | |
300 | if new_loc != self.current_loc[2] or new_src != self.current_loc[3]: | |
301 | self.flush_current_loc() | |
302 | ||
303 | if self.current_loc is None: | |
304 | self.current_loc = [ss, es, new_loc, new_src] | |
305 | else: | |
306 | self.current_loc[1] = es | |
307 | ||
308 | # Report function name only when it changes. | |
309 | if self.current_func is not None: | |
310 | if new_func != self.current_func[2]: | |
311 | self.flush_current_func() | |
312 | ||
313 | if self.current_func is None: | |
314 | self.current_func = [ss, es, new_func] | |
315 | else: | |
316 | self.current_func[1] = es | |
317 | ||
318 | # Report instruction every time. | |
319 | if new_dis: | |
320 | if exec_status: | |
321 | a = [6, ['Executed: ' + new_dis, new_dis, new_dis.split()[0]]] | |
322 | else: | |
323 | a = [7, ['Not executed: ' + new_dis, new_dis, new_dis.split()[0]]] | |
324 | self.put(ss, es, self.out_ann, a) | |
325 | ||
326 | if exec_status: | |
327 | self.current_pc = target_e | |
328 | else: | |
329 | self.current_pc = target_n | |
330 | ||
331 | def get_packet_type(self, byte): | |
332 | '''Identify packet type based on its first byte. | |
333 | See ARM IHI0014Q section "ETMv3 Signal Protocol" "Packet Types" | |
334 | ''' | |
335 | if byte & 0x01 == 0x01: | |
336 | return 'branch' | |
337 | elif byte == 0x00: | |
338 | return 'a_sync' | |
339 | elif byte == 0x04: | |
340 | return 'cyclecount' | |
341 | elif byte == 0x08: | |
342 | return 'i_sync' | |
343 | elif byte == 0x0C: | |
344 | return 'trigger' | |
345 | elif byte & 0xF3 in (0x20, 0x40, 0x60): | |
346 | return 'ooo_data' | |
347 | elif byte == 0x50: | |
348 | return 'store_failed' | |
349 | elif byte == 0x70: | |
350 | return 'i_sync' | |
351 | elif byte & 0xDF in (0x54, 0x58, 0x5C): | |
352 | return 'ooo_place' | |
353 | elif byte == 0x3C: | |
354 | return 'vmid' | |
355 | elif byte & 0xD3 == 0x02: | |
356 | return 'data' | |
357 | elif byte & 0xFB == 0x42: | |
358 | return 'timestamp' | |
359 | elif byte == 0x62: | |
360 | return 'data_suppressed' | |
361 | elif byte == 0x66: | |
362 | return 'ignore' | |
363 | elif byte & 0xEF == 0x6A: | |
364 | return 'value_not_traced' | |
365 | elif byte == 0x6E: | |
366 | return 'context_id' | |
367 | elif byte == 0x76: | |
368 | return 'exception_exit' | |
369 | elif byte == 0x7E: | |
370 | return 'exception_entry' | |
371 | elif byte & 0x81 == 0x80: | |
372 | return 'p_header' | |
373 | else: | |
374 | return 'unknown' | |
375 | ||
376 | def fallback(self, buf): | |
377 | ptype = self.get_packet_type(buf[0]) | |
378 | return [0, ['Unhandled ' + ptype + ': ' + ' '.join(['%02x' % b for b in buf])]] | |
379 | ||
380 | def handle_a_sync(self, buf): | |
381 | if buf[-1] == 0x80: | |
382 | return [0, ['Synchronization']] | |
383 | ||
384 | def handle_exception_exit(self, buf): | |
385 | return [2, ['Exception exit']] | |
386 | ||
387 | def handle_exception_entry(self, buf): | |
388 | return [2, ['Exception entry']] | |
389 | ||
390 | def handle_i_sync(self, buf): | |
391 | contextid_bytes = 0 # This is the default ETM config. | |
392 | ||
393 | if len(buf) < 6: | |
394 | return None # Packet definitely not full yet. | |
395 | ||
396 | if buf[0] == 0x08: # No cycle count. | |
397 | cyclecount = None | |
398 | idx = 1 + contextid_bytes # Index to info byte. | |
399 | elif buf[0] == 0x70: # With cycle count. | |
400 | cyclecount, cyclen = parse_varint(buf[1:6]) | |
401 | idx = 1 + cyclen + contextid_bytes | |
402 | ||
403 | if len(buf) <= idx + 4: | |
404 | return None | |
405 | infobyte = buf[idx] | |
406 | addr = parse_uint(buf[idx+1:idx+5]) | |
407 | ||
408 | reasoncode = (infobyte >> 5) & 3 | |
409 | reason = ('Periodic', 'Tracing enabled', 'After overflow', 'Exit from debug')[reasoncode] | |
410 | jazelle = (infobyte >> 4) & 1 | |
411 | nonsec = (infobyte >> 3) & 1 | |
412 | altisa = (infobyte >> 2) & 1 | |
413 | hypervisor = (infobyte >> 1) & 1 | |
414 | thumb = addr & 1 | |
415 | addr &= 0xFFFFFFFE | |
416 | ||
417 | if reasoncode == 0 and self.current_pc != addr: | |
418 | self.put(self.startsample, self.prevsample, self.out_ann, | |
419 | [0, ['WARN: Unexpected PC change 0x%08x -> 0x%08x' % \ | |
420 | (self.current_pc, addr)]]) | |
421 | elif reasoncode != 0: | |
422 | # Reset location when the trace has been interrupted. | |
423 | self.flush_current_loc() | |
424 | self.flush_current_func() | |
425 | ||
426 | self.last_branch = addr | |
427 | self.current_pc = addr | |
428 | ||
429 | if jazelle: | |
430 | self.cpu_state = 'jazelle' | |
431 | elif thumb: | |
432 | self.cpu_state = 'thumb' | |
433 | else: | |
434 | self.cpu_state = 'arm' | |
435 | ||
436 | cycstr = '' | |
437 | if cyclecount is not None: | |
438 | cycstr = ', cyclecount %d' % cyclecount | |
439 | ||
440 | if infobyte & 0x80: # LSIP packet | |
441 | self.put(self.startsample, self.prevsample, self.out_ann, | |
442 | [0, ['WARN: LSIP I-Sync packet not implemented']]) | |
443 | ||
444 | return [0, ['I-Sync: %s, PC 0x%08x, %s state%s' % \ | |
445 | (reason, addr, self.cpu_state, cycstr), \ | |
446 | 'I-Sync: %s 0x%08x' % (reason, addr)]] | |
447 | ||
448 | def handle_trigger(self, buf): | |
449 | return [0, ['Trigger event', 'Trigger']] | |
450 | ||
451 | def handle_p_header(self, buf): | |
452 | # Only non cycle-accurate mode supported. | |
453 | if buf[0] & 0x83 == 0x80: | |
454 | n = (buf[0] >> 6) & 1 | |
455 | e = (buf[0] >> 2) & 15 | |
456 | ||
457 | self.instructions_executed([1] * e + [0] * n) | |
458 | ||
459 | if n: | |
460 | return [3, ['%d instructions executed, %d skipped due to ' \ | |
461 | 'condition codes' % (e, n), | |
462 | '%d ins exec, %d skipped' % (e, n), | |
463 | '%dE,%dN' % (e, n)]] | |
464 | else: | |
465 | return [3, ['%d instructions executed' % e, | |
466 | '%d ins exec' % e, '%dE' % e]] | |
467 | elif buf[0] & 0xF3 == 0x82: | |
468 | i1 = (buf[0] >> 3) & 1 | |
469 | i2 = (buf[0] >> 2) & 1 | |
470 | self.instructions_executed([not i1, not i2]) | |
471 | txt1 = ('executed', 'skipped') | |
472 | txt2 = ('E', 'S') | |
473 | return [3, ['Instruction 1 %s, instruction 2 %s' % (txt1[i1], txt1[i2]), | |
474 | 'I1 %s, I2 %s' % (txt2[i1], txt2[i2]), | |
475 | '%s,%s' % (txt2[i1], txt2[i2])]] | |
476 | else: | |
477 | return self.fallback(buf) | |
478 | ||
479 | def handle_branch(self, buf): | |
480 | if buf[-1] & 0x80 != 0x00: | |
481 | return None # Not complete yet. | |
482 | ||
483 | brinfo = parse_branch_addr(buf, self.last_branch, self.cpu_state, | |
484 | self.options['branch_enc']) | |
485 | ||
486 | if brinfo is None: | |
487 | return None # Not complete yet. | |
488 | ||
489 | addr, addrlen, cpu_state, exc_info = brinfo | |
490 | self.last_branch = addr | |
491 | self.current_pc = addr | |
492 | ||
493 | txt = '' | |
494 | ||
495 | if cpu_state != self.cpu_state: | |
496 | txt += ', to %s state' % cpu_state | |
497 | self.cpu_state = cpu_state | |
498 | ||
499 | annidx = 1 | |
500 | ||
501 | if exc_info: | |
502 | annidx = 2 | |
503 | ns, exc, cancel, altisa, hyp, resume = exc_info | |
504 | if ns: | |
505 | txt += ', to non-secure state' | |
506 | if exc: | |
507 | if exc < len(exc_names): | |
508 | txt += ', exception %s' % exc_names[exc] | |
509 | else: | |
510 | txt += ', exception 0x%02x' % exc | |
511 | if cancel: | |
512 | txt += ', instr cancelled' | |
513 | if altisa: | |
514 | txt += ', to AltISA' | |
515 | if hyp: | |
516 | txt += ', to hypervisor' | |
517 | if resume: | |
518 | txt += ', instr resume 0x%02x' % resume | |
519 | ||
520 | return [annidx, ['Branch to 0x%08x%s' % (addr, txt), | |
521 | 'B 0x%08x%s' % (addr, txt)]] | |
522 | ||
523 | def decode(self, ss, es, data): | |
524 | ptype, rxtx, pdata = data | |
525 | ||
526 | if ptype != 'DATA': | |
527 | return | |
528 | ||
529 | # Reset packet if there is a long pause between bytes. | |
530 | # This helps getting the initial synchronization. | |
531 | self.byte_len = es - ss | |
532 | if ss - self.prevsample > 16 * self.byte_len: | |
533 | self.flush_current_loc() | |
534 | self.flush_current_func() | |
535 | self.buf = [] | |
536 | self.prevsample = es | |
537 | ||
538 | self.buf.append(pdata[0]) | |
539 | ||
540 | # Store the start time of the packet. | |
541 | if len(self.buf) == 1: | |
542 | self.startsample = ss | |
543 | ||
544 | # Keep separate buffer for detection of sync packets. | |
545 | # Sync packets override everything else, so that we can regain sync | |
546 | # even if some packets are corrupted. | |
547 | self.syncbuf = self.syncbuf[-4:] + [pdata[0]] | |
548 | if self.syncbuf == [0x00, 0x00, 0x00, 0x00, 0x80]: | |
549 | self.buf = self.syncbuf | |
550 | self.syncbuf = [] | |
551 | ||
552 | # See if it is ready to be decoded. | |
553 | ptype = self.get_packet_type(self.buf[0]) | |
554 | if hasattr(self, 'handle_' + ptype): | |
555 | func = getattr(self, 'handle_' + ptype) | |
556 | data = func(self.buf) | |
557 | else: | |
558 | data = self.fallback(self.buf) | |
559 | ||
560 | if data is not None: | |
561 | if data: | |
562 | self.put(self.startsample, es, self.out_ann, data) | |
563 | self.buf = [] |