tictacsync 0.2a8__tar.gz → 0.3a1__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.2a8
3
+ Version: 0.3a1
4
4
  Summary: command for syncing audio video recordings
5
5
  Home-page: https://tictacsync.org/
6
6
  Author: Raymond Lutz
@@ -83,7 +83,7 @@ To also produce _synced_ ISO audio files, specify `--isos` . A directory named `
83
83
 
84
84
  When called with the `-p` flag, zoomable plots will be produced for diagnostic purpose (close the plotting window for the 2nd one) and the decoded starting time will be output to stdin:
85
85
 
86
- > tictacsync -p tictacsync/tests/dailies/loose/MVI_0024.MP4
86
+ > tictacsync -p dailies/loose/MVI_0024.MP4
87
87
 
88
88
  Typical first plot produced :
89
89
 
@@ -60,7 +60,7 @@ To also produce _synced_ ISO audio files, specify `--isos` . A directory named `
60
60
 
61
61
  When called with the `-p` flag, zoomable plots will be produced for diagnostic purpose (close the plotting window for the 2nd one) and the decoded starting time will be output to stdin:
62
62
 
63
- > tictacsync -p tictacsync/tests/dailies/loose/MVI_0024.MP4
63
+ > tictacsync -p dailies/loose/MVI_0024.MP4
64
64
 
65
65
  Typical first plot produced :
66
66
 
@@ -31,7 +31,7 @@ setup(
31
31
  'multi2polywav = tictacsync.multi2polywav:main',
32
32
  ]
33
33
  },
34
- version = '0.2a8',
34
+ version = '0.3a1',
35
35
  description = "command for syncing audio video recordings",
36
36
  long_description_content_type='text/markdown',
37
37
  long_description = long_descr,
@@ -58,28 +58,6 @@ def print_grby(grby):
58
58
  for e in keylist:
59
59
  print(' ', e)
60
60
 
61
- def media_at_path(p):
62
- # return Media object for mediafile using ffprobe
63
- dev_UID, dt = get_device_ffprobe_UID(p)
64
- if dt == 'CAM':
65
- streams = ffmpeg.probe(p)['streams']
66
- audio_streams = [
67
- stream
68
- for stream
69
- in streams
70
- if stream['codec_type']=='audio'
71
- ]
72
- if len(audio_streams) > 1:
73
- raise Exception('ffprobe gave multiple audio streams?')
74
- audio_str = audio_streams[0]
75
- n = audio_str['channels']
76
- # pprint(ffmpeg.probe(p))
77
- else:
78
- n = sox.file_info.channels(_pathname(p)) # eg 2
79
- logger.debug('for file %s dev_UID established %s'%(p.name, dev_UID))
80
- return Media(p,
81
- Device(UID=dev_UID, folder=p.parent, name=None, dev_type=dt,
82
- n_chan=n, tracks=None))
83
61
 
84
62
  @dataclass
85
63
  class Tracks:
@@ -111,8 +89,33 @@ class Media:
111
89
  """
112
90
  path: Path
113
91
  device: Device
114
-
115
-
92
+ def media_at_path(input_structure, p):
93
+ # return Media object for mediafile using ffprobe
94
+ dev_UID, dt = get_device_ffprobe_UID(p)
95
+ dev_name = None
96
+ if input_structure == 'folder_is_device':
97
+ dev_name = p.parent.name
98
+ if dev_UID is None:
99
+ dev_UID = hash(dev_name)
100
+ if dt == 'CAM':
101
+ streams = ffmpeg.probe(p)['streams']
102
+ audio_streams = [
103
+ stream
104
+ for stream
105
+ in streams
106
+ if stream['codec_type']=='audio'
107
+ ]
108
+ if len(audio_streams) > 1:
109
+ raise Exception('ffprobe gave multiple audio streams?')
110
+ audio_str = audio_streams[0]
111
+ n = audio_str['channels']
112
+ # pprint(ffmpeg.probe(p))
113
+ else:
114
+ n = sox.file_info.channels(_pathname(p)) # eg 2
115
+ logger.debug('for file %s dev_UID established %s'%(p.name, dev_UID))
116
+ return Media(p,
117
+ Device(UID=dev_UID, folder=p.parent, name=dev_name, dev_type=dt,
118
+ n_chan=n, tracks=None))
116
119
 
117
120
  def get_device_ffprobe_UID(file):
118
121
  """
