tictacsync 1.4.0b0__py3-none-any.whl → 1.4.6b0__py3-none-any.whl

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.

@@ -159,7 +159,7 @@ def get_device_ffprobe_UID(file):
159
159
  except ffmpeg.Error as e:
160
160
  print('ffmpeg.probe error')
161
161
  print(e.stderr, file)
162
- return None, None #-----------------------------------------------------
162
+ return None, None, None #-----------------------------------------------------
163
163
  # fall back to folder name
164
164
  logger.debug('ffprobe %s'%probe)
165
165
  streams = probe['streams']
@@ -271,7 +271,7 @@ class Scanner:
271
271
  with open(p, 'r') as fh:
272
272
  done = set(fh.read().split()) # sets of strings of abs path
273
273
  logger.debug(f'done clips: {pformat(done)}')
274
- files = Path(self.top_directory).rglob('*')
274
+ files = [p for p in Path(self.top_directory).rglob('*') if not p.name[0] == '.']
275
275
  clip_paths = []
276
276
  some_done = False
277
277
  for raw_path in files:
tictacsync/entry.py CHANGED
@@ -117,21 +117,10 @@ def main():
117
117
  nargs=1,
118
118
  help="directory_name or media_file"
119
119
  )
120
- # parser.add_argument("directory", nargs="?", help="path of media directory")
121
- # parser.add_argument('-v', action='store_true')
122
120
  parser.add_argument('-v',
123
121
  action='store_true', #ie default False
124
122
  dest='verbose_output',
125
123
  help='Set verbose ouput')
126
- # parser.add_argument('--stop_mirroring',
127
- # action='store_true', #ie default False
128
- # dest='stop_mirroring',
129
- # help='Stop mirroring mode, will write synced files alongside originals.')
130
- # parser.add_argument('--start-project', '-s' ,
131
- # nargs=2,
132
- # dest='proj_folders',
133
- # default = [],
134
- # help='start mirrored tree output mode and specifies 2 folders: source (RAW) and destination (synced).')
135
124
  parser.add_argument('-t','--timelineoffset',
136
125
  nargs=1,
137
126
  default=['00:00:00:00'],
@@ -145,10 +134,6 @@ def main():
145
134
  action='store_true',
146
135
  dest='dont_write_cam_folder',
147
136
  help="Even if originals are inside CAM folders, don't put synced clips inside a CAM identified folder")
