-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtimeline.go
More file actions
1763 lines (1697 loc) · 74.7 KB
/
Copy pathtimeline.go
File metadata and controls
1763 lines (1697 loc) · 74.7 KB
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package waxflow
import (
"fmt"
"io"
"math"
"sort"
"time"
"github.com/colespringer/waxflow/audio"
"github.com/colespringer/waxflow/codec"
"github.com/colespringer/waxflow/container"
"github.com/colespringer/waxflow/dsp"
"github.com/colespringer/waxflow/dsp/dither"
"github.com/colespringer/waxflow/dsp/resample"
"github.com/colespringer/waxflow/format"
"github.com/colespringer/waxflow/waxerr"
)
// concatContainer names the synthetic container a Concat reports, the way
// format.FromDemuxer labels an assembled one: there is no file here, so
// Info.Container has to say what the media actually is.
const concatContainer = "timeline"
// ToEnd is Slice's open-ended upper bound: the span runs to the end of the
// source.
const ToEnd = -1
// Slice bounds med to the sample range [from, to) of its own timeline, as
// a Media whose sample 0 is med's sample from and whose length is to-from.
// to is exclusive; ToEnd means to the end. The returned Media owns med and
// closes it.
//
// It is the primitive behind three things that looked like three features:
// a split job's cut points, an end trim (a span with a from of 0), and a
// virtual track streamed over an offset range of one file. All three are
// "bound this stream to a sample range", so they land once.
//
// A wrapper rather than a TranscodeOptions field, deliberately. An end
// bound as an option would need about six branches in the most
// invariant-dense function in the library, permanently: a clamp in the
// length math, a refusal in the segmented plan, the right interaction with
// the projected output length that feeds both the muxer's declared length
// and the edit list (get that wrong and every M4B lies about its
// duration), the progress total, and both canonical cache-key strings. A
// Media that is already the bounded stream needs none of them, because
// every one of those reads the length off the track and the track is
// already right.
//
// It composes, which is the clinching part. A sliced Media is the shifted
// stream, addressing from 0, so it hands the segmented path a start offset
// that PlanSegments refuses to take as an option ("segments address time");
// and "start the album at track 3" is Concat(members[2:]) with no new
// option at all.
//
// # Exactness is conditional, and the condition is worth stating
//
// A slice hands the chain a stream starting at sample from, and the chain
// starts fresh there, so any stateful node primes from nothing exactly as
// it does after a seek.
//
// - A cut with no rate change is exact, and that is the case that
// matters. A CUE split to FLAC at the source rate builds a chain with
// no resampler and no limiter, so there is no state to prime and each
// piece's sample 0 is the source's sample from, bit for bit.
// TestSliceSplitRoundTrip proves precisely that: a transient would make
// a bit-exact rejoin fail.
// - A resampled span would carry a short transient at sample 0, because
// the resampler's FIR window starts zero-filled, and that is what
// Headroom exists to remove: a span is a window onto a longer stream, so
// unlike a file it genuinely has audio before its own sample 0 to prime
// with. The segmented run uses it, so a virtual track's first sample is
// the same audio a continuous run of the whole source delivers there.
// That is what lets consecutive virtual tracks of one rip play gaplessly.
//
// Cut points are not assumed frame- or packet-aligned. Slice sits
// downstream of decode, so it cuts at any sample, which is the whole reason
// it is sample-exact where a packet-level cut would not be.
func Slice(med format.Media, from, to int64) (format.Media, error) {
if med == nil {
return nil, waxerr.New(waxerr.CodeInvalidRequest, "waxflow: Slice of a nil Media")
}
track := med.Info().Default()
spanned, err := SpanTrack(track, from, to)
if err != nil {
return nil, err
}
s := &slice{med: med, from: from, limit: ToEnd, fmt: track.Fmt}
// limit is the clamp; the track's Samples is what it advertises. They
// differ on purpose for the open-ended form: an explicit end is a
// declaration this holds the source to, while a slice that just trims
// the front inherits the source's own length and the source's own
// honesty about it. Clamping the open form at an advisory length would
// truncate a source that is merely mis-declared, which the unsliced
// Media tolerates and which is not this wrapper's business to change.
if to >= 0 {
s.limit = to - from
}
in := med.Info()
s.info = &format.Info{
Container: in.Container,
Tracks: []container.Track{spanned},
Chapters: spanChapters(in.Chapters, from, s.limit, track.Fmt.Rate),
Warnings: in.Warnings,
}
return s, nil
}
// spanChapters rebases a source's chapters onto the window [from, from+limit)
// of its own timeline: shifted so the window's start is zero, clipped to the
// window, and with everything outside it dropped. limit is ToEnd for an
// unbounded window. A chapter straddling an edge survives with its title and
// the part of its range that is inside.
//
// A zero End is the start-only chapter form (see container.Chapter): the
// chapter runs until the next one, or to the end of the stream. It stays zero
// on the way out, which is exact rather than a punt, because both things a
// consumer resolves it against are already this Media's own: the next chapter
// is in this list, rebased, and the stream ends where the window does.
// Writing an end here instead would declare one the source never did.
//
// The unbounded window clips nothing at the far end, for the reason Slice's
// limit exists: an open span holds the source to no length of its own, so a
// chapter running past the source's advisory end is the source's own business,
// exactly as the audio past it is.
//
// A rate of zero cannot place a chapter on a sample window at all, and there
// is then no answer to give rather than a wrong one to give (see slice).
func spanChapters(chapters []container.Chapter, from, limit int64, rate int) []container.Chapter {
if len(chapters) == 0 || rate <= 0 {
return nil
}
start := SampleTime(from, rate)
end := time.Duration(-1)
if limit >= 0 {
end = SampleTime(from+limit, rate)
}
var out []container.Chapter
for i, ch := range chapters {
// Begins at or after the window's end, or is over before its start:
// outside either way. The far test is skipped for an unbounded
// window, which has no far end to be past. A start-only chapter that
// begins exactly where the window does is inside it even when the
// next chapter shares its instant, so a span from the top of a file
// lists what a probe of the file lists.
if end >= 0 && ch.Start >= end {
continue
}
if e := chapterEnd(chapters, i); e >= 0 && e <= start && !(ch.End == 0 && ch.Start == start) {
continue
}
ch.Start = max(ch.Start-start, 0)
if ch.End > 0 {
if end >= 0 {
ch.End = min(ch.End, end)
}
ch.End -= start
}
out = append(out, ch)
}
return out
}
// chapterEnd is where chapter i really ends, for deciding whether it reaches
// a window at all: its own End, or the next chapter's start for the
// start-only form (a zero End). -1 means it runs to the end of the stream,
// which no window can begin after: that is the last chapter of a start-only
// list, and it always reaches.
//
// It resolves what spanChapters deliberately does not write down. Where a
// start-only chapter ends decides whether it is in the window, so the test
// needs the answer; the rebased list does not, and inventing an End there
// would put a boundary in the output the source never declared.
func chapterEnd(chapters []container.Chapter, i int) time.Duration {
if e := chapters[i].End; e > 0 {
return e
}
if i+1 < len(chapters) {
return chapters[i+1].Start
}
return -1
}
// SampleTime is sample n's position on a stream's clock at rate. It is the
// shared overflow-safe sample-to-duration converter: Slice's chapter retiming
// here and a merge's chapter offsets (internal/jobs) both place samples on a
// clock, and both must round the same way.
//
// The division is split so the whole-second part stays exact at any stream
// length: the direct n*time.Second/rate overflows an int64 past about 53
// hours at 48 kHz, and a long file is exactly the kind that carries chapters.
// What is left rounds toward zero, below the nanosecond the Duration itself
// resolves.
func SampleTime(n int64, rate int) time.Duration {
sec, rem := n/int64(rate), n%int64(rate)
return time.Duration(sec)*time.Second + time.Duration(rem)*time.Second/time.Duration(rate)
}
// SpanTrack computes the track a Slice of track to [from, to) presents: the
// same format, the window's length, and no gapless trims. It is a pure
// function of the header, so planning a span and running it cannot disagree
// about what gets delivered.
//
// It is the single funnel, the discipline ConcatTrack applies to a
// timeline: Slice resolves its track through this at open, and a caller
// planning a span resolves through it too, from the probed track alone and
// without opening anything. Without that, a plan's length and the slice's
// actual delivery drift, and the drift is invisible until a cache entry
// holds segments for a track that is not the one being served.
//
// to is exclusive; ToEnd means to the end of track.
func SpanTrack(track container.Track, from, to int64) (container.Track, error) {
switch {
case from < 0:
return container.Track{}, waxerr.New(waxerr.CodeInvalidRequest,
fmt.Sprintf("waxflow: negative span start %d", from))
case to < ToEnd:
return container.Track{}, waxerr.New(waxerr.CodeInvalidRequest,
fmt.Sprintf("waxflow: span end %d: want a sample offset or %d for the end of the source", to, ToEnd))
case to >= 0 && to < from:
return container.Track{}, waxerr.New(waxerr.CodeInvalidRequest,
fmt.Sprintf("waxflow: span [%d, %d) ends before it starts", from, to))
}
total := track.Samples
// A span past the end is refused rather than clamped, and that is the
// same call ConcatTrack makes about a member's declared length. A span
// is content identity: it says which samples are this track. So a cut
// point past the end means the caller's cut points do not describe this
// file (a CUE sheet paired with the wrong rip, a chapter list from a
// different edition), and silently clamping would hand back a track
// shorter than the caller believes it asked for, with no way to notice.
// That is precisely the desync a prefix sum cannot survive.
//
// The bound is the declared length whatever SamplesExact says, which
// looks like the wrong predicate and is not. SamplesExact is a
// truncation instruction (the decoder over-produces and must be cut back
// to this), not a claim about precision, so gating on it would drop the
// refusal for exactly the sources a split is usually pointed at: WAV and
// FLAC leave it false because their totals can lie, not because they are
// approximate. Gating here would trade a real refusal on the common case
// for a narrow one on Matroska, whose advisory total a third-party muxer
// can put on either side of the audio it has. Matroska this library wrote
// is not in that group: its Duration round-trips to the exact sample count,
// so a cut ending at the declared total is accepted, not refused by a hair.
if total >= 0 {
if from > total {
return container.Track{}, waxerr.New(waxerr.CodeInvalidRequest, fmt.Sprintf(
"waxflow: span starts at sample %d, past the source's %d samples", from, total))
}
if to > total {
return container.Track{}, waxerr.New(waxerr.CodeInvalidRequest, fmt.Sprintf(
"waxflow: span ends at sample %d, past the source's %d samples", to, total))
}
}
out := track
switch {
case to >= 0:
out.Samples, out.SamplesExact = to-from, true
case total >= 0:
out.Samples = total - from
}
// Zero, and load-bearing rather than incidental, exactly as a Concat's
// envelope is: a Media delivers gapless-trimmed PCM, so both trims
// happened inside it before a slice sees a sample. Passing the
// container's declaration through would make a downstream consumer trim
// a second time, against a stream that has no delay left to cut.
out.Delay, out.Padding = 0, 0
// The source's own codec is kept, unlike a Concat's synthetic PCM
// envelope, and that is not cosmetic: the codec is what names the
// decoder revision in a plan's Versions, so a span of a FLAC keys on the
// FLAC decoder and a decoder fix invalidates its cached bytes. A Concat
// cannot do that (N members, N codecs), which is why it has to repair
// the hole afterward; a span has exactly one source and keeps the truth.
return out, nil
}
// Headroomer is implemented by a Media that has real audio before its own
// sample 0, as a window onto a longer stream does. It is an optional
// capability in the same idiom as container.Indexer, container.Warner, and
// dsp.Settler: a Media opened from a file has nothing before its first
// sample and does not implement it, so the assertion is an honest gate.
//
// It exists because priming a chain and starting a stream are different
// questions, and only a span can answer the first one for its own sample 0.
// A stateful node primes from nothing at a stream's start, which is correct
// for a file (there is nothing earlier) and wrong for a span (there is). A
// consumer that wants a span's sample 0 to hold the same audio a continuous
// run of the whole source delivers there reads Headroom, seeks to a
// negative position, and discards the output it fed through.
//
// Positions below 0 are the whole point of the interface and are legal only
// on a Media that implements it. They stay within [-Headroom(), 0): the
// samples are real, they are simply upstream of the window this Media
// presents.
type Headroomer interface {
// Headroom is how many samples of real audio lie before sample 0, so a
// caller knows how far back it may seek. Zero means none.
Headroom() int64
}
// slice is Slice's Media: one source, positioned at the window's start and
// cut off at its end.
//
// What it says about the source follows one rule: rebase onto the window's
// own timeline where a right answer exists there, and answer nothing where
// none does.
//
// Chapters have one, so they are rebased (spanChapters). A chapter is a
// range on the very timeline the window cuts, so the part of the list lying
// inside the window is a fact about the window rather than a guess at it.
// Forwarding the source's list verbatim is the wrong answer and not a
// cautious one: it says a span holds chapters it does not, at times it does
// not hold them, and a consumer that writes them into the output (a split
// job stamping a piece with its source's metadata) has no way to notice.
//
// format.Composite has no right answer, so it is deliberately not forwarded.
// A slice of a timeline is not a timeline of the same members (its window
// covers some part of some of them) and no member list describes the window,
// so answering with the inner Media's would be a plain lie to a consumer
// keying a cache on it. Nothing slices a Concat today; the point is that if
// something does, it gets no answer rather than a wrong one.
//
// container.Indexer needs no forwarding either, for a different reason: the
// engine wraps index restore and save around the Media inside OpenStream,
// under this, and the save fires on Close, which this delegates. The
// sidecar keeps working through a slice without this knowing about it.
type slice struct {
med format.Media
info *format.Info
fmt audio.Format
// from is the window's first sample on med's timeline.
from int64
// limit is the window's length, ToEnd for an unbounded one. See Slice.
limit int64
pos int64 // delivered-timeline position of the next frame out
started bool // med has been positioned at from
discont bool
closed bool
}
func (s *slice) Info() *format.Info { return s.info }
// Headroom is the audio ahead of the window: the samples between the inner
// media's start and this span's, plus whatever the inner media can itself
// reach back to.
//
// The second term is what makes a span of a span report the truth. Nothing
// nests them today, and the sum is still the right answer rather than
// speculation: headroom means "how far back can I be positioned", and for
// an inner span that is its own window's start plus its own headroom, all
// of which SeekSample below can actually deliver. Reporting only from
// would under-report it, which does not fail loudly. It quietly primes a
// chain with less than it asked for.
func (s *slice) Headroom() int64 {
if h, ok := s.med.(Headroomer); ok {
return s.from + h.Headroom()
}
return s.from
}
func (s *slice) Close() error {
if s.closed {
return nil
}
s.closed = true
return s.med.Close()
}
// ensureStart positions med at the window's start, lazily.
//
// Lazily, and only when from is nonzero, so that a pure end trim (a span
// starting at 0) neither seeks nor requires a seekable source. The
// unbounded, unshifted slice is then free.
//
// A failed seek leaves the span unstarted, so the next read attempts it
// again, and that is the opposite of the call concat makes about its own
// failed seek. The two are not inconsistent: the difference is whether
// re-attempting is even defined. concat's seek walks toward a target the
// caller chose, moving the state its position is relative to on the way
// (which member is open, its chain, where that member's media sits), so a
// failure part way leaves pos describing somewhere the stream no longer is
// and nothing coherent to retry; it has to latch. This has one target for
// the life of the Media, from, and reaches it in one step. A failure writes
// nothing, so the state after it is the state before it, and the next read
// re-attempts the identical seek from the identical place: a sticky error
// would refuse what a retry can still get right, and would need a field to
// say what started already says.
//
// What both answers share is the only part that is not a choice: a position
// that never succeeded never becomes one a read can deliver samples against.
// started latches after the seek here, exactly as it does in SeekSample.
func (s *slice) ensureStart() error {
if s.started {
return nil
}
if s.from == 0 {
s.started = true
return nil
}
landed, err := s.med.SeekSample(s.from)
if err != nil {
return err
}
// A landing past the ask is what container.Seeker permits when the
// stream's first sync point lies beyond the target, and it means the
// span really does start late. Report where the stream is rather than
// pretending, exactly as a Concat's member seek does; for every
// seekable source in the tree the Media pre-rolls and this is 0.
//
// The floor is not the same floor SeekSample deliberately does without,
// and the difference is which question was asked. A negative target
// there is a caller reaching into the headroom on purpose, so a
// negative answer is the truth. Here the target is the window's own
// start, and a format.Media seek cannot land below its target except by
// running out of stream: it lands on a sync point at or before the ask
// and then decodes forward to the ask exactly, so a short answer means
// the source ended early, not that this is somehow positioned in its own
// headroom. Zero is then the honest position, and it is what lets
// endOfSource say the source ended n samples into a span of m rather
// than report a negative count.
s.pos = max(landed-s.from, 0)
s.started = true
return nil
}
// ReadChunk fills dst from the window.
//
// The front of the window is handled by the seek in ensureStart rather than
// by shifting a buffer, which is what lets the inner Media fill dst
// directly: an audio.Buffer is planar with a stride and has no sub-buffer
// view, so a shifted fill would need a copy. The back is a clamp on dst.N,
// the same shape the gapless padding trim already uses one layer down.
func (s *slice) ReadChunk(dst *audio.Buffer) error {
switch {
case s.closed:
return waxerr.New(waxerr.CodeInternal, "waxflow: ReadChunk on a closed span")
case dst.Fmt != s.fmt:
return waxerr.New(waxerr.CodeInvalidRequest,
fmt.Sprintf("waxflow: chunk buffer is %v, span is %v", dst.Fmt, s.fmt))
case dst.Cap() == 0:
return waxerr.New(waxerr.CodeInvalidRequest, "waxflow: zero-capacity chunk buffer")
}
if err := s.ensureStart(); err != nil {
return err
}
if s.limit >= 0 && s.pos >= s.limit {
return io.EOF
}
dst.N = 0
err := s.med.ReadChunk(dst)
if err == io.EOF {
return s.endOfSource()
}
if err != nil {
return err
}
if dst.N == 0 {
return waxerr.New(waxerr.CodeInternal,
"waxflow: a span's source returned no frames and no error; io.EOF is the only empty answer")
}
if s.limit >= 0 {
if allowed := s.limit - s.pos; int64(dst.N) >= allowed {
dst.N = int(max(allowed, 0))
}
}
dst.Pos = s.pos
dst.Discont = s.discont
s.discont = false
s.pos += int64(dst.N)
return nil
}
// endOfSource reports the source running out, which is only legal when the
// window did not declare where it ends.
//
// A bounded span whose source ends early is an error rather than a short
// stream, and for the same reason a Concat holds its members to their
// declared lengths: the track this Media advertises says to-from samples,
// a plan has already promised a segment count built from that number, and
// delivering fewer produces the tail 404 that number exists to prevent.
// Failing here names the real cause instead.
func (s *slice) endOfSource() error {
if s.limit >= 0 && s.pos < s.limit {
return waxerr.New(waxerr.CodeSourceUnreadable, fmt.Sprintf(
"waxflow: the source ended %d samples into a span that declared %d; its cut points do not describe this file",
s.pos, s.limit))
}
return io.EOF
}
// SeekSample repositions to target on the window's own timeline.
//
// A negative target is legal here, and only here, down to -Headroom(): it
// addresses the real audio ahead of the window, which is what a consumer
// priming a chain for the span's sample 0 asks for. Everything below the
// window is still the source's own audio, so the seek is an ordinary one
// once rebased. See Headroomer.
func (s *slice) SeekSample(target int64) (int64, error) {
// The bound is what Headroom advertises, not from alone: the two have to
// agree, or a caller that primes by exactly the headroom it was told
// about gets refused for asking.
room := s.Headroom()
switch {
case s.closed:
return 0, waxerr.New(waxerr.CodeInternal, "waxflow: SeekSample on a closed span")
case target < -room:
return 0, waxerr.New(waxerr.CodeInvalidRequest, fmt.Sprintf(
"waxflow: seek to %d is %d samples before the source's start; the span has %d samples of headroom",
target, -target-room, room))
}
// Past the window's end lands at its end, as a single Media does at the
// end of a file.
if s.limit >= 0 {
target = min(target, s.limit)
}
landed, err := s.med.SeekSample(s.from + target)
if err != nil {
return 0, err
}
s.started = true
// Rebased, and not floored at 0: a landing inside the headroom is a
// real position on this timeline, just a negative one.
s.pos = landed - s.from
if s.limit >= 0 {
s.pos = min(s.pos, s.limit)
}
s.discont = true
return s.pos, nil
}
// ConcatSource is one member of a timeline: its track, as Probe reported it,
// so a timeline can be planned without opening anything, and a function that
// opens it on demand.
type ConcatSource struct {
// Track describes the member from its headers. Concat holds the member
// to this declaration: one that opens in a different format, or delivers
// a different number of samples, fails the run rather than silently
// desyncing every position after it.
Track container.Track
// Open opens the member's decodable media. Concat calls it when the
// timeline reaches this member and closes the result on advance, so a
// 500-track queue costs one file descriptor rather than 500.
//
// Any context this closure binds must be the engine's own, never a
// request's. Open fires lazily, mid-stream, long after the call that
// built the Concat returned: live pipelines resolve under the server's
// base context by design, so read-behind can finish an encode after the
// client has left, and a request context captured here would instead
// kill a member's first read at a track boundary minutes into playback.
// container.Contextual exists for exactly this handoff.
Open func() (format.Media, error)
}
// ConcatOptions configures a Concat.
//
// # Hand the same options to the plan and to the run
//
// PlanSegmentsTimeline(tracks, copts, ...) and Concat(members, copts) are two
// calls taking two separately constructed ConcatOptions, and nothing checks
// that they match. Both fields below make a mismatch a silent wrong answer
// rather than an error, so the convention is: build one ConcatOptions and pass
// it to both.
//
// For Profile a mismatch is a wrong cache key: the plan names one profile in
// its Versions and the run resamples through another, so the cached bytes
// describe processing that did not happen.
//
// For Crossfade it is worse, and it is why this paragraph exists rather than
// the field being left to speak for itself. A crossfade changes the timeline's
// length, so a plan built with one and a run built without it disagree about
// how many samples exist: the plan promises the sum less (N-1)*Crossfade
// against a run delivering the full sum. That is the prefix-sum desync and the
// tail 404 that ADR-0009's advisory-length section exists to prevent, arriving
// by a different door.
type ConcatOptions struct {
// Profile selects the resampler quality profile for normalizing members
// whose rate is not the envelope's; empty means resample.HQ.
//
// It must be the profile the transcode's own TranscodeOptions carry. See
// the convention above.
Profile resample.Profile
// Crossfade is how many samples of each seam are a blend of the two
// members meeting there, on the envelope's timeline. Zero, the default, is
// a butt-join: sample len(a) is b's sample 0, exactly, which is what every
// existing caller gets and what ADR-0009's primitive is.
//
// There is no nonzero default and there will not be one. A gapless album
// must never blend, because the seam it would smear is the artifact this
// primitive exists to deliver intact. A crossfade is a thing a caller asks
// for on material that wants it (a declick between two independently
// recorded takes, a play queue of unrelated tracks), never something the
// library decides on their behalf.
//
// Each seam costs X samples of total length: N members crossfaded by X
// deliver sum(len) - (N-1)*X. Member i's tail zone and member i+1's head
// zone are the same region of the timeline, which is what the overlap is.
// The blend is equal-power (cos/sin), so uncorrelated material holds its
// level across the zone where a linear fade would dip 3 dB.
//
// Bounded twice, both refused at ConcatTrack so a plan and a run refuse
// identically: every member must be long enough for the zones it carries
// (head plus tail, so the edge members need only one), and a zone must fit
// maxCrossfadeBytes.
Crossfade int64
}
// maxCrossfadeBytes bounds one blend buffer, and the number is derived rather
// than chosen: audio/pool.go's top size class is maxClassBits = 22, "4 Mi
// samples = 16 MiB int32/float32", and audio.Get sizes on frames*Channels. So
// X*ch*4 <= 16 MiB is exactly X*ch <= 4 Mi, which is exactly the largest blend
// audio.Get will pool.
//
// One sample more and classBits returns -1: the buffer allocates directly and
// is dropped on Put (pool.go:14-17), so every seam of every timeline becomes a
// 16 MB allocate-and-discard. That is the cliff this refuses at, and it is why
// the constant is this number and not a round one near it.
//
// timeline.MaxMembers is the precedent for the form: a named constant carrying
// its own reason. (ADR-0009 and /caps spell that bound maxTimelineMembers,
// which is the wire field's name and not an identifier in the tree.)
const maxCrossfadeBytes = 16 << 20
// concatLayout is the single walk every timeline-shape function reads from,
// so the length a plan projects, the offsets a run seeks to, and the
// boundaries the wire reports are one arithmetic rather than three copies of
// it that agree only by inspection. ConcatTrack reads env+total, Concat reads
// starts+lens to rebase positions, and ConcatBoundaries maps starts+lens onto
// the wire.
//
// It owns the whole of what a timeline's shape is: the envelope format, the
// per-member normalized lengths, the crossfade refusals, and the start
// recurrence. The rationale for each of those choices lives on ConcatTrack,
// the public function whose returned track this produces.
//
// starts has len(tracks)+1 entries: starts[i] is member i's first sample and
// starts[len(tracks)] is the whole timeline's length, which equals total.
// lens[i] is member i's own normalized length; under a crossfade the two
// differ, since starts[i+1]-starts[i] is lens[i]-X while member i still
// occupies lens[i] samples (see the concat struct's field docs).
func concatLayout(tracks []container.Track, opts ConcatOptions) (env audio.Format, lens, starts []int64, total int64, err error) {
if len(tracks) == 0 {
return audio.Format{}, nil, nil, 0, waxerr.New(waxerr.CodeInvalidRequest,
"waxflow: a timeline needs at least one member")
}
env = audio.Format{Type: audio.Int}
for i, t := range tracks {
if err := t.Fmt.Valid(); err != nil {
return audio.Format{}, nil, nil, 0, waxerr.Wrap(waxerr.CodeUnsupportedFormat,
fmt.Sprintf("waxflow: timeline member %d", i), err)
}
if t.Samples < 0 {
return audio.Format{}, nil, nil, 0, waxerr.New(waxerr.CodeInvalidRequest, fmt.Sprintf(
"waxflow: timeline member %d has no declared length; measure it before planning a timeline", i))
}
env.Rate = max(env.Rate, t.Fmt.Rate)
env.Channels = max(env.Channels, t.Fmt.Channels)
env.BitDepth = max(env.BitDepth, t.Fmt.BitDepth)
if t.Fmt.Type == audio.Float {
env.Type = audio.Float
}
}
if env.Type == audio.Float {
env.BitDepth = 32
}
env.Layout = audio.DefaultLayout(env.Channels)
// The envelope's layout has to be the conventional one, because that is
// the only layout the mix node targets: a member with fewer channels is
// mixed up to audio.DefaultLayout(env.Channels), so any other envelope
// layout would be one no normalized member could reach. A member that
// already has the envelope's channel count runs no mix and keeps its own
// layout, so its layout has to match already. That is true of every
// layout the decoders produce; a WAVEFORMATEXTENSIBLE mask naming some
// other pair of speakers is the one case it is not, and it is refused by
// name rather than relabelled, since calling a back-left channel
// front-right is a silent lie about what the file says it holds.
for i, t := range tracks {
if t.Fmt.Channels == env.Channels && t.Fmt.Layout != env.Layout {
return audio.Format{}, nil, nil, 0, waxerr.New(waxerr.CodeUnsupportedFormat, fmt.Sprintf(
"waxflow: timeline member %d lays its %d channels out as %v, not the conventional %v; "+
"a timeline normalizes channel counts, not speaker assignments",
i, t.Fmt.Channels, t.Fmt.Layout, env.Layout))
}
}
lens = make([]int64, len(tracks))
starts = make([]int64, len(tracks)+1)
for i, t := range tracks {
lens[i] = concatMemberSamples(t, env)
total += lens[i]
// The next member begins where this one's tail zone does, which is X
// before this one ends: the two share that region. The last member
// carries no tail, so it contributes its whole length and starts[N] is
// the total below. Running starts[i+1] = starts[i] + lens[i] - X through
// the last hop instead would leave the timeline X short of its own track.
tail := int64(0)
if i < len(tracks)-1 {
tail = opts.Crossfade
}
starts[i+1] = starts[i] + lens[i] - tail
}
if err := checkCrossfade(lens, env, opts.Crossfade); err != nil {
return audio.Format{}, nil, nil, 0, err
}
// One zone per seam, and there are N-1 seams. Subtracted after every
// member's own ceil, so the sum-of-ceils the members produce is untouched;
// starts[N] already carries the same subtraction, so the two agree.
total -= int64(len(tracks)-1) * opts.Crossfade
return env, lens, starts, total, nil
}
// ConcatTrack computes the synthetic track a Concat of these members
// presents: the common (envelope) format, the summed normalized length, and
// no gapless trims. It is a pure function of the headers, so planning and
// running cannot disagree about the delivered format.
//
// The envelope is the format no member loses information to reach: the
// maximum rate, the maximum channel count, and the wider sample domain
// (float if any member is float). Refusing mixed members instead would not
// push the problem to the caller, it would delete the feature for the normal
// case: HLS cannot change format mid-variant without an EXT-X-DISCONTINUITY
// and a second init, which one chain, one init, and one edit list forbid
// structurally, and a play queue is mixed by nature.
//
// A member whose format already equals the envelope is read straight
// through, with no chain and no copy, so a uniform timeline (a gapless
// album, which is one master at one rate) pays nothing for the machinery.
// That is structural rather than an optimization: it falls out of the
// envelope being a maximum.
//
// One member at 96 kHz makes every other member resample twice, member to
// envelope and envelope to output. The cost is real and deliberately
// unaddressed: collapsing it needs the output format, which is not known
// here (an output row's adjust hook owns the real rate, which is how Opus
// forces 48 kHz whatever the caller asked for). Likewise the channel count
// is a maximum and is not capped at stereo: capping would silently destroy a
// surround member, and it looks cheaper only because the output is usually
// stereo, which is the same output-aware knowledge this function does not
// have. The common mixed-channel case is a mono track in a stereo queue,
// where the maximum is exact and free.
//
// Delay and Padding are zero, and that is load-bearing rather than
// incidental: format.Media delivers already-trimmed PCM, so both trims
// happened inside each member before Concat saw a sample. That is exactly
// why concatenation is sample-exact, and a nonzero trim here would make a
// downstream consumer trim a second time.
//
// It takes the options because concatLayout is the single funnel and a
// crossfade changes the length: opts.Crossfade shortens the total by X per
// seam, and every refusal a crossfade needs lives in concatLayout so that
// planning a timeline and running one refuse the same requests for the same
// reasons.
func ConcatTrack(tracks []container.Track, opts ConcatOptions) (container.Track, error) {
env, _, _, total, err := concatLayout(tracks, opts)
if err != nil {
return container.Track{}, err
}
return concatSynthetic(env, total), nil
}
// concatSynthetic is the track ConcatTrack returns and the track a Concat
// reports as its own, built in one place so the two cannot describe different
// audio. Samples is authoritative rather than advisory, so SamplesExact is
// honest: Concat holds every member to its declared length and fails the run
// instead of delivering some other count.
func concatSynthetic(env audio.Format, total int64) container.Track {
return container.Track{
Codec: codec.PCM,
Fmt: env,
Samples: total,
SamplesExact: true,
Default: true,
}
}
// MemberBoundary is one member's place on a concatenated timeline. Both fields
// are in samples at the envelope rate (the timeline's normalized rate,
// reported alongside as the envelope rate). OffsetSamples is the member's
// actual start on the timeline; DurationSamples is its own raw normalized
// length.
//
// Under a crossfade of X, consecutive members OVERLAP: member i occupies
// [OffsetSamples, OffsetSamples+DurationSamples), which runs X past where
// member i+1 begins, so OffsetSamples+DurationSamples can exceed the next
// member's OffsetSamples and sum(DurationSamples) is total + (N-1)X, not total.
// Only at X=0 do the members tile without overlap.
type MemberBoundary struct {
OffsetSamples int64 `json:"offsetSamples"`
DurationSamples int64 `json:"durationSamples"`
}
// ConcatBoundaries reports where each member lands on the concatenated
// timeline and how long it is, from the members' headers alone (no decode, no
// open), plus the envelope format the offsets are measured on. It reads the
// same concatLayout ConcatTrack and Concat read, so a boundary reported here
// is the position the run actually plays.
//
// The offsets are actual timeline positions and overlap under a crossfade; see
// MemberBoundary for the contract, which is pinned from the first release so a
// consumer does not build on a meaning that changes when a crossfade is
// threaded to the wire.
func ConcatBoundaries(tracks []container.Track, opts ConcatOptions) ([]MemberBoundary, audio.Format, error) {
env, lens, starts, _, err := concatLayout(tracks, opts)
if err != nil {
return nil, audio.Format{}, err
}
bounds := make([]MemberBoundary, len(tracks))
for i := range tracks {
bounds[i] = MemberBoundary{OffsetSamples: starts[i], DurationSamples: lens[i]}
}
return bounds, env, nil
}
// CrossfadeSamples converts a crossfade expressed in seconds into the envelope
// samples ConcatOptions.Crossfade carries. The wire spells a crossfade in
// seconds because a caller cannot know the envelope rate (the maximum member
// rate) before the members are measured, and so cannot express the blend in the
// samples the option wants; this is where the two meet.
//
// The rate is read from the same concatLayout ConcatTrack, Concat, and
// ConcatBoundaries read, so a crossfade converted here is measured on exactly
// the rate the run blends on. That is what lets a plan and a run convert one
// signed number the same way and never come to disagree about how long the seam
// is: the envelope is a pure function of the members' formats, which are pinned,
// so the count is deterministic for a given set of members. The result is
// rounded to the nearest sample.
//
// A non-positive (or NaN) seconds is a butt-join, the zero the default every
// timeline that does not ask for a blend gets. An absurdly large seconds is
// clamped to the int64 ceiling rather than wrapped, so it reaches checkCrossfade
// as the refusal it is ("more than this timeline can blend") instead of a
// silently wrapped small value: the fit and memory bounds are checkCrossfade's
// to enforce inside ConcatTrack, not this converter's.
func CrossfadeSamples(tracks []container.Track, seconds float64) (int64, error) {
if !(seconds > 0) {
// False for <=0 and for NaN. Validation refuses a NaN upstream, but
// guarding it here keeps a bad value from ever reaching the conversion.
return 0, nil
}
env, _, _, _, err := concatLayout(tracks, ConcatOptions{})
if err != nil {
return 0, err
}
x := math.Round(seconds * float64(env.Rate))
if x >= float64(math.MaxInt64) {
return math.MaxInt64, nil
}
return int64(x), nil
}
// checkCrossfade holds a crossfade to what the members and the envelope can
// actually carry: a legal length, a blend that fits one pooled buffer, and a
// zone that fits every member it lands on.
//
// It runs inside ConcatTrack, which is what makes a plan and a run refuse
// identically. lens are the members' normalized lengths, in the envelope's
// samples, which is the timeline X is measured on too.
func checkCrossfade(lens []int64, env audio.Format, x int64) error {
if x == 0 {
return nil
}
if x < 0 {
return waxerr.New(waxerr.CodeInvalidRequest,
fmt.Sprintf("waxflow: negative crossfade %d", x))
}
// Divide rather than multiply: x is a caller's int64, so x*ch*4 overflows
// before it refuses, and the refusal is the point. The message obeys the
// same rule, which is why it quotes no byte count: x*perFrame would
// overflow here too, on exactly the inputs this exists to catch.
if perFrame := int64(env.Channels) * 4; x > maxCrossfadeBytes/perFrame {
limit := maxCrossfadeBytes / perFrame
// In the caller's own units. They set a frame count, not a byte count,
// so an answer in MiB would leave them to rediscover the channel
// arithmetic that produced it; the seconds are what make the number
// mean anything at the rate they are actually running.
return waxerr.New(waxerr.CodeInvalidRequest, fmt.Sprintf(
"waxflow: a crossfade of %d samples is more than this timeline can blend; the most it can is "+
"%d samples (%.1f s at %d Hz, %d channels), which is the largest buffer the sample pool holds",
x, limit, float64(limit)/float64(env.Rate), env.Rate, env.Channels))
}
// The fit rule is head+tail <= L, not 2X <= L: the first and last members
// carry one zone rather than two, and stating it this way makes N=1 pass
// with no special case (its only member is both first and last, so it
// carries neither zone).
for i, l := range lens {
var need int64
if i > 0 {
need += x
}
if i < len(lens)-1 {
need += x
}
if need > l {
return waxerr.New(waxerr.CodeInvalidRequest, fmt.Sprintf(
"waxflow: timeline member %d is %d samples, too short for the %d samples of crossfade it carries "+
"(a crossfade of %d, on %d of its seams)",
i, l, need, x, need/x))
}
}
return nil
}
// concatMemberSamples is the member's length on the envelope timeline. It
// goes through the resampler's own exact output count, so the prefix sum a
// Concat rebases positions by and the length a plan projects are the same
// arithmetic rather than two roundings that agree by inspection.
func concatMemberSamples(t container.Track, env audio.Format) int64 {
return resample.OutputLen(t.Samples, t.Fmt.Rate, env.Rate)
}
// concatSpec is the normalization one member needs to reach the envelope, in
// one place so the version accounting (timelineVersions) and the run build
// the identical chain.
//
// The dither strategy is pinned to TPDF rather than left to the zero value
// it happens to equal. TPDF is keyed by absolute position and holds no
// history, so a member normalized from a mid-stream start requantizes to the
// same samples a continuous run produces; Shaped carries error feedback,
// which would make a restarted worker's segments differ from a continuous
// worker's for a reason no test here would name.
func concatSpec(env audio.Format, opts ConcatOptions) dsp.ChainSpec {
spec := dsp.ChainSpec{
Rate: env.Rate,
Channels: env.Channels,
Profile: opts.Profile,
Shaping: dither.TPDF,
}
if env.Type == audio.Float {
spec.Float = true
} else {
spec.BitDepth = env.BitDepth
}
return spec
}
// Concat sequences members into one gapless format.Media: a single
// continuous timeline whose sample len(a) is b's sample 0, exactly, unless
// opts.Crossfade asks for a blend.
//
// The butt-join is the default and the primitive. It is sample-exact by
// construction rather than by arithmetic: format.Media already delivers
// gapless-trimmed PCM, so there is no encoder delay or padding left to reason
// about at the seam, and concatenation is just reading one stream after
// another.
//
// A crossfade trades exactly that away, on purpose and only when asked: the
// seam becomes a zone of Crossfade samples that is both members at once, and
// the timeline shortens by one zone per seam. See ConcatOptions.Crossfade,
// which is zero for every caller that does not want it.
//
// Members open on demand and close on advance. That is the design and not an
// optimization: besides costing one file descriptor for a queue of any
// length, it makes planning and running symmetric (both are driven by the
// members' tracks alone) and it removes the rewind problem outright, since a
// member reached a second time is a member opened a second time, from the
// top, with no state to have gone stale.
//
// The returned Media owns nothing until it is read and closes whatever it
// opened on Close. It also satisfies format.Composite, so a consumer keying
// its own cache can reach the members' tracks rather than only the envelope.
func Concat(members []ConcatSource, opts ConcatOptions) (format.Media, error) {
tracks := make([]container.Track, len(members))
for i := range members {
if members[i].Open == nil {
return nil, waxerr.New(waxerr.CodeInvalidRequest,
fmt.Sprintf("waxflow: timeline member %d has no Open function", i))
}
tracks[i] = members[i].Track
}
env, lens, starts, total, err := concatLayout(tracks, opts)
if err != nil {
return nil, err
}
return &concat{
members: members,
tracks: tracks,
opts: opts,
fmt: env,
starts: starts,
lens: lens,
info: &format.Info{Container: concatContainer, Tracks: []container.Track{concatSynthetic(env, total)}},
}, nil
}
// concat is Concat's Media: one member open at a time, each normalized to
// the envelope by its own chain, positions rebased by the prefix sum.
type concat struct {
members []ConcatSource
tracks []container.Track
opts ConcatOptions
info *format.Info
fmt audio.Format
// starts is where each member begins on the timeline: starts[i] is member
// i's first sample, and starts[len(members)] is the whole timeline's
// length.
//