-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathchapters.go
More file actions
127 lines (119 loc) · 4.65 KB
/
Copy pathchapters.go
File metadata and controls
127 lines (119 loc) · 4.65 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
package waxtap
import (
"cmp"
"slices"
"time"
"github.com/colespringer/waxlabel"
"github.com/colespringer/waxtap/v3/internal/cutrange"
"github.com/colespringer/waxtap/v3/internal/pipeline"
)
// appliedCut describes a rendered timeline cut, for remapping source-timeline
// metadata (chapter marks) onto the delivered audio.
type appliedCut struct {
keeps []cutrange.Range
crossfade time.Duration
total time.Duration // source duration, resolving until-next chapter ends
}
// removed reports whether the cut took the source instant t: inside the
// source, [0, total), and in no keep. An instant at or past the end marks no
// removed audio whatever the cut did.
func (c *appliedCut) removed(t time.Duration) bool {
if t < 0 || t >= c.total {
return false
}
for _, k := range c.keeps {
if t >= k.Start && t < k.End {
return false
}
}
return true
}
// appliedCutFrom extracts the cut a pipeline run applied, or nil when the
// delivered timeline matches the source.
func appliedCutFrom(pres pipeline.Result) *appliedCut {
if !pres.Cut || len(pres.Keeps) == 0 {
return nil
}
return &appliedCut{keeps: pres.Keeps, crossfade: pres.Crossfade, total: pres.SourceDuration}
}
// remapChapters maps chapters from the source timeline onto a cut's output
// timeline. A chapter whose content was entirely removed is dropped; the rest
// shift by the audio removed before them.
//
// A zero End means "until the next chapter" and stays zero: after the shift
// the next surviving chapter still starts exactly where this one's content
// ends, so the convention remaps itself. The span is still resolved (against
// the next chapter, or total for the last) to decide whether any content
// survived. A chapter with no content of its own inside the source, one that
// shares its start with the next or sits at or past the end (forms the
// Musepack and ASF readers keep), is a point mark and follows the rule
// remapSyncedLyrics applies to a line: dropped when the cut took its instant,
// otherwise shifted with the audio around it.
func remapChapters(chs []waxlabel.Chapter, cut *appliedCut) []waxlabel.Chapter {
// Sort a copy by start first: until-next resolution reads the following
// chapter, and containers do not promise file order is timeline order. An
// out-of-order list would resolve inverted spans and drop live chapters.
chs = slices.Clone(chs)
slices.SortStableFunc(chs, func(a, b waxlabel.Chapter) int { return cmp.Compare(a.Start, b.Start) })
out := make([]waxlabel.Chapter, 0, len(chs))
for i, ch := range chs {
end := ch.End
if end == 0 {
if i+1 < len(chs) {
end = chs[i+1].Start
} else {
end = cut.total
}
}
ms := cutrange.MapTime(cut.keeps, cut.crossfade, ch.Start)
me := cutrange.MapTime(cut.keeps, cut.crossfade, end)
// Only content inside the source is the cut's to remove: a span that
// maps to nothing was removed; a point mark went with its instant.
content := min(end, cut.total) > ch.Start
if (content && me <= ms) || (!content && cut.removed(ch.Start)) {
continue
}
nc := waxlabel.Chapter{Start: ms, Title: ch.Title}
if ch.End > 0 {
nc.End = me
}
out = append(out, nc)
}
return out
}
// remapSyncedLyrics maps lyric sets from the source timeline onto a cut's
// output timeline. A line whose instant lies in a removed span is dropped,
// because its timestamp would otherwise point at audio that is no longer there;
// the rest shift by the audio removed before them. Sets left with no lines are
// removed, and dropped counts every line that went.
//
// The rule is the line's instant, not a span, because a lyric line has no
// duration of its own: it is the moment its text appears. That makes one case
// worth stating plainly: a cut starting at zero removes a line at 0:00, since
// the instant it marks is exactly the audio the cut took.
//
// Only instants inside the source ([0, total)) are the cut's to judge. A line
// at or past the declared end marks no removed audio whatever the cut did, so
// it survives and shifts like everything after the removals, which lands it at
// the output's end.
func remapSyncedLyrics(sls []waxlabel.SyncedLyrics, cut *appliedCut) (out []waxlabel.SyncedLyrics, dropped int) {
for _, sl := range sls {
lines := make([]waxlabel.SyncedLine, 0, len(sl.Lines))
for _, l := range sl.Lines {
if cut.removed(l.Time) {
dropped++
continue
}
l.Time = cutrange.MapTime(cut.keeps, cut.crossfade, l.Time)
lines = append(lines, l)
}
if len(lines) == 0 {
// WaxLabel's SetSyncedLyrics silently skips an empty set, so an
// emptied one is dropped here where its lines can still be counted.
continue
}
sl.Lines = lines
out = append(out, sl)
}
return out, dropped
}