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.
- tictacsync/device_scanner.py +21 -243
- tictacsync/entry.py +14 -10
- tictacsync/load_fieldr_reaper.py +352 -0
- tictacsync/mamconf.py +15 -35
- tictacsync/mamsync.py +8 -70
- tictacsync/multi2polywav.py +0 -1
- tictacsync/new-sound-resolve.py +469 -0
- tictacsync/splitmix.py +87 -0
- tictacsync/timeline.py +28 -26
- tictacsync/yaltc.py +357 -29
- {tictacsync-1.1.0a0.dist-info → tictacsync-1.3.0b0.dist-info}/METADATA +2 -2
- tictacsync-1.3.0b0.dist-info/RECORD +22 -0
- {tictacsync-1.1.0a0.dist-info → tictacsync-1.3.0b0.dist-info}/entry_points.txt +2 -1
- tictacsync-1.1.0a0.dist-info/RECORD +0 -19
- {tictacsync-1.1.0a0.dist-info → tictacsync-1.3.0b0.dist-info}/LICENSE +0 -0
- {tictacsync-1.1.0a0.dist-info → tictacsync-1.3.0b0.dist-info}/WHEEL +0 -0
- {tictacsync-1.1.0a0.dist-info → tictacsync-1.3.0b0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import json, pathlib, itertools, os, re, ffmpeg
|
|
2
|
+
import argparse, platformdirs, configparser, sys
|
|
3
|
+
from loguru import logger
|
|
4
|
+
from pprint import pformat
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from rich import print
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from . import mamconf
|
|
10
|
+
except:
|
|
11
|
+
import mamconf
|
|
12
|
+
|
|
13
|
+
dev = 'Cockos Incorporated'
|
|
14
|
+
app ='REAPER'
|
|
15
|
+
|
|
16
|
+
REAPER_SCRIPT_LOCATION = pathlib.Path(platformdirs.user_data_dir(app, dev)) / 'Scripts' / 'Atomic'
|
|
17
|
+
|
|
18
|
+
REAPER_LUA_CODE = """reaper.Main_OnCommand(40577, 0) -- lock left/right move
|
|
19
|
+
reaper.Main_OnCommand(40569, 0) -- lock enabled
|
|
20
|
+
local function placeWavsBeginingAtTrack(clip, start_idx)
|
|
21
|
+
for i, file in ipairs(clip.files) do
|
|
22
|
+
local track_idx = start_idx + i - 1
|
|
23
|
+
local track = reaper.GetTrack(nil,track_idx-1)
|
|
24
|
+
reaper.SetOnlyTrackSelected(track)
|
|
25
|
+
local left_trim = clip.in_time - clip.start_time
|
|
26
|
+
local where = clip.timeline_pos - left_trim
|
|
27
|
+
reaper.SetEditCurPos(where, false, false)
|
|
28
|
+
reaper.InsertMedia(file, 0 )
|
|
29
|
+
local item_cnt = reaper.CountTrackMediaItems( track )
|
|
30
|
+
local item = reaper.GetTrackMediaItem( track, item_cnt-1 )
|
|
31
|
+
local take = reaper.GetTake(item, 0)
|
|
32
|
+
-- reaper.GetSetMediaItemTakeInfo_String(take, "P_NAME", clip.name, true)
|
|
33
|
+
local pos = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
|
|
34
|
+
reaper.BR_SetItemEdges(item, clip.timeline_pos, clip.timeline_pos + clip.cut_duration)
|
|
35
|
+
reaper.SetMediaItemInfo_Value(item, "C_LOCK", 2)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
--cut here--
|
|
40
|
+
|
|
41
|
+
sample of the clips nested table (this will be discarded)
|
|
42
|
+
each clip has an EDL info table plus a sequence of ISO files:
|
|
43
|
+
|
|
44
|
+
clips =
|
|
45
|
+
{
|
|
46
|
+
{
|
|
47
|
+
name="canon24fps01.MOV", start_time=7.25, in_time=21.125, cut_duration=6.875, timeline_pos=3600,
|
|
48
|
+
files=
|
|
49
|
+
{
|
|
50
|
+
"/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/leftCAM/card01/canon24fps01_SND/ISOfiles/Alice_canon24fps01.wav",
|
|
51
|
+
"/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/leftCAM/card01/canon24fps01_SND/ISOfiles/Bob_canon24fps01.wav"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{name="DSC_8063.MOV", start_time=0.0, in_time=5.0, cut_duration=20.25, timeline_pos=3606.875,
|
|
55
|
+
files={"/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/rightCAM/ROLL01/DSC_8063_SND/ISOfiles/Alice_DSC_8063.wav",
|
|
56
|
+
"/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/rightCAM/ROLL01/DSC_8063_SND/ISOfiles/Bob_DSC_8063.wav"}},
|
|
57
|
+
{name="canon24fps02.MOV", start_time=35.166666666666664, in_time=35.166666666666664, cut_duration=20.541666666666668, timeline_pos=3627.125, files={"/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/leftCAM/card01/canon24fps02_SND/ISOfiles/Alice_canon24fps02.wav",
|
|
58
|
+
"/Users/lutzray/Downloads/SoundForMyMovie/MyBigMovie/day01/leftCAM/card01/canon24fps02_SND/ISOfiles/Bob_canon24fps02.wav"}}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
--cut here--
|
|
62
|
+
-- make room fro the tracks to come
|
|
63
|
+
amplitude_top = 0
|
|
64
|
+
amplitude_bottom = 0
|
|
65
|
+
for i_clip, cl in pairs(clips) do
|
|
66
|
+
if i_clip%2 ~= 1 then
|
|
67
|
+
amplitude_top = math.max(amplitude_top, #cl.files)
|
|
68
|
+
else
|
|
69
|
+
amplitude_bottom = math.max(amplitude_bottom, #cl.files)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
for i = 1 , amplitude_top + amplitude_bottom + 1 do
|
|
73
|
+
reaper.InsertTrackAtIndex( -1, false ) -- at end
|
|
74
|
+
end
|
|
75
|
+
track_count = reaper.CountTracks(0)
|
|
76
|
+
-- ISOs will be up and down the base_track index
|
|
77
|
+
base_track = track_count - amplitude_bottom
|
|
78
|
+
for iclip, clip in ipairs(clips) do
|
|
79
|
+
start_track_number = base_track
|
|
80
|
+
-- alternating even/odd, odd=below base_track
|
|
81
|
+
if iclip%2 == 0 then -- above base_track, start higher
|
|
82
|
+
start_track_number = base_track - #clip.files
|
|
83
|
+
end
|
|
84
|
+
placeWavsBeginingAtTrack(clip, start_track_number)
|
|
85
|
+
if #clips > 1 then -- interclips editing
|
|
86
|
+
reaper.AddProjectMarker(0, false, clip.timeline_pos, 0, '', -1)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
reaper.SetEditCurPos(3600, false, false)
|
|
90
|
+
reaper.Main_OnCommand(40151, 0)
|
|
91
|
+
if #clips > 1 then -- interclips editing
|
|
92
|
+
-- last marker at the end
|
|
93
|
+
last_clip = clips[#clips]
|
|
94
|
+
reaper.AddProjectMarker(0, false, last_clip.timeline_pos + last_clip.cut_duration, 0, '', -1)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
logger.level("DEBUG", color="<yellow>")
|
|
100
|
+
logger.add(sys.stdout, level="DEBUG")
|
|
101
|
+
logger.remove()
|
|
102
|
+
|
|
103
|
+
def parse_and_check_arguments():
|
|
104
|
+
# parses directories from command arguments
|
|
105
|
+
# check for consistencies and warn user and exits,
|
|
106
|
+
# returns parser.parse_args()
|
|
107
|
+
descr = "Parse the submitted OTIO timeline and build a Reaper Script to load the corresponding ISO files from SNDROOT (see mamconf --show)"
|
|
108
|
+
parser = argparse.ArgumentParser(description=descr)
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"a_file_argument",
|
|
111
|
+
type=str,
|
|
112
|
+
nargs=1,
|
|
113
|
+
help="path of timeline saved under OTIO format"
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument('--interval',
|
|
116
|
+
dest='interval',
|
|
117
|
+
nargs=2,
|
|
118
|
+
help="One or two timecodes, space seperated, delimiting the zone to process (if not specified the whole timeline is processed)")
|
|
119
|
+
args = parser.parse_args()
|
|
120
|
+
logger.debug('args %s'%args)
|
|
121
|
+
return args
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class Clip:
|
|
125
|
+
# all time in seconds
|
|
126
|
+
start_time: float # the start time of the clip in
|
|
127
|
+
in_time: float # time of 'in' point, relative to clip start_time
|
|
128
|
+
cut_duration: float
|
|
129
|
+
whole_duration: float # unedited clip duration
|
|
130
|
+
name: str #
|
|
131
|
+
path: str # path of clip
|
|
132
|
+
timeline_pos: float # when on the timeline the clip starts
|
|
133
|
+
ISOdir: None # folder of ISO files for clip
|
|
134
|
+
|
|
135
|
+
def clip_info_from_json(jsoncl):
|
|
136
|
+
"""
|
|
137
|
+
parse data from an OTIO json Clip
|
|
138
|
+
https://opentimelineio.readthedocs.io/en/latest/tutorials/otio-serialized-schema.html#clip-2
|
|
139
|
+
returns a list composed of (all times are in seconds):
|
|
140
|
+
st, start time (from clip metadata TC)
|
|
141
|
+
In, the "in time"
|
|
142
|
+
cd, the cut duration
|
|
143
|
+
wl, the whole length of the unedited clip
|
|
144
|
+
the clip file path (string)
|
|
145
|
+
name (string)
|
|
146
|
+
NB: the position on the global timeline is not stored but latter computed from summing cut times
|
|
147
|
+
"""
|
|
148
|
+
def _float_time(json_rationaltime):
|
|
149
|
+
return json_rationaltime['value']/json_rationaltime['rate']
|
|
150
|
+
av_range = jsoncl['media_references']['DEFAULT_MEDIA']['available_range']
|
|
151
|
+
src_rg = jsoncl['source_range']
|
|
152
|
+
st = av_range['start_time']
|
|
153
|
+
In = src_rg['start_time']
|
|
154
|
+
cd = src_rg['duration']
|
|
155
|
+
wl = av_range['duration']
|
|
156
|
+
path = jsoncl['media_references']['DEFAULT_MEDIA']['target_url']
|
|
157
|
+
name = jsoncl['media_references']['DEFAULT_MEDIA']['name']
|
|
158
|
+
return Clip(*[_float_time(t) for t in [st, In, cd, wl,]] + \
|
|
159
|
+
[name, path, 0, None])
|
|
160
|
+
|
|
161
|
+
def get_SND_dirs(snd_root):
|
|
162
|
+
# returns all directories found under snd_root
|
|
163
|
+
def _searchDirectory(cwd,searchResults):
|
|
164
|
+
dirs = os.listdir(cwd)
|
|
165
|
+
for dir in dirs:
|
|
166
|
+
fullpath = os.path.join(cwd,dir)
|
|
167
|
+
if os.path.isdir(fullpath):
|
|
168
|
+
searchResults.append(fullpath)
|
|
169
|
+
_searchDirectory(fullpath,searchResults)
|
|
170
|
+
searchResults = []
|
|
171
|
+
_searchDirectory(snd_root,searchResults)
|
|
172
|
+
return searchResults
|
|
173
|
+
|
|
174
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "find_and_set_ISO_dir")
|
|
175
|
+
def find_and_set_ISO_dir(clip, SND_dirs):
|
|
176
|
+
"""
|
|
177
|
+
SND_dirs contains all the *_SND directories found in snd_root.
|
|
178
|
+
This fct finds out which one corresponds to the clip
|
|
179
|
+
and sets the found path to clip.ISOdir.
|
|
180
|
+
Returns nothing.
|
|
181
|
+
"""
|
|
182
|
+
clip_stem = pathlib.Path(clip.path).stem
|
|
183
|
+
logger.debug(f'clip_stem {clip_stem}')
|
|
184
|
+
m = re.match('(.*)v([AB]*)', clip_stem)
|
|
185
|
+
logger.debug(f'{clip_stem} match (.*)v([AB]*) { m.groups() if m != None else None}')
|
|
186
|
+
if m != None:
|
|
187
|
+
clip_stem = m.groups()[0]
|
|
188
|
+
# /MyBigMovie/day01/leftCAM/card01/canon24fps01_SND -> canon24fps01_SND
|
|
189
|
+
names_only = [p.name for p in SND_dirs]
|
|
190
|
+
logger.debug(f'names-only {pformat(names_only)}')
|
|
191
|
+
clip_stem_SND = f'{clip_stem}_SND'
|
|
192
|
+
if clip_stem_SND in names_only:
|
|
193
|
+
where = names_only.index(clip_stem_SND)
|
|
194
|
+
else:
|
|
195
|
+
print(f'Error: OTIO file contains clip not in SYNCEDROOT: {clip_stem} (check with mamconf --show)')
|
|
196
|
+
sys.exit(0)
|
|
197
|
+
complete_path = SND_dirs[where]
|
|
198
|
+
logger.debug(f'found {complete_path}')
|
|
199
|
+
clip.ISOdir = str(complete_path)
|
|
200
|
+
|
|
201
|
+
def gen_lua_table(clips):
|
|
202
|
+
# returns a string defining a lua nested table
|
|
203
|
+
# top level: a sequence of clips
|
|
204
|
+
# a clip has keys: name, start_time, in_time, cut_duration, timeline_pos, files
|
|
205
|
+
# clip.files is a sequence of ISO wav files
|
|
206
|
+
def _list_ISO(dir):
|
|
207
|
+
iso_dir = pathlib.Path(dir)/'ISOfiles'
|
|
208
|
+
ISOs = [f for f in iso_dir.iterdir() if f.suffix.lower() == '.wav']
|
|
209
|
+
# ISOs = [f for f in ISOs if f.name[:2] != 'tc'] # no timecode
|
|
210
|
+
logger.debug(f'ISOs {ISOs}')
|
|
211
|
+
sequence = '{'
|
|
212
|
+
for file in ISOs:
|
|
213
|
+
sequence += f'"{file}",\n'
|
|
214
|
+
sequence += '}'
|
|
215
|
+
return sequence
|
|
216
|
+
lua_clips = '{'
|
|
217
|
+
for cl in clips:
|
|
218
|
+
ISOs = _list_ISO(cl.ISOdir)
|
|
219
|
+
# logger.debug(f'sequence {ISOs}')
|
|
220
|
+
clip_table = f'{{name="{cl.name}", start_time={cl.start_time}, in_time={cl.in_time}, cut_duration={cl.cut_duration}, timeline_pos={cl.timeline_pos}, files={ISOs}}}'
|
|
221
|
+
lua_clips += f'{clip_table},\n'
|
|
222
|
+
logger.debug(f'clip_table {clip_table}')
|
|
223
|
+
lua_clips += '}'
|
|
224
|
+
return lua_clips
|
|
225
|
+
|
|
226
|
+
def read_OTIO_file(f):
|
|
227
|
+
"""
|
|
228
|
+
returns framerate and a list of Clip instances parsed from
|
|
229
|
+
the OTIO file passed as (string) argument f;
|
|
230
|
+
warns and exists if more than one video track.
|
|
231
|
+
"""
|
|
232
|
+
with open(f) as fh:
|
|
233
|
+
oti = json.load(fh)
|
|
234
|
+
video_tracks = [tr for tr in oti['tracks']['children'] if tr['kind'] == 'Video']
|
|
235
|
+
if len(video_tracks) > 1:
|
|
236
|
+
print(f"Can only process timeline with one video track, this one has {len(video_tracks)}. Bye.")
|
|
237
|
+
sys.exit(0)
|
|
238
|
+
video_track = video_tracks[0]
|
|
239
|
+
clips = [clip_info_from_json(jscl) for jscl in video_track['children']]
|
|
240
|
+
logger.debug(f'clips: {pformat(clips)}')
|
|
241
|
+
# compute each clip global timeline position
|
|
242
|
+
clip_starts = [0] + list(itertools.accumulate([cl.cut_duration for cl in clips]))[:-1]
|
|
243
|
+
# Reaper can't handle negative item position (for the trimmed part)
|
|
244
|
+
# so starts at 1:00:00
|
|
245
|
+
clip_starts = [t + 3600 for t in clip_starts]
|
|
246
|
+
logger.debug(f'clip_starts: {clip_starts}')
|
|
247
|
+
for time, clip in zip(clip_starts, clips):
|
|
248
|
+
clip.timeline_pos = time
|
|
249
|
+
return int(oti['global_start_time']['rate']), clips
|
|
250
|
+
|
|
251
|
+
def reaper_save_action(wav_destination):
|
|
252
|
+
return f"""reaper.GetSetProjectInfo_String(0, "RENDER_FILE","{wav_destination.parent}",true)
|
|
253
|
+
reaper.GetSetProjectInfo_String(0, "RENDER_PATTERN","{wav_destination.name}",true)
|
|
254
|
+
reaper.SNM_SetIntConfigVar("projintmix", 4)
|
|
255
|
+
reaper.Main_OnCommand(40015, 0)
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "complete_clip_path")
|
|
259
|
+
def complete_clip_path(clip_stem, synced_proj):
|
|
260
|
+
match = []
|
|
261
|
+
for (root,dirs,files) in os.walk(synced_proj):
|
|
262
|
+
for f in files:
|
|
263
|
+
p = pathlib.Path(root)/f
|
|
264
|
+
if p.is_symlink() or p.suffix == '.reapeaks':
|
|
265
|
+
continue
|
|
266
|
+
# logger.debug(f'{f}')
|
|
267
|
+
if clip_stem in f.split('.')[0]: # match XYZvA.mov
|
|
268
|
+
match.append(p)
|
|
269
|
+
logger.debug(f'matches {match}')
|
|
270
|
+
if len(match) > 1:
|
|
271
|
+
print(f'Warning, some filenames collide {pformat(match)}, Bye.')
|
|
272
|
+
sys.exit(0)
|
|
273
|
+
if len(match) == 0:
|
|
274
|
+
print(f"Error, didn't find any clip containing *{clip_stem}*. Bye.")
|
|
275
|
+
sys.exit(0)
|
|
276
|
+
return match[0]
|
|
277
|
+
|
|
278
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "main")
|
|
279
|
+
def main():
|
|
280
|
+
def _where(a,x):
|
|
281
|
+
# find in which clip time x (in seconds) does fall.
|
|
282
|
+
n = 0
|
|
283
|
+
while n<len(a):
|
|
284
|
+
if a[n].timeline_pos > x:
|
|
285
|
+
break
|
|
286
|
+
else:
|
|
287
|
+
n += 1
|
|
288
|
+
return n-1
|
|
289
|
+
raw_root, synced_root, snd_root, proxies = mamconf.get_proj(False)
|
|
290
|
+
proj_name = pathlib.Path(raw_root).stem
|
|
291
|
+
synced_proj = pathlib.Path(synced_root)/proj_name
|
|
292
|
+
logger.debug(f'proj_name {proj_name}')
|
|
293
|
+
logger.debug(f'will search {snd_root} for ISOs')
|
|
294
|
+
all_SNDROOT_dirs = [pathlib.Path(f) for f in get_SND_dirs(snd_root)]
|
|
295
|
+
# keep only XYZ_SND dirs
|
|
296
|
+
SND_dirs = [p for p in all_SNDROOT_dirs if p.name[-4:] == '_SND']
|
|
297
|
+
logger.debug(f'SND_dirs {pformat(SND_dirs)}')
|
|
298
|
+
args = parse_and_check_arguments()
|
|
299
|
+
file_arg = pathlib.Path(args.a_file_argument[0])
|
|
300
|
+
# check if its intraclip or interclip sound edit
|
|
301
|
+
# if otio file then interclip
|
|
302
|
+
if file_arg.suffix == '.otio':
|
|
303
|
+
logger.debug('interclip sound edit, filling up clips')
|
|
304
|
+
_, clips = read_OTIO_file(file_arg)
|
|
305
|
+
[find_and_set_ISO_dir(clip, SND_dirs) for clip in clips]
|
|
306
|
+
else:
|
|
307
|
+
logger.debug('intraclip sound edit, clips will have one clip')
|
|
308
|
+
# traverse synced_root to find clip path
|
|
309
|
+
clip_path = complete_clip_path(file_arg.stem, synced_proj)
|
|
310
|
+
probe = ffmpeg.probe(clip_path)
|
|
311
|
+
duration = float(probe['format']['duration'])
|
|
312
|
+
clips = [Clip(
|
|
313
|
+
start_time=0,
|
|
314
|
+
in_time=0,
|
|
315
|
+
cut_duration=duration,
|
|
316
|
+
whole_duration=duration,
|
|
317
|
+
name=file_arg.stem,
|
|
318
|
+
path=clip_path,
|
|
319
|
+
timeline_pos=3600,
|
|
320
|
+
ISOdir='')]
|
|
321
|
+
[find_and_set_ISO_dir(clip, SND_dirs) for clip in clips]
|
|
322
|
+
print(f'For video clip \n{clip_path}\nfound audio in\n{clips[0].ISOdir}')
|
|
323
|
+
logger.debug(f'clips with found ISOdir: {pformat(clips)}')
|
|
324
|
+
lua_clips = gen_lua_table(clips)
|
|
325
|
+
logger.debug(f'lua_clips {lua_clips}')
|
|
326
|
+
# title = "Load cut26_MyBigMovie" or "Load clip026_MyBigMovie"
|
|
327
|
+
arg_name = pathlib.Path(args.a_file_argument[0]).stem
|
|
328
|
+
title = f'Load {arg_name}_{pathlib.Path(raw_root).stem}'
|
|
329
|
+
# script_path = pathlib.Path(REAPER_SCRIPT_LOCATION)/f'{title}.lua'
|
|
330
|
+
script_path = pathlib.Path(REAPER_SCRIPT_LOCATION)/f'Load Clip Audio.lua'
|
|
331
|
+
# script += f'os.remove("{script_path}")\n' # doesnt work
|
|
332
|
+
Lua_script_pre, _ , Lua_script_post = REAPER_LUA_CODE.split('--cut here--')
|
|
333
|
+
script = Lua_script_pre + 'clips=' + lua_clips + Lua_script_post
|
|
334
|
+
with open(script_path, 'w') as fh:
|
|
335
|
+
fh.write(script)
|
|
336
|
+
print(f'Wrote script {script_path}')
|
|
337
|
+
if file_arg.suffix != 'otio':
|
|
338
|
+
# build "Set rendering for" action
|
|
339
|
+
destination = pathlib.Path(clips[0].ISOdir)/'mix.wav'
|
|
340
|
+
logger.debug(f'will build set rendering for {arg_name} with dest: {destination}')
|
|
341
|
+
render_action = reaper_save_action(destination)
|
|
342
|
+
logger.debug(f'clip\n{render_action}')
|
|
343
|
+
# script_path = pathlib.Path(REAPER_SCRIPT_LOCATION)/f'Set rendering for {arg_name}.lua'
|
|
344
|
+
script_path = pathlib.Path(REAPER_SCRIPT_LOCATION)/f'Render Clip Audio.lua'
|
|
345
|
+
with open(script_path, 'w') as fh:
|
|
346
|
+
fh.write(render_action)
|
|
347
|
+
print(f'Wrote script {script_path}')
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
if __name__ == '__main__':
|
|
351
|
+
main()
|
|
352
|
+
|
tictacsync/mamconf.py
CHANGED
|
@@ -3,6 +3,13 @@ from loguru import logger
|
|
|
3
3
|
from pprint import pprint, pformat
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
# [TODO] add in the doc:
|
|
7
|
+
# RAWROOT (sources with TC): "/Users/foobar/movies/MyBigMovie/"
|
|
8
|
+
# SYNCEDROOT (where RAWROOT will be mirrored, but with synced clips): "/Users/foobar/synced"
|
|
9
|
+
# SNDROOT (destination of ISOs sound files): "/Users/foobar/MovieSounds"
|
|
10
|
+
# then
|
|
11
|
+
# "/Users/foobar/synced/MyBigMovie" and "/Users/foobar/MovieSounds/MyBigMovie" will be created
|
|
12
|
+
|
|
6
13
|
|
|
7
14
|
CONF_FILE = 'mamsync.cfg'
|
|
8
15
|
LOG_FILE = 'mamdone.txt'
|
|
@@ -13,8 +20,8 @@ logger.remove()
|
|
|
13
20
|
|
|
14
21
|
|
|
15
22
|
def print_out_conf(raw_root, synced_root, snd_root, proxies=''):
|
|
16
|
-
print(f'RAWROOT (
|
|
17
|
-
print(f'SYNCEDROOT (
|
|
23
|
+
print(f'RAWROOT (sources with TC): "{raw_root}"')
|
|
24
|
+
print(f'SYNCEDROOT (where RAWROOT will be mirrored, but with synced clips): "{synced_root}"')
|
|
18
25
|
print(f'SNDROOT (destination of ISOs sound files): "{snd_root}"')
|
|
19
26
|
if proxies != '':
|
|
20
27
|
print(f'PROXIES (NLE proxy clips folder): "{proxies}"')
|
|
@@ -52,7 +59,7 @@ def get_proj(print_conf_stdout=False):
|
|
|
52
59
|
conf_file = Path(conf_dir)/CONF_FILE
|
|
53
60
|
logger.debug('try reading config in %s'%conf_file)
|
|
54
61
|
if print_conf_stdout:
|
|
55
|
-
print(f'
|
|
62
|
+
print(f'Will read configuration from file {conf_file}')
|
|
56
63
|
if conf_file.exists():
|
|
57
64
|
conf_prs = configparser.ConfigParser()
|
|
58
65
|
conf_prs.read(conf_file)
|
|
@@ -82,22 +89,22 @@ def get_proj(print_conf_stdout=False):
|
|
|
82
89
|
|
|
83
90
|
def new_parser():
|
|
84
91
|
parser = argparse.ArgumentParser()
|
|
85
|
-
parser.add_argument('--
|
|
92
|
+
parser.add_argument('--rr',
|
|
86
93
|
nargs = 1,
|
|
87
94
|
dest='rawroot',
|
|
88
95
|
help='Sets new value for raw root folder (i.e.: clips with TC)')
|
|
89
|
-
parser.add_argument('--
|
|
96
|
+
parser.add_argument('--sr',
|
|
90
97
|
nargs = 1,
|
|
91
98
|
dest='syncedroot',
|
|
92
99
|
help="""Sets where the synced files will be written, to be used by the NLE. Will contain a mirror copy of RAWROOT """)
|
|
93
|
-
parser.add_argument('--
|
|
100
|
+
parser.add_argument('--pr',
|
|
94
101
|
nargs = 1,
|
|
95
102
|
dest='proxies',
|
|
96
103
|
help='Sets where the proxy files are stored by the NLE')
|
|
97
|
-
parser.add_argument('--
|
|
104
|
+
parser.add_argument('--sf',
|
|
98
105
|
nargs = 1,
|
|
99
106
|
dest='sndfolder',
|
|
100
|
-
help='Sets
|
|
107
|
+
help='Sets value for sound folder (will contain a mirror copy of RAWROOT, but with ISO files only)')
|
|
101
108
|
parser.add_argument('--clearconf',
|
|
102
109
|
action='store_true',
|
|
103
110
|
dest='clearconf',
|
|
@@ -143,33 +150,6 @@ def main():
|
|
|
143
150
|
get_proj()
|
|
144
151
|
print_out_conf(*get_proj(True))
|
|
145
152
|
sys.exit(0)
|
|
146
|
-
# roots = get_proj(False)
|
|
147
|
-
# if any([r == '' for r in roots]):
|
|
148
|
-
# print("Can't sync if some folders are not set:")
|
|
149
|
-
# print_out_conf(*get_proj())
|
|
150
|
-
# print('Bye.')
|
|
151
|
-
# sys.exit(0)
|
|
152
|
-
# for r in roots:
|
|
153
|
-
# if not r.is_absolute():
|
|
154
|
-
# print(f'\rError: folder {r} must be an absolute path. Bye')
|
|
155
|
-
# sys.exit(0)
|
|
156
|
-
# if not r.exists():
|
|
157
|
-
# print(f'\rError: folder {r} does not exist. Bye')
|
|
158
|
-
# sys.exit(0)
|
|
159
|
-
# if not r.is_dir():
|
|
160
|
-
# print(f'\rError: path {r} is not a folder. Bye')
|
|
161
|
-
# sys.exit(0)
|
|
162
|
-
# raw_root, synced_root, snd_root = roots
|
|
163
|
-
# if args.sub_dir != None:
|
|
164
|
-
# top_dir = args.sub_dir
|
|
165
|
-
# logger.debug(f'sub _dir: {args.sub_dir}')
|
|
166
|
-
# if not Path(top_dir).exists():
|
|
167
|
-
# print(f"\rError: folder {top_dir} doesn't exist, bye.")
|
|
168
|
-
# sys.exit(0)
|
|
169
|
-
# else:
|
|
170
|
-
# top_dir = raw_root
|
|
171
|
-
# if args.resync:
|
|
172
|
-
# clear_log()
|
|
173
153
|
|
|
174
154
|
if __name__ == '__main__':
|
|
175
155
|
main()
|
tictacsync/mamsync.py
CHANGED
|
@@ -10,12 +10,14 @@ try:
|
|
|
10
10
|
from . import timeline
|
|
11
11
|
from . import multi2polywav
|
|
12
12
|
from . import mamconf
|
|
13
|
+
from . import entry
|
|
13
14
|
except:
|
|
14
15
|
import yaltc
|
|
15
16
|
import device_scanner
|
|
16
17
|
import timeline
|
|
17
18
|
import multi2polywav
|
|
18
19
|
import mamconf
|
|
20
|
+
import entry
|
|
19
21
|
|
|
20
22
|
import argparse, tempfile, configparser
|
|
21
23
|
from loguru import logger
|
|
@@ -42,76 +44,13 @@ m4v svi 3gp 3g2 mxf roq nsv flv f4v f4p f4a f4b 3gp aa aac aax act aiff alac
|
|
|
42
44
|
amr ape au awb dss dvf flac gsm iklax ivs m4a m4b m4p mmf mp3 mpc msv nmf
|
|
43
45
|
ogg oga mogg opus ra rm raw rf64 sln tta voc vox wav wma wv webm 8svx cda""".split()
|
|
44
46
|
|
|
47
|
+
logger.level("DEBUG", color="<yellow>")
|
|
48
|
+
logger.add(sys.stdout, level="DEBUG")
|
|
45
49
|
logger.remove()
|
|
46
|
-
# logger.add(sys.stdout,
|
|
47
|
-
# logger.add(sys.stdout, filter=lambda r: r["function"] == "
|
|
50
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "__init__" and r["module"] == "yaltc")
|
|
51
|
+
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_fit_length")
|
|
48
52
|
# logger.add(sys.stdout, filter=lambda r: r["function"] == "_write_ISOs")
|
|
49
53
|
|
|
50
|
-
def process_single(file, args):
|
|
51
|
-
# argument is a single file
|
|
52
|
-
m = device_scanner.media_at_path(None, Path(file))
|
|
53
|
-
if args.plotting:
|
|
54
|
-
print('\nPlots can be zoomed and panned...')
|
|
55
|
-
print('Close window for next one.')
|
|
56
|
-
a_rec = yaltc.Recording(m, do_plots=args.plotting)
|
|
57
|
-
time = a_rec.get_start_time()
|
|
58
|
-
# time = a_rec.get_start_time(plots=args.plotting)
|
|
59
|
-
if time != None:
|
|
60
|
-
frac_time = int(time.microsecond / 1e2)
|
|
61
|
-
d = '%s.%s'%(time.strftime("%Y-%m-%d %H:%M:%S"),frac_time)
|
|
62
|
-
if args.terse:
|
|
63
|
-
print('%s UTC:%s pulse: %i in chan %i'%(file, d, a_rec.sync_position,
|
|
64
|
-
a_rec.TicTacCode_channel))
|
|
65
|
-
else:
|
|
66
|
-
print('\nRecording started at [gold1]%s[/gold1] UTC'%d)
|
|
67
|
-
print('true sample rate: [gold1]%.3f Hz[/gold1]'%a_rec.true_samplerate)
|
|
68
|
-
print('first sync at [gold1]%i[/gold1] samples in channel %i'%(a_rec.sync_position,
|
|
69
|
-
a_rec.TicTacCode_channel))
|
|
70
|
-
print('N.B.: all results are precise to the displayed digits!\n')
|
|
71
|
-
else:
|
|
72
|
-
if args.terse:
|
|
73
|
-
print('%s UTC: None'%(file))
|
|
74
|
-
else:
|
|
75
|
-
print('Start time couldnt be determined')
|
|
76
|
-
sys.exit(1)
|
|
77
|
-
|
|
78
|
-
def process_lag_adjustement(media_object):
|
|
79
|
-
# trim channels that are lagging (as stated in tracks.txt)
|
|
80
|
-
# replace the old file, and rename the old one with .wavbk
|
|
81
|
-
# if .wavbk exist, process was done already, so dont process
|
|
82
|
-
# returns nothing
|
|
83
|
-
lags = media_object.device.tracks.lag_values
|
|
84
|
-
logger.debug('will process %s lags'%[lags])
|
|
85
|
-
channels = timeline._sox_split_channels(media_object.path)
|
|
86
|
-
# add bk to file on filesystem, but media_object.path is unchanged (?)
|
|
87
|
-
backup_name = str(media_object.path) + 'bk'
|
|
88
|
-
if Path(backup_name).exists():
|
|
89
|
-
logger.debug('%s exists, so return now.'%backup_name)
|
|
90
|
-
return
|
|
91
|
-
media_object.path.replace(backup_name)
|
|
92
|
-
logger.debug('channels %s'%channels)
|
|
93
|
-
def _trim(lag, chan_file):
|
|
94
|
-
# for lag
|
|
95
|
-
if lag == None:
|
|
96
|
-
return chan_file
|
|
97
|
-
else:
|
|
98
|
-
logger.debug('process %s for lag of %s'%(chan_file, lag))
|
|
99
|
-
sox_transform = sox.Transformer()
|
|
100
|
-
sox_transform.trim(float(lag)*1e-3)
|
|
101
|
-
output_fh = tempfile.NamedTemporaryFile(suffix='.wav', delete=DEL_TEMP)
|
|
102
|
-
out_file = timeline._pathname(output_fh)
|
|
103
|
-
input_file = timeline._pathname(chan_file)
|
|
104
|
-
logger.debug('sox in and out files: %s %s'%(input_file, out_file))
|
|
105
|
-
logger.debug('calling sox_transform.build()')
|
|
106
|
-
status = sox_transform.build(input_file, out_file, return_output=True )
|
|
107
|
-
logger.debug('sox.build exit code %s'%str(status))
|
|
108
|
-
return output_fh
|
|
109
|
-
new_channels = [_trim(*e) for e in zip(lags, channels)]
|
|
110
|
-
logger.debug('new_channels %s'%new_channels)
|
|
111
|
-
trimmed_multichanfile = timeline._sox_combine(new_channels)
|
|
112
|
-
logger.debug('trimmed_multichanfile %s'%timeline._pathname(trimmed_multichanfile))
|
|
113
|
-
Path(timeline._pathname(trimmed_multichanfile)).replace(media_object.path)
|
|
114
|
-
|
|
115
54
|
def copy_to_syncedroot(raw_root, synced_root):
|
|
116
55
|
# args are str
|
|
117
56
|
# copy dirs and non AV files
|
|
@@ -155,8 +94,6 @@ def copy_raw_root_tree_to_sndroot(raw_root, snd_root):
|
|
|
155
94
|
if raw_path.is_dir():
|
|
156
95
|
synced_path.mkdir(parents=True, exist_ok=True)
|
|
157
96
|
|
|
158
|
-
|
|
159
|
-
|
|
160
97
|
def new_parser():
|
|
161
98
|
parser = argparse.ArgumentParser()
|
|
162
99
|
parser.add_argument('--resync',
|
|
@@ -240,7 +177,7 @@ def main():
|
|
|
240
177
|
logger.debug('%s has lag_values %s'%(
|
|
241
178
|
m.path, m.device.tracks.lag_values))
|
|
242
179
|
# any lag for a channel is specified by user in tracks.txt
|
|
243
|
-
process_lag_adjustement(m)
|
|
180
|
+
entry.process_lag_adjustement(m)
|
|
244
181
|
audio_REC_only = all([m.device.dev_type == 'REC' for m
|
|
245
182
|
in scanner.found_media_files])
|
|
246
183
|
if not args.terse:
|
|
@@ -285,6 +222,7 @@ def main():
|
|
|
285
222
|
for rec in recordings
|
|
286
223
|
if rec.get_start_time()
|
|
287
224
|
]
|
|
225
|
+
[r.load_track_info() for r in recordings_with_time if r.is_audio()]
|
|
288
226
|
if not args.terse:
|
|
289
227
|
table = Table(title="tictacsync results")
|
|
290
228
|
table.add_column("Recording\n", justify="center", style='gold1')
|