@mentra/acs-meeting 3.2.0-dev.120
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +20 -0
- package/README.md +412 -0
- package/THIRD_PARTY +7 -0
- package/android/build.gradle +64 -0
- package/android/src/main/AndroidManifest.xml +5 -0
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +80 -0
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +846 -0
- package/android/src/main/java/com/mentra/acsmeeting/RemoteRoster.kt +123 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/AcsAudioPolicy.kt +90 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/AudioPolicyApplier.kt +176 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/IncomingAudioPump.kt +112 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/IncomingRateProbe.kt +65 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/PcmBridge.kt +108 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/PcmResampler.kt +159 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/PhoneMicCapturer.kt +115 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkPacer.kt +302 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkSender.kt +230 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/BitmapFont.kt +99 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/CloudflareWhepSource.kt +578 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/FirstFrameGate.kt +45 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/GlassesMediaSource.kt +101 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/MediaListeners.kt +57 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticFrameFactory.kt +205 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticI420Source.kt +214 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/TrackRegistry.kt +31 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/VideoSourceArm.kt +41 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/ChromaProbe.kt +78 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineStats.kt +281 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineTicker.kt +51 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/ProcessCpu.kt +15 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/RingPercentile.kt +33 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/AcsFrameSender.kt +475 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/AcsTimestamp.kt +26 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/FrameGeometry.kt +22 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/I420FormatSpec.kt +90 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/I420Packer.kt +72 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/Nv12FormatSpec.kt +36 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/Nv12Packer.kt +51 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/SendGate.kt +27 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/VideoProfile.kt +57 -0
- package/android/src/test/java/com/mentra/acsmeeting/RemoteRosterTest.kt +40 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/AcsAudioPolicyTest.kt +204 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/AudioPolicyApplierTest.kt +223 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/CapturePolicyTest.kt +12 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/IncomingAudioPumpTest.kt +97 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/IncomingRateProbeTest.kt +78 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/PcmResamplerTest.kt +125 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/UplinkPacerTest.kt +201 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/UplinkSenderTest.kt +166 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/AcsInvestigationTest.kt +26 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/BitmapFontTest.kt +22 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/FirstFrameGateTest.kt +60 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/GlassesMediaSourceTest.kt +133 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/I420PlanesTest.kt +67 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticFrameFactoryTest.kt +193 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticI420SourceTest.kt +176 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/TrackRegistryTest.kt +36 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/ChromaProbeTest.kt +61 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/PipelineStatsTest.kt +256 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/ProcessCpuTest.kt +18 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/RingPercentileTest.kt +49 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/AcsTimestampTest.kt +43 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/FrameGeometryTest.kt +33 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/I420FormatSpecTest.kt +77 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/I420PackerTest.kt +126 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/Nv12FormatSpecTest.kt +36 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/Nv12PackerTest.kt +58 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/SendGateTest.kt +79 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/VideoProfileTest.kt +68 -0
- package/build/AcsMeeting.types.d.ts +59 -0
- package/build/AcsMeeting.types.d.ts.map +1 -0
- package/build/AcsMeeting.types.js +2 -0
- package/build/AcsMeeting.types.js.map +1 -0
- package/build/AcsMeetingModule.d.ts +15 -0
- package/build/AcsMeetingModule.d.ts.map +1 -0
- package/build/AcsMeetingModule.js +3 -0
- package/build/AcsMeetingModule.js.map +1 -0
- package/build/AcsMeetingModule.web.d.ts +16 -0
- package/build/AcsMeetingModule.web.d.ts.map +1 -0
- package/build/AcsMeetingModule.web.js +31 -0
- package/build/AcsMeetingModule.web.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AcsFrameSender.swift +47 -0
- package/ios/AcsMeeting.podspec +27 -0
- package/ios/AcsMeetingModule.swift +558 -0
- package/ios/PcmBridge.swift +117 -0
- package/ios/PhoneMicCapturer.swift +64 -0
- package/ios/PolicyKit/Package.swift +17 -0
- package/ios/PolicyKit/Sources/AcsAudioPolicy/AcsAudioPolicy.swift +102 -0
- package/ios/PolicyKit/Sources/AcsAudioPolicy/AudioPolicyApplier.swift +162 -0
- package/ios/PolicyKit/Sources/AcsAudioPolicy/GlassesMediaSource.swift +68 -0
- package/ios/PolicyKit/Tests/AcsAudioPolicyTests/AcsAudioPolicyTests.swift +273 -0
- package/ios/WhepVideoSource.swift +356 -0
- package/package.json +56 -0
- package/src/AcsMeeting.types.ts +66 -0
- package/src/AcsMeetingModule.ts +16 -0
- package/src/AcsMeetingModule.web.ts +33 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.audio
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.azure.android.communication.calling.RawAudioBuffer
|
|
5
|
+
import com.azure.android.communication.calling.RawOutgoingAudioStream
|
|
6
|
+
import com.mentra.acsmeeting.telemetry.RingPercentile
|
|
7
|
+
import java.nio.ByteBuffer
|
|
8
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
9
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
10
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
11
|
+
import java.util.concurrent.locks.LockSupport
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Submits one paced frame to ACS.
|
|
15
|
+
*
|
|
16
|
+
* The implementation owns immutable storage for the frame until it invokes
|
|
17
|
+
* `onComplete`; the caller's array is free the moment `send` returns. This is
|
|
18
|
+
* the whole point of the seam: `sendRawAudioBuffer` is asynchronous, so a
|
|
19
|
+
* buffer reused on the next tick could be overwritten mid-send.
|
|
20
|
+
*/
|
|
21
|
+
fun interface UplinkTransport {
|
|
22
|
+
fun send(frame: ByteArray, onComplete: (Throwable?) -> Unit)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** [UplinkTransport] over ACS's raw outgoing audio stream. */
|
|
26
|
+
class AcsUplinkTransport(private val stream: RawOutgoingAudioStream) : UplinkTransport {
|
|
27
|
+
override fun send(frame: ByteArray, onComplete: (Throwable?) -> Unit) {
|
|
28
|
+
// Fresh direct storage per submission. Pooling is a valid follow-up, but
|
|
29
|
+
// only if buffers return to the pool on completion rather than on return.
|
|
30
|
+
val direct = ByteBuffer.allocateDirect(frame.size)
|
|
31
|
+
direct.put(frame)
|
|
32
|
+
direct.flip()
|
|
33
|
+
val buffer = RawAudioBuffer()
|
|
34
|
+
buffer.buffer = direct
|
|
35
|
+
stream.sendRawAudioBuffer(buffer).whenComplete { _, error ->
|
|
36
|
+
try {
|
|
37
|
+
buffer.close()
|
|
38
|
+
} catch (closeError: Exception) {
|
|
39
|
+
Log.w(TAG, "RawAudioBuffer close failed", closeError)
|
|
40
|
+
}
|
|
41
|
+
onComplete(error)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private companion object {
|
|
46
|
+
const val TAG = "ACS-SPIKE"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Drains [UplinkPacer] on a monotonic deadline and hands frames to ACS.
|
|
52
|
+
*
|
|
53
|
+
* A fixed-rate executor is deliberately not used: after a GC or CPU-contention
|
|
54
|
+
* stall it is allowed to replay the missed periods back to back, recreating
|
|
55
|
+
* exactly the burstiness the pacer exists to remove. This loop advances a
|
|
56
|
+
* `System.nanoTime()` deadline instead and sends exactly one frame per wake,
|
|
57
|
+
* counting what it skipped.
|
|
58
|
+
*/
|
|
59
|
+
class UplinkSender(
|
|
60
|
+
private val pacer: UplinkPacer,
|
|
61
|
+
private val transport: UplinkTransport,
|
|
62
|
+
private val clock: () -> Long = System::nanoTime,
|
|
63
|
+
private val log: (String) -> Unit = { Log.i(TAG, it) },
|
|
64
|
+
private val logError: (String, Throwable?) -> Unit = { message, error -> Log.e(TAG, message, error) },
|
|
65
|
+
) {
|
|
66
|
+
data class Stats(
|
|
67
|
+
val framesSubmitted: Long,
|
|
68
|
+
val skippedTicks: Long,
|
|
69
|
+
val tickLateMaxMs: Long,
|
|
70
|
+
val inFlight: Int,
|
|
71
|
+
val sendFailures: Long,
|
|
72
|
+
val backpressureDrops: Long,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
private val running = AtomicBoolean(false)
|
|
76
|
+
private val inFlight = AtomicInteger(0)
|
|
77
|
+
private val sendFailures = AtomicLong(0)
|
|
78
|
+
private val framesSubmitted = AtomicLong(0)
|
|
79
|
+
private val lateness = RingPercentile(64)
|
|
80
|
+
private val completion = RingPercentile(64)
|
|
81
|
+
|
|
82
|
+
private var thread: Thread? = null
|
|
83
|
+
@Volatile private var nextDeadlineNanos: Long? = null
|
|
84
|
+
@Volatile private var skippedTicks = 0L
|
|
85
|
+
@Volatile private var maxLateNanos = 0L
|
|
86
|
+
@Volatile private var backpressureDrops = 0L
|
|
87
|
+
@Volatile private var lastLogNanos: Long? = null
|
|
88
|
+
@Volatile private var lastFailureLogNanos: Long? = null
|
|
89
|
+
|
|
90
|
+
/** Idempotent: a second call is a no-op so a session can only ever pace once. */
|
|
91
|
+
@Synchronized
|
|
92
|
+
fun start() {
|
|
93
|
+
if (thread != null) {
|
|
94
|
+
log("P8 audio-up sender already started; ignoring")
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
nextDeadlineNanos = null
|
|
98
|
+
lastLogNanos = null
|
|
99
|
+
running.set(true)
|
|
100
|
+
// Audio cadence: a late frame is an artifact, so outrank the RN and
|
|
101
|
+
// decoder threads this competes with.
|
|
102
|
+
val started = Thread(::loop, "acs-uplink-pacer").apply {
|
|
103
|
+
priority = Thread.MAX_PRIORITY
|
|
104
|
+
isDaemon = true
|
|
105
|
+
}
|
|
106
|
+
thread = started
|
|
107
|
+
started.start()
|
|
108
|
+
log("P8 audio-up sender started periodMs=$PERIOD_MS frameBytes=${UplinkPacer.FRAME_BYTES}")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Idempotent. Stops feeding ACS and joins the pacing thread. */
|
|
112
|
+
@Synchronized
|
|
113
|
+
fun stop() {
|
|
114
|
+
val current = thread ?: return
|
|
115
|
+
thread = null
|
|
116
|
+
running.set(false)
|
|
117
|
+
current.interrupt()
|
|
118
|
+
current.join(JOIN_TIMEOUT_MS)
|
|
119
|
+
log(
|
|
120
|
+
"P8 audio-up sender stopped frames=${framesSubmitted.get()} " +
|
|
121
|
+
"skippedTicks=$skippedTicks sendFailures=${sendFailures.get()}",
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fun isRunning(): Boolean = running.get()
|
|
126
|
+
|
|
127
|
+
fun stats(): Stats = Stats(
|
|
128
|
+
framesSubmitted = framesSubmitted.get(),
|
|
129
|
+
skippedTicks = skippedTicks,
|
|
130
|
+
tickLateMaxMs = maxLateNanos / 1_000_000L,
|
|
131
|
+
inFlight = inFlight.get(),
|
|
132
|
+
sendFailures = sendFailures.get(),
|
|
133
|
+
backpressureDrops = backpressureDrops,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
private fun loop() {
|
|
137
|
+
while (running.get()) {
|
|
138
|
+
val deadline = nextDeadlineNanos
|
|
139
|
+
val now = clock()
|
|
140
|
+
if (deadline == null || now - deadline >= 0) {
|
|
141
|
+
pumpOnce(now)
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
LockSupport.parkNanos(deadline - now)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* One pacing period: exactly one frame out, deadline advanced, nothing
|
|
150
|
+
* replayed. Returns the next deadline. Package-visible so tests can run the
|
|
151
|
+
* loop body against a fake clock.
|
|
152
|
+
*/
|
|
153
|
+
internal fun pumpOnce(nowNanos: Long): Long {
|
|
154
|
+
val deadline = nextDeadlineNanos ?: nowNanos
|
|
155
|
+
val lateNanos = nowNanos - deadline
|
|
156
|
+
if (lateNanos > 0) {
|
|
157
|
+
lateness.record(lateNanos)
|
|
158
|
+
if (lateNanos > maxLateNanos) maxLateNanos = lateNanos
|
|
159
|
+
}
|
|
160
|
+
val next = if (lateNanos >= PERIOD_NANOS) {
|
|
161
|
+
// A whole period or more was lost. Re-base on now instead of catching
|
|
162
|
+
// up, so the frames ACS already missed stay missed.
|
|
163
|
+
skippedTicks += lateNanos / PERIOD_NANOS
|
|
164
|
+
nowNanos + PERIOD_NANOS
|
|
165
|
+
} else {
|
|
166
|
+
deadline + PERIOD_NANOS
|
|
167
|
+
}
|
|
168
|
+
nextDeadlineNanos = next
|
|
169
|
+
submit(pacer.tick(nowNanos))
|
|
170
|
+
maybeLog(nowNanos)
|
|
171
|
+
return next
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private fun submit(frame: UplinkPacer.Frame) {
|
|
175
|
+
// ACS is not keeping up; feeding it harder only grows the queue.
|
|
176
|
+
if (inFlight.get() >= MAX_IN_FLIGHT) {
|
|
177
|
+
backpressureDrops += 1
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
inFlight.incrementAndGet()
|
|
181
|
+
framesSubmitted.incrementAndGet()
|
|
182
|
+
val startedNanos = clock()
|
|
183
|
+
try {
|
|
184
|
+
transport.send(frame.bytes) { error ->
|
|
185
|
+
inFlight.decrementAndGet()
|
|
186
|
+
completion.record(clock() - startedNanos)
|
|
187
|
+
if (error != null) recordFailure(error)
|
|
188
|
+
}
|
|
189
|
+
} catch (error: Exception) {
|
|
190
|
+
inFlight.decrementAndGet()
|
|
191
|
+
recordFailure(error)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private fun recordFailure(error: Throwable) {
|
|
196
|
+
sendFailures.incrementAndGet()
|
|
197
|
+
val now = clock()
|
|
198
|
+
val last = lastFailureLogNanos
|
|
199
|
+
if (last != null && now - last < LOG_INTERVAL_NANOS) return
|
|
200
|
+
lastFailureLogNanos = now
|
|
201
|
+
logError("P8 audio-up sendRawAudioBuffer failed total=${sendFailures.get()}", error)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private fun maybeLog(nowNanos: Long) {
|
|
205
|
+
val last = lastLogNanos
|
|
206
|
+
if (last != null && nowNanos - last < LOG_INTERVAL_NANOS) return
|
|
207
|
+
lastLogNanos = nowNanos
|
|
208
|
+
if (last == null) return
|
|
209
|
+
val pace = pacer.snapshot(nowNanos)
|
|
210
|
+
log(
|
|
211
|
+
"P8 audio-up state=${pace.state} depthMs=${pace.depthMs} targetMs=${pace.targetMs} " +
|
|
212
|
+
"sentFps=${"%.1f".format(pace.sentFps)} silenceFrames=${pace.silenceFrames} " +
|
|
213
|
+
"overflowDroppedMs=${pace.overflowDroppedMs} driftCorrections=${pace.driftCorrections} " +
|
|
214
|
+
"driftDroppedMs=${pace.driftDroppedMs} driftInsertedMs=${pace.driftInsertedMs} " +
|
|
215
|
+
"tickLateP95Ms=${lateness.p95()} tickLateMaxMs=${maxLateNanos / 1_000_000L} " +
|
|
216
|
+
"skippedTicks=$skippedTicks inFlight=${inFlight.get()} " +
|
|
217
|
+
"sendFailures=${sendFailures.get()} sendCompletionP95Ms=${completion.p95()} " +
|
|
218
|
+
"backpressureDrops=$backpressureDrops",
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
companion object {
|
|
223
|
+
private const val TAG = "ACS-SPIKE"
|
|
224
|
+
const val PERIOD_MS = UplinkPacer.FRAME_MS
|
|
225
|
+
const val PERIOD_NANOS = PERIOD_MS * 1_000_000L
|
|
226
|
+
const val MAX_IN_FLIGHT = 10
|
|
227
|
+
private const val JOIN_TIMEOUT_MS = 500L
|
|
228
|
+
private const val LOG_INTERVAL_NANOS = 1_000_000_000L
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.source
|
|
2
|
+
|
|
3
|
+
import java.nio.ByteBuffer
|
|
4
|
+
|
|
5
|
+
/** 5x7 glyphs for the synthetic stamp. No Android canvas, so this is the font. */
|
|
6
|
+
internal object BitmapFont {
|
|
7
|
+
const val GLYPH_W = 5
|
|
8
|
+
const val GLYPH_H = 7
|
|
9
|
+
|
|
10
|
+
fun draw(
|
|
11
|
+
dest: ByteBuffer,
|
|
12
|
+
frameWidth: Int,
|
|
13
|
+
frameHeight: Int,
|
|
14
|
+
text: String,
|
|
15
|
+
originX: Int,
|
|
16
|
+
originY: Int,
|
|
17
|
+
scale: Int,
|
|
18
|
+
on: Byte,
|
|
19
|
+
) {
|
|
20
|
+
var x = originX
|
|
21
|
+
for (ch in text) {
|
|
22
|
+
val glyph = glyph(ch) ?: continue
|
|
23
|
+
blit(dest, frameWidth, frameHeight, glyph, x, originY, scale, on)
|
|
24
|
+
x += (GLYPH_W + 1) * scale
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fun textWidth(length: Int, scale: Int): Int =
|
|
29
|
+
if (length <= 0) 0 else length * (GLYPH_W + 1) * scale - scale
|
|
30
|
+
|
|
31
|
+
fun textHeight(scale: Int): Int = GLYPH_H * scale
|
|
32
|
+
|
|
33
|
+
private fun blit(
|
|
34
|
+
dest: ByteBuffer,
|
|
35
|
+
frameWidth: Int,
|
|
36
|
+
frameHeight: Int,
|
|
37
|
+
glyph: IntArray,
|
|
38
|
+
originX: Int,
|
|
39
|
+
originY: Int,
|
|
40
|
+
scale: Int,
|
|
41
|
+
on: Byte,
|
|
42
|
+
) {
|
|
43
|
+
for (row in 0 until GLYPH_H) {
|
|
44
|
+
val bits = glyph[row]
|
|
45
|
+
for (col in 0 until GLYPH_W) {
|
|
46
|
+
if ((bits shr (GLYPH_W - 1 - col)) and 1 == 0) continue
|
|
47
|
+
val x0 = originX + col * scale
|
|
48
|
+
val y0 = originY + row * scale
|
|
49
|
+
val xEnd = (x0 + scale).coerceAtMost(frameWidth)
|
|
50
|
+
val yEnd = (y0 + scale).coerceAtMost(frameHeight)
|
|
51
|
+
val xStart = x0.coerceAtLeast(0)
|
|
52
|
+
val yStart = y0.coerceAtLeast(0)
|
|
53
|
+
if (xStart >= xEnd || yStart >= yEnd) continue
|
|
54
|
+
for (y in yStart until yEnd) {
|
|
55
|
+
val rowOff = y * frameWidth
|
|
56
|
+
for (x in xStart until xEnd) dest.put(rowOff + x, on)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private fun glyph(ch: Char): IntArray? = when (ch) {
|
|
63
|
+
'0' -> G0
|
|
64
|
+
'1' -> G1
|
|
65
|
+
'2' -> G2
|
|
66
|
+
'3' -> G3
|
|
67
|
+
'4' -> G4
|
|
68
|
+
'5' -> G5
|
|
69
|
+
'6' -> G6
|
|
70
|
+
'7' -> G7
|
|
71
|
+
'8' -> G8
|
|
72
|
+
'9' -> G9
|
|
73
|
+
'F' -> GF
|
|
74
|
+
'P' -> GP
|
|
75
|
+
'S' -> GS
|
|
76
|
+
'X' -> GX
|
|
77
|
+
'x' -> GX
|
|
78
|
+
'.' -> GDOT
|
|
79
|
+
' ' -> GSPACE
|
|
80
|
+
else -> null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private val G0 = intArrayOf(0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110)
|
|
84
|
+
private val G1 = intArrayOf(0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110)
|
|
85
|
+
private val G2 = intArrayOf(0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111)
|
|
86
|
+
private val G3 = intArrayOf(0b01110, 0b10001, 0b00001, 0b00110, 0b00001, 0b10001, 0b01110)
|
|
87
|
+
private val G4 = intArrayOf(0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010)
|
|
88
|
+
private val G5 = intArrayOf(0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110)
|
|
89
|
+
private val G6 = intArrayOf(0b01110, 0b10000, 0b11110, 0b10001, 0b10001, 0b10001, 0b01110)
|
|
90
|
+
private val G7 = intArrayOf(0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000)
|
|
91
|
+
private val G8 = intArrayOf(0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110)
|
|
92
|
+
private val G9 = intArrayOf(0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00001, 0b01110)
|
|
93
|
+
private val GF = intArrayOf(0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000)
|
|
94
|
+
private val GP = intArrayOf(0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000)
|
|
95
|
+
private val GS = intArrayOf(0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110)
|
|
96
|
+
private val GX = intArrayOf(0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001)
|
|
97
|
+
private val GDOT = intArrayOf(0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00100, 0b00100)
|
|
98
|
+
private val GSPACE = intArrayOf(0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000)
|
|
99
|
+
}
|