148
- # parser.add_argument('-m',
149
- # action='store_true',
150
- # dest='multicam',
151
- # help='Outputs multicam structure for NLE program (based on Davinci Resolve input processing)')
152
137
  parser.add_argument('--isos',
153
138
  action='store_true',
154
139
  dest='write_ISOs',
@@ -308,15 +293,7 @@ def main():
308
293
  if asked_ISOs and scanner.input_structure != 'ordered':
309
294
  print('Warning, can\'t write ISOs without structured folders: [gold1]--isos[/gold1] option ignored.\n')
310
295
  asked_ISOs = False
311
- # output_dir = args.o
312
- # if args.verbose_output or args.terse: # verbose, so no progress bars
313
- print('Merging...')
314
- # for merger in matcher.mergers:
315
- # merger.build_audio_and_write_merged_media(top_dir,
316
- # args.dont_write_cam_folder,
317
- # asked_ISOs,
318
- # audio_REC_only)
319
- for merger in matcher.mergers:
296
+ for merger in track(matcher.mergers, description='Merging audio with video...'):
320
297
  if audio_REC_only:
321
298
  # rare
322
299
  merger._build_and_write_audio(top_dir)
tictacsync/mamdav.py CHANGED
@@ -41,7 +41,14 @@ wmv yuv rm rmvb viv asf amv mp4 m4p m4v mpg mp2 mpeg mpe mpv mpg mpeg m2v
41
41
  m4v svi 3gp 3g2 mxf roq nsv flv f4v f4p f4a f4b 3gp""".split()
42
42
 
43
43
 
44
- DAVINCI_RESOLVE_SCRIPT_LOCATION = '/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Utility/'
44
+ MAC = '/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Utility/'
45
+
46
+ WIN = pathlib.Path('C:/ProgramData/Blackmagic Design/DaVinci Resolve/Fusion/Scripts')
47
+
48
+ is_windows = hasattr(sys, 'getwindowsversion')
49
+ DAVINCI_RESOLVE_SCRIPT_LOCATION = WIN if is_windows else MAC
50
+
51
+
45
52
 
46
53
  DAVINCI_RESOLVE_SCRIPT_TEMPLATE_LUA = """local function ClipWithPartialPath(partial_path)
47
54
  local media_pool = app:GetResolve():GetProjectManager():GetCurrentProject():GetMediaPool()
@@ -624,7 +631,9 @@ def go(mode, otio_path, movie_path, wav_path):
624
631
  logger.debug(f'changes {pformat(changes)}')
625
632
  script = load_New_Sound_lua_script(changes)
626
633
  script_path = Path(DAVINCI_RESOLVE_SCRIPT_LOCATION)/'Load New Sound.lua'
627
- # script += f'os.remove("{script_path}")\n' # doesnt work
634
+ if is_windows:
635
+ escaped_script = script.replace("\\", "\\\\")
636
+ script = escaped_script
628
637
  with open(script_path, 'w') as fh:
629
638
  fh.write(script)
630
639
  print(f'Wrote new Lua script: run it in Resolve under Workspace/Scripts/{script_path.stem};')
tictacsync/mamreap.py CHANGED
@@ -7,16 +7,6 @@ from rich import print
7
7
  from enum import Enum
8
8
  from rich.progress import Progress
9
9
 
10
- # send-to-sound DSC085 -> one clip only, find the ISOs, load the clip
11
- # send-to-sound cut27.otio -> whole project
12
- # send-to-sound cut27.otio cut27.mov
13
- # cut27.mov has TC + duration -> can find clips in otio...
14
- # place cut27.mov according to its TC
15
- # produce a cut27mix.wav saved in SNDROOT/postprod
16
- # three modes: one clip; some clips; all clips
17
-
18
-
19
-
20
10
  try:
21
11
  from . import mamconf
22
12
  from . import mamdav
@@ -26,7 +16,12 @@ except:
26
16
 
27
17
  dev = 'Cockos Incorporated'
28
18
  app ='REAPER'
29
- REAPER_SCRIPT_LOCATION = pathlib.Path(platformdirs.user_data_dir(app, dev)) / 'Scripts' / 'Atomic'
19
+ MAC = pathlib.Path(platformdirs.user_data_dir(app, dev)) / 'Scripts' / 'Atomic'
20
+ WIN = pathlib.Path(platformdirs.user_data_dir('Scripts', 'Reaper', roaming=True))
21
+
22
+ is_windows = hasattr(sys, 'getwindowsversion')
23
+ REAPER_SCRIPT_LOCATION = WIN if is_windows else MAC
24
+
30
25
  REAPER_LUA_CODE = """reaper.Main_OnCommand(40577, 0) -- lock left/right move
31
26
  reaper.Main_OnCommand(40569, 0) -- lock enabled
32
27
  local function placeWavsBeginingAtTrack(clip, start_idx)
@@ -298,12 +293,13 @@ def find_and_set_ISO_dir(clip, SND_dirs):
298
293
  logger.debug(f'found {complete_path}')
299
294
  clip.ISOdir = str(complete_path)
300
295
 
296
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "gen_lua_table")
301
297
  def gen_lua_table(clips):
302
298
  # returns a string defining a lua nested table
303
299
  # top level: a sequence of clips
304
300
  # a clip has keys: name, start_time, in_time, cut_duration, timeline_pos, files
305
301
  # clip.files is a sequence of ISO wav files
306
- def _list_ISO(dir):
302
+ def _list_ISO(dir):
307
303
  iso_dir = pathlib.Path(dir)/'ISOfiles'
308
304
  ISOs = [f for f in iso_dir.iterdir() if f.suffix.lower() == '.wav']
309
305
  # ISOs = [f for f in ISOs if f.name[:2] != 'tc'] # no timecode
@@ -436,6 +432,10 @@ def main():
436
432
  script_path = pathlib.Path(REAPER_SCRIPT_LOCATION)/f'{title}.lua'
437
433
  Lua_script_pre, _ , Lua_script_post = REAPER_LUA_CODE.split('--cut here--')
438
434
  script = Lua_script_pre + 'clips=' + lua_clips + Lua_script_post
435
+ # is_windows = hasattr(sys, 'getwindowsversion')
436
+ if is_windows:
437
+ escaped_script = script.replace("\\", "\\\\")
438
+ script = escaped_script
439
439
  with open(script_path, 'w') as fh:
440
440
  fh.write(script)
441
441
  print(f'Wrote ReaScripts "{script_path.stem}"', end=' ')
@@ -452,9 +452,12 @@ def main():
452
452
  render_destination.unlink()
453
453
  logger.debug(f'clip\n{lua_code}')
454
454
  script_path = pathlib.Path(REAPER_SCRIPT_LOCATION)/f'Render Movie Audio.lua'
455
+ if is_windows:
456
+ escaped_script = lua_code.replace("\\", "\\\\")
457
+ lua_code = escaped_script
455
458
  with open(script_path, 'w') as fh:
456
459
  fh.write(lua_code)
457
- print(f'and "{script_path.stem}"')
460
+ print(f'and "{script_path.stem}" in directory \n"{REAPER_SCRIPT_LOCATION}"')
458
461
  print(f'Reaper will render audio to "{render_destination.absolute()}"')
459
462
  if mode in [mamdav.Modes.INTERCLIP_ALL, mamdav.Modes.INTERCLIP_SOME]:
460
463
  print(f'Warning: once saved, "{render_destination.name}" wont be of any use if not paired with "{op.name}", so keep them in the same directory.')
tictacsync/mamsync.py CHANGED
@@ -45,7 +45,7 @@ amr ape au awb dss dvf flac gsm iklax ivs m4a m4b m4p mmf mp3 mpc msv nmf
45
45
  ogg oga mogg opus ra rm raw rf64 sln tta voc vox wav wma wv webm 8svx cda""".split()
46
46
 
47
47
  logger.remove()
48
- logger.level("DEBUG", color="<yellow>")
48
+ # logger.level("DEBUG", color="<yellow>")
49
49
  # logger.add(sys.stdout, level="DEBUG")
50
50
  # logger.add(sys.stdout, filter=lambda r: r["function"] == "__init__" and r["module"] == "yaltc")
51
51
  # logger.add(sys.stdout, filter=lambda r: r["function"] == "_fit_length")
@@ -85,24 +85,6 @@ def copy_to_syncedroot(raw_root, synced_root):
85
85
  logger.debug('same content, next')
86
86
  continue # next raw_path in loop
87
87
 
88
- # logger.add(sys.stdout, filter=lambda r: r["function"] == "clean_synced")
89
- # def clean_synced(raw_root, synced_root):
90
- # # removes <>v<capitalLetter>.mov if any
91
- # # returns a list of deleted pathlib.Path
92
- # project = Path(raw_root).name
93
- # logger.debug(f'project {project}')
94
- # synced_proj = Path(synced_root)/project
95
- # if not synced_proj.exists():
96
- # synced_proj.mkdir()
97
- # deleted = []
98
- # for raw_path in Path(synced_proj).rglob('*'):
99
- # m = re.match(r'.*(v[A-Z])\..+', raw_path.name)
100
- # if m != None:
101
- # logger.debug(f'cleaning {raw_path}')
102
- # raw_path.unlink()
103
- # deleted.append(raw_path)
104
- # return deleted
105
-
106
88
  def copy_raw_root_tree_to_sndroot(raw_root, snd_root):
107
89
  # args are str
108
90
  # copy only tree structure, no files
@@ -29,8 +29,10 @@ def print_grby(grby):
29
29
  print(' ', e)
30
30
 
31
31
  def wav_recursive_scan(top_directory):
32
- files_lower_case = Path(top_directory).rglob('*.wav')
32
+ files_lower_case = Path(top_directory).rglob('*.wav')
33
+ files_lower_case = [p for p in files_lower_case if p.name[0] != '.']
33
34
  files_upper_case = Path(top_directory).rglob('*.WAV')
35
+ files_upper_case = [p for p in files_upper_case if p.name[0] != '.']
34
36
  files = set(list(files_lower_case) + list(files_upper_case))
35
37
  paths = [
36
38
  p
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tictacsync
3
- Version: 1.4.0b0
3
+ Version: 1.4.6b0
4
4
  Summary: commands for syncing audio video recordings
5
5
  Home-page: https://tictacsync.org/
6
6
  Author: Raymond Lutz
@@ -26,6 +26,7 @@ Requires-Dist: loguru >=0.6.0
26
26
  Requires-Dist: matplotlib >=3.7.1
27
27
  Requires-Dist: numpy >=1.24.3
28
28
  Requires-Dist: rich >=10.12.0
29
+ Requires-Dist: lmfit
29
30
  Requires-Dist: scikit-image
30
31
  Requires-Dist: scipy >=1.10.1
31
32
  Requires-Dist: platformdirs
@@ -0,0 +1,16 @@
1
+ tictacsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ tictacsync/device_scanner.py,sha256=lYCMWSCeI0KNQclrRMOzeIERh2ME2gvBxV-q9Y2uou0,26175
3
+ tictacsync/entry.py,sha256=JmOx7B6d4LnGLWGuuMFpKV9GxkWhmAG4fdKayI7qzUA,15024
4
+ tictacsync/mamconf.py,sha256=nfXTwabx-tJmBcpnDR4CRkFe9W4fudzfnbq_nHUg0qE,6424
5
+ tictacsync/mamdav.py,sha256=x0xbIoxNIEvjcZEozegwA9PORNCIW5c6qrjJKAG3gMs,27670
6
+ tictacsync/mamreap.py,sha256=ed3wcu_5Xy7oq-POmQAlOJ0QmWP18Y67FfFkTzz1smg,21278
7
+ tictacsync/mamsync.py,sha256=orwP-TzKdRTiTCoiM7BsQgVK1KtAIs3SpKe9K8ZWM_Q,13872
8
+ tictacsync/multi2polywav.py,sha256=OX72eDtanaax-lGc6JJXwOz9MaveNcYlgBfBijzR8oA,7583
9
+ tictacsync/timeline.py,sha256=ykmB8EfnprQZoEHXRYzriASNWZ7bHfkmQ2-TR6gxZ6Y,75985
10
+ tictacsync/yaltc.py,sha256=xrgL7qokP1A7B_VF4W_BZcC7q9APSmYpmtWH8_t3VWc,68003
11
+ tictacsync-1.4.6b0.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
12
+ tictacsync-1.4.6b0.dist-info/METADATA,sha256=LAeuk9MefhDk-MZDoNYsA-1hGKr69oWvFZ7nuVj0FPQ,5689
13
+ tictacsync-1.4.6b0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
14
+ tictacsync-1.4.6b0.dist-info/entry_points.txt,sha256=0R8K6T0iUJGj87LDZ0vNO8pToshbkxrXZqTRgcjBlMk,244
15
+ tictacsync-1.4.6b0.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
16
+ tictacsync-1.4.6b0.dist-info/RECORD,,
@@ -1,7 +1,7 @@
1
1
  [console_scripts]
2
2
  mamconf = tictacsync.mamconf:main
3
- mamdav = mamdav:main
4
- mamreap = mamreap:main
3
+ mamdav = tictacsync.mamdav:called_from_cli
4
+ mamreap = tictacsync.mamreap:main
5
5
  mamsync = tictacsync.mamsync:main
6
6
  multi2polywav = tictacsync.multi2polywav:main
7
7
  tictacsync = tictacsync.entry:main
tictacsync/LTCcheck.py DELETED
@@ -1,394 +0,0 @@
1
- print('Loading modules')
2
- import subprocess, io
3
- import argparse, os, sys, ffmpeg
4
- from loguru import logger
5
- from pathlib import Path
6
- from scipy.io import wavfile
7
- import numpy as np
8
- import matplotlib.pyplot as plt
9
- from rich.progress import track, Progress
10
- from pprint import pprint
11
- from collections import deque
12
- import wave
13
- try:
14
- from . import yaltc
15
- except:
16
- import yaltc
17
- try:
18
- from . import device_scanner
19
- except:
20
- import device_scanner
21
-
22
- # LEVELMODE = 'over_noise_silence'
23
- LEVELMODE = 'mean_silence_AFSK'
24
-
25
- OFFSET_NXT_PULSE = 50 # samples
26
- LENGTH_EXTRACT = int(14e-3 * 96000) # samples max freq
27
-
28
- logger.level("DEBUG", color="<yellow>")
29
- logger.remove()
30
- # logger.add(sys.stdout, filter="tictacsync.LTCcheck")
31
- # logger.add(sys.stdout, filter="tictacsync.yaltc")
32
-
33
- def ppm(a,b):
34
- return 1e6*(max(a,b)/min(a,b)-1)
35
-
36
- class TCframe:
37
- def __init__(self, string, max_FF):
38
- # string is 'HH:MM:SS:FF' or ;|,|.
39
- # max_FF is int for max frame number (hence fps-1)
40
- string = string.replace('.',':').replace(';',':').replace(',',':')
41
- ints = [int(e) for e in string.split(':')]
42
- self.HH = ints[0]
43
- self.MM = ints[1]
44
- self.SS = ints[2]
45
- self.FF = ints[3]
46
- self.MAXFF = max_FF
47
-
48
- def __repr__(self):
49
- # return '%s-%s-%s-%s/%i'%(*self.ints(), self.MAXFF)
50
- return '%02i-%02i-%02i-%02i'%self.ints()
51
-
52
- def ints(self):
53
- return (self.HH,self.MM,self.SS,self.FF)
54
-
55
- def __eq__(self, other):
56
- a,b,c,d = self.ints()
57
- h,m,s,f = other.ints()
58
- return a==h and b==m and c==s and d==f
59
-
60
- def __sub__(self, tcf2):
61
- # H1, M1, S1, F1 = self.ints()
62
- # H2, M2, S2, F2 = tcf2.ints()
63
- f1 = np.array(self.ints())
64
- f2 = np.array(tcf2.ints())
65
- HR, MR, SR, FR = f1 - f2
66
- if FR < 0:
67
- FR += self.MAXFF + 1
68
- SR -= 1 # borrow
69
- if SR < 0:
70
- SR += 60
71
- MR -= 1 # borrow
72
- if MR < 0:
73
- MR += 60
74
- HR -= 1 # borrow
75
- if HR < 0:
76
- HR += 24 # underflow?
77
- # logger.debug('%s %s'%(self.ints(), tcf2.ints()))
78
- return TCframe('%02i:%02i:%02i:%02i'%(HR,MR,SR,FR), self.MAXFF)
79
-
80
- def read_whole_audio_data(path):
81
- dryrun = (ffmpeg
82
- .input(str(path))
83
- .output('pipe:', format='s16le', acodec='pcm_s16le')
84
- .get_args())
85
- dryrun = ' '.join(dryrun)
86
- logger.debug('using ffmpeg-python built args to pipe wav file into numpy array:\nffmpeg %s'%dryrun)
87
- try:
88
- out, _ = (ffmpeg
89
- .input(str(path))
90
- .output('pipe:', format='s16le', acodec='pcm_s16le')
91
- .global_args("-loglevel", "quiet")
92
- .global_args("-nostats")
93
- .global_args("-hide_banner")
94
- .run(capture_stdout=True))
95
- data = np.frombuffer(out, np.int16)
96
- except ffmpeg.Error as e:
97
- print('error',e.stderr)
98
- with wave.open(path, 'rb') as f:
99
- samplerate = f.getframerate()
100
- n_chan = f.getnchannels()
101
- all_channels_data = data.reshape(int(len(data)/n_chan),n_chan).T
102
- return all_channels_data
103
-
104
- def find_nearest_fps(value):
105
- array = np.asarray([24, 25, 30])
106
- idx = (np.abs(array - value)).argmin()
107
- return array[idx]
108
-
109
- def fps_rel_to_audio(frame_pos, samplerate):
110
- _, first_frame_pos = frame_pos[0]
111
- _, scnd_last_frame_pos = frame_pos[-2]
112
- frame_duration = (scnd_last_frame_pos - first_frame_pos)/len(frame_pos[:-2]) # in audio samples
113
- fps = float(samplerate) / frame_duration
114
- return fps
115
-
116
- # def HHMMSSFF_from_line(line):
117
- # line = line.replace('.',':')
118
- # line = line.replace(';',':')
119
- # ll = line.split()[1].split(':')
120
- # return [int(e) for e in ll]
121
-
122
- def check_continuity_and_DF(LTC_frames_and_pos):
123
- errors = []
124
- DF_flag = False
125
- oneframe = TCframe('00:00:00:01',None)
126
- threeframes = TCframe('00:00:00:03',None)
127
- last_two_TC = deque([], maxlen=2)
128
- last_two_TC.append(LTC_frames_and_pos[0][0])
129
- last_two_TC.append(LTC_frames_and_pos[1][0])
130
- for frame, pos in track(LTC_frames_and_pos[2:],
131
- description="Checking each frame increment"):
132
- last_two_TC.append(frame)
133
- past, now = last_two_TC
134
- diff = now - past
135
- if diff not in [oneframe, threeframes]:
136
- errors.append((frame, pos))
137
- continue
138
- if diff == oneframe:
139
- continue
140
- if diff == threeframes:
141
- # DF? check if it is 59:xx and minutes are not mult. of tens
142
- if past.SS != 59 or now.MM%10 == 0:
143
- errors.append((frame, pos))
144
- DF_flag = True
145
- return errors, DF_flag
146
-
147
- def ltcdump_and_check(file, channel):
148
- # returns list of anormal frames, a bool if TC is DF, fps and
149
- # a list of tuples (frame => str, sample position in file => int) as
150
- # determined by external util ltcdump https://github.com/x42/ltc-tools
151
- process_list = ["ltcdump","-c %i"%channel, file]
152
- logger.debug('process %s'%process_list)
153
- proc = subprocess.Popen(process_list, stdout=subprocess.PIPE)
154
- LTC_frames_and_pos = []
155
- iter_io = io.TextIOWrapper(proc.stdout, encoding="utf-8")
156
- next(iter_io) # ltcdump 1st line: User bits Timecode | Pos. (samples)
157
- print()
158
- try:
159
- next(iter_io) # ltcdump 2nd line: #DISCONTINUITY
160
- except StopIteration:
161
- print('ltcdump has no output, is channel #%i really LTC?'%channel)
162
- quit()
163
- old = 0
164
- for line in track(iter_io,
165
- description=' Parsing ltcdump output'): # next ones
166
- # print(line)
167
- if line == '#DISCONTINUITY\n':
168
- # print('#DISCONTINUITY!')
169
- continue
170
- user_bits, HHMMSSFF_str, _, start_sample, end_sample =\
171
- line.split()
172
- audio_position = int(end_sample)
173
- # print(audio_position - old, end=' ')
174
- # old = audio_position
175
- # audio_position = int(start_sample)
176
- tc = HHMMSSFF_str
177
- LTC_frames_and_pos.append((tc, audio_position))
178
- with wave.open(file, 'rb') as f:
179
- samplerate = f.getframerate()
180
- fps = fps_rel_to_audio(LTC_frames_and_pos, samplerate)
181
- rounded_fps = round(fps)
182
- LTC_frames_and_pos = [(TCframe(tc, rounded_fps-1), pos) for tc, pos in LTC_frames_and_pos]
183
- errors, DF_flag = check_continuity_and_DF(LTC_frames_and_pos)
184
- return errors, DF_flag, fps, LTC_frames_and_pos
185
-
186
- def find_pulses(TTC_data, recording):
187
- samplerate = recording.true_samplerate
188
- i_samplerate = round(samplerate)
189
- pulse_position = recording.sync_position
190
- logger.debug('first detected pulse %i'%pulse_position)
191
- # first_pulse_nbr_of_seconds = int(pulse_position/samplerate)
192
- # if first_pulse_nbr_of_seconds > 1:
193
- # pulse_position = pulse_position%i_samplerate # very first pulse in file
194
- # print('0 %i'%pulse_position)
195
- pulse_position = pulse_position%i_samplerate
196
- logger.debug('starting at %i'%pulse_position)
197
- second = 0
198
- duration = int(recording.get_duration())
199
- decoder = recording.decoder
200
- pulse_detection_level = decoder._get_pulse_detection_level()
201
- logger.debug(' detection level %f'%pulse_detection_level)
202
- pulses = []
203
- approx_next_pulse = pulse_position
204
- skipped_printed = False
205
- while second < duration - 1:
206
- second += 1
207
- approx_next_pulse -= OFFSET_NXT_PULSE
208
- start_of_extract = approx_next_pulse
209
- sound_extract = TTC_data[start_of_extract:start_of_extract + LENGTH_EXTRACT]
210
- abs_signal = abs(sound_extract)
211
- detected_point = \
212
- np.argmax(abs_signal > pulse_detection_level)
213
- old_pulse_position = pulse_position
214
- pulse_position = detected_point + start_of_extract
215
- diff = pulse_position - old_pulse_position
216
- logger.debug('pulse_position %f old_pulse_position %f diff %f'%(pulse_position,
217
- old_pulse_position, diff))
218
- if not np.isclose(diff, samplerate, rtol=1e-4):
219
- if not skipped_printed:
220
- print('\nSkipped: ', end='')
221
- skipped_printed = True
222
- print('%i, '%(pulse_position), end='')
223
- # if diff < samplerate:
224
- # else:
225
- # print('skipped: samples %i and %i are too far'%(pulse_position, old_pulse_position))
226
- else:
227
- pulses.append((second, pulse_position))
228
- approx_next_pulse = pulse_position + i_samplerate
229
- if skipped_printed:
230
- print('\n')
231
- return pulses
232
-
233
- def main():
234
- print('in main()')
235
- parser = argparse.ArgumentParser()
236
- parser.add_argument(
237
- "LTC_chan",
238
- type=int,
239
- # nargs=2,
240
- help="LTC channel number"
241
- )
242
- parser.add_argument(
243
- "file_argument",
244
- type=str,
245
- nargs=1,
246
- help="media file"
247
- )
248
- args = parser.parse_args()
249
- # print(args.channels)
250
- LTC_chan = args.LTC_chan
251
- file_argument = args.file_argument[0]
252
- logger.info('args.file_argument: %s'%file_argument)
253
- if os.path.isdir(file_argument):
254
- print('argument shoud be a media file, not a directory. Bye...')
255
- quit()
256
- # print(file_argument)
257
- if not os.path.exists(file_argument):
258
- print('%s does not exist, bye'%file_argument)
259
- quit()
260
- errors, DF_flag, fps_rel_to_audio, LTC_frames_and_pos = ltcdump_and_check(file_argument, LTC_chan)
261
- if errors:
262
- print('errors! %s'%errors)
263
- print('Some errors in those %i but detected FPS rel to audio is %0.3f%s'%(len(LTC_frames_and_pos),
264
- fps_rel_to_audio, 'DF' if DF_flag else 'NDF'))
265
- else:
266
- print('\nAll %i frames are sequential and detected FPS rel to audio is %0.3f%s\n'%(len(LTC_frames_and_pos),
267
- fps_rel_to_audio, 'DF' if DF_flag else 'NDF'))
268
- # print('trying to decode TTC...')
269
- with Progress(transient=True) as progress:
270
- task = progress.add_task("trying to decode TTC...")
271
- progress.start()
272
- m = device_scanner.media_at_path(Path(file_argument))
273
- logger.debug('media_at_path %s'%m)
274
- recording = yaltc.Recording(m, )
275
- logger.debug('Rec %s'%recording)
276
- time = recording.get_start_time(progress=progress, task=task)
277
- if time == None:
278
- print('Start time couldnt be determined')
279
- else:
280
- audio_samplerate_gps_corrected = recording.true_samplerate
281
- audio_error = audio_samplerate_gps_corrected/recording.get_samplerate()
282
- gps_corrected_framerate = fps_rel_to_audio*audio_error
283
- print('gps_corrected_framerate',gps_corrected_framerate,audio_error)
284
- frac_time = int(time.microsecond / 1e2)
285
- d = '%s.%s'%(time.strftime("%Y-%m-%d %H:%M:%S"),frac_time)
286
- base = os.path.basename(file_argument)
287
- print('%s UTC:%s pulse: %i on chan %i'%(base, d,
288
- recording.sync_position,
289
- recording.TicTacCode_channel))
290
- print('audio samplerate (gps)', audio_samplerate_gps_corrected)
291
- all_channels_data = read_whole_audio_data(file_argument)
292
- TTC_data = all_channels_data[recording.TicTacCode_channel]
293
- sec_and_pulses = find_pulses(TTC_data, recording)
294
- secs, pulses = list(zip(*sec_and_pulses))
295
- pulses = list(pulses)
296
- logger.debug('pulses %s'%pulses)
297
- samples_between_UTC_pulses = []
298
- for n1, n2 in zip(pulses[1:], pulses):
299
- delta = n1 - n2
300
- if np.isclose(delta, audio_samplerate_gps_corrected, rtol=1e-3):
301
- samples_between_UTC_pulses.append(delta - audio_samplerate_gps_corrected)
302
- samples_between_UTC_pulses = np.array(samples_between_UTC_pulses)
303
- pulse_length_std = samples_between_UTC_pulses.std()
304
- max_min_over_2 = abs(samples_between_UTC_pulses.max() - samples_between_UTC_pulses.min())/2
305
- # print(samples_between_UTC_pulses)
306
- # print('time is measured with a precision of %f audio samples'%(pulse_length_std))
307
- precision = 1e6*max_min_over_2/audio_samplerate_gps_corrected
308
- print('Time is measured with a precision of %0.1f audio samples (%0.1f μs)'%(max_min_over_2, precision))
309
- frame_duration = 1/fps_rel_to_audio
310
- rel_min_error = 100*1e-6*precision/frame_duration
311
- print('so LTC syncword jitter less than %0.1f %% wont be detected'%(rel_min_error))
312
- # fig, ax = plt.subplots()
313
- # n, bins, patches = ax.hist(samples_between_UTC_pulses)
314
- # plt.show()
315
- # x = range(len(pulses))
316
- a, b = np.polyfit(pulses, secs, 1)
317
- logger.debug('slope, b = %f %f'%(a,b))
318
- # sr_slope = 1/a
319
- # print(sr_slope/recording.true_samplerate)
320
- coherent_sr = np.isclose(a*audio_samplerate_gps_corrected, 1, rtol=1e-7)
321
- logger.debug('samplerates (slope VS rec) are close: %s ratio %f'%(coherent_sr,
322
- a*audio_samplerate_gps_corrected))
323
- if not coherent_sr:
324
- print('warning, wav samplerate are incoherent (Rec + Decode VS slope)')
325
- def make_sample2time(a, b):
326
- return lambda n : a*n + b
327
- sample2time = make_sample2time(a, b)
328
- logger.debug('sample2time fct: %s'%sample2time)
329
- LTC_samples = [N for _, N in LTC_frames_and_pos]
330
- LTC_times = [sample2time(N) for N in LTC_samples]
331
- slope_fps, _ = np.polyfit(LTC_times, range(len(LTC_times)), 1)
332
- print('slope_fps l329', ppm(slope_fps,24))
333
- print('diff slope, ppm',ppm(gps_corrected_framerate, slope_fps))
334
- LTC_frame_durations_samples = [a - b for a, b in zip(LTC_samples[1:], LTC_samples)]
335
- # print(LTC_frame_durations_samples)
336
- frame_duration = 1/fps_rel_to_audio
337
- errors_useconds = [1e6*(frame_duration -(a - b)) for a, b in zip(LTC_times[1:], LTC_times)]
338
- # print(errors_useconds)
339
- errors_useconds = np.array(errors_useconds)
340
- LTC_std = abs(errors_useconds).std()
341
- LTC_max_min = abs(errors_useconds.max() - errors_useconds.min())/2
342
- # print('Mean frame duration is %i audio samples'%)
343
- print('\nhere LTC frame duration varies by %f μs ('%LTC_max_min, end='')
344
- print('%0.3fFPS nominal frame duration is %0.0f μs)\n'%(fps_rel_to_audio, 1e6/fps_rel_to_audio))
345
- # print(errors_useconds[:200])
346
- # audio_sampling_period = 1/samplerate
347
- # print(audio_sampling_period)
348
- # errors_in_audiosamples = [int(e/audio_sampling_period) for e in errors_seconds]
349
- # print(delta_milliseconds)
350
- # plt.plot(LTC_times, marker='.', markersize='1',
351
- # linestyle='None', color='black')
352
- # plt.show()
353
- # print(LTC_times)
354
- # fig, ax = plt.subplots()
355
-
356
- # the histogram of the data
357
- # print(errors_in_audiosamples)
358
- fig, ax = plt.subplots()
359
- n, bins, patches = ax.hist(errors_useconds, bins=40)
360
- plt.show()
361
- quit()
362
-
363
- if __name__ == '__main__':
364
- main()
365
-
366
- # import matplotlib.pyplot as plt
367
- # import numpy as np
368
-
369
- # rng = np.random.default_rng(19680801)
370
-
371
- # # example data
372
- # mu = 106 # mean of distribution
373
- # sigma = 17 # standard deviation of distribution
374
- # x = rng.normal(loc=mu, scale=sigma, size=420)
375
-
376
- # num_bins = 42
377
-
378
- # fig, ax = plt.subplots()
379
-
380
- # # the histogram of the data
381
- # n, bins, patches = ax.hist(x, num_bins, density=True)
382
-
383
- # # add a 'best fit' line
384
- # y = ((1 / (np.sqrt(2 * np.pi) * sigma)) *
385
- # np.exp(-0.5 * (1 / sigma * (bins - mu))**2))
386
- # ax.plot(bins, y, '--')
387
- # ax.set_xlabel('Value')
388
- # ax.set_ylabel('Probability density')
389
- # ax.set_title('Histogram of normal distribution sample: '
390
- # fr'$\mu={mu:.0f}$, $\sigma={sigma:.0f}$')
391
-
392
- # # Tweak spacing to prevent clipping of ylabel
393
- # fig.tight_layout()
394
- # plt.show()