davinci-resolve-mcp 2.114.0 → 2.115.0
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.
- package/docs/reference/api-limitations.md +8 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/author-interchange.mjs +48 -4
- package/resolve-advanced/server/editorial.mjs +8 -4
- package/resolve-advanced/server/tools/drt.mjs +1 -1
- package/resolve-advanced/vendor/drp-format/assemble-timeline.js +8 -0
- package/resolve-advanced/vendor/drp-format/cut-media.js +56 -7
- package/resolve-advanced/vendor/drp-format/templates/media-clip-r19.drp +0 -0
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/api_truth.py +26 -0
|
@@ -12,7 +12,7 @@ that none exists).
|
|
|
12
12
|
|
|
13
13
|
**Verified on:** DaVinci Resolve Studio 21.0.2
|
|
14
14
|
|
|
15
|
-
**Totals:** 33 missing capabilities,
|
|
15
|
+
**Totals:** 33 missing capabilities, 44 bugs / unreliable behaviors.
|
|
16
16
|
|
|
17
17
|
The authoritative source is the runtime-queryable `api_truth` ledger
|
|
18
18
|
(`resolve_control api_truth "<query>"`); this document is generated from
|
|
@@ -526,6 +526,13 @@ values, or automation-hostile modal prompts.
|
|
|
526
526
|
- **Reference:** [issue #171](https://github.com/samuelgursky/davinci-resolve-mcp/issues/171)
|
|
527
527
|
- **Tags:** timeline, import, silent-failure, unreliable-return
|
|
528
528
|
|
|
529
|
+
### Audio tracks cannot be grown in an imported timeline; Fairlight strips live in the pool Sm2Sequence.FieldsBlob
|
|
530
|
+
|
|
531
|
+
- **Object:** `Sm2TiTrack (audio) / FLStudioModelBA`
|
|
532
|
+
- **Behavior:** An audio track added to a SeqContainer by cloning imports fine, reads back fine (track count, items, everything), and renders SILENT. The per-timeline Fairlight model (FLStudioModelBA, ~7KB compressed to ~420KB, inside the media pool's Sm2Sequence.FieldsBlob) holds one strip per audio track, and a track without a strip is mute. Measured by elimination on 19.1.3.7: clip byte-identical to a live-authored one, track byte-identical (SubType is the CHANNEL FORMAT code - 1=mono - not an ordinal), pool entry shared with a playing A1 clip - still silent; only a timeline whose template was CAPTURED with the tracks plays.
|
|
533
|
+
- **Workaround / current handling:** Never clone audio tracks offline. Capture the template with the audio tracks already present (the r19 media template carries 8 mono tracks with valid strips; drt.assemble audioOnly cuts land on them and render at native level). Refuse placements beyond the captured ceiling. Audio aliveness is readback-blind: verify by rendered RMS, not structure.
|
|
534
|
+
- **Tags:** fairlight, audio, import, silent-failure, drt
|
|
535
|
+
|
|
529
536
|
### MediaTimemapBA keyframes are generation-split; 19.x silently ignores the R21 protobuf form
|
|
530
537
|
|
|
531
538
|
- **Object:** `Sm2TimeMap (per-clip retime blob)`
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.
|
|
40
|
+
VERSION = "2.115.0"
|
|
41
41
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
42
42
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
43
43
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
|
@@ -207,7 +207,9 @@ export function eventsToOTIO(events, opts = {}) {
|
|
|
207
207
|
* dropped transitions (cross-dissolves are AUTHORED when the predecessor
|
|
208
208
|
* abuts the cut and both sides have handle media — render-verified on
|
|
209
209
|
* 19.1.3.7; otherwise dropped with the reason, as a cut at the boundary),
|
|
210
|
-
* and
|
|
210
|
+
* and audio: A-track events are AUTHORED as audioOnly cuts on their own
|
|
211
|
+
* audio tracks (render-verified; the template carries 8 Fairlight-valid
|
|
212
|
+
* audio tracks) — their presence suppresses the A1 convenience mirror.
|
|
211
213
|
*
|
|
212
214
|
* @param {Array} events - normalized events (parseInterchange shape)
|
|
213
215
|
* @param {object} opts
|
|
@@ -226,11 +228,13 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
226
228
|
throw new TypeError('eventsToAssembleSpec: sourceMap {reel: {mediaFilePath, spec}} is required');
|
|
227
229
|
}
|
|
228
230
|
const ORIGIN = 86400;
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
+
const isAudio = (t) => /^A\d*$/.test(String(t || ''));
|
|
232
|
+
const vids = events.filter((e) => !isAudio(e.track) && e.recIn != null && e.recOut != null);
|
|
233
|
+
const auds = events.filter((e) => isAudio(e.track) && e.recIn != null && e.recOut != null);
|
|
234
|
+
const audioSkipped = events.length - vids.length - auds.length;
|
|
231
235
|
if (!vids.length) throw new Error('eventsToAssembleSpec: no video events with record ranges');
|
|
232
236
|
|
|
233
|
-
const unmapped = [...new Set(vids.map((e) => e.source).filter((srcName) => !sourceMap[srcName]))];
|
|
237
|
+
const unmapped = [...new Set([...vids, ...auds].map((e) => e.source).filter((srcName) => !sourceMap[srcName]))];
|
|
234
238
|
if (unmapped.length) {
|
|
235
239
|
throw new Error(
|
|
236
240
|
`eventsToAssembleSpec: unmapped source reel(s): ${unmapped.join(', ')} — ` +
|
|
@@ -287,6 +291,44 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
287
291
|
perSource.get(e.source).push(cut);
|
|
288
292
|
}
|
|
289
293
|
|
|
294
|
+
// Audio events become explicit audioOnly cuts on their own audio tracks —
|
|
295
|
+
// this SUPPRESSES the convenience A1 mirror downstream (explicit audio
|
|
296
|
+
// wins). Render-verified on 19.1.3.7: an offline A3 cut plays at the
|
|
297
|
+
// native control level through the captured 8-audio-track template's
|
|
298
|
+
// Fairlight strips. Retimed audio is not authored (no audio timemap yet).
|
|
299
|
+
const audioTrackNum = (t) => { const m = /^A(\d+)?$/.exec(String(t)); return m && m[1] ? parseInt(m[1], 10) : 1; };
|
|
300
|
+
const audioPlacements = [];
|
|
301
|
+
const audioRetimesSkipped = [];
|
|
302
|
+
for (const e of auds) {
|
|
303
|
+
const recIn = ORIGIN + (toTl(e.recIn, e.fps) - minRec);
|
|
304
|
+
const recOut = ORIGIN + (toTl(e.recOut, e.fps) - minRec);
|
|
305
|
+
const durationFrames = recOut - recIn;
|
|
306
|
+
if (durationFrames <= 0) continue;
|
|
307
|
+
if ((e.speed ?? 100) !== 100 || e.reverse) {
|
|
308
|
+
audioRetimesSkipped.push({ index: e.index, source: e.source, speed: e.speed, reverse: !!e.reverse, reason: 'audio retime not supported — played at 100%' });
|
|
309
|
+
}
|
|
310
|
+
const track = audioTrackNum(e.track);
|
|
311
|
+
const cut = { startFrame: recIn, durationFrames, srcIn: toTl(e.srcIn ?? 0, e.fps), audioOnly: true, track };
|
|
312
|
+
audioPlacements.push({ start: recIn, end: recOut, index: e.index, track });
|
|
313
|
+
if (!perSource.has(e.source)) perSource.set(e.source, []);
|
|
314
|
+
perSource.get(e.source).push(cut);
|
|
315
|
+
}
|
|
316
|
+
audioPlacements.sort((a, b) => a.start - b.start);
|
|
317
|
+
const audByTrack = new Map();
|
|
318
|
+
for (const pl of audioPlacements) {
|
|
319
|
+
if (!audByTrack.has(pl.track)) audByTrack.set(pl.track, []);
|
|
320
|
+
audByTrack.get(pl.track).push(pl);
|
|
321
|
+
}
|
|
322
|
+
for (const [trk, pls] of audByTrack) {
|
|
323
|
+
for (let i = 1; i < pls.length; i += 1) {
|
|
324
|
+
if (pls[i].start < pls[i - 1].end) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`eventsToAssembleSpec: audio events ${pls[i - 1].index} and ${pls[i].index} ` +
|
|
327
|
+
`overlap on audio track ${trk} after frame conversion — one track cannot hold both.`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
290
332
|
// Overlap is judged PER VIDEO TRACK — V2 stacking over V1 is legitimate
|
|
291
333
|
// conform geometry (render-verified: an upper-track clip covers the lower).
|
|
292
334
|
placements.sort((a, b) => a.start - b.start);
|
|
@@ -349,6 +391,8 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
349
391
|
videoEvents: vids.length,
|
|
350
392
|
sources: media.length,
|
|
351
393
|
audioEventsSkipped: audioSkipped,
|
|
394
|
+
authoredAudioEvents: audioPlacements.length,
|
|
395
|
+
audioRetimesSkipped,
|
|
352
396
|
upperTrackCutsVideoOnly: placements.filter((pl) => pl.track > 1).length,
|
|
353
397
|
flattenedRetimes,
|
|
354
398
|
authoredRetimes,
|
|
@@ -91,7 +91,9 @@ export function parseEDL(text, opts = {}) {
|
|
|
91
91
|
const head = tokens.slice(0, tokens.indexOf(tcs[tcs.length - 4]));
|
|
92
92
|
const [eventNum, reel, channel, transition] = head;
|
|
93
93
|
const dur = transition && transition !== 'C' ? Number(head[4]) : 0;
|
|
94
|
-
const
|
|
94
|
+
const chan = String(channel || '');
|
|
95
|
+
const am = /^A(\d+)?$/i.exec(chan);
|
|
96
|
+
const track = am && !/V/i.test(chan) ? (am[1] && am[1] !== '1' ? `A${am[1]}` : 'A') : (/A/i.test(chan) && !/V/i.test(chan) ? 'A' : 'V');
|
|
95
97
|
events.push(
|
|
96
98
|
evt({
|
|
97
99
|
index: Number(eventNum),
|
|
@@ -117,11 +119,13 @@ export function parseOTIO(otio, opts = {}) {
|
|
|
117
119
|
const events = [];
|
|
118
120
|
let idx = 1;
|
|
119
121
|
let vNum = 0;
|
|
122
|
+
let aNum = 0;
|
|
120
123
|
for (const track of tracks) {
|
|
121
124
|
const isAudio = track.kind === 'Audio';
|
|
122
|
-
if (
|
|
123
|
-
// First
|
|
124
|
-
|
|
125
|
+
if (isAudio) aNum += 1; else vNum += 1;
|
|
126
|
+
// First track of each kind keeps the bare letter (compat); higher tracks
|
|
127
|
+
// are numbered ('V2', 'A2', …).
|
|
128
|
+
const kind = isAudio ? (aNum === 1 ? 'A' : `A${aNum}`) : (vNum === 1 ? 'V' : `V${vNum}`);
|
|
125
129
|
let rec = 0;
|
|
126
130
|
for (const child of track.children || []) {
|
|
127
131
|
const schema = child.OTIO_SCHEMA || '';
|
|
@@ -46,7 +46,7 @@ const assembleSchema = z.object({
|
|
|
46
46
|
spec: z
|
|
47
47
|
.object({})
|
|
48
48
|
.passthrough()
|
|
49
|
-
.describe("assembleTimeline spec: { timelineName?, media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed?/reverse? (constant retime, e.g. 0.5, forward or backwards; video-only; readback+render-verified on 19)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, generatorName? ('Solid Color'|'SMPTE Color Bar'|'Grey Scale' render-verified on 19), ...}], transitions? }. startFrame is timeline-absolute (origin 86400)."),
|
|
49
|
+
.describe("assembleTimeline spec: { timelineName?, media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed?/reverse? (constant retime, e.g. 0.5, forward or backwards; video-only; readback+render-verified on 19),audioOnly?+track? (explicit AUDIO placement on audio track 1-8; presence suppresses the A1 mirror; render-verified on 19)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, generatorName? ('Solid Color'|'SMPTE Color Bar'|'Grey Scale' render-verified on 19), ...}], transitions? }. startFrame is timeline-absolute (origin 86400)."),
|
|
50
50
|
outputPath: z.string().describe('Absolute path where the importable .drt will be written'),
|
|
51
51
|
targetAppVersion: z
|
|
52
52
|
.union([z.string(), z.number()])
|
|
@@ -114,6 +114,14 @@ async function assembleTimeline(spec = {}) {
|
|
|
114
114
|
sources.forEach((src, i) => {
|
|
115
115
|
(src.cuts || []).forEach((cut) => {
|
|
116
116
|
const out = mediaRefs[i] ? { ...cut, mediaRef: mediaRefs[i] } : { ...cut };
|
|
117
|
+
if (cut.audioOnly) {
|
|
118
|
+
out.srcMeta = {
|
|
119
|
+
name: src.mediaFilePath.split('/').pop(),
|
|
120
|
+
mediaFilePath: src.mediaFilePath,
|
|
121
|
+
fps: Math.round(src.spec.fps || 24),
|
|
122
|
+
frameCount: src.spec.frameCount,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
117
125
|
if (cut.reverse || (cut.speed !== undefined && cut.speed !== 1)) {
|
|
118
126
|
// Constant-speed retime (forward only). The Sm2TimeMap spans the
|
|
119
127
|
// whole source stretched by 1/speed; the clip windows into it with
|
|
@@ -38,6 +38,10 @@ const { clipDbId, setClipStart, setClipDuration, setClipIn } = require('./splice
|
|
|
38
38
|
* track is the 1-based VIDEO track (default 1); missing tracks are grown as
|
|
39
39
|
* empty clones. Cuts on track > 1 are placed VIDEO-ONLY — their audio would
|
|
40
40
|
* overlap the track-1 cuts' audio on A1 (the template has a single A1).
|
|
41
|
+
* audioOnly:true makes a cut an AUDIO clip placement instead (track then
|
|
42
|
+
* indexes AUDIO tracks, grown as needed) — and the presence of ANY
|
|
43
|
+
* audioOnly cut suppresses the implicit A1 mirror of track-1 video cuts
|
|
44
|
+
* (explicit audio wins over the convenience mirror).
|
|
41
45
|
* @param {string} [opts.timelineUuid]
|
|
42
46
|
* @returns {Promise<{buffer: Buffer, cutCount: number, clipDbIds: string[]}>}
|
|
43
47
|
*/
|
|
@@ -79,6 +83,26 @@ async function cutSourceIntoClips(drpInput, opts = {}) {
|
|
|
79
83
|
// element instead of the donor's.
|
|
80
84
|
c = c.replace(/<MediaRef>[0-9a-f-]{36}<\/MediaRef>/, `<MediaRef>${cut.mediaRef}</MediaRef>`);
|
|
81
85
|
}
|
|
86
|
+
if (cut.audioOnly && cut.srcMeta) {
|
|
87
|
+
// An audio-only clone must carry ITS OWN source identity — a live
|
|
88
|
+
// A2-placed clip (harvested via AppendToTimeline mediaType:2) differs
|
|
89
|
+
// from a donor clone in Name, MediaFilePath, MediaFrameRate (the
|
|
90
|
+
// template donor's was 29.97!), the identity MediaTimemapBA extent,
|
|
91
|
+
// and FieldsBlob. A clone keeping donor values imports and reads back
|
|
92
|
+
// fine but renders SILENT off A1.
|
|
93
|
+
const { name, mediaFilePath, fps, frameCount } = cut.srcMeta;
|
|
94
|
+
c = c.replace(/<Name>[\s\S]*?<\/Name>/, `<Name>${name}</Name>`);
|
|
95
|
+
c = c.replace(/<MediaFilePath>[\s\S]*?<\/MediaFilePath>/, `<MediaFilePath>${mediaFilePath}</MediaFilePath>`);
|
|
96
|
+
const rate = Buffer.alloc(16);
|
|
97
|
+
rate.writeDoubleLE(fps, 0);
|
|
98
|
+
c = c.replace(/<MediaFrameRate>[0-9a-fA-F]*<\/MediaFrameRate>/, `<MediaFrameRate>${rate.toString('hex')}</MediaFrameRate>`);
|
|
99
|
+
const idm = Buffer.alloc(9);
|
|
100
|
+
idm.writeUInt8(0x02, 0);
|
|
101
|
+
idm.writeDoubleBE((frameCount - 1) / fps, 1);
|
|
102
|
+
c = c.replace(/<MediaTimemapBA>[0-9a-fA-F]*<\/MediaTimemapBA>/, `<MediaTimemapBA>${idm.toString('hex')}</MediaTimemapBA>`);
|
|
103
|
+
// Generic audio-clip FieldsBlob, verbatim from the live A2 harvest.
|
|
104
|
+
c = c.replace(/<FieldsBlob>[0-9a-fA-F]*<\/FieldsBlob>/, '<FieldsBlob>0000000200000005800a022001</FieldsBlob>');
|
|
105
|
+
}
|
|
82
106
|
if (cut.timemap) {
|
|
83
107
|
// Constant-speed retime: swap the identity MediaTimemapBA for the
|
|
84
108
|
// caller-built Sm2TimeMap (r19 keyed form render/readback-verified;
|
|
@@ -96,19 +120,44 @@ async function cutSourceIntoClips(drpInput, opts = {}) {
|
|
|
96
120
|
if (!clips.length) continue; // audio-less media: nothing to cut on A1
|
|
97
121
|
const donor = clips[0];
|
|
98
122
|
if (trackType === 'audio') {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
const audioCuts = cuts.filter((cut) => cut.audioOnly);
|
|
124
|
+
if (audioCuts.length) {
|
|
125
|
+
// Explicit audio placements: grow audio tracks and place per track;
|
|
126
|
+
// the implicit A1 mirror is suppressed (explicit audio wins).
|
|
127
|
+
const maxA = Math.max(1, ...audioCuts.map((cut) => cut.track ?? 1));
|
|
128
|
+
if (maxA > tracks.length) {
|
|
129
|
+
// Audio tracks CANNOT be grown by cloning: the per-timeline
|
|
130
|
+
// Fairlight model (FLStudioModelBA inside the pool's
|
|
131
|
+
// Sm2Sequence.FieldsBlob) holds one strip per track, and a cloned
|
|
132
|
+
// track without a strip imports fine, reads back fine, and renders
|
|
133
|
+
// SILENT (measured). The r19 media template is captured with 8
|
|
134
|
+
// audio tracks (valid strips included); beyond that, refuse.
|
|
135
|
+
throw new Error(
|
|
136
|
+
`cutSourceIntoClips: audio track ${maxA} exceeds the template's ${tracks.length} ` +
|
|
137
|
+
'audio tracks — audio tracks cannot be grown offline (no Fairlight strip = silent). ' +
|
|
138
|
+
'Re-capture a template with more audio tracks to raise the ceiling.');
|
|
139
|
+
}
|
|
140
|
+
for (let t = 1; t <= tracks.length; t += 1) {
|
|
141
|
+
const mine = audioCuts.filter((cut) => (cut.track ?? 1) === t);
|
|
142
|
+
if (t > 1 && !mine.length) continue;
|
|
143
|
+
tracks[t - 1] = setItemsInner(tracks[t - 1], mine.map((cut) => cloneCut(donor, cut)).join(''));
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// A1 mirrors track-1 video cuts only; higher video tracks stay
|
|
147
|
+
// video-only, and so do RETIMED cuts (the audio clone would need its
|
|
148
|
+
// own timemap and pitch handling — video-only is stated, not silent).
|
|
149
|
+
const a1 = cuts.filter((cut) => (cut.track ?? 1) === 1 && !cut.timemap).map((cut) => cloneCut(donor, cut));
|
|
150
|
+
tracks[0] = setItemsInner(tracks[0], a1.join(''));
|
|
151
|
+
}
|
|
104
152
|
xml = replaceTrackVec(xml, trackType, match, tracks);
|
|
105
153
|
continue;
|
|
106
154
|
}
|
|
107
|
-
const
|
|
155
|
+
const vCuts = cuts.filter((cut) => !cut.audioOnly);
|
|
156
|
+
const maxTrack = Math.max(1, ...vCuts.map((cut) => cut.track ?? 1));
|
|
108
157
|
const cloneSource = tracks[0];
|
|
109
158
|
while (tracks.length < maxTrack) tracks.push(emptyTrackClone(cloneSource));
|
|
110
159
|
for (let t = 1; t <= tracks.length; t += 1) {
|
|
111
|
-
const mine =
|
|
160
|
+
const mine = vCuts.filter((cut) => (cut.track ?? 1) === t);
|
|
112
161
|
if (t > 1 && !mine.length) continue; // grown-empty or untouched track keeps its items
|
|
113
162
|
const clones = mine.map((cut) => cloneCut(donor, cut));
|
|
114
163
|
for (const c of clones) clipDbIds.push(clipDbId(c));
|
|
Binary file
|
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.
|
|
90
|
+
VERSION = "2.115.0"
|
|
91
91
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
92
92
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
93
93
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/server.py
CHANGED
package/src/utils/api_truth.py
CHANGED
|
@@ -1914,6 +1914,32 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1914
1914
|
"structural readback cannot see this class.",
|
|
1915
1915
|
"tags": ["media-pool", "import", "render", "silent-failure", "readback"],
|
|
1916
1916
|
},
|
|
1917
|
+
{
|
|
1918
|
+
"symbol": "Audio tracks cannot be grown in an imported timeline; Fairlight strips live in the pool Sm2Sequence.FieldsBlob",
|
|
1919
|
+
"object": "Sm2TiTrack (audio) / FLStudioModelBA",
|
|
1920
|
+
"reality": "An audio track added to a SeqContainer by cloning "
|
|
1921
|
+
"imports fine, reads back fine (track count, items, "
|
|
1922
|
+
"everything), and renders SILENT. The per-timeline "
|
|
1923
|
+
"Fairlight model (FLStudioModelBA, ~7KB compressed to "
|
|
1924
|
+
"~420KB, inside the media pool's Sm2Sequence.FieldsBlob) "
|
|
1925
|
+
"holds one strip per audio track, and a track without a "
|
|
1926
|
+
"strip is mute. Measured by elimination on 19.1.3.7: "
|
|
1927
|
+
"clip byte-identical to a live-authored one, track "
|
|
1928
|
+
"byte-identical (SubType is the CHANNEL FORMAT code - "
|
|
1929
|
+
"1=mono - not an ordinal), pool entry shared with a "
|
|
1930
|
+
"playing A1 clip - still silent; only a timeline whose "
|
|
1931
|
+
"template was CAPTURED with the tracks plays.",
|
|
1932
|
+
"recommended": "Never clone audio tracks offline. Capture the "
|
|
1933
|
+
"template with the audio tracks already present "
|
|
1934
|
+
"(the r19 media template carries 8 mono tracks with "
|
|
1935
|
+
"valid strips; drt.assemble audioOnly cuts land on "
|
|
1936
|
+
"them and render at native level). Refuse placements "
|
|
1937
|
+
"beyond the captured ceiling. Audio aliveness is "
|
|
1938
|
+
"readback-blind: verify by rendered RMS, not "
|
|
1939
|
+
"structure.",
|
|
1940
|
+
"tags": ["fairlight", "audio", "import", "silent-failure", "drt"],
|
|
1941
|
+
"submit": "bug",
|
|
1942
|
+
},
|
|
1917
1943
|
{
|
|
1918
1944
|
"symbol": "MediaTimemapBA keyframes are generation-split; 19.x silently ignores the R21 protobuf form",
|
|
1919
1945
|
"object": "Sm2TimeMap (per-clip retime blob)",
|