@@ -250,7 +253,7 @@ class Scanner:
250
253
  and 'SyncedMedia' not in p.parts
251
254
  ]
252
255
  for p in paths:
253
- new_media = media_at_path(p) # dev UID set here
256
+ new_media = media_at_path(self.input_structure, p) # dev UID set here
254
257
  self.found_media_files.append(new_media)
255
258
  # files from devices without UID or name
256
259
  def _same(l):
@@ -289,7 +292,7 @@ class Scanner:
289
292
  logger.debug('Scanner.found_media_files = %s'%self.found_media_files)
290
293
  if self.input_structure == 'folder_is_device':
291
294
  self._check_folders_have_same_device()
292
- self._use_folder_as_device_name()
295
+ # self._use_folder_as_device_name()
293
296
  devices = set([m.device for m in self.found_media_files])
294
297
  audio_devices = [d for d in devices if d.dev_type == 'REC']
295
298
  for recorder in audio_devices:
@@ -353,16 +356,16 @@ class Scanner:
353
356
  logger.debug('no tracks.txt file found')
354
357
  return None
355
358
 
356
- def _use_folder_as_device_name(self):
357
- """
358
- For each media in self.found_media_files replace existing Device.name by
359
- folder name.
359
+ # def _use_folder_as_device_name(self):
360
+ # """
361
+ # For each media in self.found_media_files replace existing Device.name by
362
+ # folder name.
360
363
 
361
- Returns nothing
362
- """
363
- for m in self.found_media_files:
364
- m.device.name = m.path.parent.name
365
- logger.debug(self.found_media_files)
364
+ # Returns nothing
365
+ # """
366
+ # for m in self.found_media_files:
367
+ # m.device.name = m.path.parent.name
368
+ # logger.debug(self.found_media_files)
366
369
 
367
370
  def _check_folders_have_same_device(self):
