File size: 4,374 Bytes
a3da2c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f02c67e
a3da2c1
 
 
 
f02c67e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3da2c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"""Shared preview/full-set rendering entrypoints.

This module exists so preview exports and full-set exports cannot drift into
separate tempo, pitch, and headroom policies again. Both paths call the same
AutomationIR renderer and therefore share:

* source-tempo default rendering;
* pitch-preserving stretch only when explicitly enabled in plan metadata;
* component-lane handoff back to the untouched full source clip;
* attenuation-only output protection.
"""

from __future__ import annotations

from typing import Any, Callable, Mapping

from automation_ir import AutomationIR, render_automation_ir, build_transition_ir
from automation_set_renderer import build_set_automation_ir

AudioLoader = Callable[[str, float, float, int], Any]
TimeStretcher = Callable[[Any, float], Any]
StemResolver = Callable[[Any, str, Any, int], Any | None]


def render_ir(
    ir: AutomationIR,
    *,
    load_audio_segment: AudioLoader,
    time_stretch_audio: TimeStretcher,
    stem_resolver: StemResolver | None = None,
) -> Any:
    return render_automation_ir(
        ir,
        load_audio_segment=load_audio_segment,
        time_stretch_audio=time_stretch_audio,
        stem_resolver=stem_resolver,
    )


def render_transition_preview(
    plan: Any,
    track_a: Any,
    track_b: Any,
    *,
    candidate_rank: int = 0,
    load_audio_segment: AudioLoader,
    time_stretch_audio: TimeStretcher,
    stem_resolver: StemResolver | None = None,
    sr: int = 44100,
) -> tuple[Any, AutomationIR, Mapping[str, Any] | None]:
    candidate = None
    alternatives = list(getattr(plan, "alternatives", []) or [])
    if candidate_rank > 0 and candidate_rank <= len(alternatives):
        candidate = alternatives[candidate_rank - 1]
    ir = build_transition_ir(plan, track_a, track_b, candidate=candidate, sr=sr)
    audio = render_ir(
        ir,
        load_audio_segment=load_audio_segment,
        time_stretch_audio=time_stretch_audio,
        stem_resolver=stem_resolver,
    )
    try:
        from transition_diagnostics import diagnose_transition_audio
        diag = diagnose_transition_audio(audio, sr=sr, anchor_seconds=ir.anchor_seconds)
        ir.metadata["rendered_diagnostics"] = diag
        metrics = dict(diag.get("metrics", {}) or {})
        rendered_scores = {
            "kick_overlap_score": round(float(metrics.get("onset_regularity", 0.0) or 0.0), 3),
            "bass_overlap_score": round(max(0.0, min(1.0, 1.0 / max(float(metrics.get("anchor_low_balance_ratio", 1.0) or 1.0), 1.0))), 3),
            "energy_dip_jump_score": round(max(0.0, min(1.0, float(metrics.get("anchor_min_rms_ratio", 1.0) or 1.0) / max(float(metrics.get("anchor_rms_balance_ratio", 1.0) or 1.0), 1.0))), 3),
            "spectral_harshness_score": round(max(0.0, min(1.0, 1.0 / max(float(metrics.get("high_band_spike_ratio", 1.0) or 1.0) / 2.0, 1.0))), 3),
            "phrase_arrival_confidence": float((ir.metadata.get("candidate", {}) or {}).get("score_breakdown", {}).get("arrival_quality", 0.0) or 0.0),
        }
        ir.metadata["rendered_candidate_scores"] = rendered_scores
        if isinstance(candidate, dict):
            candidate["rendered_diagnostics"] = diag
            candidate["rendered_candidate_scores"] = rendered_scores
    except Exception:
        pass
    return audio, ir, candidate


def render_full_set(
    tracks: list[Any],
    order: list[int],
    transitions: list[Any],
    *,
    load_audio_segment: AudioLoader,
    time_stretch_audio: TimeStretcher,
    stem_resolver: StemResolver | None = None,
    sr: int = 44100,
) -> tuple[Any, dict[str, Any], AutomationIR]:
    ir = build_set_automation_ir(tracks, order, transitions, sr=sr)
    audio = render_ir(
        ir,
        load_audio_segment=load_audio_segment,
        time_stretch_audio=time_stretch_audio,
        stem_resolver=stem_resolver,
    )
    info = {
        "tracks": ir.metadata.get("tracks", []),
        "transitions": ir.metadata.get("transitions", []),
        "total_duration": audio.shape[-1] / sr,
        "automation_ir": {
            "clips": len(ir.clips),
            "lanes": len(ir.lanes),
            "duration_seconds": ir.duration_seconds,
            "component_lane_method": ir.metadata.get("component_lane_method"),
            "render_protection": ir.metadata.get("render_protection", {}),
        },
    }
    return audio, info, ir