tictacsync 0.91a0__tar.gz → 0.95a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tictacsync might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tictacsync
3
- Version: 0.91a0
3
+ Version: 0.95a0
4
4
  Summary: command for syncing audio video recordings
5
5
  Home-page: https://tictacsync.org/
6
6
  Author: Raymond Lutz
@@ -32,7 +32,7 @@ setup(
32
32
  'multi2polywav = tictacsync.multi2polywav:main',
33
33
  ]
34
34
  },
35
- version = '0.91a',
35
+ version = '0.95a',
36
36
  description = "command for syncing audio video recordings",
37
37
  long_description_content_type='text/markdown',
38
38
  long_description = long_descr,
@@ -69,6 +69,7 @@ class Tracks:
69
69
  others: list #of all other tags: (tag, track#) tuples
70
70
  rawtrx: list # list of strings read from file
71
71
  error_msg: str # 'None' if none
72
+ lag_values: list # list of lag in ms, entry is None if not specified.
72
73
 
73
74
  @dataclass
74
75
  class Device:
@@ -109,7 +110,13 @@ def media_at_path(input_structure, p):
109
110
  if stream['codec_type']=='audio'
110
111
  ]
111
112
  if len(audio_streams) > 1:
112
- raise Exception('ffprobe gave multiple audio streams?')
113
+ print('for [gold1]%s[/gold1], ffprobe gave multiple audio streams, quitting.'%p)
114
+ quit()
115
+ # raise Exception('ffprobe gave multiple audio streams?')
116
+ if len(audio_streams) == 0:
117
+ print('ffprobe gave no audio stream for [gold1]%s[/gold1], quitting.'%p)
118
+ quit()
119
+ # raise Exception('ffprobe gave no audio stream for %s, quitting'%p)
113
120
  audio_str = audio_streams[0]
114
121
  n = audio_str['channels']
115
122
  # pprint(ffmpeg.probe(p))
@@ -146,6 +153,7 @@ def get_device_ffprobe_UID(file):
146
153
  print(e.stderr, file)
147
154
  return None, None #-----------------------------------------------------
148
155
  # fall back to folder name
156
+ logger.debug('ffprobe %s'%probe)
149
157
  streams = probe['streams']
150
158
  codecs = [stream['codec_type'] for stream in streams]
151
159
  device_type = 'CAM' if 'video' in codecs else 'REC'
@@ -155,6 +163,7 @@ def get_device_ffprobe_UID(file):
155
163
  probe_lines = [l for l in probe_string.split('\n')
156
164
  if '_time' not in l
157
165
  and 'time_' not in l
166
+ and 'location' not in l
158
167
  and 'date' not in l ]
159
168
  # this removes any metadata related to the file
160
169
  # but keeps metadata related to the device
@@ -302,12 +311,12 @@ class Scanner:
302
311
  audio_devices = [d for d in devices if d.dev_type == 'REC']
303
312
  for recorder in audio_devices:
304
313
  recorder.tracks = self._get_tracks_from_file(recorder)
314
+ if recorder.tracks:
315
+ if not all([lv == None for lv in recorder.tracks.lag_values]):
316
+ logger.debug('%s has lag_values %s'%(
317
+ recorder.name, recorder.tracks.lag_values))
305
318
  no_name_devices = [m.device for m in self.found_media_files
306
319
  if not m.device.name]
307
- # if no_name_devices:
308
- # pprint_no_name = pformat([(d, self.get_media_for_device(d)) for d in no_name_devices])
309
- # print('those are anon devices%s\n'%pprint_no_name)
310
- # logger.debug('those media have anon device%s'%no_name_devices)
311
320
  for anon_dev in no_name_devices:
312
321
  medias = self.get_media_for_device(anon_dev)
313
322
  guess_name = _try_name(medias)
@@ -321,7 +330,7 @@ class Scanner:
321
330
 
322
331
  def _get_tracks_from_file(self, device) -> Tracks:
323
332
  """
324
- Look for track names in TRACKSFN file, possibly stored inside the
333
+ Look for eventual track names in TRACKSFN file, stored inside the
325
334
  recorder folder alongside the audio files. If there, returns a Tracks
326
335
  object, if not returns None.
327
336
  """
@@ -477,14 +486,16 @@ class Scanner:
477
486
  read track names for naming separated ISOs
478
487
  from tracks_file.
479
488
 
489
+ tokens looked for: mix mixL mixR 0 ttc
490
+
480
491
  repeting prefixes signals a stereo track
481
492
  and entries will correspondingly panned into
482
493
  a stero mix named mixL.wav and mixR.wav
483
494
 
484
- mic1 L # space is optionnal and will be removed
485
- mic1 R
486
- mic2 L
487
- mic2 R
495
+ xyz L # spaces are ignored |
496
+ zyz R | stereo pair
497
+ abc L
498
+ abc R
488
499
 
489
500
  mixL
490
501
 
@@ -502,41 +513,56 @@ class Scanner:
502
513
  ch = [c for c in chaine if c != ' ']
503
514
  return ''.join(ch)
504
515
  def _WO_LR(chaine):
505
- ch = [c for c in chaine if c not in 'lr']
516
+ ch = [c for c in chaine if c not in 'LR']
506
517
  return ''.join(ch)
507
518
  def _seemsStereoMic(tag):
508
519
  # is tag likely a stereo pair tag?
509
- # should start with 'mic' and end with 'l' or 'r'
510
- return tag[:3]=='mic' and tag[-1] in 'lr'
520
+ # should start with 'mic' and end with 'L' or 'R'
521
+ return tag[:3]=='mic' and tag[-1] in 'LR'
511
522
  file=open(tracks_file,"r")
512
523
  whole_txt = file.read()
513
524
  logger.debug('all_lines:\n%s'%whole_txt)
514
- tracks_lines = [l.split('#')[0] for l in whole_txt.splitlines() if len(l) > 0 ]
515
- tracks_lines = [_WOspace(l).lower() for l in tracks_lines if len(l) > 0 ]
525
+ tracks_lines = [l.split('#')[0] for l in whole_txt.splitlines()
526
+ if len(l) > 0 ]
527
+ tracks_lines = [_WOspace(l) for l in tracks_lines if len(l) > 0 ]
516
528
  rawtrx = tracks_lines
529
+ # add index with tuples, starting at 1
530
+ logger.debug('tracks_lines whole: %s'%tracks_lines)
531
+ def _detach_lag_value(line):
532
+ # look for ";number" ending any line, returns a two-list
533
+ splt = line.split(';')
534
+ if len(splt) == 1:
535
+ splt += [None]
536
+ if len(splt) != 2:
537
+ # error
538
+ print('Text error in %s, line %s has too many ";"'%(
539
+ tracks_file, line))
540
+ return splt
541
+ tracks_lines, lag_values = zip(*[_detach_lag_value(l) for l
542
+ in tracks_lines])
543
+ logger.debug('tracks_lines WO lag: %s'%[tracks_lines])
544
+ logger.debug('lag_values: %s'%[lag_values])
517
545
  tracks_lines = [(t,ix+1) for ix,t in enumerate(tracks_lines)]