368
371
  """
@@ -71,7 +71,7 @@ def process_files(medias):
71
71
 
72
72
  def process_single(file, args):
73
73
  # argument is a single file
74
- m = device_scanner.media_at_path(Path(file))
74
+ m = device_scanner.media_at_path(None, Path(file))
75
75
  a_rec = yaltc.Recording(m)
76
76
  time = a_rec.get_start_time(plots=args.plotting)
77
77
  if time != None:
@@ -126,9 +126,10 @@ def main():
126
126
  # logger.add(sys.stdout, filter="__main__")
127
127
  # logger.add(sys.stdout, filter="device_scanner")
128
128
  # logger.add(sys.stdout, filter="yaltc") _extract_sound_to_merge
129
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "media_at_path")
130
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "scan_media_and_build_devices_UID")
131
129
  # logger.add(sys.stdout, filter=lambda r: r["function"] == "build_audio_and_write_video")
130
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "_get_audio_devices")
131
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "_get_mix")
132
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "_sox_mix")
132
133
  top_dir = args.directory[0]
133
134
  if os.path.isfile(top_dir):
134
135
  file = top_dir
@@ -7,7 +7,7 @@ import shutil
7
7
 
8
8
 
9
9
  logger.remove()
10
- OUT_DIR = 'SyncedMedia'
10
+ # OUT_DIR = 'SyncedMedia'
11
11
 
12
12
  def _pathname(tempfile_or_path) -> str:
13
13
  if isinstance(tempfile_or_path, str):
@@ -129,7 +129,7 @@ def main():
129
129
  "directory",
130
130
  type=str,
131
131
  nargs='+',
132
- help="path of media directory containing SyncedMedia/",
132
+ help="path of media directory containing Synced videos and their ISOs",
133
133
  default='.'
134
134
  )
135
135
  args = parser.parse_args()
@@ -10,6 +10,7 @@ from itertools import groupby
10
10
  # import opentimelineio as otio
11
11
  from datetime import timedelta
12
12
  import pprint, shutil, os
13
+ from subprocess import Popen, PIPE
13
14
 
14
15
  from inspect import currentframe, getframeinfo
15
16
  try:
@@ -19,6 +20,7 @@ except:
19
20
 
20
21
  CLUSTER_GAP = 0.5 # secs between multicam clusters
21
22
  DEL_TEMP = False
23
+ DB_OSX_NORM = -6 #dB
22
24
  OUT_DIR_DEFAULT = 'SyncedMedia'
23
25
 
24
26
  # utility for accessing pathnames
@@ -60,6 +62,51 @@ def _extr_channel(source, dest, channel):
60
62
  status = sox_transform.build(str(source), str(dest))
61
63
  logger.debug('sox status %s'%status)
62
64
 
65
+ def _sox_keep(audio_file, kept_channels) -> tempfile.NamedTemporaryFile:
66
+ """
67
+ Returns a NamedTemporaryFile containing the selected kept_channels
68
+
69
+ Building dict according to pysox.remix format.
70
+ https://pysox.readthedocs.io/en/latest/api.html#sox.transform.Transformer.remix
71
+ eg: 4 channels with TicTacCode_channel at #2
72
+ returns {1: [1], 2: [3], 3: [4]}
73
+ ie the number of channels drops by one and chan 2 is missing
74
+ excluded_channels is a list of Zero Based indexing chan numbers
75
+
76
+ """
77
+ audio_file = _pathname(audio_file)
78
+ nchan = sox.file_info.channels(audio_file)
79
+ logger.debug('in file of %i chan, have to keep %s'%
80
+ (nchan, kept_channels))
81
+ all_channels = range(1, nchan + 1) # from 1 to nchan included
82
+ # list of list for pysox API
83
+ # eg [[1], [3], [4]]
84
+ kept_channels = [[n] for n in kept_channels]
85
+ sox_remix_dict = dict(zip(all_channels, kept_channels))
86
+ # {1: [1], 2: [3], 3: [4]} -> from 4 to 3 chan and chan 2 is dropped
87
+ output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
88
+ out_file = _pathname(output_fh)
89
+ logger.debug('sox in and out files: %s %s'%(audio_file, out_file))
90
+ # sox_transform.set_output_format(channels=1)
91
+ sox_transform = sox.Transformer()
92
+ sox_transform.remix(sox_remix_dict)
93
+ logger.debug('sox remix transform: %s'%sox_transform)
94
+ logger.debug('sox remix dict: %s'%sox_remix_dict)
95
+ status = sox_transform.build(audio_file, out_file, return_output=True )
96
+ logger.debug('sox.build exit code %s'%str(status))
97
+ p = Popen('ffprobe %s -hide_banner'%audio_file,
98
+ shell=True, stdout=PIPE, stderr=PIPE)
99
+ stdout, stderr = p.communicate()
100
+ logger.debug('remixed input_file ffprobe:\n%s'%(stdout +
101
+ stderr).decode('utf-8'))
102
+ p = Popen('ffprobe %s -hide_banner'%out_file,
103
+ shell=True, stdout=PIPE, stderr=PIPE)
104
+ stdout, stderr = p.communicate()
105
+ logger.debug('remixed out_file ffprobe:\n%s'%(stdout +
106
+ stderr).decode('utf-8'))
107
+ return output_fh
108
+
109
+
63
110
  def _split_channels(multi_chan_audio:Path) -> list:
64
111
  nchan = sox.file_info.channels(_pathname(multi_chan_audio))
65
112
  source = _pathname(multi_chan_audio)
@@ -78,11 +125,10 @@ def _split_channels(multi_chan_audio:Path) -> list:
78
125
  logger.debug('paths %s'%paths)
79
126
  return paths
80
127
 
81
-
82
128
  def _sox_combine(paths) -> Path:
83
129
  """
