Skip to content

Commit 41b84f4

Browse files
authored
Add crossfade delay (#3704)
1 parent 7bf657f commit 41b84f4

File tree

11 files changed

+778
-128
lines changed

11 files changed

+778
-128
lines changed

doc/content/migrating.md

+21
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,27 @@ so, always best to first to a trial run before putting things to production!
1717

1818
## From 2.2.x to 2.3.x
1919

20+
### Crossfade transitions and track marks
21+
22+
Track marks can now be properly passed through crossfade transitions. This means that you also have to make sure
23+
that your transition function is fallible! For instance, this silly transition function:
24+
25+
```liquidsoap
26+
def transition(_, _) =
27+
blank(duration=2.)
28+
end
29+
```
30+
31+
Will never terminate!
32+
33+
Typically, to insert a jingle you would do:
34+
35+
```liquidsoap
36+
def transition(old, new) =
37+
sequence([old.source, single("/path/to/jingle.mp3"), new.source])
38+
end
39+
```
40+
2041
### Replaygain
2142

2243
- There is a new `metadata.replaygain` function that extracts the replay gain value in _dB_ from the metadata.

src/core/operators/cross.ml

+4-56
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,7 @@ class cross val_source ~duration_getter ~override_duration ~persist_override
203203
| `After after_source -> Some after_source)
204204
| `After after_source
205205
when self#can_reselect
206-
~reselect:
207-
(match reselect with
208-
| `After_position _ -> reselect
209-
| _ -> `Ok)
206+
~reselect:(match reselect with `Force -> `Ok | _ -> reselect)
210207
after_source ->
211208
Some after_source
212209
| `After _ -> Some self#prepare_before
@@ -268,7 +265,6 @@ class cross val_source ~duration_getter ~override_duration ~persist_override
268265
in
269266
let buffered_before = Generator.length gen_before in
270267
let buffered_after = Generator.length gen_after in
271-
let buffered = min buffered_before buffered_after in
272268
let after =
273269
Clock.collect_after (fun () ->
274270
let metadata = function
@@ -277,52 +273,20 @@ class cross val_source ~duration_getter ~override_duration ~persist_override
277273
in
278274
let before_metadata = metadata before_metadata in
279275
let after_metadata = metadata after_metadata in
280-
let before_head =
281-
if buffered < buffered_before then (
282-
let head =
283-
Generator.slice gen_before (buffered_before - buffered)
284-
in
285-
let head_gen =
286-
Generator.create ~content:head
287-
(Generator.content_type gen_before)
288-
in
289-
let s = new consumer head_gen in
290-
s#set_id (self#id ^ "_before_head");
291-
Typing.(s#frame_type <: self#frame_type);
292-
Some s)
293-
else None
294-
in
295276
let before = new consumer gen_before in
296277
Typing.(before#frame_type <: self#frame_type);
297278
let before = new Insert_metadata.replay before_metadata before in
298279
Typing.(before#frame_type <: self#frame_type);
299-
let after_tail =
300-
if buffered < buffered_after then (
301-
let head = Generator.slice gen_after buffered in
302-
let head_gen =
303-
Generator.create ~content:head
304-
(Generator.content_type gen_after)
305-
in
306-
let tail_gen = gen_after in
307-
gen_after <- head_gen;
308-
let s = new consumer tail_gen in
309-
Typing.(s#frame_type <: self#frame_type);
310-
s#set_id (self#id ^ "_after_tail");
311-
Some s)
312-
else None
313-
in
280+
before#set_id (self#id ^ "_before");
314281
let after = new consumer gen_after in
315282
Typing.(after#frame_type <: self#frame_type);
316283
let after = new Insert_metadata.replay after_metadata after in
317284
Typing.(after#frame_type <: self#frame_type);
318-
before#set_id (self#id ^ "_before");
319285
after#set_id (self#id ^ "_after");
320286
self#log#important "Analysis: %fdB / %fdB (%.2fs / %.2fs)" db_before
321287
db_after
322288
(Frame.seconds_of_main buffered_before)
323289
(Frame.seconds_of_main buffered_after);
324-
self#log#important "Computing crossfade over first and last %.2fs"
325-
(Frame.seconds_of_main buffered);
326290
let compound =
327291
let params =
328292
[
@@ -334,9 +298,7 @@ class cross val_source ~duration_getter ~override_duration ~persist_override
334298
( "expected_duration",
335299
Lang.float (Frame.seconds_of_main cross_length) );
336300
( "buffered",
337-
Lang.float
338-
(Frame.seconds_of_main
339-
(Generator.length gen_before)) );
301+
Lang.float (Frame.seconds_of_main buffered_before) );
340302
("metadata", Lang.metadata before_metadata);
341303
] );
342304
( "",
@@ -347,29 +309,15 @@ class cross val_source ~duration_getter ~override_duration ~persist_override
347309
( "expected_duration",
348310
Lang.float (Frame.seconds_of_main cross_length) );
349311
( "buffered",
350-
Lang.float
351-
(Frame.seconds_of_main (Generator.length gen_after))
352-
);
312+
Lang.float (Frame.seconds_of_main buffered_after) );
353313
("metadata", Lang.metadata after_metadata);
354314
] );
355315
]
356316
in
357317
Lang.to_source (Lang.apply transition params)
358318
in
359319
Typing.(compound#frame_type <: self#frame_type);
360-
let compound =
361-
match (before_head, after_tail) with
362-
| None, None -> compound
363-
| Some s, None ->
364-
(new Sequence.sequence ~merge:true [s; compound]
365-
:> Source.source)
366-
| None, Some s ->
367-
(new Sequence.sequence ~merge:true [compound; s]
368-
:> Source.source)
369-
| Some _, Some _ -> assert false
370-
in
371320
Clock.unify ~pos:self#pos compound#clock s#clock;
372-
Typing.(compound#frame_type <: self#frame_type);
373321
compound)
374322
in
375323
self#prepare_source after;

src/core/sources/blank.ml

+19-9
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,30 @@ open Mm
2424
open Source
2525

2626
class blank duration =
27-
let ticks = if duration < 0. then -1 else Frame.main_of_seconds duration in
27+
let ticks () =
28+
let d = duration () in
29+
if d < 0. then -1 else Frame.main_of_seconds d
30+
in
2831
object (self)
2932
inherit source ~name:"blank" ()
3033

3134
(** Remaining time, -1 for infinity. *)
32-
val mutable remaining = ticks
35+
val mutable remaining = None
36+
37+
method remaining =
38+
match remaining with
39+
| Some r -> r
40+
| None ->
41+
let r = ticks () in
42+
remaining <- Some r;
43+
r
3344

34-
method remaining = remaining
3545
method stype = `Infallible
3646
method private can_generate_frame = true
3747
method self_sync = (`Static, false)
3848
method! seek x = x
3949
method seek_source = (self :> Source.source)
40-
method abort_track = remaining <- 0
50+
method abort_track = remaining <- Some 0
4151
val mutable is_first = true
4252

4353
method generate_frame =
@@ -84,15 +94,15 @@ class blank duration =
8494
self#content_type
8595
(Frame.create ~length Frame.Fields.empty)
8696
in
87-
match (was_first, remaining) with
97+
match (was_first, self#remaining) with
8898
| true, _ -> Frame.add_track_mark frame 0
8999
| _, -1 -> frame
90100
| _, r ->
91101
if r < length then (
92-
remaining <- ticks - r;
102+
remaining <- Some (ticks () - r);
93103
Frame.add_track_mark frame r)
94104
else (
95-
remaining <- r - length;
105+
remaining <- Some (r - length);
96106
frame)
97107
end
98108

@@ -102,12 +112,12 @@ let blank =
102112
~descr:"Produce silence and blank images." ~return_t
103113
[
104114
( "duration",
105-
Lang.float_t,
115+
Lang.getter_t Lang.float_t,
106116
Some (Lang.float (-1.)),
107117
Some
108118
"Duration of blank tracks in seconds, Negative value means forever."
109119
);
110120
]
111121
(fun p ->
112-
let d = Lang.to_float (List.assoc "duration" p) in
122+
let d = Lang.to_float_getter (List.assoc "duration" p) in
113123
(new blank d :> source))

src/libs/extra/fades.liq

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Plot the first crossfade transition. Used for visualizing and testing
2+
# crossfade transitions.
3+
# @flag extra
4+
def cross.plot(~png=null(), ~dir=null(), s) =
5+
dir =
6+
if
7+
null.defined(dir)
8+
then
9+
null.get(dir)
10+
else
11+
dir = file.temp_dir("plot")
12+
on_cleanup({file.rmdir(dir)})
13+
dir
14+
end
15+
16+
old_txt = path.concat(dir, "old.txt")
17+
new_txt = path.concat(dir, "new.txt")
18+
19+
def gnuplot_cmd(filename) =
20+
'set term png; set output "#{filename}"; plot "#{new_txt}" using 1:2 with \
21+
lines title "new track", "#{old_txt}" using 1:2 with lines title "old \
22+
track"'
23+
end
24+
25+
def store_rms(~id, s) =
26+
s = rms(duration=settings.frame.duration(), s)
27+
t0 = ref(null())
28+
29+
source.on_frame(
30+
before=false,
31+
s,
32+
{
33+
let t0 =
34+
if
35+
null.defined(t0())
36+
then
37+
null.get(t0())
38+
else
39+
t0 := source.time(s)
40+
null.get(t0())
41+
end
42+
let v = s.rms()
43+
let p = source.time(s) - t0
44+
fname = id == "old" ? old_txt : new_txt
45+
file.write(append=true, data="#{p}\t#{v}\n", fname)
46+
}
47+
)
48+
end
49+
50+
plotted = ref(false)
51+
52+
def transition(old, new) =
53+
old = store_rms(id="old", fade.out(old.source))
54+
new = store_rms(id="new", fade.in(new.source))
55+
56+
s = blank(duration=0.1)
57+
s =
58+
source.on_frame(
59+
s,
60+
{
61+
if
62+
null.defined(png) and not plotted()
63+
then
64+
ignore(
65+
process.run(
66+
"gnuplot -e #{process.quote(gnuplot_cmd(null.get(png)))}"
67+
)
68+
)
69+
end
70+
plotted := true
71+
}
72+
)
73+
74+
sequence(merge=true, [add(normalize=false, [new, old]), once(s)])
75+
end
76+
77+
cross(transition, s)
78+
end

0 commit comments

Comments
 (0)