]>
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 = 3 | |
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.reset() | |
174 | ||
175 | def reset(self): | |
176 | self.buf = [] | |
177 | self.syncbuf = [] | |
178 | self.prevsample = 0 | |
179 | self.last_branch = 0 | |
180 | self.cpu_state = 'arm' | |
181 | self.current_pc = 0 | |
182 | self.current_loc = None | |
183 | self.current_func = None | |
184 | self.next_instr_lookup = {} | |
185 | self.file_lookup = {} | |
186 | self.func_lookup = {} | |
187 | self.disasm_lookup = {} | |
188 | self.source_lookup = {} | |
189 | ||
190 | def start(self): | |
191 | self.out_ann = self.register(srd.OUTPUT_ANN) | |
192 | self.load_objdump() | |
193 | ||
194 | def load_objdump(self): | |
195 | '''Parse disassembly obtained from objdump into two tables: | |
196 | next_instr_lookup: Find the next PC addr from current PC. | |
197 | disasm_lookup: Find the instruction text from current PC. | |
198 | source_lookup: Find the source code line from current PC. | |
199 | ''' | |
200 | if not (self.options['objdump'] and self.options['elffile']): | |
201 | return | |
202 | ||
203 | opts = [self.options['objdump']] | |
204 | opts += self.options['objdump_opts'].split() | |
205 | opts += [self.options['elffile']] | |
206 | ||
207 | try: | |
208 | disasm = subprocess.check_output(opts) | |
209 | except subprocess.CalledProcessError: | |
210 | return | |
211 | ||
212 | disasm = disasm.decode('utf-8', 'replace') | |
213 | ||
214 | instpat = re.compile('\s*([0-9a-fA-F]+):\t+([0-9a-fA-F ]+)\t+([a-zA-Z][^;]+)\s*;?.*') | |
215 | branchpat = re.compile('(b|bl|b..|bl..|cbnz|cbz)(?:\.[wn])?\s+(?:r[0-9]+,\s*)?([0-9a-fA-F]+)') | |
216 | filepat = re.compile('[^\s]+[/\\\\]([a-zA-Z0-9._-]+:[0-9]+)(?:\s.*)?') | |
217 | funcpat = re.compile('[0-9a-fA-F]+\s*<([^>]+)>:.*') | |
218 | ||
219 | prev_src = '' | |
220 | prev_file = '' | |
221 | prev_func = '' | |
222 | ||
223 | for line in disasm.split('\n'): | |
224 | m = instpat.match(line) | |
225 | if m: | |
226 | addr = int(m.group(1), 16) | |
227 | raw = m.group(2) | |
228 | disas = m.group(3).strip().replace('\t', ' ') | |
229 | self.disasm_lookup[addr] = disas | |
230 | self.source_lookup[addr] = prev_src | |
231 | self.file_lookup[addr] = prev_file | |
232 | self.func_lookup[addr] = prev_func | |
233 | ||
234 | # Next address in direct sequence. | |
235 | ilen = len(raw.replace(' ', '')) // 2 | |
236 | next_n = addr + ilen | |
237 | ||
238 | # Next address if branch is taken. | |
239 | bm = branchpat.match(disas) | |
240 | if bm: | |
241 | next_e = int(bm.group(2), 16) | |
242 | else: | |
243 | next_e = next_n | |
244 | ||
245 | self.next_instr_lookup[addr] = (next_n, next_e) | |
246 | else: | |
247 | m = funcpat.match(line) | |
248 | if m: | |
249 | prev_func = m.group(1) | |
250 | prev_src = None | |
251 | else: | |
252 | m = filepat.match(line) | |
253 | if m: | |
254 | prev_file = m.group(1) | |
255 | prev_src = None | |
256 | else: | |
257 | prev_src = line.strip() | |
258 | ||
259 | def flush_current_loc(self): | |
260 | if self.current_loc is not None: | |
261 | ss, es, loc, src = self.current_loc | |
262 | if loc: | |
263 | self.put(ss, es, self.out_ann, [9, [loc]]) | |
264 | if src: | |
265 | self.put(ss, es, self.out_ann, [8, [src]]) | |
266 | self.current_loc = None | |
267 | ||
268 | def flush_current_func(self): | |
269 | if self.current_func is not None: | |
270 | ss, es, func = self.current_func | |
271 | if func: | |
272 | self.put(ss, es, self.out_ann, [10, [func]]) | |
273 | self.current_func = None | |
274 | ||
275 | def instructions_executed(self, exec_status): | |
276 | '''Advance program counter based on executed instructions. | |
277 | Argument is a list of False for not executed and True for executed | |
278 | instructions. | |
279 | ''' | |
280 | ||
281 | if len(exec_status) == 0: | |
282 | return | |
283 | ||
284 | tdelta = max(1, (self.prevsample - self.startsample) / len(exec_status)) | |
285 | ||
286 | for i, exec_status in enumerate(exec_status): | |
287 | pc = self.current_pc | |
288 | default_next = pc + 2 if self.cpu_state == 'thumb' else pc + 4 | |
289 | target_n, target_e = self.next_instr_lookup.get(pc, (default_next, default_next)) | |
290 | ss = self.startsample + round(tdelta * i) | |
291 | es = self.startsample + round(tdelta * (i+1)) | |
292 | ||
293 | self.put(ss, es, self.out_ann, | |
294 | [5, ['PC 0x%08x' % pc, '0x%08x' % pc, '%08x' % pc]]) | |
295 | ||
296 | new_loc = self.file_lookup.get(pc) | |
297 | new_src = self.source_lookup.get(pc) | |
298 | new_dis = self.disasm_lookup.get(pc) | |
299 | new_func = self.func_lookup.get(pc) | |
300 | ||
301 | # Report source line only when it changes. | |
302 | if self.current_loc is not None: | |
303 | if new_loc != self.current_loc[2] or new_src != self.current_loc[3]: | |
304 | self.flush_current_loc() | |
305 | ||
306 | if self.current_loc is None: | |
307 | self.current_loc = [ss, es, new_loc, new_src] | |
308 | else: | |
309 | self.current_loc[1] = es | |
310 | ||
311 | # Report function name only when it changes. | |
312 | if self.current_func is not None: | |
313 | if new_func != self.current_func[2]: | |
314 | self.flush_current_func() | |
315 | ||
316 | if self.current_func is None: | |
317 | self.current_func = [ss, es, new_func] | |
318 | else: | |
319 | self.current_func[1] = es | |
320 | ||
321 | # Report instruction every time. | |
322 | if new_dis: | |
323 | if exec_status: | |
324 | a = [6, ['Executed: ' + new_dis, new_dis, new_dis.split()[0]]] | |
325 | else: | |
326 | a = [7, ['Not executed: ' + new_dis, new_dis, new_dis.split()[0]]] | |
327 | self.put(ss, es, self.out_ann, a) | |
328 | ||
329 | if exec_status: | |
330 | self.current_pc = target_e | |
331 | else: | |
332 | self.current_pc = target_n | |
333 | ||
334 | def get_packet_type(self, byte): | |
335 | '''Identify packet type based on its first byte. | |
336 | See ARM IHI0014Q section "ETMv3 Signal Protocol" "Packet Types" | |
337 | ''' | |
338 | if byte & 0x01 == 0x01: | |
339 | return 'branch' | |
340 | elif byte == 0x00: | |
341 | return 'a_sync' | |
342 | elif byte == 0x04: | |
343 | return 'cyclecount' | |
344 | elif byte == 0x08: | |
345 | return 'i_sync' | |
346 | elif byte == 0x0C: | |
347 | return 'trigger' | |
348 | elif byte & 0xF3 in (0x20, 0x40, 0x60): | |
349 | return 'ooo_data' | |
350 | elif byte == 0x50: | |
351 | return 'store_failed' | |
352 | elif byte == 0x70: | |
353 | return 'i_sync' | |
354 | elif byte & 0xDF in (0x54, 0x58, 0x5C): | |
355 | return 'ooo_place' | |
356 | elif byte == 0x3C: | |
357 | return 'vmid' | |
358 | elif byte & 0xD3 == 0x02: | |
359 | return 'data' | |
360 | elif byte & 0xFB == 0x42: | |
361 | return 'timestamp' | |
362 | elif byte == 0x62: | |
363 | return 'data_suppressed' | |
364 | elif byte == 0x66: | |
365 | return 'ignore' | |
366 | elif byte & 0xEF == 0x6A: | |
367 | return 'value_not_traced' | |
368 | elif byte == 0x6E: | |
369 | return 'context_id' | |
370 | elif byte == 0x76: | |
371 | return 'exception_exit' | |
372 | elif byte == 0x7E: | |
373 | return 'exception_entry' | |
374 | elif byte & 0x81 == 0x80: | |
375 | return 'p_header' | |
376 | else: | |
377 | return 'unknown' | |
378 | ||
379 | def fallback(self, buf): | |
380 | ptype = self.get_packet_type(buf[0]) | |
381 | return [0, ['Unhandled ' + ptype + ': ' + ' '.join(['%02x' % b for b in buf])]] | |
382 | ||
383 | def handle_a_sync(self, buf): | |
384 | if buf[-1] == 0x80: | |
385 | return [0, ['Synchronization']] | |
386 | ||
387 | def handle_exception_exit(self, buf): | |
388 | return [2, ['Exception exit']] | |
389 | ||
390 | def handle_exception_entry(self, buf): | |
391 | return [2, ['Exception entry']] | |
392 | ||
393 | def handle_i_sync(self, buf): | |
394 | contextid_bytes = 0 # This is the default ETM config. | |
395 | ||
396 | if len(buf) < 6: | |
397 | return None # Packet definitely not full yet. | |
398 | ||
399 | if buf[0] == 0x08: # No cycle count. | |
400 | cyclecount = None | |
401 | idx = 1 + contextid_bytes # Index to info byte. | |
402 | elif buf[0] == 0x70: # With cycle count. | |
403 | cyclecount, cyclen = parse_varint(buf[1:6]) | |
404 | idx = 1 + cyclen + contextid_bytes | |
405 | ||
406 | if len(buf) <= idx + 4: | |
407 | return None | |
408 | infobyte = buf[idx] | |
409 | addr = parse_uint(buf[idx+1:idx+5]) | |
410 | ||
411 | reasoncode = (infobyte >> 5) & 3 | |
412 | reason = ('Periodic', 'Tracing enabled', 'After overflow', 'Exit from debug')[reasoncode] | |
413 | jazelle = (infobyte >> 4) & 1 | |
414 | nonsec = (infobyte >> 3) & 1 | |
415 | altisa = (infobyte >> 2) & 1 | |
416 | hypervisor = (infobyte >> 1) & 1 | |
417 | thumb = addr & 1 | |
418 | addr &= 0xFFFFFFFE | |
419 | ||
420 | if reasoncode == 0 and self.current_pc != addr: | |
421 | self.put(self.startsample, self.prevsample, self.out_ann, | |
422 | [0, ['WARN: Unexpected PC change 0x%08x -> 0x%08x' % \ | |
423 | (self.current_pc, addr)]]) | |
424 | elif reasoncode != 0: | |
425 | # Reset location when the trace has been interrupted. | |
426 | self.flush_current_loc() | |
427 | self.flush_current_func() | |
428 | ||
429 | self.last_branch = addr | |
430 | self.current_pc = addr | |
431 | ||
432 | if jazelle: | |
433 | self.cpu_state = 'jazelle' | |
434 | elif thumb: | |
435 | self.cpu_state = 'thumb' | |
436 | else: | |
437 | self.cpu_state = 'arm' | |
438 | ||
439 | cycstr = '' | |
440 | if cyclecount is not None: | |
441 | cycstr = ', cyclecount %d' % cyclecount | |
442 | ||
443 | if infobyte & 0x80: # LSIP packet | |
444 | self.put(self.startsample, self.prevsample, self.out_ann, | |
445 | [0, ['WARN: LSIP I-Sync packet not implemented']]) | |
446 | ||
447 | return [0, ['I-Sync: %s, PC 0x%08x, %s state%s' % \ | |
448 | (reason, addr, self.cpu_state, cycstr), \ | |
449 | 'I-Sync: %s 0x%08x' % (reason, addr)]] | |
450 | ||
451 | def handle_trigger(self, buf): | |
452 | return [0, ['Trigger event', 'Trigger']] | |
453 | ||
454 | def handle_p_header(self, buf): | |
455 | # Only non cycle-accurate mode supported. | |
456 | if buf[0] & 0x83 == 0x80: | |
457 | n = (buf[0] >> 6) & 1 | |
458 | e = (buf[0] >> 2) & 15 | |
459 | ||
460 | self.instructions_executed([1] * e + [0] * n) | |
461 | ||
462 | if n: | |
463 | return [3, ['%d instructions executed, %d skipped due to ' \ | |
464 | 'condition codes' % (e, n), | |
465 | '%d ins exec, %d skipped' % (e, n), | |
466 | '%dE,%dN' % (e, n)]] | |
467 | else: | |
468 | return [3, ['%d instructions executed' % e, | |
469 | '%d ins exec' % e, '%dE' % e]] | |
470 | elif buf[0] & 0xF3 == 0x82: | |
471 | i1 = (buf[0] >> 3) & 1 | |
472 | i2 = (buf[0] >> 2) & 1 | |
473 | self.instructions_executed([not i1, not i2]) | |
474 | txt1 = ('executed', 'skipped') | |
475 | txt2 = ('E', 'S') | |
476 | return [3, ['Instruction 1 %s, instruction 2 %s' % (txt1[i1], txt1[i2]), | |
477 | 'I1 %s, I2 %s' % (txt2[i1], txt2[i2]), | |
478 | '%s,%s' % (txt2[i1], txt2[i2])]] | |
479 | else: | |
480 | return self.fallback(buf) | |
481 | ||
482 | def handle_branch(self, buf): | |
483 | if buf[-1] & 0x80 != 0x00: | |
484 | return None # Not complete yet. | |
485 | ||
486 | brinfo = parse_branch_addr(buf, self.last_branch, self.cpu_state, | |
487 | self.options['branch_enc']) | |
488 | ||
489 | if brinfo is None: | |
490 | return None # Not complete yet. | |
491 | ||
492 | addr, addrlen, cpu_state, exc_info = brinfo | |
493 | self.last_branch = addr | |
494 | self.current_pc = addr | |
495 | ||
496 | txt = '' | |
497 | ||
498 | if cpu_state != self.cpu_state: | |
499 | txt += ', to %s state' % cpu_state | |
500 | self.cpu_state = cpu_state | |
501 | ||
502 | annidx = 1 | |
503 | ||
504 | if exc_info: | |
505 | annidx = 2 | |
506 | ns, exc, cancel, altisa, hyp, resume = exc_info | |
507 | if ns: | |
508 | txt += ', to non-secure state' | |
509 | if exc: | |
510 | if exc < len(exc_names): | |
511 | txt += ', exception %s' % exc_names[exc] | |
512 | else: | |
513 | txt += ', exception 0x%02x' % exc | |
514 | if cancel: | |
515 | txt += ', instr cancelled' | |
516 | if altisa: | |
517 | txt += ', to AltISA' | |
518 | if hyp: | |
519 | txt += ', to hypervisor' | |
520 | if resume: | |
521 | txt += ', instr resume 0x%02x' % resume | |
522 | ||
523 | return [annidx, ['Branch to 0x%08x%s' % (addr, txt), | |
524 | 'B 0x%08x%s' % (addr, txt)]] | |
525 | ||
526 | def decode(self, ss, es, data): | |
527 | ptype, rxtx, pdata = data | |
528 | ||
529 | if ptype != 'DATA': | |
530 | return | |
531 | ||
532 | # Reset packet if there is a long pause between bytes. | |
533 | # This helps getting the initial synchronization. | |
534 | self.byte_len = es - ss | |
535 | if ss - self.prevsample > 16 * self.byte_len: | |
536 | self.flush_current_loc() | |
537 | self.flush_current_func() | |
538 | self.buf = [] | |
539 | self.prevsample = es | |
540 | ||
541 | self.buf.append(pdata[0]) | |
542 | ||
543 | # Store the start time of the packet. | |
544 | if len(self.buf) == 1: | |
545 | self.startsample = ss | |
546 | ||
547 | # Keep separate buffer for detection of sync packets. | |
548 | # Sync packets override everything else, so that we can regain sync | |
549 | # even if some packets are corrupted. | |
550 | self.syncbuf = self.syncbuf[-4:] + [pdata[0]] | |
551 | if self.syncbuf == [0x00, 0x00, 0x00, 0x00, 0x80]: | |
552 | self.buf = self.syncbuf | |
553 | self.syncbuf = [] | |
554 | ||
555 | # See if it is ready to be decoded. | |
556 | ptype = self.get_packet_type(self.buf[0]) | |
557 | if hasattr(self, 'handle_' + ptype): | |
558 | func = getattr(self, 'handle_' + ptype) | |
559 | data = func(self.buf) | |
560 | else: | |
561 | data = self.fallback(self.buf) | |
562 | ||
563 | if data is not None: | |
564 | if data: | |
565 | self.put(self.startsample, es, self.out_ann, data) | |
566 | self.buf = [] |