84
- Combines files referred by the list of Path into a new temporary files
85
- passed on return each files are stacked in a different channel, so
130
+ Combines (stacks) files referred by the list of Path into a new temporary
131
+ files passed on return each files are stacked in a different channel, so
86
132
  len(paths) == n_channels
87
133
  """
88
134
  if len(paths) == 1: # one device only, nothing to stack
@@ -117,13 +163,26 @@ def _sox_mix(paths:list) -> tempfile.NamedTemporaryFile:
117
163
  """
118
164
  mix files referred by the list of Path into a new temporary files passed on return
119
165
  """
166
+ def _sox_norm(tempf):
167
+ normed_tempfile = tempfile.NamedTemporaryFile(suffix='.wav',
168
+ delete=DEL_TEMP)
169
+ tfm = sox.Transformer()
170
+ tfm.norm(DB_OSX_NORM)
171
+ status = tfm.build(_pathname(tempf),_pathname(normed_tempfile))
172
+ logger.debug('sox.build status for norm(): %s'%status)
173
+ if status != True:
174
+ print('Error, sox did not normalize file in _sox_mix()')
175
+ sys.exit(1)
176
+ return normed_tempfile
177
+ paths = [_sox_norm(p) for p in paths]
120
178
  cbn = sox.Combiner()
121
179
  N = len(paths)
122
180
  if N == 1: # nothing to mix
181
+ logger.debug('one file: nothing to mix')
123
182
  return paths[0]
124
183
  cbn.set_input_format(file_type=['wav']*N)
125
184
  filenames = [_pathname(p) for p in paths]
126
- logger.debug('files to mix %s'%filenames)
185
+ logger.debug('%i files to mix %s'%(N, filenames))
127
186
  mixed_tempf = tempfile.NamedTemporaryFile(suffix='.wav',delete=DEL_TEMP)
128
187
  status = cbn.build(filenames,
129
188
  _pathname(mixed_tempf),
@@ -135,7 +194,7 @@ def _sox_mix(paths:list) -> tempfile.NamedTemporaryFile:
135
194
  sys.exit(1)
136
195
  normed_tempfile = tempfile.NamedTemporaryFile(suffix='.wav',delete=DEL_TEMP)
137
196
  tfm = sox.Transformer()
138
- tfm.norm(-6)
197
+ tfm.norm(DB_OSX_NORM)
139
198
  status = tfm.build(_pathname(mixed_tempf),_pathname(normed_tempfile))
140
199
  logger.debug('sox.build status for norm(): %s'%status)
141
200
  if status != True:
@@ -216,8 +275,11 @@ class AudioStitcherVideoMerger:
216
275
  return list(self.edited_audio.keys())
217
276
 
218
277
  def _get_audio_devices(self):
219
- # ex {'RCR_A', 'RCR_B', 'RCR_C'} if multisound
220
- return set([r.device for r in self.get_matched_audio_recs()])
278
+ devices = set([r.device for r in self.get_matched_audio_recs()])
279
+ logger.debug('get_matched_audio_recs: %s'%
280
+ pprint.pformat(self.get_matched_audio_recs()))
281
+ logger.debug('devices %s'%devices)
282
+ return devices
221
283
 
222
284
  def _get_all_recordings_for(self, device):
223
285
  # return recordings for a particular device, sorted by time
@@ -433,20 +495,14 @@ class AudioStitcherVideoMerger:
433
495
  # audio_rec.edited_version = output_fh
434
496
  self.edited_audio[audio_rec] = output_fh
435
497
 
436
-
437
498
  def _write_ISOs(self, edited_audio_all_devices):
438
499
  """
439
500
  Writes isolated audio files that were synced to synced_clip_file,
440
501
  each track will have its dedicated monofile, named sequentially or with
441
502
  the name find in TRACKSFN if any, see Scanner._get_tracks_from_file()
442
503
 
443
- synced_clip_file:
444
- video to which audio has been merged
445
- device:
446
- instance of device_scanner.Device
447
- dev_joined_audio:
448
- NamedTemporaryFile for the joined synced (multi channel) audio for
449
- a specific device
504
+ edited_audio_all_devices:
505
+ a list of (name, mono_tempfile)
450
506
 
