Recently Written · git

subrep-android

Subrep on Android: live captions of the sound of the phone, made on the phone by Whisper, shared by link.

git clone https://github.com/equwal/subrep-android

Log | Files | Refs


app/src/main/kotlin/com/honjimaku/subrep/Feed.kt (4669 bytes)

1 package com.honjimaku.subrep
2 
3 import android.os.Handler
4 import android.os.Looper
5 import android.os.SystemClock
6 import java.util.concurrent.CopyOnWriteArraySet
7 
8 /**
9  * What the screen shows: the state of the service, the model download, and the last captions.
10  * The service writes it from its threads; the screen reads it on the main thread after each
11  * change.
12  */
13 object Feed {
14 
15     fun interface Listener {
16         fun onChange()
17     }
18 
19     private val main = Handler(Looper.getMainLooper())
20     private val listeners = CopyOnWriteArraySet<Listener>()
21     private val lock = Any()
22     private val finals = ArrayDeque<String>()
23     private var lastLevelAt = 0L
24 
25     @Volatile var running = false
26         private set
27 
28     /** [Engine.STATE_LOADING], [Engine.STATE_READY], or an error with its reason. */
29     @Volatile var engine = ""
30         private set
31 
32     /** The time Whisper took for the last piece, divided by the length of the piece. */
33     @Volatile var speed = 0f
34         private set
35     @Volatile var late = 0
36         private set
37     @Volatile var relayUp = false
38         private set
39     @Volatile var relayOn = false
40         private set
41     @Volatile var overlayAnswer = ""
42         private set
43     @Volatile var error = ""
44         private set
45 
46     /** The loudest sample of the last frame, 0 to 32768. Shows that sound comes in at all. */
47     @Volatile var peak = 0
48         private set
49     @Volatile var partial = ""
50         private set
51 
52     /** The model that downloads now, and its progress: bytes so far, total (-1 unknown). */
53     @Volatile var downloading: Model? = null
54         private set
55     @Volatile var downloadDone = 0L
56         private set
57     @Volatile var downloadTotal = -1L
58         private set
59     @Volatile var downloadError = ""
60         private set
61 
62     /** The seconds of cloud captions that the account has, -1 while unknown. */
63     @Volatile var cloudSecondsLeft = -1L
64         private set
65 
66     fun add(listener: Listener) = listeners.add(listener)
67 
68     fun remove(listener: Listener) = listeners.remove(listener)
69 
70     fun start(relayOn: Boolean) {
71         synchronized(lock) { finals.clear() }
72         partial = ""
73         error = ""
74         engine = ""
75         speed = 0f
76         late = 0
77         overlayAnswer = ""
78         peak = 0
79         relayUp = false
80         this.relayOn = relayOn
81         running = true
82         changed()
83     }
84 
85     fun stop() {
86         running = false
87         relayUp = false
88         peak = 0
89         changed()
90     }
91 
92     fun fail(reason: String) {
93         error = reason
94         changed()
95     }
96 
97     fun engine(state: String) {
98         engine = state
99         changed()
100     }
101 
102     fun progress(speed: Float, late: Int) {
103         this.speed = speed
104         this.late = late
105     }
106 
107     fun relay(up: Boolean) {
108         relayUp = up
109         changed()
110     }
111 
112     fun overlay(answer: String) {
113         if (answer == overlayAnswer) return
114         overlayAnswer = answer
115         changed()
116     }
117 
118     /** The level meter of the screen, 0 to 10. */
119     fun bars(): Int = (peak * 10 / 32768).coerceIn(0, 10)
120 
121     fun level(peak: Int) {
122         val before = bars()
123         this.peak = peak
124         // Frames come ten times a second. The screen redraws only when the meter changes, and
125         // not more than twice a second: an e-ink screen cannot draw more.
126         val now = SystemClock.elapsedRealtime()
127         if (bars() == before || now - lastLevelAt < 500) return
128         lastLevelAt = now
129         changed()
130     }
131 
132     fun caption(caption: Caption) {
133         synchronized(lock) {
134             when (caption.type) {
135                 "final" -> {
136                     finals.addLast(caption.text)
137                     while (finals.size > LINES) finals.removeFirst()
138                     partial = ""
139                 }
140                 "partial" -> partial = caption.text
141                 "clear" -> {
142                     finals.clear()
143                     partial = ""
144                 }
145             }
146         }
147         changed()
148     }
149 
150     fun download(model: Model?, done: Long, total: Long, error: String = "") {
151         downloading = model
152         downloadDone = done
153         downloadTotal = total
154         downloadError = error
155         changed()
156     }
157 
158     fun cloudLeft(seconds: Long) {
159         cloudSecondsLeft = seconds
160         changed()
161     }
162 
163     /** The last lines, and the partial line after them. */
164     fun text(): String {
165         val lines = synchronized(lock) { finals.toList() }
166         return (lines + listOf(partial).filter { it.isNotEmpty() }.map { "$it …" }).joinToString("\n")
167     }
168 
169     private fun changed() = main.post { listeners.forEach { it.onChange() } }
170 
171     private const val LINES = 8
172 }