renoise-kit 0.5.0__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.
@@ -0,0 +1,307 @@
1
+ Metadata-Version: 2.4
2
+ Name: renoise-kit
3
+ Version: 0.5.0
4
+ Summary: rn: frame-exact probing, contact sheets, EDL assembly, finishing and QA for AI video edits
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://renoise.ai
7
+ Project-URL: Source, https://github.com/renoise-ai/renoise-kit
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: pillow>=10.1
12
+ Requires-Dist: numpy
13
+ Provides-Extra: listen
14
+ Requires-Dist: librosa; extra == "listen"
15
+ Requires-Dist: matplotlib; extra == "listen"
16
+ Provides-Extra: transcribe
17
+ Requires-Dist: faster-whisper; extra == "transcribe"
18
+ Provides-Extra: faces
19
+ Requires-Dist: opencv-python-headless>=4.8; extra == "faces"
20
+ Provides-Extra: capture
21
+ Requires-Dist: playwright; extra == "capture"
22
+ Provides-Extra: captions
23
+ Requires-Dist: pysubs2; extra == "captions"
24
+ Provides-Extra: fonts
25
+ Requires-Dist: fonttools; extra == "fonts"
26
+ Provides-Extra: matte
27
+ Requires-Dist: rembg[cpu]; extra == "matte"
28
+ Provides-Extra: export
29
+ Requires-Dist: opentimelineio; extra == "export"
30
+ Requires-Dist: otio-fcpx-xml-adapter; extra == "export"
31
+ Requires-Dist: otio-fcp-adapter; extra == "export"
32
+ Requires-Dist: pysubs2; extra == "export"
33
+ Provides-Extra: all
34
+ Requires-Dist: renoise-kit[captions,capture,export,faces,fonts,listen,matte,transcribe]; extra == "all"
35
+ Dynamic: license-file
36
+
37
+ # renoise-kit · `rn`
38
+
39
+ **English** · [简体中文](README.zh-CN.md)
40
+
41
+ The mechanical, error-prone half of editing video, as one command-line tool: frame-exact probing, contact sheets you
42
+ can actually look at, audio turned into numbers, code rendered into frames, an edit decision list (EDL) assembled
43
+ frame-exactly, delivery finishing, QA and export to other editors.
44
+
45
+ `rn` measures and guarantees the mechanics. Every creative decision stays yours, or your agent's. It was built for AI
46
+ agents editing generated video in the [Renoise](https://renoise.ai) media sandbox, and the same tool runs on your
47
+ machine.
48
+
49
+ - **Measure.** Turns media into facts a model can read: frames, shots, beats, word times, faces, on-screen text,
50
+ per-frame signals.
51
+ - **Render.** Turns any code into frame-exact pictures, alpha included: HTML/canvas pages, numpy, Manim, Blender. They
52
+ all meet in one format, a PNG sequence.
53
+ - **Assemble and verify.** Builds a frame-exact cut from an EDL, then checks it against the delivery standard.
54
+ - **No named effects, no auto-editing.** The EDL adds general capabilities (keyframes, anchors, masks, blend modes, raw
55
+ ffmpeg filters), never a menu of looks. A common effect comes as readable source you can call, change or ignore.
56
+
57
+ ## Install
58
+
59
+ `rn` drives the system's ffmpeg and ffprobe, version 7 or later, built with libass for burned-in subtitles.
60
+
61
+ | | macOS | Debian / Ubuntu |
62
+ |---|---|---|
63
+ | ffmpeg with libass | `brew install ffmpeg-full`, then `export PATH="$(brew --prefix ffmpeg-full)/bin:$PATH"` (keg-only; Homebrew's plain `ffmpeg` has no libass) | `apt install ffmpeg fontconfig` |
64
+ | OCR for `rn text` (optional) | comes with `ffmpeg-full` | `apt install tesseract-ocr tesseract-ocr-chi-sim` |
65
+
66
+ ```sh
67
+ uv tool install 'renoise-kit[all]' # or: pipx install 'renoise-kit[all]'
68
+ playwright install chromium # only for rn capture
69
+ rn --version
70
+ ```
71
+
72
+ The core install is Pillow and numpy. Heavier dependencies come as extras, so you can install only what you use. When
73
+ a command needs a missing extra, its error line names the extra to install.
74
+
75
+ | Extra | For | Installs |
76
+ |---|---|---|
77
+ | `listen` | `rn listen`, `rn ref`, `rn signal -o` | librosa, matplotlib |
78
+ | `transcribe` | `rn listen --transcribe` | faster-whisper |
79
+ | `faces` | `rn faces` | OpenCV (headless); the YuNet model is fetched once into `~/.cache/rn` and checked against its sha256 |
80
+ | `capture` | `rn capture` | Playwright |
81
+ | `captions` | `rn captions` | pysubs2 |
82
+ | `fonts` | `rn fonts --licenses` | fontTools |
83
+ | `matte` | `rn matte` | rembg (onnxruntime); models download on first use |
84
+ | `export` | `rn export` | OpenTimelineIO and its Final Cut / FCP 7 adapters |
85
+ | `all` | everything above | |
86
+
87
+ ## A first cut
88
+
89
+ Starting from two generated shots, a music bed, a whoosh and an end card:
90
+
91
+ ```sh
92
+ rn sheet shots/s2.mp4 --fps 4 # look at a take: labelled frames, small enough to view
93
+ rn listen music.wav --json music.json # loudness, beats, tempo
94
+ rn capture title.html -o title --frames 90 --size 1080x300 # an HTML title, frame by frame, transparent
95
+ ```
96
+
97
+ `title.html` draws frame `i` when asked. The frame number is the time, so any frame order or number of shards gives
98
+ the same pixels:
99
+
100
+ ```html
101
+ <canvas id=c></canvas><script>
102
+ const x = c.getContext('2d'); c.width = RN.width; c.height = RN.height;
103
+ window.renderFrame = i => {
104
+ x.clearRect(0, 0, c.width, c.height);
105
+ x.globalAlpha = Math.min(1, i / 10);
106
+ x.fillStyle = '#fff'; x.font = '700 120px sans-serif'; x.textAlign = 'center';
107
+ x.fillText('RENOISE', c.width / 2, 160);
108
+ };</script>
109
+ ```
110
+
111
+ `edit.json` puts it together:
112
+
113
+ ```json
114
+ {
115
+ "output": {"width": 1080, "height": 1920, "fps": 30},
116
+ "tracks": [
117
+ {"clips": [
118
+ {"id": "open", "src": "shots/s1.mp4", "in": 0.4, "out": 3.2},
119
+ {"id": "hero", "src": "shots/s2.mp4", "ranges": [[0, 1.5], [2.5, 4]],
120
+ "transition": {"type": "fade", "duration": 0.3}},
121
+ {"id": "end", "image": "card.png", "duration": 1.5, "fit": "contain"}
122
+ ]},
123
+ {"clips": [
124
+ {"id": "title", "frames": "title/%05d.png", "fps": 30, "y": 1500,
125
+ "at": {"clip": "hero", "offset": 0.2}, "until": {"clip_end": "hero"}}
126
+ ]}
127
+ ],
128
+ "audio": [
129
+ {"src": "music.wav", "role": "music", "pivot": 12.0, "at": {"clip": "hero"}, "fade_out": 1},
130
+ {"src": "whoosh.wav", "role": "sfx", "pivot": 0.18, "at": {"clip": "hero"}}
131
+ ],
132
+ "mix": {"lufs": -14, "cut_fade": 0.01}
133
+ }
134
+ ```
135
+
136
+ What each part does:
137
+
138
+ - **Main track.** It plays back to back. `hero` plays two spans of one take, dissolving in from `open`.
139
+ - **Title.** It follows the `hero` clip by id rather than by hand-computed seconds. If `open` gets longer, the title
140
+ moves with it.
141
+ - **Music.** `pivot` lands the music's 12 s point (say, the drop) on the cut into `hero`, and the head of the track is
142
+ trimmed away.
143
+ - **Whoosh.** Its peak lands on the same cut.
144
+
145
+ ```sh
146
+ rn assemble edit.json --dry-run # the laid-out timeline, frame by frame, nothing rendered
147
+ rn assemble edit.json -o draft.mp4 --preview # half-size draft, about 3x faster
148
+ rn assemble edit.json -o cut.mp4 # full render; unchanged clips are reused from .rn-cache
149
+ rn qa cut.mp4 --edl edit.json # exit 1 on FAIL; WARN means look at it
150
+ ```
151
+
152
+ ```text
153
+ timeline 1080x1920 30fps 210 frames 7.0s
154
+ open: frames 0..83
155
+ hero: frames 75..119 (xfade 9f)
156
+ hero.1: frames 120..164
157
+ end: frames 165..209
158
+ title: overlay frames 81..164
159
+ ...
160
+ WARN level_jumps: 6.0s -40->-13 dB, 6.5s -45->-19 dB
161
+ qa PASS: 0 failed, 1 warnings, 17 passed (cut.mp4)
162
+ ```
163
+
164
+ A WARN may be intended. Here the jumps are the music's own hard-hitting beats. You can waive it in the EDL with
165
+ `"accept": [{"check": "level_jumps", "note": "the music's hits"}]`, or with `rn qa --accept level_jumps`. A FAIL can
166
+ never be waived.
167
+
168
+ Make further changes by operation instead of rewriting the JSON:
169
+
170
+ ```sh
171
+ rn edit edit.json split open --at 1.5
172
+ rn edit edit.json set title meta.copy="new tagline"
173
+ rn edit edit.json delete end --lift
174
+ ```
175
+
176
+ ## Commands
177
+
178
+ `rn <command> --help` is the reference. Every command prints a short summary, and `--json FILE` writes the full result.
179
+
180
+ **Measure**
181
+
182
+ | Command | What it gives |
183
+ |---|---|
184
+ | `rn probe FILE` | Decoded frame count, rational fps, size, color tags, separate video and audio durations, faststart (JSON) |
185
+ | `rn frame FILE --index N \| --time T \| --last -o out.png` | One exact frame, e.g. a tail frame to continue a shot |
186
+ | `rn sheet FILE... [--fps R \| --every N \| --indices ... \| --around T,.. --diff]` | Contact sheets labelled with frame number and timecode, each under 125 KB |
187
+ | `rn cuts FILE [--sheet JPG]` | Shot boundaries by decoded frame, plus weaker "suspects" to check |
188
+ | `rn listen FILE [--transcribe]` | Loudness, silences, onsets, tempo with half/double candidates, beat times, Whisper word times, one picture |
189
+ | `rn signal FILE [-o PNG]` | Per-frame luma, motion, saturation, loudness, low/mid/high bands, onset strength; flash frames, clicks |
190
+ | `rn align A B` | Where recording B sits inside A, by sound: offset, `match`, `distinct` |
191
+ | `rn faces FILE` | Face boxes and landmarks per frame (measurement only) |
192
+ | `rn text FILE` | On-screen text lines, their boxes and when they are on screen (tesseract) |
193
+ | `rn matte FILE -o DIR` | Subject alpha per frame as grey PNGs, ready to use as an EDL `mask`; `--cutout` for RGBA |
194
+ | `rn ref FILE` | A reference breakdown: shots, sound, cut-to-beat sync, every frame on sheets, and a reading guide |
195
+
196
+ **Render**
197
+
198
+ | Command | What it gives |
199
+ |---|---|
200
+ | `rn capture page.html -o DIR --frames N --size WxH` | An HTML page rendered frame by frame (`window.renderFrame(i)`) to RGBA PNGs, sharded across Chromiums |
201
+ | `rn fx [--source]` | The effects library (grain, vignette, leak, flash, glitch, ...) as readable source for pages and numpy |
202
+ | `rn captions WORDS.json --size WxH -o caps.ass` | Your script aligned to Whisper timing; CJK chunking, balanced lines, safe areas, kept off faces |
203
+ | `rn fonts [--script ja]` | Installed families with the weights that really exist, and the scripts they cover |
204
+ | `rn assets search\|get` | Licensed SFX and overlay footage, checked against sha256 |
205
+
206
+ **Edit and assemble**
207
+
208
+ | Command | What it gives |
209
+ |---|---|
210
+ | `rn edit EDL ids\|set\|split\|delete\|insert\|move` | Changes an EDL by clip id. Each step is checked against the whole timeline before the file is written |
211
+ | `rn assemble EDL -o OUT [--preview] [--range A-B] [--placeholders]` | Renders the EDL frame-exactly. `--placeholders` turns missing shots into labelled cards |
212
+
213
+ **Check and deliver**
214
+
215
+ | Command | What it gives |
216
+ |---|---|
217
+ | `rn qa FILE [--edl EDL] [--accept CHECK[@T]]` | Delivery checks. Format: frames, fps, size, codec, color, faststart, fixed frame rate. Sound: A/V length, loudness, true peak, dead air, level jumps, clicks. Picture: black, frozen, bars |
218
+ | `rn compare A B [--cuts] [--video sbs.mp4]` | Two videos at the same times on one sheet, cut timing paired, optional side-by-side video |
219
+ | `rn finish FILE -o OUT [--height N]` | Brings any video to the delivery standard; `--height` makes a scaled copy |
220
+ | `rn export EDL -o cut.otio\|.fcpxml\|.xml [--srt]` | The edit as a project for Final Cut, Premiere, Resolve or any OTIO tool |
221
+
222
+ ## The EDL in one screen
223
+
224
+ - **Tracks.** `tracks[0]` is the main track: its clips play back to back. Later tracks are overlays, placed with `at`
225
+ in time and with `x / y / w / h` in space.
226
+ - **Audio items.** Items in `audio` are placed with `at`. `role` is `music`, `voice` or `sfx`; music ducks under voice
227
+ when `mix.duck_db` is set.
228
+ - **One time grid.** `output.fps` is the only clock. A clip of N seconds is exactly `round(N × fps)` frames.
229
+ - **Sources.** A clip's source is one of: `src` (video, or another `.json` EDL to nest), `frames` (a numbered PNG
230
+ sequence, alpha kept), `image`, or `color`.
231
+ - **Positions inside a source.** `in` and `out` take seconds, `{"frame": n}` or `{"shot": n}` (a shot found by
232
+ `rn cuts`). `ranges` plays several spans of one source in a row.
233
+ - **Placing on the timeline.** `at` and `until` take seconds or an anchor plus `offset`:
234
+ - `{"clip": "id"}` and `{"clip_end": "id"}`
235
+ - `{"marker": name}`
236
+ - `{"beat": n, "listen": file}` and `{"word": n, "listen": file}`
237
+
238
+ `pivot` places a point inside the source on the anchor instead of the source's start.
239
+ - **Keys.** Written as `[t, value, ease]`. Overlays animate `scale`, `x`, `y`, `rotate`, `opacity` and `speed`; the
240
+ main track `scale`, `x`, `y`, `rotate`, `speed` and `gain_db`; audio items `gain_db`.
241
+ - **Compositing.** `blend` takes `screen`, `multiply`, `add` and other blend modes. `mask` takes a full-frame matte, as
242
+ luma or alpha, optionally inverted. `vf` and `af` insert one raw ffmpeg filter chain.
243
+ - **Bookkeeping.** `id` and `meta` go on anything; `meta` is kept as given. `accept` waives qa warnings judged
244
+ intended.
245
+
246
+ The full schema is in `rn assemble --help` and [SPEC.md](SPEC.md) §4.
247
+
248
+ ## Conventions
249
+
250
+ - **Output.** A few summary lines on stdout. `--json FILE` holds the details. `rn probe` prints JSON directly.
251
+ - **Errors.** A failure exits non-zero. Its last line is `rn: error: <what to fix>`, written so you can relay it as is.
252
+ If ffmpeg is missing, the exit code is 2. `rn qa` exits 1 on FAIL.
253
+ - **Frame exactness.** Frames are picked and cut by decoded index or decode timestamp, never by input seeking. Every
254
+ render is checked against the expected frame count.
255
+ - **Viewing budget.** Sheets default to 125 KB, so four fit in one 512 KB look.
256
+ - **Cache.** Rendered slots and nested edits are stored by content in `.rn-cache/` next to the EDL. Moving or
257
+ renaming a clip reuses its render. `--no-cache` renders everything afresh. Delete the folder to reclaim space.
258
+ - **Environment variables.**
259
+ - `RN_CACHE`: cache folder
260
+ - `RN_YUNET_MODEL`: face model file
261
+ - `RN_ASSETS_URL` / `RN_ASSETS_CACHE`: asset library
262
+ - `U2NET_HOME`: rembg models
263
+
264
+ ## Delivery standard
265
+
266
+ `rn assemble` and `rn finish` produce this standard, and `rn qa` checks it:
267
+
268
+ - **Video.** H.264 High, yuv420p, BT.709 limited range (converted, not just tagged), square pixels, `moov` first. The
269
+ SPS sets `fixed_frame_rate_flag=1`; with 0, some players show a 30 fps video at half its length.
270
+ - **Audio.** AAC 48 kHz stereo at −14 LUFS (configurable), with the true peak measured on the **encoded** AAC at or
271
+ below −1 dBTP.
272
+ - **Length.** Audio is cut to the video by sample, so the container is never longer than the picture.
273
+
274
+ ## With agents
275
+
276
+ `rn` is designed to be called by a model. `--help` is the documentation and loads only when needed. One sandbox call
277
+ can chain several commands. Summaries are short, and errors say what to change. Sheets, `listen`, `signal` and `align`
278
+ exist because a model cannot watch or listen; check the result through them rather than through the format alone.
279
+ `rn qa` passing does not mean the edit is good.
280
+
281
+ In the Renoise media sandbox, `rn` comes preinstalled at the version pinned by the sandbox template.
282
+
283
+ ## Development
284
+
285
+ ```sh
286
+ uv run pytest -q # Python 3.12 (as the sandbox); needs ffmpeg with libass; playwright install chromium for capture tests
287
+ ```
288
+
289
+ CI runs on Debian trixie with ffmpeg 7.1 (the same base as the sandbox) and on macOS with ffmpeg-full. The tests
290
+ generate their own media.
291
+
292
+ To release:
293
+
294
+ 1. Bump `__version__` in `rn/__init__.py`.
295
+ 2. Tag the commit:
296
+
297
+ ```sh
298
+ git tag v$(sed -n "s/^__version__ = '\(.*\)'/\1/p" rn/__init__.py) && git push --tags
299
+ ```
300
+
301
+ `.github/workflows/release.yml` then publishes the tag to PyPI through trusted publishing.
302
+
303
+ Design notes, the full EDL contract and the roadmap are in [SPEC.md](SPEC.md) (in Chinese).
304
+
305
+ ## License
306
+
307
+ MIT
@@ -0,0 +1,33 @@
1
+ renoise_kit-0.5.0.dist-info/licenses/LICENSE,sha256=UrTmgBkpFSS63iC4X_Vt1YEglPvBt2X5dJ_YOWr_0rc,1064
2
+ rn/__init__.py,sha256=JyGbOqFS5uX5HY6tfc-EQRbjR90F_MuoZC27A8Vsrp8,132
3
+ rn/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
4
+ rn/align.py,sha256=fiEa4isdj6sZ1awk2jcGR77cMLPMC8sYrtF5PUB4kOw,5111
5
+ rn/assemble.py,sha256=kZzUGrxNsKdhXO0e7DPtMjwHERpMcFVJncMTp5QsNPA,33658
6
+ rn/assets.py,sha256=eO9bHmJKoNQuy8SINI_IFRljKbCYjC-W7AkNx3HXTAs,5187
7
+ rn/audio.py,sha256=PZcy5C8bSu53w9kwl9FNKoyqwKIPr8LxHy45645p8EA,4286
8
+ rn/captions.py,sha256=sx4IJVF5bkdpBpTunOaLErtASqDoV_UN9Kcl7Th8-g0,18792
9
+ rn/capture.py,sha256=kuuCrinwOETX2RWDVGm66HFDC44FnkDf3Xo0-G9SfSg,5161
10
+ rn/cli.py,sha256=0p1xykHpr8QYFNgQzPh5vle9HDpVkdJOGUCDKFb3MG0,39519
11
+ rn/compare.py,sha256=-ELBwu8igLRbgDCJWYUkIWHbsWpyt36A1L3RA-PQDs0,6491
12
+ rn/cuts.py,sha256=hvHNjK5AzVzppHBIFZ82mrJxzny-sgBfMt7-AmC91uA,3977
13
+ rn/editops.py,sha256=l2J0gSi8XDKv7wO28KWG2wfEOjItZx5jYD82Q6Jw4BI,11138
14
+ rn/edl.py,sha256=d6-DUoSkNd0ge9W9EM5ZnYNtV01IMP84OP7DjeTw8HA,46805
15
+ rn/export.py,sha256=HxpayyLpG4qNaZH3Kgo6KsNV3LNyBrmLECTJGMnHQrM,10725
16
+ rn/faces.py,sha256=5I4ck2Mtwe_Sh8eT5WFntM2npiPM1yZF2Yr-4KcQ0HM,7054
17
+ rn/ff.py,sha256=puTeMvB4r5qshpqqGrzA9LPOID633PhS2t8KtJucL7o,8319
18
+ rn/fonts.py,sha256=pL-6YxUf0KUB8G5Cx8BX9VLRbczM4oqajWESAND_y8s,4029
19
+ rn/frames.py,sha256=b-pzMYv1NB6ectMgyq_r-qq6aJI7PwHo5ACyCNlqeOw,10343
20
+ rn/listen.py,sha256=QuVPPzXw9RPkFsV15tySdUUAVUdIvdxaGomifjgxYws,10232
21
+ rn/matte.py,sha256=uka85o-5tVCVP_h0N7qX3RRjc3g9Lq_kvKH49sxR9Mo,2759
22
+ rn/qa.py,sha256=MpSz8nFDVP0xOKzxS2DRppVAcYvYPQHIw6V2gPhAndI,13328
23
+ rn/ref.py,sha256=xEFc2lFMU6pu-PQxlbEt1YLEvz2v6eyTqOdoIHuHt9o,4139
24
+ rn/ref_guide.md,sha256=5DD0XKANkHbPpmpnKCNaHcFyLVxiSqM9uiQ7LjodZ-E,16072
25
+ rn/signal.py,sha256=0g7iuc3KKwfc_2p6T_b4wx3UfH6ykLIfdySL1C-vp9A,9336
26
+ rn/text.py,sha256=F00CLac4yVUu0jKcoPaMsyLdlRZ5tJdw5rhWUf66fD4,5446
27
+ rn/fx/__init__.py,sha256=2CG8uWi3XvKn9Q78VNqbTKc69q8_2-vqjX_usMcRoWk,8742
28
+ rn/fx/fx.js,sha256=eNvKSTZBKH3CT9doMGhy_1B4DMYiAaJfPPXsIQ5fRuE,8805
29
+ renoise_kit-0.5.0.dist-info/METADATA,sha256=FXkFuQcRjEjmEkMCiFiRYtP6W7H3jspwLOQYa3LHfwU,15010
30
+ renoise_kit-0.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
31
+ renoise_kit-0.5.0.dist-info/entry_points.txt,sha256=gHfCS6a60H1bKsOKWiWwE_c33y68bss5n5RsxjqdKbM,35
32
+ renoise_kit-0.5.0.dist-info/top_level.txt,sha256=ijW4XJ8V9ycQoEbMh6E26F5Jh9CYx-UNrWrIJxiuyaQ,3
33
+ renoise_kit-0.5.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ rn = rn.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Renoise
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ rn
rn/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """Renoise kit (`rn`, PyPI `renoise-kit`). Released on its own; the media sandbox template pins a version."""
2
+ __version__ = '0.5.0'
rn/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
rn/align.py ADDED
@@ -0,0 +1,92 @@
1
+ """rn align: where one recording sits inside another, by their sound. Finds a song inside a reference edit, syncs camera
2
+ angles or a separately recorded voice, checks picture against sound. Measurement only: it reports the offset and how sure
3
+ it is; placing anything is yours (e.g. an audio item's "at", or "in" on a clip).
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import numpy as np
8
+
9
+ from .ff import RnError, probe
10
+ from .signal import pcm
11
+
12
+ COARSE_RATE = 200 # Hz envelope for the global search (5 ms)
13
+ FINE_RATE = 8000 # Hz waveform for refinement (0.125 ms)
14
+
15
+
16
+ def _envelope(samples: np.ndarray, rate: int) -> np.ndarray:
17
+ """Onset-like envelope: log energy rises, which survive different mixes, levels and codecs better than waveforms."""
18
+ hop = rate // COARSE_RATE
19
+ frames = len(samples) // hop
20
+ energy = np.log1p(1000 * np.sqrt(np.mean(samples[:frames * hop].reshape(frames, hop) ** 2, axis=1)))
21
+ rise = np.maximum(np.diff(energy, prepend=energy[:1]), 0)
22
+ return (rise - rise.mean()) / (rise.std() or 1.0)
23
+
24
+
25
+ def _lags(a: np.ndarray, b: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
26
+ """Correlation of a with b at every lag (B's start inside A, in samples)."""
27
+ size = 1 << int(np.ceil(np.log2(len(a) + len(b))))
28
+ corr = np.fft.irfft(np.fft.rfft(a, size) * np.conj(np.fft.rfft(b, size)), size)
29
+ lags = np.concatenate([np.arange(0, len(a)), np.arange(-len(b) + 1, 0)])
30
+ values = np.concatenate([corr[:len(a)], corr[size - len(b) + 1:]])
31
+ return lags, values
32
+
33
+
34
+ def align_command(path_a: str, path_b: str, *, max_offset: float | None) -> dict:
35
+ for path in (path_a, path_b):
36
+ if not probe(path)['audio']:
37
+ raise RnError(f'{path}: no audio to align by')
38
+ a, b = pcm(path_a, FINE_RATE), pcm(path_b, FINE_RATE)
39
+ if min(len(a), len(b)) < FINE_RATE:
40
+ raise RnError('both recordings need at least 1 s of sound')
41
+ env_a, env_b = _envelope(a, FINE_RATE), _envelope(b, FINE_RATE)
42
+ lags, values = _lags(env_a, env_b)
43
+ # Normalise by the overlap so short overlaps at the edges do not win on their own.
44
+ overlap = np.minimum(len(env_a) - np.maximum(lags, 0), len(env_b) + np.minimum(lags, 0)).clip(min=1)
45
+ score = values / overlap
46
+ usable = overlap >= min(len(env_a), len(env_b)) * 0.25
47
+ if max_offset is not None:
48
+ usable &= np.abs(lags) <= max_offset * COARSE_RATE
49
+ if not usable.any():
50
+ raise RnError('no offset leaves enough overlap (at least a quarter of the shorter recording)')
51
+ score = np.where(usable, score, -np.inf)
52
+ best = int(np.argmax(score))
53
+ lag = int(lags[best])
54
+ # Confidence: the best peak against the best one at least 0.25 s away (a beat-periodic song has near-equal peaks).
55
+ away = usable & (np.abs(lags - lag) > COARSE_RATE // 4)
56
+ runner = float(score[away].max()) if away.any() else 0.0
57
+ peak = float(score[best])
58
+ # Refine on the waveform within +-10 ms of the coarse lag.
59
+ coarse = lag / COARSE_RATE
60
+ start_a = max(0, int(coarse * FINE_RATE))
61
+ start_b = max(0, int(-coarse * FINE_RATE))
62
+ length = min(len(a) - start_a, len(b) - start_b, 30 * FINE_RATE)
63
+ window = FINE_RATE // 100
64
+ seg_b = b[start_b:start_b + length]
65
+ best_fine, best_value = 0, -np.inf
66
+ if length > 2 * window:
67
+ seg_a = a[max(0, start_a - window):start_a + length + window]
68
+ fine_lags, fine_values = _lags(seg_a, seg_b)
69
+ pad = start_a - max(0, start_a - window)
70
+ mask = np.abs(fine_lags - pad) <= window
71
+ if mask.any():
72
+ pick = np.argmax(np.where(mask, fine_values, -np.inf))
73
+ best_fine, best_value = int(fine_lags[pick]) - pad, float(fine_values[pick])
74
+ offset = round(coarse + best_fine / FINE_RATE, 4)
75
+ # How alike the two are where they overlap at that offset (Pearson on the waveform, up to 30 s).
76
+ shift_a = max(0, round(offset * FINE_RATE))
77
+ shift_b = max(0, round(-offset * FINE_RATE))
78
+ span = min(len(a) - shift_a, len(b) - shift_b, 30 * FINE_RATE)
79
+ match = float(np.corrcoef(a[shift_a:shift_a + span], b[shift_b:shift_b + span])[0, 1]) if span > FINE_RATE // 10 else 0.0
80
+ info_a = probe(path_a)
81
+ fps = info_a['video']['fps_float'] if info_a['video'] and not info_a['video']['still'] else None
82
+ return {'a': path_a, 'b': path_b, 'offset': offset, 'offset_frames': round(offset * fps) if fps else None,
83
+ 'match': round(match, 3), 'distinct': round(max(0.0, min(1.0, (peak - runner) / peak)) if peak > 0 else 0.0, 3)}
84
+
85
+
86
+ def summary_lines(result: dict) -> list[str]:
87
+ frames = f' ({result["offset_frames"]} frames of A)' if result['offset_frames'] is not None else ''
88
+ where = 'starts' if result['offset'] >= 0 else 'started'
89
+ return [f'B {where} at {result["offset"]:+.4f}s on A\'s clock{frames}: A time = B time + offset',
90
+ f'match {result["match"]} (waveform correlation where they overlap: near 1 the same sound, ~0 unrelated; a different '
91
+ f'mix of the same song reads lower), distinct {result["distinct"]} (how far ahead of the best offset 0.25 s or more '
92
+ 'away; low for repetitive music, whose next bar looks the same)']