451
507
  Returns nothing, output is written to filesystem as below.
452
508
  ISOs subfolders structure when user invokes the --isos flag:
@@ -508,16 +564,52 @@ class AudioStitcherVideoMerger:
508
564
  # ISO_multi_chan = ISOdir / 'ISO_multi_chan.wav'
509
565
  # logger.debug('temp file: %s'%(ISO_multi_chan))
510
566
  # logger.debug('will split audio to %s'%(ISOdir))
511
- for name, audio_tempfile in edited_audio_all_devices:
567
+ for name, mono_tmpfl in edited_audio_all_devices:
512
568
  # pad(start_duration: float = 0.0, end_duration: float = 0.0)[source]
513
569
  destination = ISOdir/('%s.wav'%name)
514
- audio_tempfile_trimpad = _fit_length(audio_tempfile)
515
- shutil.copy(_pathname(audio_tempfile_trimpad), destination)
570
+ mono_tmpfl_trimpad = _fit_length(mono_tmpfl)
571
+ shutil.copy(_pathname(mono_tmpfl_trimpad), destination)
516
572
  logger.debug('destination:%s'%destination)
517
573
  # # mixNnormed = _sox_mix(tempfiles)
518
574
  # # print('516', _pathname(mixNnormed))
519
575
  # os.remove(ISO_multi_chan)
520
576
 
577
+ def _get_mix(self, device, multichan_tmpfl) -> tempfile.NamedTemporaryFile:
578
+ """
579
+ If device has an associated Tracks description that declares a (mono or
580
+ stereo) mix track, returns a tmpfl containing the corresponding
581
+ tracks. If not, mix all the tracks with sox.
582
+
583
+ """
584
+ if device.tracks != None:
585
+ mix_tracks = device.tracks.mix
586
+ if len(mix_tracks) > 0:
587
+ logger.debug('%s has mix %s'%(device.name, mix_tracks))
588
+ logger.debug('device %s'%device)
589
+ if 'ttc' in device.tracks.rawtrx:
590
+ sox_TTC_chan = device.tracks.rawtrx.index('ttc')
591
+ elif 'tc' in device.tracks.rawtrx:
592
+ sox_TTC_chan = device.tracks.rawtrx.index('tc')
593
+ else:
594
+ print('Error: no tc or ttc tag in track.txt')
595
+ sys.exit(1)
596
+ sox_TTC_chan += 1 # sox NZBIDX
597
+ logger.debug('TTC chan %i'%sox_TTC_chan)
598
+ # redo indexing since tracks.txt numbers refere to complete
599
+ # files and here audio file had TTC and muted channels
600
+ # removed.
601
+ shift = 0
602
+ if mix_tracks[0] > sox_TTC_chan:
603
+ shift += 1
604
+ for unused_tr in device.tracks.unused:
605
+ if mix_tracks[0] > unused_tr:
606
+ shift += 1
607
+ mix_tracks = [t-shift for t in mix_tracks]
608
+ logger.debug('new mix_tracks: %s'%mix_tracks)
609
+ return _sox_keep(multichan_tmpfl, mix_tracks)
610
+ else: # no tracks declaration, mix programmatically
611
+ return _sox_mix(_split_channels(multichan_tmpfl))
612
+
521
613
  def build_audio_and_write_video(self, top_dir, output_dir,
522
614
  write_multicam_structure,
523
615
  asked_ISOs):
@@ -532,7 +624,7 @@ class AudioStitcherVideoMerger:
532
624
  asked_ISOs: bool flag specified as CLI argument
533
625
 
534
626
  For each audio devices found overlapping self.ref_recording: pad, trim
535
- or stretch audio files calling _get_concatenated_audiofile_for(), and
627
+ or stretch audio files by calling _get_concatenated_audiofile_for(), and
536
628
  put them in merged_audio_files_by_device. More than one audio recorder
537
629
  can be used for a shot: that's why merged_audio_files_by_device is a
538
630
  list
@@ -560,6 +652,7 @@ class AudioStitcherVideoMerger:
560
652
  synced_clip_file = synced_clip_dir/\
