-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdoc.go
More file actions
64 lines (64 loc) · 2.69 KB
/
Copy pathdoc.go
File metadata and controls
64 lines (64 loc) · 2.69 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
// Package playclock reconstructs, on the server, when a client is actually
// producing sound from audio the server streamed to it.
//
// # The problem
//
// A server streaming synthesized speech knows two things about every chunk it
// hands off: when it sent the chunk, and how long that chunk plays. It does not
// observe the client's speaker. Yet plenty of decisions depend on knowing
// whether the client is still talking — whether a user's speech is an
// interruption or a natural turn, whether to send the next utterance, whether a
// barge-in arrived mid-sentence and where.
//
// The obvious estimate is wrong:
//
// end := startedAt + sum(chunk.Duration) // wrong
//
// Audio is generated while it plays. As soon as one chunk is produced more
// slowly than the previous chunk takes to play, the client runs dry, and
// playback resumes only when the next chunk lands. That silence is not in any
// chunk's duration, so the real end of playback moves later — and because every
// stall shifts everything after it, the error accumulates across a stream.
//
// # What this package does
//
// A [Timeline] advances a cursor chunk by chunk:
//
// cursor = max(cursor, chunk.SentAt) + chunk.Duration
//
// Taking the later of "when the previous chunk finished" and "when this chunk
// arrived" reproduces each stall instead of ignoring it, so the reconstructed
// timeline tracks the client rather than the generator. The stalls it finds are
// reported as [Gap] values, which is a direct measure of whether the generator
// is keeping up with realtime.
//
// # The tail buffer
//
// The server cannot observe the last hop: network delay plus the client's own
// decode and playback buffering sit between "chunk sent" and "sound audible".
// That delay is unmeasurable from the server, but its sign is not — the server's
// estimate is always early, never late. A tail buffer therefore extends the
// timeline forward only (see [WithTailBuffer]).
//
// Inside that window [Timeline] reports playback as ongoing but refuses to name
// a position, because the stream has most likely finished and a position would
// be a guess. Callers that act on the position — resuming an interrupted
// utterance from where it stopped, for instance — must check [State.Chunk] for
// nil rather than assume that IsPlaying implies a known position.
//
// # Usage
//
// tl := playclock.New()
//
// // as each chunk is handed to the client
// tl.Append(time.Now(), chunkDuration)
//
// // when the user starts speaking
// if st := tl.State(); st.IsPlaying {
// if st.Chunk != nil {
// log.Printf("interrupted in chunk %d at %v", st.Chunk.Index, st.Offset)
// }
// }
//
// A Timeline is safe for concurrent use by multiple goroutines.
package playclock