@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.
Files changed (102) hide show
  1. package/NOTICE +20 -0
  2. package/README.md +412 -0
  3. package/THIRD_PARTY +7 -0
  4. package/android/build.gradle +64 -0
  5. package/android/src/main/AndroidManifest.xml +5 -0
  6. package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +80 -0
  7. package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +846 -0
  8. package/android/src/main/java/com/mentra/acsmeeting/RemoteRoster.kt +123 -0
  9. package/android/src/main/java/com/mentra/acsmeeting/audio/AcsAudioPolicy.kt +90 -0
  10. package/android/src/main/java/com/mentra/acsmeeting/audio/AudioPolicyApplier.kt +176 -0
  11. package/android/src/main/java/com/mentra/acsmeeting/audio/IncomingAudioPump.kt +112 -0
  12. package/android/src/main/java/com/mentra/acsmeeting/audio/IncomingRateProbe.kt +65 -0
  13. package/android/src/main/java/com/mentra/acsmeeting/audio/PcmBridge.kt +108 -0
  14. package/android/src/main/java/com/mentra/acsmeeting/audio/PcmResampler.kt +159 -0
  15. package/android/src/main/java/com/mentra/acsmeeting/audio/PhoneMicCapturer.kt +115 -0
  16. package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkPacer.kt +302 -0
  17. package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkSender.kt +230 -0
  18. package/android/src/main/java/com/mentra/acsmeeting/source/BitmapFont.kt +99 -0
  19. package/android/src/main/java/com/mentra/acsmeeting/source/CloudflareWhepSource.kt +578 -0
  20. package/android/src/main/java/com/mentra/acsmeeting/source/FirstFrameGate.kt +45 -0
  21. package/android/src/main/java/com/mentra/acsmeeting/source/GlassesMediaSource.kt +101 -0
  22. package/android/src/main/java/com/mentra/acsmeeting/source/MediaListeners.kt +57 -0
  23. package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticFrameFactory.kt +205 -0
  24. package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticI420Source.kt +214 -0
  25. package/android/src/main/java/com/mentra/acsmeeting/source/TrackRegistry.kt +31 -0
  26. package/android/src/main/java/com/mentra/acsmeeting/source/VideoSourceArm.kt +41 -0
  27. package/android/src/main/java/com/mentra/acsmeeting/telemetry/ChromaProbe.kt +78 -0
  28. package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineStats.kt +281 -0
  29. package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineTicker.kt +51 -0
  30. package/android/src/main/java/com/mentra/acsmeeting/telemetry/ProcessCpu.kt +15 -0
  31. package/android/src/main/java/com/mentra/acsmeeting/telemetry/RingPercentile.kt +33 -0
  32. package/android/src/main/java/com/mentra/acsmeeting/video/AcsFrameSender.kt +475 -0
  33. package/android/src/main/java/com/mentra/acsmeeting/video/AcsTimestamp.kt +26 -0
  34. package/android/src/main/java/com/mentra/acsmeeting/video/FrameGeometry.kt +22 -0
  35. package/android/src/main/java/com/mentra/acsmeeting/video/I420FormatSpec.kt +90 -0
  36. package/android/src/main/java/com/mentra/acsmeeting/video/I420Packer.kt +72 -0
  37. package/android/src/main/java/com/mentra/acsmeeting/video/Nv12FormatSpec.kt +36 -0
  38. package/android/src/main/java/com/mentra/acsmeeting/video/Nv12Packer.kt +51 -0
  39. package/android/src/main/java/com/mentra/acsmeeting/video/SendGate.kt +27 -0
  40. package/android/src/main/java/com/mentra/acsmeeting/video/VideoProfile.kt +57 -0
  41. package/android/src/test/java/com/mentra/acsmeeting/RemoteRosterTest.kt +40 -0
  42. package/android/src/test/java/com/mentra/acsmeeting/audio/AcsAudioPolicyTest.kt +204 -0
  43. package/android/src/test/java/com/mentra/acsmeeting/audio/AudioPolicyApplierTest.kt +223 -0
  44. package/android/src/test/java/com/mentra/acsmeeting/audio/CapturePolicyTest.kt +12 -0
  45. package/android/src/test/java/com/mentra/acsmeeting/audio/IncomingAudioPumpTest.kt +97 -0
  46. package/android/src/test/java/com/mentra/acsmeeting/audio/IncomingRateProbeTest.kt +78 -0
  47. package/android/src/test/java/com/mentra/acsmeeting/audio/PcmResamplerTest.kt +125 -0
  48. package/android/src/test/java/com/mentra/acsmeeting/audio/UplinkPacerTest.kt +201 -0
  49. package/android/src/test/java/com/mentra/acsmeeting/audio/UplinkSenderTest.kt +166 -0
  50. package/android/src/test/java/com/mentra/acsmeeting/source/AcsInvestigationTest.kt +26 -0
  51. package/android/src/test/java/com/mentra/acsmeeting/source/BitmapFontTest.kt +22 -0
  52. package/android/src/test/java/com/mentra/acsmeeting/source/FirstFrameGateTest.kt +60 -0
  53. package/android/src/test/java/com/mentra/acsmeeting/source/GlassesMediaSourceTest.kt +133 -0
  54. package/android/src/test/java/com/mentra/acsmeeting/source/I420PlanesTest.kt +67 -0
  55. package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticFrameFactoryTest.kt +193 -0
  56. package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticI420SourceTest.kt +176 -0
  57. package/android/src/test/java/com/mentra/acsmeeting/source/TrackRegistryTest.kt +36 -0
  58. package/android/src/test/java/com/mentra/acsmeeting/telemetry/ChromaProbeTest.kt +61 -0
  59. package/android/src/test/java/com/mentra/acsmeeting/telemetry/PipelineStatsTest.kt +256 -0
  60. package/android/src/test/java/com/mentra/acsmeeting/telemetry/ProcessCpuTest.kt +18 -0
  61. package/android/src/test/java/com/mentra/acsmeeting/telemetry/RingPercentileTest.kt +49 -0
  62. package/android/src/test/java/com/mentra/acsmeeting/video/AcsTimestampTest.kt +43 -0
  63. package/android/src/test/java/com/mentra/acsmeeting/video/FrameGeometryTest.kt +33 -0
  64. package/android/src/test/java/com/mentra/acsmeeting/video/I420FormatSpecTest.kt +77 -0
  65. package/android/src/test/java/com/mentra/acsmeeting/video/I420PackerTest.kt +126 -0
  66. package/android/src/test/java/com/mentra/acsmeeting/video/Nv12FormatSpecTest.kt +36 -0
  67. package/android/src/test/java/com/mentra/acsmeeting/video/Nv12PackerTest.kt +58 -0
  68. package/android/src/test/java/com/mentra/acsmeeting/video/SendGateTest.kt +79 -0
  69. package/android/src/test/java/com/mentra/acsmeeting/video/VideoProfileTest.kt +68 -0
  70. package/build/AcsMeeting.types.d.ts +59 -0
  71. package/build/AcsMeeting.types.d.ts.map +1 -0
  72. package/build/AcsMeeting.types.js +2 -0
  73. package/build/AcsMeeting.types.js.map +1 -0
  74. package/build/AcsMeetingModule.d.ts +15 -0
  75. package/build/AcsMeetingModule.d.ts.map +1 -0
  76. package/build/AcsMeetingModule.js +3 -0
  77. package/build/AcsMeetingModule.js.map +1 -0
  78. package/build/AcsMeetingModule.web.d.ts +16 -0
  79. package/build/AcsMeetingModule.web.d.ts.map +1 -0
  80. package/build/AcsMeetingModule.web.js +31 -0
  81. package/build/AcsMeetingModule.web.js.map +1 -0
  82. package/build/index.d.ts +3 -0
  83. package/build/index.d.ts.map +1 -0
  84. package/build/index.js +3 -0
  85. package/build/index.js.map +1 -0
  86. package/expo-module.config.json +9 -0
  87. package/ios/AcsFrameSender.swift +47 -0
  88. package/ios/AcsMeeting.podspec +27 -0
  89. package/ios/AcsMeetingModule.swift +558 -0
  90. package/ios/PcmBridge.swift +117 -0
  91. package/ios/PhoneMicCapturer.swift +64 -0
  92. package/ios/PolicyKit/Package.swift +17 -0
  93. package/ios/PolicyKit/Sources/AcsAudioPolicy/AcsAudioPolicy.swift +102 -0
  94. package/ios/PolicyKit/Sources/AcsAudioPolicy/AudioPolicyApplier.swift +162 -0
  95. package/ios/PolicyKit/Sources/AcsAudioPolicy/GlassesMediaSource.swift +68 -0
  96. package/ios/PolicyKit/Tests/AcsAudioPolicyTests/AcsAudioPolicyTests.swift +273 -0
  97. package/ios/WhepVideoSource.swift +356 -0
  98. package/package.json +56 -0
  99. package/src/AcsMeeting.types.ts +66 -0
  100. package/src/AcsMeetingModule.ts +16 -0
  101. package/src/AcsMeetingModule.web.ts +33 -0
  102. 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
+ }