561
653
  Path(self.ref_recording.new_rec_name).name
562
654
  logger.debug('editing files for %s'%synced_clip_file)
655
+ self.ref_recording.final_synced_file = synced_clip_file # relative
563
656
  # collecting edited audio by device, in (Device, tempfile) pairs:
564
657
  merged_audio_files_by_device = [
565
658
  (d, self._get_concatenated_audiofile_for(d))
@@ -567,45 +660,77 @@ class AudioStitcherVideoMerger:
567
660
  if len(merged_audio_files_by_device) == 0:
568
661
  # no audio file overlaps for this clip
569
662
  return
570
- # split audio channels in mono wav tempfiles:
571
- edited_audio_all_devices = []
572
- for device, multi_chan_audio in merged_audio_files_by_device:
573
- mono_files = _split_channels(multi_chan_audio)
574
- logger.debug('%i mono files for %s'%(len(mono_files), device.name))
575
- dev_audio_tuples = [(device, idx, m) for idx, m
576
- in enumerate(mono_files)]
577
- edited_audio_all_devices += dev_audio_tuples
663
+ if len(merged_audio_files_by_device) == 1:
664
+ # only one audio recorder was used, pick singleton in list
665
+ dev, concatenate_audio_file = merged_audio_files_by_device[0]
666
+ logger.debug('one audio device only: %s'%dev)
667
+ # check if this sole recorder is stereo
668
+ if dev.n_chan == 2:
669
+ # stereo minus TTC chan = mono, check consistency:
670
+ nchan_sox = sox.file_info.channels(
671
+ _pathname(concatenate_audio_file))
672
+ logger.debug('nchan_sox: %i mono?'%nchan_sox)
673
+ if not nchan_sox == 1:
674
+ raise Exception('Error in channel processing')
675
+ # all OK, merge and return
676
+ logger.debug('simply mono to merge')
677
+ self.ref_recording.synced_audio = concatenate_audio_file
678
+ self._merge_audio_and_video()
679
+ return
680
+ # if still here, either multitracks and/or multi recorders so check if a
681
+ # mix has been done on location and identified as is in atracks.txt
682
+ # file. Split audio channels in mono wav tempfiles at the same time
683
+ #
578
684
  multiple_recorders = len(merged_audio_files_by_device) > 1
579
685
  logger.debug('multiple_recorder: %s'%multiple_recorders)
580
- # replace device, idx pair with track name (+ device name if many)
581
- def _trnm(dev, idx): # used in the list comprehension just below
582
- if dev.tracks == None:
583
- tag = 'chan%s'%str(idx+1).zfill(2)
584
- else:
585
- audio_tags = [tag for tag in dev.tracks.rawtrx
586
- if tag not in ['ttc','0','tc']]
587
- tag = audio_tags[idx]
588
- if multiple_recorders:
589
- tag += '_' + dev.name
590
- return tag
591
- edited_audio_all_devices = [(_trnm(dev, idx), audio_tempfile)
592
- for dev, idx, audio_tempfile in edited_audio_all_devices]
593
- logger.debug('edited_audio_all_devices %s'%edited_audio_all_devices)
594
- video_path = self.ref_recording.AVpath
595
- # stacked audio contains all audio recorders if many
596
- mixed_audio = _sox_combine([audio for _, audio
597
- in merged_audio_files_by_device]) # all devices
598
- logger.debug('will merge with %s'%(_pathname(mixed_audio)))
599
- self.ref_recording.synced_audio = mixed_audio
600
- nchan = sox.file_info.channels(_pathname(mixed_audio))
601
- logger.debug('mixed_audio n chan: %i'%nchan)
602
- self.ref_recording.final_synced_file = synced_clip_file # relative
686
+ # dev_mixes_mix contains all audio recorders if many
687
+ mixes = [self._get_mix(device, multi_chan_audio)
688
+ for device, multi_chan_audio
689
+ in merged_audio_files_by_device]
690
+ logger.debug('thera are %i dev mixes'%len(mixes))
691
+ dev_mixes_mix = _sox_mix(mixes)
692
+ # dev_mixes_mix = _sox_combine([audio for _, audio
693
+ # in merged_audio_files_by_device]) # all devices
694
+ logger.debug('will merge with %s'%(_pathname(dev_mixes_mix)))
695
+ self.ref_recording.synced_audio = dev_mixes_mix
696
+ logger.debug('dev_mixes_mix n chan: %i'%
697
+ sox.file_info.channels(_pathname(dev_mixes_mix)))
603
698
  self._merge_audio_and_video()
699
+ # devices_and_monofiles is list of (device, [monofiles])
700
+ # [(dev1, multichan1), (dev2, multichan2)] in
701
+ # merged_audio_files_by_device ->
702
+ # [(dev1, [mono1_ch1, mono1_ch2]), (dev2, [mono2_ch1, mono2_ch2)]] in
703
+ # devices_and_monofiles:
604
704
  if asked_ISOs:
605
- self._write_ISOs(edited_audio_all_devices)
705
+ devices_and_monofiles = [(device, _split_channels(multi_chan_audio))
706
+ for device, multi_chan_audio
707
+ in merged_audio_files_by_device]
708
+ logger.debug('devices_and_monofiles: %s'%
709
+ pprint.pformat(devices_and_monofiles))
710
+ def _trnm(dev, idx): # used in the list comprehension just below
711
+ # generates track name for later if asked_ISOs
712
+ # idx is from 0 to nchan-1 for this device
713
+ if dev.tracks == None:
714
+ tag = 'chan%s'%str(idx+1).zfill(2)
715
+ else:
716
+ audio_tags = [tag for tag in dev.tracks.rawtrx
717
+ if tag not in ['ttc','0','tc']]
718
+ tag = audio_tags[idx]
719
+ if multiple_recorders:
720
+ tag += '_' + dev.name
721
+ return tag
722
+ # replace device, idx pair with track name (+ device name if many)
723
+ # loop over devices than loop over tracks
724
+ names_audio_tempfiles = []
725
+ for dev, mono_tmpfiles_list in devices_and_monofiles:
726
+ for idx, monotf in enumerate(mono_tmpfiles_list):
727
+ names_audio_tempfiles.append((_trnm(dev, idx), monotf))
728
+ logger.debug('names_audio_tempfiles %s'%names_audio_tempfiles)
729
+ self._write_ISOs(names_audio_tempfiles)
606
730
  logger.debug('merged_audio_files_by_device %s'%
607
731
  merged_audio_files_by_device)
608
- for idx, pair in enumerate(merged_audio_files_by_device): # This loop for logging purpose only.
732
+ # This loop below for logging purpose only:
733
+ for idx, pair in enumerate(merged_audio_files_by_device):
609
734
  # dev_joined_audio is mono, stereo or even polywav from multitrack
610
735
  # recorders. For one video there could be more than one dev_joined_audio
611
736
  # if multiple audio recorders where used during the take.
@@ -617,15 +742,6 @@ class AudioStitcherVideoMerger:
617
742
  (_pathname(dev_joined_audio), nchan))
