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.
- tictacsync/device_scanner.py +2 -2
- tictacsync/entry.py +1 -24
- tictacsync/mamdav.py +11 -2
- tictacsync/mamreap.py +16 -13
- tictacsync/mamsync.py +1 -19
- tictacsync/multi2polywav.py +3 -1
- {tictacsync-1.4.0b0.dist-info → tictacsync-1.4.6b0.dist-info}/METADATA +2 -1
- tictacsync-1.4.6b0.dist-info/RECORD +16 -0
- {tictacsync-1.4.0b0.dist-info → tictacsync-1.4.6b0.dist-info}/entry_points.txt +2 -2
- tictacsync/LTCcheck.py +0 -394
- tictacsync/load_fieldr_reaper.py +0 -352
- tictacsync/new-sound-resolve.py +0 -469
- tictacsync/newmix.py +0 -483
- tictacsync/remergemix.py +0 -259
- tictacsync/remrgmx.py +0 -116
- tictacsync/splitmix.py +0 -87
- tictacsync/synciso.py +0 -143
- tictacsync-1.4.0b0.dist-info/RECORD +0 -24
- {tictacsync-1.4.0b0.dist-info → tictacsync-1.4.6b0.dist-info}/LICENSE +0 -0
- {tictacsync-1.4.0b0.dist-info → tictacsync-1.4.6b0.dist-info}/WHEEL +0 -0
- {tictacsync-1.4.0b0.dist-info → tictacsync-1.4.6b0.dist-info}/top_level.txt +0 -0
tictacsync/device_scanner.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
tictacsync/multi2polywav.py
CHANGED
|
@@ -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.
|
|
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:
|
|
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()
|