518
- # tracks_lines = [l for l in tracks_lines if l not in ['ttc', '0']]
519
- # track_names = [l.split()[0] for l in tracks_lines if l != 'ttc']
520
- logger.debug('tracks_lines: %s'%tracks_lines)
521
546
  # first check for stereo mic pairs (could be more than one pair):
522
547
  spairs = [e for e in tracks_lines if _seemsStereoMic(e[0])]
523
548
  # spairs is stereo pairs candidates
524
549
  msg = 'Confusing stereo pair tags: %s'%' '.join([e[0]
525
550
  for e in spairs])
526
- error_output_stereo = Tracks(None,[],[],[],[],[],msg)
551
+ error_output_stereo = Tracks(None,[],[],[],[],[],msg,[])
527
552
  if len(spairs)%2 == 1: # not pairs?, quit.
528
553
  return error_output_stereo
529
554
  logger.debug('_seemsStereoM: %s'%spairs)
530
- output_tracks = Tracks(None,[],[],[],[],rawtrx,None)
531
- def _LR(p):
532
- # p = (('mic1l', 1), ('mic1r', 2))
533
- # check if L then R
534
- p1, p2 = p
535
- return p1[0][-1] == 'l' and p2[0][-1] == 'r'
555
+ output_tracks = Tracks(None,[],[],[],[],rawtrx,None,[])
556
+ output_tracks.lag_values = lag_values
557
+ # def _LR(p):
558
+ # # p = (('mic1l', 1), ('mic1r', 2))
559
+ # # check if L then R
560
+ # p1, p2 = p
561
+ # return p1[0][-1] == 'l' and p2[0][-1] == 'r'
536
562
  if spairs:
537
563
  even_idxes = range(0,len(spairs),2)
538
564
  paired = [(spairs[i], spairs[i+1]) for i in even_idxes]
539
- # eg [(('mic1l', 1), ('mic1r', 2)), (('mic2l', 3), ('mic2r', 4))]
565
+ # eg [(('mic1L', 1), ('mic1R', 2)), (('mic2L', 3), ('mic2R', 4))]
540
566
  def _mic_same(p):
541
567
  # p = (('mic1l', 1), ('mic1r', 2))
542
568
  # check if mic1 == mic1
@@ -546,16 +572,22 @@ class Scanner:
546
572
  logger.debug('mic_prefix_OK: %s'%mic_prefix_OK)
547
573
  if not mic_prefix_OK:
548
574
  return error_output_stereo
549
- mic_LR_OK = all([_LR(p) for p in paired])
550
- logger.debug('mic_LR_OK %s'%mic_LR_OK)
551
- if not mic_LR_OK:
552
- return error_output_stereo
575
+ # mic_LR_OK = all([_LR(p) for p in paired])
576
+ # logger.debug('mic_LR_OK %s'%mic_LR_OK)
577
+ # if not mic_LR_OK:
578
+ # return error_output_stereo
553
579
  def _stereo_mic_pref_chan(p):
554
- # p = (('mic1l', 1), ('mic1r', 2))
580
+ # p = (('mic1R', 1), ('mic1L', 2))
555
581
  # returns ('mic1', (1,2))
556
582
  first, second = p
557
583
  mic_prefix = _WO_LR(first[0])
558
- return (mic_prefix, (first[1], second[1]) )
584
+ # check if first token last char
585
+ if p[0][0][-1] == 'L':
586
+ logger.debug('sequence %s is L+R'%[p])
587
+ return (mic_prefix, (first[1], second[1]) )
588
+ else:
589
+ logger.debug('sequence %s is R+L'%[p])
590
+ return (mic_prefix, (second[1], first[1]) )
559
591
  grouped_stereo_mic_channels = [_stereo_mic_pref_chan(p) for p
560
592
  in paired]