618
743
  logger.debug('duration %f s'%
619
744
  sox.file_info.duration(_pathname(dev_joined_audio)))
620
- # generates ISOs too
621
- # if asked_ISOs:
622
- # print('Writing ISO files for:')
623
- # for idx, pair in enumerate(merged_audio_files_by_device):
624
- # device, dev_joined_audio = pair
625
- # logger.debug('device %i, dev_joined_audio %s %s'%
626
- # (idx, device, _pathname(dev_joined_audio)))
627
- # self._write_ISOs_for(synced_clip_file,
628
- # device, dev_joined_audio)
629
745
 
630
746
  def _keep_VIDEO_only(self, video_path):
631
747
  # return file handle to a temp video file formed from the video_path
@@ -642,12 +758,15 @@ class AudioStitcherVideoMerger:
642
758
 
643
759
  def _merge_audio_and_video(self):
644
760
  """
645
- Calls ffmpeg to join audio and video.
761
+ Calls ffmpeg to join video in self.ref_recording.AVpath to
762
+ audio in self.ref_recording.synced_audio
646
763
 
647
764
  On entry, ref_recording.final_synced_file is a Path to an non existing
648
765
  file (contrarily to ref_recording.synced_audio).
649
- On exit, ref_recording.final_synced_file points to the final synced
650
- video file. Returns nothing.
766
+ On exit, self.ref_recording.final_synced_file points to the final synced
767
+ video file.
768
+
769
+ Returns nothing.
651
770
  """
