tictacsync 0.1a14__py3-none-any.whl → 1.4.4b0__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.

@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.1
2
+ Name: tictacsync
3
+ Version: 1.4.4b0
4
+ Summary: commands for syncing audio video recordings
5
+ Home-page: https://tictacsync.org/
6
+ Author: Raymond Lutz
7
+ Author-email: lutzrayblog@mac.com
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Operating System :: POSIX
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Multimedia :: Sound/Audio
17
+ Classifier: Topic :: Utilities
18
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Capture/Recording
19
+ Classifier: Topic :: Multimedia :: Video :: Non-Linear Editor
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: sox >=1.4.1
24
+ Requires-Dist: ffmpeg-python >=0.2.0
25
+ Requires-Dist: loguru >=0.6.0
26
+ Requires-Dist: matplotlib >=3.7.1
27
+ Requires-Dist: numpy >=1.24.3
28
+ Requires-Dist: rich >=10.12.0
29
+ Requires-Dist: lmfit
30
+ Requires-Dist: scikit-image
31
+ Requires-Dist: scipy >=1.10.1
32
+ Requires-Dist: platformdirs
33
+
34
+ # tictacsync
35
+
36
+ ## Warning: this is at beta stage
37
+
38
+ Unfinished sloppy code ahead, but should run without errors. Some functionalities are still missing. Don't run the code without parental supervision. Suggestions and enquiries are welcome via the [lists hosted on sourcehut](https://sr.ht/~proflutz/TicTacSync/lists).
39
+
40
+ ## Description
41
+
42
+ `tictacsync` is a python script to sync, cut and join audio files against camera files shot using a specific hardware timecode generator
43
+ called [Tic Tac Sync](https://tictacsync.org). The timecode is named TicTacCode and should be recorded on a scratch
44
+ track on each device for `tictacsync` to work.
45
+ ## Status
46
+
47
+ Feature complete! `tictacsync` scans for audio video files and then merges overlapping audio and video recordings, It
48
+
49
+ * Decodes the TicTacCode audio track alongside your audio tracks
50
+ * Establishes UTC start time (and end time) within 100 μs!
51
+ * Syncs, cuts and joins any concurrent audio to camera files (using `FFmpeg`)
52
+ * Processes _multiple_ audio recorders
53
+ * Corrects device clock drift so _both_ ends coincide (thanks to `sox`)
54
+ * Sets video metadata TC of multicam files for NLE timeline alignement
55
+ * Writes _synced_ ISO files with dedicated file names declared in `tracks.txt`
56
+ * Produces nice plots.
57
+
58
+
59
+ ## Installation
60
+
61
+ This uses the [python interpreter](https://www.python.org/downloads/) and multiple packages (so you need python 3 + pip). Also, you need to install two non-python command line executables: [ffmpeg](https://windowsloop.com/install-ffmpeg-windows-10/) and [sox](https://sourceforge.net/projects/sox/files/). Make sure those are _accessible through your `PATH` system environment variable_.
62
+ Then pip install the syncing program:
63
+
64
+
65
+ > pip install tictacsync
66
+
67
+
68
+ This should install python dependencies _and_ the `tictacsync` command.
69
+ ## Usage
70
+
71
+ Download multiple sample files [here](https://nuage.lutz.quebec/s/4jw4xgqysLPS8EQ/download/dailies1_3.zip) (700+ MB, sorry) unzip and run:
72
+
73
+ > tictacsync dailies/loose
74
+ The program `tictacsync` will recursively scan the directory given as argument, find all audio that coincide with any video and merge them into a subfolder named `SyncedMedia`. When the argument is an unique media file (not a directory), no syncing will occur but the decoded starting time will be printed to stdout:
75
+
76
+ > tictacsync dailies/loose/MVI_0024.MP4
77
+
78
+ Recording started at 2024-03-12 23:07:01.4281 UTC
79
+ true sample rate: 48000.736 Hz
80
+ first sync at 27450 samples in channel 0
81
+ N.B.: all results are precise to the displayed digits!
82
+
83
+ If shooting multicam, put clips in their respective directories (using the camera name as folder name) _and_ the audio under their own directory. `tictacsync` will detect that structured input and will generate multicam folders ready to be imported into your NLE (for now only DaVinci Resolve has been validated).
84
+
85
+ ## Options
86
+ #### `-v`
87
+
88
+ For a very verbose output add the `-v` flag:
89
+
90
+ > tictacsync -v dailies/loose/MVI_0024.MP4
91
+ #### `--terse`
92
+ For a one line output (or to suppress the progress bars) use the `--terse` flag:
93
+
94
+ > tictacsync --terse dailies/loose/MVI_0024.MP4
95
+ dailies/loose/MVI_0024.MP4 UTC:2024-03-12 23:07:01.4281 pulse: 27450 in chan 0
96
+ #### `--isos`
97
+
98
+ Specifying `--isos` produces _synced_ ISO audio files: for each synced \<video-clip\> a directory named `<video-clip>_ISO` will contain a set of ISO audio files each of exact same length, padded or trimmed to coincide with the video start. After re-editing and re-mixing in your DAW of choice a `remergemix` command will resync the new audio with the video and _the new sound track will be updated on your NLE timeline_, _automagically_ on some NLEs or on command for [Davinci Resolve](https://www.niwa.nu/dr-scripts/).
99
+
100
+ > tictacsync --isos dailies/structured
101
+ #### `-p`
102
+
103
+ 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:
104
+
105
+ > tictacsync -p dailies/loose/MVI_0024.MP4
106
+
107
+ Typical first plot produced :
108
+
109
+ ![word](https://mamot.fr/system/media_attachments/files/110/279/794/002/305/269/original/0198908c6eb5c592.png)
110
+
111
+ Typical second plot produced (note the 34 [FSK](https://en.wikipedia.org/wiki/Frequency-shift_keying) encoded bits `0010111101001111100110000110010000`):
112
+ ![slicing](https://mamot.fr/system/media_attachments/files/110/279/794/021/372/766/original/6ec62bb417115f52.png)
113
+
114
+
115
+ <!-- To run some tests, from top level `git cloned` dir:
116
+
117
+ cd tictacsync ; python -m pytest
118
+ Yes, the coverage is low. -->
@@ -0,0 +1,16 @@
1
+ tictacsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ tictacsync/device_scanner.py,sha256=YxA3_0O1ZPE1ZuD-OgD-dWhtTWE-LamDhqXXyLo3IMw,26132
3
+ tictacsync/entry.py,sha256=pcGuS4_o0o5dREpcccx1_X3w14PeHdQi5z1Ikzmhpwk,16198
4
+ tictacsync/mamconf.py,sha256=nfXTwabx-tJmBcpnDR4CRkFe9W4fudzfnbq_nHUg0qE,6424
5
+ tictacsync/mamdav.py,sha256=2we8tfIbJBtDMQdpZZVlCQ9hCQRMbKmV2aU3dDEUf2k,27457
6
+ tictacsync/mamreap.py,sha256=ej7Ap8nbVBCkfah2j5hrE7QBWuqL6Zm-OEsQpNK8mYg,21085
7
+ tictacsync/mamsync.py,sha256=mpoHUAuJWiZ1JfVCECiiSLH_HNdXNV1Z_VlUlJBlPcM,14565
8
+ tictacsync/multi2polywav.py,sha256=qJJhjwIgP1BCTpi2e0wfR95XlgZ2-EIqmefVh-jUBPc,7438
9
+ tictacsync/timeline.py,sha256=ykmB8EfnprQZoEHXRYzriASNWZ7bHfkmQ2-TR6gxZ6Y,75985
10
+ tictacsync/yaltc.py,sha256=xrgL7qokP1A7B_VF4W_BZcC7q9APSmYpmtWH8_t3VWc,68003
11
+ tictacsync-1.4.4b0.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
12
+ tictacsync-1.4.4b0.dist-info/METADATA,sha256=R8XV5GMFARw0IBnpbaZ19AAAejTr2RYbcw73zpyj9LM,5689
13
+ tictacsync-1.4.4b0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
14
+ tictacsync-1.4.4b0.dist-info/entry_points.txt,sha256=0R8K6T0iUJGj87LDZ0vNO8pToshbkxrXZqTRgcjBlMk,244
15
+ tictacsync-1.4.4b0.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
16
+ tictacsync-1.4.4b0.dist-info/RECORD,,
@@ -0,0 +1,7 @@
1
+ [console_scripts]
2
+ mamconf = tictacsync.mamconf:main
3
+ mamdav = tictacsync.mamdav:called_from_cli
4
+ mamreap = tictacsync.mamreap:main
5
+ mamsync = tictacsync.mamsync:main
6
+ multi2polywav = tictacsync.multi2polywav:main
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_dict_from_path(Path(file_argument))
273
- logger.debug('media_dict_from_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.YaLTC_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.YaLTC_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()
@@ -1,96 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: tictacsync
3
- Version: 0.1a14
4
- Summary: command for syncing audio video recordings
5
- Home-page: https://sr.ht/~proflutz/TicTacSync/
6
- Author: Raymond Lutz
7
- Author-email: lutzrayblog@mac.com
8
- Classifier: Development Status :: 2 - Pre-Alpha
9
- Classifier: Environment :: Console
10
- Classifier: Intended Audience :: End Users/Desktop
11
- Classifier: License :: OSI Approved :: MIT License
12
- Classifier: Operating System :: MacOS
13
- Classifier: Operating System :: Microsoft :: Windows
14
- Classifier: Operating System :: POSIX
15
- Classifier: Programming Language :: Python :: 3
16
- Classifier: Topic :: Multimedia :: Sound/Audio
17
- Classifier: Topic :: Utilities
18
- Classifier: Topic :: Multimedia :: Sound/Audio :: Capture/Recording
19
- Classifier: Topic :: Multimedia :: Video :: Non-Linear Editor
20
- Requires-Python: >=3.10
21
- Description-Content-Type: text/markdown
22
- License-File: LICENSE
23
- Requires-Dist: sox >=1.4.1
24
- Requires-Dist: ffmpeg-python >=0.2.0
25
- Requires-Dist: loguru >=0.6.0
26
- Requires-Dist: matplotlib >=3.7.1
27
- Requires-Dist: numpy >=1.24.3
28
- Requires-Dist: rich >=10.12.0
29
- Requires-Dist: lmfit
30
- Requires-Dist: scipy >=1.10.1
31
- Requires-Dist: scikit-learn ==1.2.2
32
-
33
- # tictacsync
34
-
35
- ## Warning: this is at pre-alpha stage
36
-
37
- Unfinished sloppy code ahead, but should run without errors. Some functionalities are still missing. Don't run the code without parental supervision. Suggestions and enquiries are welcome via the [lists hosted on sourcehut](https://sr.ht/~proflutz/TicTacSync/lists).
38
-
39
- ## Description
40
-
41
- `tictacsync` is a python script to sync audio and video files shot
42
- with [dual system sound](https://www.learnlightandsound.com/blog/2017/2/23/how-to-record-sound-for-video-dual-systemsync-sound) using a specific hardware timecode generator
43
- called [Tic Tac Sync](https://tictacsync.org). The timecode is named YaLTC for *yet
44
- another longitudinal time code* and should be recorded on a scratch
45
- track on each device for the syncing to be performed, later in _postprod_ before editing.
46
-
47
- ## Status
48
-
49
- `tictacsync` scans for audio video files and displays their starting time and then merges overlapping audio and video recordings. Multicam syncing with one stereo audio recorder has been tested (spring 2023, [see demo](https://youtu.be/pklTSTi7cqs)). Multi audio recorders coming soon...
50
-
51
-
52
- ## Installation
53
-
54
- This uses the [python interpreter](https://www.python.org/downloads/) and multiple packages (so you need python 3 + pip). Also, you need to install two non-python command line executables: [ffmpeg](https://windowsloop.com/install-ffmpeg-windows-10/) and [sox](https://sourceforge.net/projects/sox/files/). Make sure those are _accessible through your `PATH` system environment variable_.
55
- Then pip install the syncing program:
56
-
57
-
58
- pip install tictacsync
59
-
60
-
61
- This should install python dependencies _and_ the `tictacsync` command.
62
- ## Usage
63
-
64
- Download some sample files [here](https://tictacsync.org/sampleFiles.zip), unzip and run
65
-
66
- tictacsync sampleFiles
67
- The resulting synced videos will be in a subfolder named `tictacsynced`. For a very verbose output add the `-v` flag:
68
-
69
- tictacsync -v sampleFiles
70
-
71
- When the argument is an unique media file (not a directory), no syncing will occur but the decoded starting time will be printed to stdout:
72
-
73
- tictacsync sampleFiles/canon24fps01.MOV
74
-
75
- Recording started at 2023-04-23 01:09:08.1605 UTC
76
- true sample rate: 48000.545 Hz
77
- first sync at 37414 samples
78
- N.B.: all results are precise to the displayed digits!
79
-
80
-
81
- 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:
82
-
83
- tictacsync -p sampleFiles/canon24fps01.MOV
84
-
85
- Typical first plot produced :
86
-
87
- ![word](https://mamot.fr/system/media_attachments/files/110/279/794/002/305/269/original/0198908c6eb5c592.png)
88
-
89
- Typical second plot produced (note the 34 [FSK](https://en.wikipedia.org/wiki/Frequency-shift_keying) encoded bits `0010111101001111100110000110010000`):
90
- ![slicing](https://mamot.fr/system/media_attachments/files/110/279/794/021/372/766/original/6ec62bb417115f52.png)
91
-
92
-
93
- <!-- To run some tests, from top level `git cloned` dir:
94
-
95
- cd tictacsync ; python -m pytest
96
- Yes, the coverage is low. -->
@@ -1,13 +0,0 @@
1
- tictacsync/LTCcheck.py,sha256=SUq93zfky5njzvnsnIj-5nC01QYf3hVBegJftPOFeJQ,15708
2
- tictacsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- tictacsync/device_scanner.py,sha256=wDkhE_H33Zh5RIjASS33HOmX5-qI-dJ3h9yyvaTrAI0,15177
4
- tictacsync/entry.py,sha256=ICTDI6ZPaPsDnkZX-c7dahr6qpG09x618j9nPh3x2jM,10279
5
- tictacsync/multi2polywav.py,sha256=7Qnb986D3HJIAkNJ3IyvPWxIdqqh3KIfwnwFoUBw0pU,7013
6
- tictacsync/timeline.py,sha256=1NpfbvBv24oDtet8SafPcpvk5ppK8qTdEybs8qz8gkc,39258
7
- tictacsync/yaltc.py,sha256=Hu827V61yp1VcjZHJ9HMlCL_EEcxlhi_4hesOQy66bc,71391
8
- tictacsync-0.1a14.dist-info/LICENSE,sha256=ZAOPXLh1zlQAnhHUd7oLslKM01YZ5UiAu3STYjwIxck,1068
9
- tictacsync-0.1a14.dist-info/METADATA,sha256=K96y62aTWDK0EmlHyFR0ISO75ZrLf9OoKETTtoM0x8I,4218
10
- tictacsync-0.1a14.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
11
- tictacsync-0.1a14.dist-info/entry_points.txt,sha256=bre_DmdWFgHljcC0cyqh_dC9siGANO04MQ_wYsO2sxY,135
12
- tictacsync-0.1a14.dist-info/top_level.txt,sha256=eaCWG-BsYTRR-gLTJbK4RfcaXajr0gjQ6wG97MkGRrg,11
13
- tictacsync-0.1a14.dist-info/RECORD,,
@@ -1,4 +0,0 @@
1
- [console_scripts]
2
- LTCcheck = tictacsync.LTCcheck:main
3
- multi2polywav = tictacsync.multi2polywav:main
4
- tictacsync = tictacsync.entry:main