561
593
  logger.debug('grouped_stereo_mic_channels: %s'%
@@ -1,4 +1,4 @@
1
- # print('Loading modules...', end='')
1
+ print('Loading modules...', end='')
2
2
 
3
3
  # I know, the following is ugly, but I need those try's to
4
4
  # run the command in my dev setting AND from
@@ -16,11 +16,11 @@ except:
16
16
  import timeline
17
17
  import multi2polywav
18
18
 
19
- import argparse
19
+ import argparse, tempfile
20
20
  from loguru import logger
21
21
  from pathlib import Path
22
22
  # import os, sys
23
- import os, sys
23
+ import os, sys, sox
24
24
  from rich.progress import track
25
25
  # from pprint import pprint
26
26
  from rich.console import Console
@@ -29,6 +29,8 @@ from rich.table import Table
29
29
  from rich import print
30
30
  from pprint import pprint
31
31
 
32
+ DEL_TEMP = False
33
+
32
34
  av_file_extensions = \
33
35
  """MOV webm mkv flv flv vob ogv ogg drc gif gifv mng avi MTS M2TS TS mov qt
34
36
  wmv yuv rm rmvb viv asf amv mp4 m4p m4v mpg mp2 mpeg mpe mpv mpg mpeg m2v
@@ -40,7 +42,7 @@ logger.level("DEBUG", color="<yellow>")
40
42
 
41
43
  def process_files_with_progress_bars(medias):
42
44
  recordings = []
43
- rec_with_yaltc = []
45
+ rec_with_TTC = []
44
46
  times = []
45
47
  for m in track(medias,
46
48
  description="1/4 Initializing Recordings:"):
@@ -49,25 +51,25 @@ def process_files_with_progress_bars(medias):
49
51
  for r in track(recordings,
50
52
  description="2/4 Looking for TicTacCode:"):
51
53
  if r.seems_to_have_TicTacCode_at_beginning():
52
- rec_with_yaltc.append(r)
53
- for r in track(rec_with_yaltc,
54
+ rec_with_TTC.append(r)
55
+ for r in track(rec_with_TTC,
54
56
  description="3/4 Finding start times:"):
55
57
  times.append(r.get_start_time())
56
- return recordings, rec_with_yaltc, times
58
+ return recordings, rec_with_TTC, times
57
59
 
58
60
  def process_files(medias):
59
61
  recordings = []
60
- rec_with_yaltc = []
62
+ rec_with_TTC = []
61
63
  times = []
62
64
  for m in medias:
63
65
  recordings.append(yaltc.Recording(m))
64
66
  for r in recordings:
65
67
  # print('%s duration %.2fs'%(r.AVpath.name, r.get_duration()))
66
68
  if r.seems_to_have_TicTacCode_at_beginning():
67
- rec_with_yaltc.append(r)
68
- for r in rec_with_yaltc:
69
+ rec_with_TTC.append(r)
70
+ for r in rec_with_TTC:
69
71
  times.append(r.get_start_time())
70
- return recordings, rec_with_yaltc, times
72
+ return recordings, rec_with_TTC, times
71
73
 
72
74
  def process_single(file, args):
73
75
  # argument is a single file
@@ -97,6 +99,42 @@ def process_single(file, args):
97
99
  print('Start time couldnt be determined')
98
100
  sys.exit(1)
99
101
 
102
+ def process_lag_adjustement(media_object):
103
+ # trim channels that are lagging (as stated in tracks.txt)
104
+ # replace the old file, and rename the old one with .wavbk
105
+ # if .wavbk exist, process was done already, so dont process
106
+ # returns nothing
107
+ lags = media_object.device.tracks.lag_values
108
+ logger.debug('will process %s lags'%[lags])
109
+ channels = timeline._split_channels(media_object.path)
110
+ # add bk to file on filesystem, but media_object.path is unchanged (?)
111
+ backup_name = str(media_object.path) + 'bk'
112
+ if Path(backup_name).exists():
113
+ logger.debug('%s exists, so return now.'%backup_name)
114
+ return
115
+ media_object.path.replace(backup_name)
116
+ logger.debug('channels %s'%channels)
117
+ def _trim(lag, chan_file):
118
+ if lag == None:
119
+ return chan_file
120
+ else:
121
+ logger.debug('process %s for lag of %s'%(chan_file, lag))
122
+ sox_transform = sox.Transformer()
123
+ sox_transform.trim(float(lag)*1e-3)
124
+ output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
125
+ out_file = timeline._pathname(output_fh)
126
+ input_file = timeline._pathname(chan_file)
127
+ logger.debug('sox in and out files: %s %s'%(input_file, out_file))
128
+ logger.debug('calling sox_transform.build()')
129
+ status = sox_transform.build(input_file, out_file, return_output=True )
130
+ logger.debug('sox.build exit code %s'%str(status))
131
+ return output_fh
132
+ new_channels = [_trim(*e) for e in zip(lags, channels)]
133
+ logger.debug('new_channels %s'%new_channels)
134
+ trimmed_multichanfile = timeline._sox_combine(new_channels)
135
+ logger.debug('trimmed_multichanfile %s'%timeline._pathname(trimmed_multichanfile))
136
+ Path(timeline._pathname(trimmed_multichanfile)).replace(media_object.path)
137
+
100
138
  def main():
101
139
  parser = argparse.ArgumentParser()
102
140
  parser.add_argument(
@@ -107,32 +145,46 @@ def main():
107
145
  )
108
146
  # parser.add_argument("directory", nargs="?", help="path of media directory")
109
147
  # parser.add_argument('-v', action='store_true')
110
- parser.add_argument('-v', action='store_true', default=False,
111
- dest='verbose_output',
112
- help='Set verbose ouput')
148
+ parser.add_argument('-v',
149
+ action='store_true', #ie default False
150
+ dest='verbose_output',
151
+ help='Set verbose ouput')
113
152
  parser.add_argument('-o', nargs=1,
114
153
  help='Where to write the SyncedMedia folder [default to "path" ]')
115
- parser.add_argument('-p', action='store_true', default=False,
154
+ parser.add_argument('-t','--timelineoffset',
155
+ nargs=1,
156
+ default=['00:00:00:00'],
157
+ dest='timelineoffset',
158
+ help='When processing multicam, where to place clips on NLE timeline (HH:MM:SS:FF)')
159
+ parser.add_argument('-p',
160
+ action='store_true',
116
161
  dest='plotting',
117
162
  help='Produce plots')
118
- parser.add_argument('--isos', action='store_true', default=False,
163
+ parser.add_argument('--isos',
164
+ action='store_true',
119
165
  dest='write_ISOs',
120
166
  help='Write ISO sound files')
121
- parser.add_argument('--nosync', action='store_true',
167
+ parser.add_argument('--nosync',
168
+ action='store_true',
122
169
  dest='nosync',
123
170
  help='Just scan and decode')
124
- parser.add_argument('--terse', action='store_true',
171
+ parser.add_argument('--terse',
172
+ action='store_true',
125
173
  dest='terse',
126
174
  help='Terse output')
127
175
  args = parser.parse_args()
176
+ # print(args)
177
+ if len(args.timelineoffset) != 1:
178
+ print('--timelineoffset needs one value, got %s'%args.timelineoffset)
179
+ quit()
128
180
  if args.verbose_output:
129
181
  logger.add(sys.stderr, level="DEBUG")
130
182
  # logger.add(sys.stdout, filter="__main__")
131
183
  # logger.add(sys.stdout, filter="yaltc")
132
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "get_start_time")
133
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "_detect_sync_pulse_position")
134
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "_get_device_mix")
135
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "_sox_mix_files")
184
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "process_lag_adjustement")
185
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "_dedrift_rec")
186
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "scan_media_and_build_devices_UID")
187
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "_parse_track_values")
136
188
  top_dir = args.path[0]
137
189
  if os.path.isfile(top_dir):
138
190
  file = top_dir
@@ -151,7 +203,13 @@ def main():
151
203
  sys.exit(1)
152
204
  multi2polywav.poly_all(top_dir)
153
205
  scanner = device_scanner.Scanner(top_dir, stay_silent=args.terse)
154
- scanner.scan_media_and_build_devices_UID()
206
+ scanner.scan_media_and_build_devices_UID()
207
+ for m in scanner.found_media_files:
208
+ if m.device.tracks:
209
+ if not all([lv == None for lv in m.device.tracks.lag_values]):
210
+ logger.debug('%s has lag_values %s'%(
211
+ m.path, m.device.tracks.lag_values))
212
+ process_lag_adjustement(m)
155
213
  if not args.terse:
156
214
  if scanner.input_structure == 'folder_is_device':
157
215
  print('\nDetected structured folders', end='')
@@ -162,10 +220,8 @@ def main():
162
220
  else:
163
221
  print('\nDetected loose structure')
164
222
  if scanner.CAM_numbers() > 1:
165
- print('\nNote: different CAMs are present, will sync audio')
166
- print('for each of them but if you want to set their')
167
- print('respective timecode for NLE timeline alignement')
168
- print('you should regroup clips by CAM under their own DIR.')
223
+ print('\nNote: different CAMs are present, will sync audio for each of them but if you want to set their')
224
+ print('respective timecode for NLE timeline alignement you should regroup clips by CAM under their own DIR.')
169
225
  print('\nFound [gold1]%i[/gold1] media files '%(
170
226
  len(scanner.found_media_files)), end='')
171
227
  print('from [gold1]%i[/gold1] devices:\n'%(
@@ -177,15 +233,11 @@ def main():
177
233
  print('[gold1]%s[/gold1]'%m.path.name, end=', ')
178
234
  print('[gold1]%s[/gold1]'%medias[-1].path.name)
179
235
  print()
180
- # if args.verbose_output or args.terse: # verbose or terse, so no progress bars
181
- # rez = process_files(scanner.found_media_files)
182
- # else:
183
- # rez = process_files_with_progress_bars(scanner.found_media_files)
184
236
  rez = process_files(scanner.found_media_files)
185
- recordings, rec_with_yaltc, times = rez
237
+ recordings, rec_with_TTC, times = rez
186
238
  recordings_with_time = [
187
239
  rec
188
- for rec in rec_with_yaltc
240
+ for rec in rec_with_TTC
189
241
  if rec.get_start_time()
190
242
  ]
191
243
  if not args.terse:
@@ -199,7 +251,7 @@ def main():
199
251
  table.add_column("Date\n", justify="center", style='gold1')
200
252
  rec_WO_time = [
201
253
  rec.AVpath.name
202
- for rec in rec_with_yaltc
254
+ for rec in rec_with_TTC
203
255
  if not rec.get_start_time()
204
256
  ]
205
257
  if rec_WO_time:
@@ -226,9 +278,6 @@ def main():
226
278
  print()
227
279
  n_devices = scanner.get_devices_number()
228
280
  OUT_struct_for_mcam = scanner.top_dir_has_multicam
229
- # if n_devices > 2:
230
- # print('\nMerging for more than 2 devices is not implemented yet, quitting...')
231
- # sys.exit(1)
232
281
  if len(recordings_with_time) < 2:
233
282
  if not args.terse:
234
283
  print('\nNothing to sync, exiting.\n')
@@ -245,31 +294,24 @@ def main():
245
294
  asked_ISOs = False
246
295
  output_dir = args.o
247
296
  # if args.verbose_output or args.terse: # verbose, so no progress bars
248
- for stitcher in matcher.video_mergers:
249
- stitcher.build_audio_and_write_video(top_dir, arg_out_dir,
297
+ for merger in matcher.video_mergers:
298
+ merger.build_audio_and_write_video(top_dir, arg_out_dir,
250
299
  OUT_struct_for_mcam,
251
300
  asked_ISOs,)
252
- # else:
253
- # print()
254
- # for stitcher in track(matcher.video_mergers,
255
- # description="4/4 Merging sound to videos:"):
256
- # stitcher.build_audio_and_write_video(top_dir, arg_out_dir,
257
- # OUT_struct_for_mcam,
258
- # asked_ISOs,)
259
301
  if not args.terse:
260
302
  print("\n")
261
303
  # find out where files were wrtitten
262
- a_stitcher = matcher.video_mergers[0]
304
+ a_merger = matcher.video_mergers[0]
263
305
  print('\nWrote output in folder [gold1]%s[/gold1]'%(
264
- a_stitcher.synced_clip_dir))
265
- for stitcher in matcher.video_mergers:
266
- print('[gold1]%s[/gold1]'%stitcher.videoclip.AVpath.name, end='')
267
- for audio in stitcher.get_matched_audio_recs():
306
+ a_merger.synced_clip_dir))
307
+ for merger in matcher.video_mergers:
308
+ print('[gold1]%s[/gold1]'%merger.videoclip.AVpath.name, end='')
309
+ for audio in merger.get_matched_audio_recs():
268
310
  print(' + [gold1]%s[/gold1]'%audio.AVpath.name, end='')
269
- new_file = stitcher.videoclip.final_synced_file.parts
270
- print(' became [gold1]%s[/gold1]'%stitcher.videoclip.final_synced_file.name)
311
+ new_file = merger.videoclip.final_synced_file.parts
312
+ print(' became [gold1]%s[/gold1]'%merger.videoclip.final_synced_file.name)
271
313
  # matcher._build_otio_tracks_for_cam()
272
- matcher.shrink_gaps_between_takes()
314
+ matcher.shrink_gaps_between_takes(args.timelineoffset)
273
315
  sys.exit(0)
274
316
 
275
317
  if __name__ == '__main__':
@@ -1,4 +1,4 @@
1
- import argparse, wave, subprocess
1
+ import argparse, wave, subprocess, sys
2
2
  from loguru import logger
3
3
  from pathlib import Path
4
4
  from itertools import groupby
@@ -303,7 +303,7 @@ class AudioStitcherVideoMerger:
303
303
  Typically each found video is associated with an AudioStitcherVideoMerger
304
304
  instance. AudioStitcherVideoMerger does the actual audio-video file
305
305
  processing of merging AudioStitcherVideoMerger.videoclip (gen. a video)
306
- with all audio files in AudioStitcherVideoMerger.edited_audio as
306
+ with all audio files in AudioStitcherVideoMerger.soxed_audio as
307
307
  determined by the Matcher object (Matcher instanciates and manages
308
308
  AudioStitcherVideoMerger objects).
309
309
 
@@ -317,46 +317,54 @@ class AudioStitcherVideoMerger:
317
317
  project.
318
318
 
319
319
 
320
+ Class attribute
321
+
322
+ tempoed_recs : dict as {Recording : path}
323
+
324
+ a cache for already time-stretched audio files. Keys are elements
325
+ of matched_audio_recordings and the value are tuples:
326
+ (factor, file_handle), the file_handle points to the precedently
327
+ produced NamedTemporaryFile; factor is the value that was used in
328
+ the sox tempo transform.
329
+
320
330
  Attributes:
321
331
 
322
332
  videoclip : a Recording instance
323
333
  The video to which audio files are synced
324
334
 
325
- edited_audio : dict as {Recording : path}
326
- keys are elements of matched_audio_recordings of class Recording
327
- and the value stores the Pathlib path of the eventual edited
328
- audio (trimmed , padded or time stretched). Before building the
329
- audio_montage, path points to the initial
330
- Recording.valid_sound
335
+ soxed_audio : dict as {Recording : path}
336
+ keys are elements of matched_audio_recordings and the value are
337
+ the Pathlib path of the eventual edited audio(trimmed or padded).
331
338
 
332
339
  synced_clip_dir : Path
333
340
  where synced clips are written
334
341
 
335
342
  """
343
+ tempoed_recs = {}
336
344
 
337
345
  def __init__(self, video_clip):
338
346
  self.videoclip = video_clip
339
347
  # self.matched_audio_recordings = []
340
- self.edited_audio = {}
348
+ self.soxed_audio = {}
341
349
  logger.debug('instantiating AudioStitcherVideoMerger for %s'%
342
350
  video_clip)
343
351
 
344
352
  def add_matched_audio(self, audio_rec):
345
353
  """
346
- Populates AudioStitcherVideoMerger.edited_audio,
354
+ Populates AudioStitcherVideoMerger.soxed_audio,
347
355
  a dict as {Recording : path}
348
356
 
349
357
  This fct is called
350
358
  within Matcher.scan_audio_for_each_videoclip()
351
359
 
352
- Returns nothing, fills self.edited_audio dict with
360
+ Returns nothing, fills self.soxed_audio dict with
353
361
  matched audio.
354
362
 
355
363
  """
356
- self.edited_audio[audio_rec] = audio_rec.AVpath
364
+ self.soxed_audio[audio_rec] = audio_rec.AVpath
357
365
  """
358
- Here at this point, self.edited_audio[audio_rec] is unedited but
359
- after a call to _edit_audio_file(), edited_audio[audio_rec] points to
366
+ Here at this point, self.soxed_audio[audio_rec] is unedited but
367
+ after a call to _edit_audio_file(), soxed_audio[audio_rec] points to
360
368
  a new file and audio_rec.AVpath is unchanged.
361
369
  """
362
370
  return
@@ -364,9 +372,9 @@ class AudioStitcherVideoMerger:
364
372
  def get_matched_audio_recs(self):
365
373
  """
366
374
  Returns audio recordings that overlap self.videoclip.
367
- Simply keys of self.edited_audio dict
375
+ Simply keys of self.soxed_audio dict
368
376
  """
369
- return list(self.edited_audio.keys())
377
+ return list(self.soxed_audio.keys())
370
378
 
371
379
  def _get_audio_devices(self):
372
380
  devices = set([r.device for r in self.get_matched_audio_recs()])
@@ -382,58 +390,77 @@ class AudioStitcherVideoMerger:
382
390
  return recs
383
391
 
384
392
  def _dedrift_rec(self, rec):
385
- initial_duration = sox.file_info.duration(
386
- _pathname(rec.AVpath))
387
- sox_transform = sox.Transformer()
388
- # tempo_scale_factor = rec.device_relative_speed
393
+ # instanciates a sox.Transformer() with tempo() effect
394
+ # add applies it via a call to _edit_audio_file(rec, sox_transform)
389
395
  tempo_scale_factor = rec.device_relative_speed
390
396
  audio_dev = rec.device.name
391
397
  video_dev = self.videoclip.device.name
398
+ print('when merging with [gold1]%s[/gold1].'%self.videoclip)
392
399
  if tempo_scale_factor > 1:
393
- print('[gold1]%s[/gold1] clock too fast relative to [gold1]%s[/gold1] so file is too long by a %f factor;\n'%
400
+ print('Because [gold1]%s[/gold1] clock too fast relative to [gold1]%s[/gold1]: file is too long by a %.12f factor;'%
394
401
  (audio_dev, video_dev, tempo_scale_factor))
395
402
  else:
396
- print('hence [gold1]%s[/gold1] clock too slow relative to [gold1]%s[/gold1] so file is short by a %f factor\n'%
403
+ print('Because [gold1]%s[/gold1] clock too slow relative to [gold1]%s[/gold1]: file is short by a %.12f factor'%
397
404
  (audio_dev, video_dev, tempo_scale_factor))
398
- sox_transform.tempo(tempo_scale_factor)
399
- # scaled_file = self._get_soxed_file(rec, sox_transform)
400
- logger.debug('sox_transform %s'%sox_transform.effects)
401
- self._edit_audio_file(rec, sox_transform)
402
- scaled_file_name = _pathname(self.edited_audio[rec])
403
- new_duration = sox.file_info.duration(scaled_file_name)
404
- # goal_duration = rec.get_corrected_duration()
405
- logger.debug('initial_duration %f new_duration %f ratio:%f'%(
406
- initial_duration, new_duration, initial_duration/new_duration))
405
+ logger.debug('tempoed_recs dict:%s'%AudioStitcherVideoMerger.tempoed_recs)
406
+ if rec in AudioStitcherVideoMerger.tempoed_recs:
407
+ logger.debug('%s already tempoed'%rec)
408
+ cached_factor, cached_file = AudioStitcherVideoMerger.tempoed_recs[rec]
409
+ error_factor = tempo_scale_factor/cached_factor
410
+ logger.debug('tempo factors, needed: %f cached %f'%(tempo_scale_factor,cached_factor))
411
+ delta_cache = abs((1 - error_factor)*rec.get_original_duration())
412
+ logger.debug('error if cache is used: %f ms'%(delta_cache*1e3))
413
+ delta_cache_is_ok = delta_cache < yaltc.MAXDRIFT
414
+ else:
415
+ delta_cache_is_ok = False
416
+ if delta_cache_is_ok:
417
+ logger.debug('ok, will use %s'%cached_file)
418
+ self.soxed_audio[rec] = cached_file
419
+ else:
420
+ logger.debug('%s not tempoed yet'%rec)
421
+ sox_transform = sox.Transformer()
422
+ sox_transform.tempo(tempo_scale_factor)
423
+ # scaled_file = self._get_soxed_file(rec, sox_transform)
424
+ logger.debug('sox_transform %s'%sox_transform.effects)
425
+ soxed_fh = self._edit_audio_file(rec, sox_transform)
426
+ scaled_file_name = _pathname(soxed_fh)
427
+ AudioStitcherVideoMerger.tempoed_recs[rec] = (tempo_scale_factor, soxed_fh)
428
+ new_duration = sox.file_info.duration(scaled_file_name)
429
+ initial_duration = sox.file_info.duration(
430
+ _pathname(rec.AVpath))
431
+ logger.debug('Verif: initial_duration %.12f new_duration %.12f ratio:%.12f'%(
432
+ initial_duration, new_duration, initial_duration/new_duration))
433
+ logger.debug('delta duration %f ms'%((new_duration-initial_duration)*1e3))
407
434
 
408
435
  def _get_concatenated_audiofile_for(self, device):
409
436
  """
410
437
  return a handle for the final audio file formed by all detected
411
- overlapping recordings, produced by the same specified device.
438
+ overlapping recordings, produced by the same audio recorder.
412
439
 
413
440
  """
414
441
  logger.debug('concatenating device %s'%str(device))
415
- recordings = self._get_all_recordings_for(device)
442
+ audio_recs = self._get_all_recordings_for(device)
416
443
  # [TODO here] Check if all unidentified device files are not
417
444
  # overlapping because they are considered produced by the same
418
445
  # device. If some overlap then necessarily they're from different
419
446
  # ones. List the files and warn the user there is a risk of error if
420
447
  # they're not from the same device.
421
448
 
422
- logger.debug('%i audio files for videoclip %s:'%(len(recordings),
449
+ logger.debug('%i audio files for videoclip %s:'%(len(audio_recs),
423
450
  self.videoclip))
424
- for r in recordings:
451
+ for r in audio_recs:
425
452
  logger.debug(' %s'%r)
453
+ # ratio between real samplerates of audio and videoclip
426
454
  speeds = numpy.array([rec.get_speed_ratio(self.videoclip)
427
- for rec in recordings])
455
+ for rec in audio_recs])
428
456
  mean_speed = numpy.mean(speeds)
429
- for r in recordings:
430
- r.device_relative_speed = mean_speed
431
- # r.device_relative_speed = 0.9
432
- logger.debug('set device_relative_speed for %s'%r)
433
- logger.debug(' value: %f'%r.device_relative_speed)
434
- r.set_time_position_to(self.videoclip)
435
- logger.debug('time_position for %s: %fs relative to %s'%(r,
436
- r.time_position, self.videoclip))
457
+ for audio in audio_recs:
458
+ audio.device_relative_speed = mean_speed
459
+ logger.debug('set device_relative_speed for %s'%audio)
460
+ logger.debug(' value: %f'%audio.device_relative_speed)
461
+ audio.set_time_position_to(self.videoclip)
462
+ logger.debug('time_position for %s: %fs relative to %s'%(audio,
463
+ audio.time_position, self.videoclip))
437
464
  # st_dev_speeds just to check for anomalous situation
438
465
  st_dev_speeds = numpy.std(speeds)
439
466
  logger.debug('mean speed for %s: %.6f std dev: %.0e'%(device,
@@ -458,7 +485,7 @@ class AudioStitcherVideoMerger:
458
485
 
459
486
  # process first element 'by hand' outside the loop
460
487
  # first_audio is a Recording, not a path nor filehandle
461
- first_audio = recordings[0]
488
+ first_audio = audio_recs[0]
462
489
  needs_dedrift, delta = first_audio.needs_dedrifting()
463
490
  logger.debug('first audio is %s'%first_audio)
464
491
  logger.debug('checking drift, first audio: delta of %0.2f ms'%(
@@ -470,8 +497,8 @@ class AudioStitcherVideoMerger:
470
497
  self._pad_or_trim_first_audio(first_audio)
471
498
  # loop for the other files
472
499
  # growing_file = first_audio.edited_version
473
- growing_file = self.edited_audio[first_audio]
474
- for i, rec in enumerate(recordings[1:]):
500
+ growing_file = self.soxed_audio[first_audio]
501
+ for i, rec in enumerate(audio_recs[1:]):
475
502
  logger.debug('Padding and joining for %s'%rec)
476
503
  needs_dedrift, delta = rec.needs_dedrifting()
477
504
  logger.debug('next audio is %s'%rec)
@@ -494,7 +521,7 @@ class AudioStitcherVideoMerger:
494
521
  (rec,rec.time_position,end_time))
495
522
  self._pad_file(rec, pad_duration)
496
523
  # new_file = rec.edited_version
497
- new_file = self.edited_audio[rec]
524
+ new_file = self.soxed_audio[rec]
498
525
  growing_file = self._concatenate_audio_files(growing_file, new_file)
499
526
  end_time = sox.file_info.duration(growing_file.name)
500
527
  logger.debug('total edited audio duration %.2f s'%end_time)
@@ -507,7 +534,7 @@ class AudioStitcherVideoMerger:
507
534
  TODO: check if first_rec is a Recording or tempfile (maybe a tempfile if dedrifted)
508
535
  NO: will change tempo after trimming/padding
509
536
 
510
- Store (into Recording.edited_audio dict) the handle of the sox processed
537
+ Store (into Recording.soxed_audio dict) the handle of the sox processed
511
538
  first recording, padded or chopped according to AudioStitcherVideoMerger.videoclip
512
539
  starting time. Length of the written file can differ from length of the
513
540
  submitted Recording object if drift is corrected with sox tempo
@@ -571,22 +598,25 @@ class AudioStitcherVideoMerger:
571
598
  def _edit_audio_file(self, audio_rec, sox_transform):
572
599
  """
573
600
  Apply the specified sox_transform onto the audio_rec and update
574
- self.edited_audio dict with the result (with audio_rec as the key)
601
+ self.soxed_audio dict with the result (with audio_rec as the key)
602
+ Returns the filehandle of the result.
575
603
  """
576
604
  output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
577
605
  logger.debug('transform: %s'%sox_transform.effects)
578
- recording_fh = self.edited_audio[audio_rec]
606
+ recording_fh = self.soxed_audio[audio_rec]
579
607
  logger.debug('for recording %s, matching %s'%(audio_rec,
580
608
  self.videoclip))
581
609
  input_file = _pathname(recording_fh)
582
- logger.debug('AudioStitcherVideoMerger.edited_audio[audio_rec]: %s'%
610
+ logger.debug('AudioStitcherVideoMerger.soxed_audio[audio_rec]: %s'%
583
611
  input_file)
584
612
  out_file = _pathname(output_fh)
585
613
  logger.debug('sox in and out files: %s %s'%(input_file, out_file))
614
+ logger.debug('calling sox_transform.build()')
586
615
  status = sox_transform.build(input_file, out_file, return_output=True )
587
616
  logger.debug('sox.build exit code %s'%str(status))
588
617
  # audio_rec.edited_version = output_fh
589
- self.edited_audio[audio_rec] = output_fh
618
+ self.soxed_audio[audio_rec] = output_fh
619
+ return output_fh
590
620
 
591
621
  def _write_ISOs(self, edited_audio_all_devices):
592
622
  """
@@ -797,10 +827,12 @@ class AudioStitcherVideoMerger:
797
827
  logger.debug('stereo_mic_idx_flat %s'%stereo_mic_idx_flat)
798
828
  mono_tracks = [i for i in range(1, device.n_chan + 1)
799
829
  if i not in stereo_mic_idx_flat]
800
- logger.debug('mono_tracks: %s'%mono_tracks)
830
+ logger.debug('mono_tracks (with ttc+zeroed included): %s'%mono_tracks)
801
831
  # remove TTC track number
802
- mono_tracks.remove(device.ttc + 1)
803
- logger.debug('mono_tracks %s'%mono_tracks)
832
+ to_remove = device.tracks.unused + [device.ttc+1]# unused is sox idx
833
+ [mono_tracks.remove(t) for t in to_remove]
834
+ # mono_tracks.remove(device.ttc + 1)
835
+ logger.debug('mono_tracks (ttc+zeroed removed)%s'%mono_tracks)
804
836
  mono_files = [_sox_keep(multichan_tmpfl, [chan]) for chan
805
837
  in mono_tracks]
806
838
  new_stereo_files = [_sox_mono2stereo(f) for f in mono_files]
@@ -988,7 +1020,7 @@ class AudioStitcherVideoMerger:
988
1020
  """
989
1021
  synced_clip_file = self.videoclip.final_synced_file
990
1022
  video_path = self.videoclip.AVpath
991
- timecode = self.videoclip.get_timecode()
1023
+ timecode = self.videoclip.get_start_timecode_string()
992
1024
  # self.videoclip.synced_audio = audio_path
993
1025
  audio_path = self.videoclip.synced_audio
994
1026
  vid_only_handle = self._keep_VIDEO_only(video_path)
@@ -1136,20 +1168,28 @@ class Matcher:
1136
1168
  case4 = R1 < A2 < R2
1137
1169
  return case1 or case2 or case3 or case4
1138
1170
 
1139
- def shrink_gaps_between_takes(self, with_gap=CLUSTER_GAP):
1171
+ def shrink_gaps_between_takes(self, CLI_offset, with_gap=CLUSTER_GAP):
1140
1172
  """
1141
1173
  for single cam shootings this simply sets the gap between takes,
1142
1174
  tweaking each vid timecode metadata to distribute them next to each
1143
- other along NLE timeline. For multicam takes, shifts are computed so
1175
+ other along NLE timeline.
1176
+
1177
+ Moves clusters at the timelineoffset
1178
+
1179
+ For multicam takes, shifts are computed so
1144
1180
  video clusters are near but dont overlap, ex:
1145
1181
 
1146
- Cluster 1 Cluster 2
1147
- 1111111111111 2222222222 (cam A)
1148
- 11111111111[...]222222222 (cam B)
1182
+ Cluster 1 Cluster 2
1183
+ 1111111111111 2222222222 (cam A)
1184
+ 11111111111[inserted gap]222222222 (cam B)
1149
1185
 
1150
1186
  or
1151
- 1111111111111 222222 (cam A)
1152
- 1111111 22222 (cam B)
1187
+ 1111111111111[inserted 222222 (cam A)
1188
+ 1111111 gap]222222222 (cam B)
1189
+
1190
+ argument:
1191
+ CLI_offset (str), option from command-line
1192
+ with_gap (float), the gap duration in seconds
1153
1193
 
1154
1194
  Returns nothing, changes are done in the video files metadata
1155
1195
  (each referenced by Recording.final_synced_file)
@@ -1210,17 +1250,25 @@ class Matcher:
1210
1250
  cummulative_offsets = [td.total_seconds() for td in cummulative_offsets]
1211
1251
  logger.debug('cummulative_offsets: %s'%cummulative_offsets)
1212
1252
  time_of_first = clusters[0]['start']
1253
+ # compute CLI_offset_in_seconds from HH:MM:SS:FF in CLI_offset
1254
+ h, m, s, f = [float(s) for s in CLI_offset[0].split(':')]
1255
+ logger.debug('CLI_offset float values %s'%[h,m,s,f])
1256
+ CLI_offset_in_seconds = 3600*h + 60*m + s + f/vids[0].get_framerate()
1257
+ logger.debug('CLI_offset in seconds %f'%CLI_offset_in_seconds)
1213
1258
  offset_for_all_clips = - from_midnight(time_of_first).total_seconds()
1259
+ offset_for_all_clips += CLI_offset_in_seconds
1214
1260
  logger.debug('time_of_first: %s'%time_of_first)
1215
1261
  logger.debug('offset_for_all_clips: %s'%offset_for_all_clips)
1216
1262
  for cluster, offset in zip(clusters, cummulative_offsets):
1263
+ # first one starts at 00:00:00:00
1217
1264
  total_offset = offset + offset_for_all_clips
1218
1265
  logger.debug('for %s offset in sec: %f'%(cluster['vids'],
1219
1266
  total_offset))
1220
1267
  for vid in cluster['vids']:
1221
- tc = vid.get_timecode(with_offset=total_offset)
1268
+ # tc = vid.get_start_timecode_string(CLI_offset, with_offset=total_offset)
1269
+ tc = vid.get_start_timecode_string(with_offset=total_offset)
1222
1270
  logger.debug('for %s old tc: %s new tc %s'%(vid,
1223
- vid.get_timecode(), tc))
1271
+ vid.get_start_timecode_string(), tc))
1224
1272
  vid.write_file_timecode(tc)
1225
1273
  return
1226
1274
 
@@ -36,7 +36,7 @@ TEENSY_MAX_LAG = 1.01*128/44100 # sec, duration of a default length audio block
36
36
  CACHING = True
37
37
  DEL_TEMP = False
38
38
  DB_RMS_SILENCE_SOX = -58
39
- MAXDRIFT = 10e-3 # in sec, normally 10e-3 (10 ms)
39
+ MAXDRIFT = 15e-3 # in sec, for end of clip
40
40
 
41
41
 
42
42
  ################## pasted from FSKfreqCalculator.py output:
@@ -46,7 +46,7 @@ SYMBOL_LENGTH = 14.286 # ms, from FSKfreqCalculator.py
46
46
  N_SYMBOLS = 35 # including sync pulse
47
47
  ##################
48
48
 
49
- MINIMUM_LENGTH = 4 # sec
49
+ MINIMUM_LENGTH = 8 # sec
50
50
  TRIAL_TIMES = [ # in seconds
51
51
  (3.5, -2),
52
52
  (3.5, -3.5),
@@ -233,7 +233,7 @@ class Decoder:
233
233
 
234
234
  Uses the conditions below:
235
235
 
236
- Extract duration is 1.143 s.
236
+ Extract duration is 1.143 s. (ie one sec + 1 symbol duration)
237
237
  In self.word_props (list of morphology.regionprops):
238
238
  if one region, duration should be in [0.499 0.512] sec
239
239
  if two regions, total duration should be in [0.50 0.655]
@@ -244,13 +244,19 @@ class Decoder:
244
244
  props = self.words_props
245
245
  if len(props) not in [1,2]:
246
246
  failing_comment = 'len(props) not in [1,2]: %i'%len(props)
247
+ else:
248
+ logger.debug('len(props), %i, is in [1,2]'%len(props))
247
249
  if len(props) == 1:
250
+ logger.debug('one region')
248
251
  w = _width(props[0])/self.samplerate
249
252
  # self.effective_word_duration = w
250
253
  # logger.debug('effective_word_duration %f (one region)'%w)
251
254
  if not 0.499 < w < 0.512: # TODO: move as TOP OF FILE PARAMS
252
255
  failing_comment = '_width %f not in [0.499 0.512]'%w
256
+ else:
257
+ logger.debug('0.499 < width < 0.512, %f'%w)
253
258
  else: # 2 regions
259
+ logger.debug('two regions')
254
260
  widths = [_width(p)/self.samplerate for p in props] # in sec
255
261
  total_w = sum(widths)
256
262
  # extra_window_duration = SOUND_EXTRACT_LENGTH - 1
@@ -260,6 +266,8 @@ class Decoder:
260
266
  failing_comment = 'two regions duration %f not in [0.50 0.655]\n%s'%(total_w, widths)
261
267
  # fig, ax = plt.subplots()
262
268
  # p(ax, sound_extract_one_bit)
269
+ else:
270
+ logger.debug('0.5 < total_w < 0.656, %f'%total_w)
263
271
  logger.debug('failing_comment: %s'%(
264
272
  'none' if failing_comment=='' else failing_comment))
265
273
  return failing_comment == '' # no comment = extract seems TicTacCode
@@ -558,7 +566,7 @@ class Recording:
558
566
  implicitly True for each video recordings (but not set)
559
567
 
560
568
  device_relative_speed : float
561
-
569
+ Set by
562
570
  the ratio of the recording device clock speed relative to the
563
571
  video recorder clock device, in order to correct clock drift with
564
572
  pysox tempo transform. If value < 1.0 then the recording is
@@ -593,13 +601,15 @@ class Recording:
593
601
 
594
602
  def __init__(self, media, do_plots=False):
595
603
  """
604
+ Set AVfilename string and check if file exists, does not read any
605
+ media data right away but uses ffprobe to parses the file and sets
606
+ probe attribute.
607
+
608
+ Logs a warning and sets Recording.decoder to None if ffprobe cant
609
+ interpret the file or if file has no audio. If file contains audio,
610
+ initialise Recording.decoder(but doesnt try to decode anything yet).
611
+
596
612
  If multifile recording, AVfilename is sox merged audio file;
597
- Set AVfilename string and check if file exists, does not read
598
- any media data right away but uses ffprobe to parses the file and
599
- sets probe attribute.
600
- Logs a warning if ffprobe cant interpret the file or if file
601
- has no audio; if file contains audio, instantiates a Decoder object
602
- (but doesnt try to decode anything yet)
603
613
 
604
614
  Parameters
605
615
  ----------
@@ -673,6 +683,7 @@ class Recording:
673
683
  print('Recording init failed: %s'%recording_init_fail)
674
684
  self.probe = None
675
685
  self.decoder = None
686
+ return
676
687
  logger.debug('ffprobe found: %s'%self.probe)
677
688
  logger.debug('n audio chan: %i'%self.get_audio_channels_nbr())
678
689
  self._read_audio_data()
@@ -708,7 +719,8 @@ class Recording:
708
719
  self.audio_data.shape))
709
720
 
710
721
  def __repr__(self):
711
- return 'Recording of %s'%_pathname(self.new_rec_name)
722
+ # return 'Recording of %s'%_pathname(self.new_rec_name)
723
+ return _pathname(self.new_rec_name)
712
724
 
713
725
  def _check_for_camera_error_correction(self):
714
726
  # look for a file number
@@ -787,13 +799,14 @@ class Recording:
787
799
 
788
800
  def needs_dedrifting(self):
789
801
  rel_sp = self.device_relative_speed
790
- if rel_sp > 1:
791
- delta = (rel_sp - 1)*self.get_original_duration()
792
- else:
793
- delta = (1 - rel_sp)*self.get_original_duration()
802
+ # if rel_sp > 1:
803
+ # delta = (rel_sp - 1)*self.get_original_duration()
804
+ # else:
805
+ # delta = (1 - rel_sp)*self.get_original_duration()
806
+ delta = abs((1 - rel_sp)*self.get_original_duration())
794
807
  logger.debug('%s delta drift %.2f ms'%(str(self), delta*1e3))
795
808
  if delta > MAXDRIFT:
796
- print('[gold1]%s[/gold1] will get drift correction: delta of [gold1]%.3f[/gold1] ms is too big'%
809
+ print('\n[gold1]%s[/gold1] will get drift correction: delta of [gold1]%.3f[/gold1] ms is too big'%
797
810
  (self.AVpath, delta*1e3))
798
811
  return delta > MAXDRIFT, delta
799
812
 
@@ -942,9 +955,12 @@ class Recording:
942
955
  if successful, returns a datetime.datetime instance;
943
956
  if not returns None.
944
957
  """
945
- logger.debug('for recording %s, recording.start_time %s'%(self,
958
+ logger.debug('for %s, recording.start_time %s'%(self,
946
959
  self.start_time))
960
+ if self.decoder is None:
961
+ return None # ffprobe failes or file too short, see __init__
947
962
  if self.start_time is not None:
963
+ logger.debug('Recording.start_time already found %s'%self.start_time)
948
964
  return self.start_time #############################################
949
965
  cached_times = {}
950
966
  def find_time(t_sec):
@@ -969,8 +985,8 @@ class Recording:
969
985
  len(TRIAL_TIMES)))
970
986
  # time_around_beginning = self._find_time_around(near_beg)
971
987
  time_around_beginning = find_time(near_beg)
972
- if self.TicTacCode_channel is None:
973
- return None ####################################################
988
+ # if self.TicTacCode_channel is None:
989
+ # return None ####################################################
974
990
  logger.debug('Trial #%i, end at %f'%(i+1, near_end))
975
991
  # time_around_end = self._find_time_around(near_end)
976
992
  time_around_end = find_time(near_end)
@@ -1048,6 +1064,7 @@ class Recording:
1048
1064
  return int(ppm)
1049
1065
 
1050
1066
  def get_speed_ratio(self, videoclip):
1067
+ # ratio between real samplerates of audio and videoclip
1051
1068
  nominal = self.get_samplerate()
1052
1069
  true = self.true_samplerate
1053
1070
  ratio = true/nominal
@@ -1067,9 +1084,10 @@ class Recording:
1067
1084
  string = self._ffprobe_video_stream()['avg_frame_rate']
1068
1085
  return eval(string) # eg eval(24000/1001)
1069
1086
 
1070
- def get_timecode(self, with_offset=0):
1071
- # returns a HHMMSS:FR string
1087
+ def get_start_timecode_string(self, with_offset=0):
1088
+ # returns a HH:MM:SS:FR string
1072
1089
  start_datetime = self.get_start_time()
1090
+ # logger.debug('CLI_offset %s'%CLI_offset)
1073
1091
  logger.debug('start_datetime %s'%start_datetime)
1074
1092
  start_datetime += timedelta(seconds=with_offset)
1075
1093
  logger.debug('shifted start_datetime %s (offset %f)'%(start_datetime,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tictacsync
3
- Version: 0.91a0
3
+ Version: 0.95a0
4
4
  Summary: command for syncing audio video recordings
5
5
  Home-page: https://tictacsync.org/
6
6
  Author: Raymond Lutz
File without changes
File without changes
File without changes