652
771
  synced_clip_file = self.ref_recording.final_synced_file
653
772
  video_path = self.ref_recording.AVpath
@@ -699,7 +818,6 @@ class AudioStitcherVideoMerger:
699
818
  print(e.stderr.decode('UTF-8'))
700
819
  sys.exit(1)
701
820
 
702
-
703
821
  class Matcher:
704
822
  """
705
823
  Matcher looks for any video in self.recordings and for each one finds out
@@ -791,11 +791,12 @@ class Decoder:
791
791
  # LSB is leftmost in TicTacCode
792
792
 
793
793
  def _demod_values_are_OK(self, values_dict):
794
+ # TODO: use _get_timedate_from_dict rather (catching any ValueError)
794
795
  ranges = {
795
796
  'seconds': range(60),
796
797
  'minutes': range(60),
797
798
  'hours': range(24),
798
- 'day': range(1,32),
799
+ 'day': range(1,32), # 32 ?
799
800
  'month': range(1,13),
800
801
  }
801
802
  for key in ranges:
@@ -1253,14 +1254,18 @@ class Recording:
1253
1254
  return self.decoder.get_time_in_sound_extract(plots)
1254
1255
 
1255
1256
  def _get_timedate_from_dict(self, time_dict):
1256
- python_datetime = datetime(
1257
- time_dict['year offset'] + YEAR_ZERO,
1258
- time_dict['month'],
1259
- time_dict['day'],
1260
- time_dict['hours'],
1261
- time_dict['minutes'],
1262
- time_dict['seconds'],
1263
- tzinfo=timezone.utc)
1257
+ try:
1258
+ python_datetime = datetime(
1259
+ time_dict['year offset'] + YEAR_ZERO,
1260
+ time_dict['month'],
1261
+ time_dict['day'],
1262
+ time_dict['hours'],
1263
+ time_dict['minutes'],
1264
+ time_dict['seconds'],
1265
+ tzinfo=timezone.utc)
1266
+ except ValueError as e:
1267
+ print('Error converting date in _get_timedate_from_dict',e)
1268
+ sys.exit(1)
1264
1269
  python_datetime += timedelta(seconds=1) # PPS precedes NMEA sequ
1265
1270
  return python_datetime
1266
1271
 
@@ -1288,6 +1293,7 @@ class Recording:
1288
1293
  """
1289
1294
  if t1 == None or t2 == None:
1290
1295
  return False
1296
+ logger.debug('t1 : %s t2: %s'%(t1, t2))
1291
1297
  datetime_1 = self._get_timedate_from_dict(t1)
1292
1298
  datetime_2 = self._get_timedate_from_dict(t2)
1293
1299
  # if datetime_2 < datetime_1:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tictacsync
3
- Version: 0.2a8
3
+ Version: 0.3a1
4
4
  Summary: command for syncing audio video recordings
5
5
  Home-page: https://tictacsync.org/
6
6
  Author: Raymond Lutz
@@ -83,7 +83,7 @@ To also produce _synced_ ISO audio files, specify `--isos` . A directory named `
83
83
 
84
84
  When called with the `-p` flag, zoomable plots will be produced for diagnostic purpose (close the plotting window for the 2nd one) and the decoded starting time will be output to stdin:
85
85
 
86
- > tictacsync -p tictacsync/tests/dailies/loose/MVI_0024.MP4
86
+ > tictacsync -p dailies/loose/MVI_0024.MP4
87
87
 
88
88
  Typical first plot produced :
89
89
 
File without changes
File without changes