tictacsync 1.1.0a0__py3-none-any.whl → 1.3.0b0__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,469 @@
1
+ import os, itertools, argparse, ffmpeg, tempfile, platformdirs
2
+ from pathlib import Path
3
+ from loguru import logger
4
+ import shutil, sys, re, sox, configparser
5
+ from pprint import pformat
6
+ from rich import print
7
+
8
+ try:
9
+ from . import mamconf
10
+ except:
11
+ import mamconf
12
+
13
+
14
+ LUA = True
15
+ OUT_DIR_DEFAULT = 'SyncedMedia'
16
+ MCCDIR = 'SyncedMulticamClips'
17
+ SEC_DELAY_CHANGED_SND = 10 #sec, SND_DIR changed if diff time is bigger
18
+ DEL_TEMP = True
19
+ CONF_FILE = 'mamsync.cfg'
20
+
21
+ logger.level("DEBUG", color="<yellow>")
22
+ # logger.remove()
23
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "_names_match")
24
+
25
+
26
+ DAVINCI_RESOLVE_SCRIPT_LOCATION = '/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Utility/'
27
+
28
+ DAVINCI_RESOLVE_SCRIPT_TEMPLATE_LUA = """local function ClipWithPartialPath(partial_path)
29
+ local media_pool = app:GetResolve():GetProjectManager():GetCurrentProject():GetMediaPool()
30
+ local queue = {media_pool:GetRootFolder()}
31
+ while #queue > 0 do
32
+ local current = table.remove(queue, 1)
33
+ local subfolders = current:GetSubFolderList()
34
+ for _, folder in ipairs(subfolders) do
35
+ table.insert(queue, folder)
36
+ end
37
+ local got_it = {}
38
+ local clips = current:GetClipList()
39
+ for _, cl in ipairs(clips) do
40
+ if string.find(cl:GetClipProperty('File Path'), partial_path) then
41
+ table.insert(got_it, cl)
42
+ end
43
+ end
44
+ if #got_it > 0 then
45
+ return got_it[1]
46
+ end
47
+ end
48
+ end
49
+
50
+ local function findAndReplace(old_file, new_file)
51
+ local clip_with_old_file = ClipWithPartialPath(old_file)
52
+ if clip_with_old_file == nil then
53
+ print('did not find clip with path ' .. old_file)
54
+ else
55
+ clip_with_old_file:ReplaceClip(new_file)
56
+ local cfp = clip_with_old_file:GetClipProperty('File Path')
57
+ local cn = clip_with_old_file:GetClipProperty('Clip Name')
58
+ if cfp == new_file then
59
+ print('Loaded ' .. cn .. ' with a new sound track')
60
+ else
61
+ print('findAndReplace ' .. old_file .. ' -> ' .. new_file .. ' failed')
62
+ end
63
+ end
64
+ end
65
+
66
+ local changes = {
67
+ """
68
+
69
+ video_extensions = \
70
+ """webm mkv flv flv vob ogv ogg drc gif gifv mng avi mov
71
+ qt wmv yuv rm rmvb viv asf mp4 m4p m4v mpg mp2 mpeg mpe
72
+ mpv mpg mpeg m2v m4v svi 3gp 3g2 mxf roq nsv""".split() # from wikipedia
73
+
74
+ def _pathname(tempfile_or_path) -> str:
75
+ # utility for obtaining a str from different filesystem objects
76
+ if isinstance(tempfile_or_path, str):
77
+ return tempfile_or_path
78
+ if isinstance(tempfile_or_path, Path):
79
+ return str(tempfile_or_path)
80
+ if isinstance(tempfile_or_path, tempfile._TemporaryFileWrapper):
81
+ return tempfile_or_path.name
82
+ else:
83
+ raise Exception('%s should be Path or tempfile...'%tempfile_or_path)
84
+
85
+ def is_synced_video(f):
86
+ # True if name as video extension
87
+ # and is under SyncedMedia or SyncedMulticamClips folders
88
+ # f is a Path
89
+ ext = f.suffix[1:] # removing leading '.'
90
+ ok_ext = ext.lower() in video_extensions
91
+ f_parts = f.parts
92
+ ok_folders = OUT_DIR_DEFAULT in f_parts or MCCDIR in f_parts
93
+ # logger.debug('ok_ext: %s ok_folders: %s'%(ok_ext, ok_folders))
94
+ return ok_ext and ok_folders
95
+
96
+ def _names_match(vidname, SND_name):
97
+ # vidname is a str and has no extension
98
+ # vidname could have vA or vB suffix as in DSC_8064vB so matches DSC_8064
99
+ # names flips between A and B for Resolve to reload them
100
+ if vidname == SND_name: # no suffix presents
101
+ return True
102
+ # m = re.match(SND_name + r'v(\d+)', vidname)
103
+ m = re.match(SND_name + r'v*([AB]*)', vidname)
104
+ if m != None:
105
+ logger.debug('its a match and letter= %s'%m.groups()[0])
106
+ return m != None
107
+
108
+ def find_SND_vids_pairs_in_udir(top):
109
+ # look for matching video name and SND in unique directory
110
+ # in alongside mode (vs MAM mode)
111
+ # eg: IMG04.mp4 and IMG04_SND
112
+ # maybe IMG04v2.mp4 if audio changed before (than it will be IMG04v3.mp4)
113
+ # returns list of (SND, vid) matches
114
+ # recursively search from 'top' argument
115
+ vids = []
116
+ SNDs = []
117
+ for (root,dirs,files) in os.walk(top):
118
+ for d in dirs:
119
+ if d[-4:] == '_SND':
120
+ SNDs.append(Path(root)/d)
121
+ for f in files:
122
+ if is_synced_video(Path(root)/f): # add being in SyncedMedia or SyncedMulticamClips folder
123
+ vids.append(Path(root)/f)
124
+ logger.debug('vids %s SNDs %s'%(pformat(vids), pformat(SNDs)))
125
+ matches = []
126
+ for pair in list(itertools.product(SNDs, vids)):
127
+ # print(pair)
128
+ SND, vid = pair # Paths
129
+ vidname, ext = vid.name.split('.') # string
130
+ if _names_match(vidname, SND.name[:-4]):
131
+ logger.debug('SND %s matches video %s'%(
132
+ Path('').joinpath(*SND.parts[-2:]),
133
+ Path('').joinpath(*vid.parts[-3:])))
134
+ matches.append(pair) # list of Paths
135
+ logger.debug('matches: %s'%pformat(matches))
136
+ return matches
137
+
138
+ def find_SND_vids_pairs_in_dual_dir(synced_root, snd_root):
139
+ # look for matching video name and SND in unique directory
140
+ # in alongside mode (vs MAM mode)
141
+ # eg: IMG04.mov and directory IMG04_SND
142
+ # maybe IMG04vA.mov if audio changed before (than it will be IMG04vB.mov)
143
+ # returns list of (SND, vid) matches
144
+ # recursively search from 'top' argument
145
+ vids = []
146
+ SNDs = []
147
+ print(f'Will look for new mix in {snd_root} compared to vids in {synced_root}')
148
+ for (root,dirs,files) in os.walk(synced_root):
149
+ for f in files:
150
+ pf = Path(root)/f
151
+ if pf.suffix[1:].lower() in video_extensions:
152
+ if not pf.is_symlink():
153
+ vids.append(pf)
154
+ for (root,dirs,files) in os.walk(snd_root):
155
+ for d in dirs:
156
+ if d[-4:] == '_SND':
157
+ SNDs.append(Path(root)/d)
158
+ logger.debug('vids %s SNDs %s'%(pformat(vids), pformat(SNDs)))
159
+ matches = []
160
+ # def _names_match(vidname, SND_name):
161
+ # # vidname is a str and has no extension
162
+ # # vidname could have vNN suffix as in DSC_8064v31 so matches DSC_8064
163
+ # if vidname == SND_name: # no suffix presents
164
+ # return True
165
+ # m = re.match(SND_name + r'v(\d+)', vidname)
166
+ # if m != None:
167
+ # logger.debug('its a natch and N= %s'%m.groups()[0])
168
+ # return m != None
169
+ for pair in list(itertools.product(SNDs, vids)):
170
+ SND, vid = pair # Paths
171
+ vidname = vid.stem
172
+ if _names_match(vidname, SND.name[:-4]):
173
+ logger.debug('SND %s matches video %s'%(
174
+ Path('').joinpath(*SND.parts[-2:]),
175
+ Path('').joinpath(*vid.parts[-3:])))
176
+ matches.append(pair) # list of Paths
177
+ logger.debug('matches: %s'%pformat(matches))
178
+ return matches
179
+
180
+ def parse_and_check_arguments():
181
+ # parses directories from command arguments
182
+ # check for consistencies and warn user and exits,
183
+ # returns parser.parse_args()
184
+
185
+ descr = "Checks in SNDROOT (see mamconf --show) if any new mix is present; if so, build a Davinci Resolve script to reload corresponding videos."
186
+ parser = argparse.ArgumentParser(description=descr)
187
+ parser.add_argument('-u',
188
+ nargs=1,
189
+ dest='unique_directory',
190
+ help='Directory scanned for both audio and video, when tictacsync is used rather than mamsync.')
191
+ parser.add_argument('--dry',
192
+ action='store_true',
193
+ dest='scan_only',
194
+ help="Just display changed audio, don't merge")
195
+ args = parser.parse_args()
196
+ logger.debug('args %s'%args)
197
+ if args.scan_only:
198
+ print('Sorry, --dry option not implemented yet, bye.')
199
+ sys.exit(0)
200
+ return args
201
+
202
+ def get_recent_mix(SND_dir, vid):
203
+ # check if there are mixl, mixr or mix files in SND_dir
204
+ # and return the paths if they are more recent than vid.
205
+ # returns empty tuple otherwise
206
+ # arguments SND_dir, vid and returned values are of Path type
207
+ wav_files = list(SND_dir.iterdir())
208
+ logger.debug(f'wav_files {wav_files} in {SND_dir}')
209
+ def is_mix(p):
210
+ re_result = re.match(r'mix([lrLR])*', p.name)
211
+ logger.debug(f'for {p.name} re_result looking for mix {re_result}')
212
+ return re_result is not None
213
+ mix_files = [p for p in wav_files if is_mix(p)]
214
+ if len(mix_files) == 0:
215
+ return ()
216
+ # consistency check, should be 1 or 2 files
217
+ if not len(mix_files) in (1,2):
218
+ print(f'\nError: too many mix files in [bold]{SND_dir}[/bold], bye.')
219
+ sys.exit(0)
220
+ # one file? it must be mix.wav
221
+ if len(mix_files) == 1:
222
+ fn = mix_files[0].name
223
+ if fn.upper() != 'MIX.WAV':
224
+ print(f'\nError in [bold]{SND_dir}[/bold], the only file should be mix.wav, not [bold]{fn}[/bold][/bold]; bye.')
225
+ sys.exit(0)
226
+ # two files? verify they are mixL and mixR and mono each
227
+ if len(mix_files) == 2:
228
+ first3uppercase = [p.name[:4].upper() for p in mix_files]
229
+ first3uppercase.sort()
230
+ first3uppercase = ''.join(first3uppercase)
231
+ if first3uppercase != 'MIXLMIXR':
232
+ print(f'\nError: mix names mismatch in [bold]{SND_dir}[/bold];')
233
+ print(f'names are [bold]{[p.name for p in mix_files]}[/bold], check they are simply mixL.wav and mixR.wav; bye.')
234
+ sys.exit(0)
235
+ def _nch(p):
236
+ return sox.file_info.channels(str(p))
237
+ are_mono = [_nch(p) == 1 for p in mix_files]
238
+ logger.debug('are_mono: %s'%are_mono)
239
+ if not all(are_mono):
240
+ print(f'\nError in [bold]{SND_dir}[/bold], some files are not mono, bye.')
241
+ sys.exit(0)
242
+ logger.debug(f'mix_files: {mix_files}')
243
+ # check dates, if two files, take first
244
+ mix_modification_time = mix_files[0].stat().st_mtime
245
+ vid_mod_time = vid.stat().st_mtime
246
+ # difference of modification time in secs
247
+ mix_more_recent_by = mix_modification_time - vid_mod_time
248
+ logger.debug('mix_more_recent_by: %s'%mix_more_recent_by)
249
+ if mix_more_recent_by > SEC_DELAY_CHANGED_SND:
250
+ if len(mix_files) == 1:
251
+ two_folders_up = mix_files[0]
252
+ # two_folders_up = Path('').joinpath(*mix_files[0].parts[-3:])
253
+ print(f'\nFound new mix: [bold]{two_folders_up}[/bold]')
254
+ return mix_files
255
+ else:
256
+ return ()
257
+
258
+ def _keep_VIDEO_only(video_path):
259
+ # return file handle to a temp video file formed from the video_path
260
+ # stripped of its sound
261
+ in1 = ffmpeg.input(_pathname(video_path))
262
+ video_extension = video_path.suffix
263
+ silenced_opts = ["-loglevel", "quiet", "-nostats", "-hide_banner"]
264
+ file_handle = tempfile.NamedTemporaryFile(suffix=video_extension,
265
+ delete=DEL_TEMP)
266
+ out1 = in1.output(file_handle.name, map='0:v', vcodec='copy')
267
+ ffmpeg.run([out1.global_args(*silenced_opts)], overwrite_output=True)
268
+ return file_handle
269
+
270
+ # logger.add(sys.stdout, filter=lambda r: r["function"] == "_change_audio4video")
271
+ def _change_audio4video(audio_path: Path, video: Path):
272
+ """
273
+ Replaces audio in video (argument) by the audio contained in
274
+ audio_path (argument)
275
+ If name has version letter (A or B), flip it (DSC_8064vB.MOV -> DSC_8064vA.MOV)
276
+ Returns string tuple of old and new video files
277
+ old file is partial (without version and suffix) e.g.: ../DSC_8064
278
+ new file is complete
279
+
280
+ returns nothing
281
+
282
+ """
283
+ vidname, video_ext = video.name.split('.')
284
+ vid_only_handle = _keep_VIDEO_only(video)
285
+ a_n = _pathname(audio_path)
286
+ v_n = _pathname(vid_only_handle)
287
+ # check v suffix
288
+ m = re.match('(.*)v([AB]*)', vidname)
289
+ if m == None:
290
+ logger.debug('no suffix, add one')
291
+ out_path = video.parent / f'{vidname}vA.{video_ext}'
292
+ out_n = _pathname(out_path)
293
+ # for Resolve search
294
+ partial_filename = str(video.parent / vidname)
295
+ else:
296
+ base, letter = m.groups()
297
+ logger.debug(f'base {base}, letter {letter}')
298
+ new_letter = 'B' if letter == 'A' else 'A'
299
+ logger.debug(f'new_letter {new_letter}')
300
+ out_path = video.parent / f'{base}v{new_letter}.{video_ext}'
301
+ out_n = _pathname(out_path)
302
+ # for Resolve search
303
+ partial_filename = str(video.parent / base)
304
+ print(f'Video [bold]{video}[/bold] \nhas new sound and is now [bold]{out_path}[/bold]')
305
+ logger.debug(f'partial filname {partial_filename}')
306
+ # old_file = str(video)
307
+ new_file = str(out_path)
308
+ video.unlink() # remove old one
309
+ # building args for debug purpose only:
310
+ ffmpeg_args = (
311
+ ffmpeg
312
+ .input(v_n)
313
+ .output(out_n, vcodec='copy')
314
+ # .output(out_n, shortest=None, vcodec='copy')
315
+ .global_args('-i', a_n, "-hide_banner")
316
+ .overwrite_output()
317
+ .get_args()
318
+ )
319
+ logger.debug('ffmpeg args: %s'%' '.join(ffmpeg_args))
320
+ try: # for real now
321
+ _, out = (
322
+ ffmpeg
323
+ .input(v_n)
324
+ # .output(out_n, shortest=None, vcodec='copy')
325
+ .output(out_n, vcodec='copy')
326
+ .global_args('-i', a_n, "-hide_banner")
327
+ .overwrite_output()
328
+ .run(capture_stderr=True)
329
+ )
330
+ logger.debug('ffmpeg output')
331
+ for l in out.decode("utf-8").split('\n'):
332
+ logger.debug(l)
333
+ except ffmpeg.Error as e:
334
+ print('ffmpeg.run error merging: \n\t %s + %s = %s\n'%(
335
+ audio_path,
336
+ video_path,
337
+ synced_clip_file
338
+ ))
339
+ print(e)
340
+ print(e.stderr.decode('UTF-8'))
341
+ sys.exit(1)
342
+ return partial_filename, new_file
343
+
344
+ def _sox_combine(paths) -> Path:
345
+ """
346
+ Combines (stacks) files referred by the list of Path into a new temporary
347
+ files passed on return each files are stacked in a different channel, so
348
+ len(paths) == n_channels
349
+ """
350
+ if len(paths) == 1: # one device only, nothing to stack
351
+ logger.debug('one device only, nothing to stack')
352
+ return paths[0] ########################################################
353
+ out_file_handle = tempfile.NamedTemporaryFile(suffix='.wav',
354
+ delete=DEL_TEMP)
355
+ filenames = [_pathname(p) for p in paths]
356
+ out_file_name = _pathname(out_file_handle)
357
+ logger.debug('combining files: %s into %s'%(
358
+ filenames,
359
+ out_file_name))
360
+ cbn = sox.Combiner()
361
+ cbn.set_input_format(file_type=['wav']*len(paths))
362
+ status = cbn.build(
363
+ filenames,
364
+ out_file_name,
365
+ combine_type='merge')
366
+ logger.debug('sox.build status: %s'%status)
367
+ if status != True:
368
+ print('Error, sox did not merge files in _sox_combine()')
369
+ sys.exit(1)
370
+ merged_duration = sox.file_info.duration(
371
+ _pathname(out_file_handle))
372
+ nchan = sox.file_info.channels(
373
+ _pathname(out_file_handle))
374
+ logger.debug('merged file duration %f s with %i channels '%
375
+ (merged_duration, nchan))
376
+ return out_file_handle
377
+
378
+ def generate_script(new_mixes):
379
+ if LUA:
380
+ script = DAVINCI_RESOLVE_SCRIPT_TEMPLATE_LUA
381
+ postlude = ''
382
+ for a,b in new_mixes:
383
+ postlude += '{"%s","%s"},\n'%(a,b)
384
+ # postlude += f"('{a}','{b}'),\n"
385
+ postlude += '}\n\nfor _, pair in ipairs(changes) do\n'
386
+ postlude += ' findAndReplace(pair[1], pair[2])\n'
387
+ postlude += ' os.remove(pair[1])\n'
388
+ postlude += 'end\n'
389
+ # postlude += 'os.remove(pair[1])\n'
390
+ else: # python
391
+ script = DAVINCI_RESOLVE_SCRIPT_TEMPLATE
392
+ postlude = '\nchanges = [\n'
393
+ for a,b in new_mixes:
394
+ postlude += '("%s","%s"),\n'%(a,b)
395
+ postlude += ']\n'
396
+ postlude += '[findAndReplace(a,b) for a, b in changes]\n\n'
397
+ # foo = '[findAndReplace(a,b) for a, b in changes\n'
398
+ # print('foo',foo)
399
+ # print(postlude + foo)
400
+ return script + postlude
401
+
402
+ def main():
403
+ # proxies_dir, originals_dir, audio_dir, unique_directory, scan_only = \
404
+ # parse_and_check_arguments()
405
+ args = parse_and_check_arguments()
406
+ if args.unique_directory != None:
407
+ # alongside
408
+ matching_pairs = find_SND_vids_pairs_in_udir(args.unique_directory[0])
409
+ else:
410
+ # MAM mode
411
+ raw_root, synced_root, snd_root, proxies = mamconf.get_proj(False)
412
+ if raw_root == None:
413
+ print('Error: without -u option, mamsync should be configured before using newmix')
414
+ sys.exit(0)
415
+
416
+ # RAWROOT (sources with TC): "/Users/foobar/movies/MyBigMovie/"
417
+ # SYNCEDROOT (where RAWROOT will be mirrored, but with synced clips): "/Users/foobar/synced"
418
+ # SNDROOT (destination of ISOs sound files): "/Users/foobar/MovieSounds"
419
+ # then
420
+ # "/Users/foobar/synced/MyBigMovie" and "/Users/foobar/MovieSounds/MyBigMovie" are created
421
+ # So, synced_root is an enclosing directory, without the "project name" so
422
+ # fetch it from snd_root and build a synced_project pathlib.Path
423
+ proj_name = Path(raw_root).name
424
+ synced_project = Path(synced_root)/proj_name
425
+ project_sounds = Path(snd_root)/proj_name
426
+ matching_pairs = find_SND_vids_pairs_in_dual_dir(synced_project, project_sounds)
427
+ logger.debug(f'matching_pairs {pformat(matching_pairs)}')
428
+ changes = []
429
+ for SND_dir, vid in matching_pairs:
430
+ new_mix_files = get_recent_mix(SND_dir, vid)
431
+ logger.debug('new_mix_files: %s'%str(new_mix_files))
432
+ if new_mix_files != ():
433
+ logger.debug(f'new mixes {new_mix_files} in {SND_dir} for {vid.name}')
434
+ if len(new_mix_files) == 2:
435
+ new_audio_wav = _sox_combine(new_mix_files)
436
+ logger.debug('stereo_wav: %s'%new_audio_wav)
437
+ else: # len == 1, mono wav file
438
+ new_audio_wav = new_mix_files[0]
439
+ old_file, new_file = _change_audio4video(new_audio_wav, vid)
440
+ changes.append((old_file, new_file))
441
+ if len(changes) == 0:
442
+ print('No new mix.')
443
+ sys.exit(0)
444
+ logger.debug(f'changes {pformat(changes)}')
445
+ script = generate_script(changes)
446
+ fist_vid, _ = changes[0]
447
+ fist_name = Path(fist_vid).name
448
+ # title = f'New sound for {fist_name}'
449
+ # new_sound_clips = [p for p in Path(DAVINCI_RESOLVE_SCRIPT_LOCATION).iterdir() if 'New sound for' in str(p)]
450
+ # if len(new_sound_clips) != 0:
451
+ # print('There is already some "New sound for" Resolve scripts')
452
+ # for p in new_sound_clips:
453
+ # print(p)
454
+ # while ( res:=input("Is it OK to delete them? (you could also do it manually). Enter y/n: ").lower() ) not in {"y", "n"}: pass
455
+ # if res == 'y':
456
+ # [p.unlink() for p in new_sound_clips]
457
+ # # title = str(fist_vid)
458
+ title = 'Load New Sound'
459
+ if len(changes) > 1:
460
+ title += 's'
461
+ script_path = Path(DAVINCI_RESOLVE_SCRIPT_LOCATION)/f'{title}.lua'
462
+ # script += f'os.remove("{script_path}")\n' # doesnt work
463
+ with open(script_path, 'w') as fh:
464
+ fh.write(script)
465
+ print(f'Wrote script {script_path}')
466
+ # print(script)
467
+
468
+ if __name__ == '__main__':
469
+ main()
tictacsync/splitmix.py ADDED
@@ -0,0 +1,87 @@
1
+ import argparse, sys, sox
2
+ from loguru import logger
3
+ from scipy.io.wavfile import write as wrt_wav
4
+
5
+ try:
6
+ from . import load_fieldr_reaper
7
+ from . import yaltc
8
+ except:
9
+ import load_fieldr_reaper
10
+ import yaltc
11
+
12
+
13
+
14
+
15
+ logger.level("DEBUG", color="<yellow>")
16
+ logger.add(sys.stdout, level="DEBUG")
17
+ # logger.remove()
18
+
19
+ def conf_and_parse_arguments():
20
+ # parses directories from command arguments
21
+ # check for consistencies and warn user and exits,
22
+ # returns parser.parse_args()
23
+ descr = "Parse the submitted OTIO timeline and split the specified mix wav file according to OTIO clips"
24
+ parser = argparse.ArgumentParser(description=descr)
25
+ parser.add_argument(
26
+ "otio_file",
27
+ type=str,
28
+ nargs=1,
29
+ help="path of timeline saved under OTIO format"
30
+ )
31
+ parser.add_argument('mix',
32
+ type=str,
33
+ nargs=1,
34
+ help="mix wav file to be splitted")
35
+ args = parser.parse_args()
36
+ logger.debug('args %s'%args)
37
+ return args
38
+
39
+ # def write_wav(file, audio, samplerate):
40
+ # # Put the channels together with shape (2, 44100).
41
+ # # audio = np.array([left_channel, right_channel]).T
42
+
43
+ # audio = (audio * (2 ** 15 - 1)).astype("<h")
44
+
45
+ # with wave.open(file, "w") as f:
46
+ # # 2 Channels.
47
+ # f.setnchannels(2)
48
+ # # 2 bytes per sample.
49
+ # f.setsampwidth(2)
50
+ # f.setframerate(samplerate)
51
+ # f.writeframes(audio.tobytes())
52
+
53
+
54
+ def main():
55
+ # [TODO] split but duplicate audio to fill in the trimed parts of each clip
56
+ # so use whole clip duration rather than cut_duration for audio length
57
+ args = conf_and_parse_arguments()
58
+ fps, clips =load_fieldr_reaper.read_OTIO_file(args.otio_file[0])
59
+ logger.debug(f'otio has {fps} fps')
60
+ wav_file = args.mix[0]
61
+ N_channels = sox.file_info.channels(wav_file)
62
+ logger.debug(f'{wav_file} has {N_channels} channels')
63
+ tracks = yaltc.read_audio_data_from_file(wav_file, N_channels)
64
+ audio_data = tracks.T # interleave channels for cutting later
65
+ logger.debug(f'audio data shape {audio_data.shape}')
66
+ logger.debug(f'data: {tracks}')
67
+ logger.debug(f'tracks shape {tracks.shape}')
68
+ # start_frames, the "in" frame number (absolute, ie first "in" is 0)
69
+ start_frames = [int(round(cl.timeline_pos*fps)) - 3600*fps for cl in clips]
70
+ logger.debug(f'start_frames {start_frames}')
71
+ durations = [int(round(cl.cut_duration*fps)) for cl in clips]
72
+ logger.debug(f'durations {durations}')
73
+ # sampling frequency, samples per second
74
+ sps = sox.file_info.sample_rate(wav_file)
75
+ # number of audio samples per frames,
76
+ spf = sps/fps
77
+ logger.debug(f'there are {spf} audio samples for each frame')
78
+ audio_slices = [audio_data[int(spf*s):int(spf*(s+d))] for s,d in zip(start_frames, durations)]
79
+ logger.debug(f'audio_slices lengths {[len(s) for s in audio_slices]}')
80
+ for a in audio_slices:
81
+ logger.debug(f'audio_slices {a}')
82
+
83
+ [wrt_wav(f'{clips[i].name.split(".")[0]}.wav', int(sps), a) for i, a in enumerate(audio_slices)]
84
+
85
+
86
+ if __name__ == '__main__':
87
+ main()