返回 CodeWhale
PetAudioOutput.kt
根目录 / pet / android / src / main / kotlin / codewhale / pet / PetAudioOutput.kt
1 package codewhale.pet
2
3 import android.content.Context
4 import android.media.AudioAttributes
5 import android.media.AudioFocusRequest
6 import android.media.AudioFormat
7 import android.media.AudioManager
8 import android.media.AudioTrack
9 import android.os.Handler
10 import android.os.Looper
11 import android.os.SystemClock
12 import java.util.concurrent.ArrayBlockingQueue
13 import java.util.concurrent.TimeUnit
14 import java.util.concurrent.atomic.AtomicBoolean
15 import java.util.concurrent.atomic.AtomicReference
16 import kotlin.concurrent.thread
17 import kotlin.math.max
18
19 /** One optional output device. Slow audio drops a bounded packet, never blocks
20 * the world worker, and never builds a backlog to play after a pause. */
21 class PetAudioOutput(context: Context, onFocusLost: () -> Unit) : AutoCloseable {
22 private data class Packet(val samples: FloatArray, val createdMs: Long = SystemClock.elapsedRealtime())
23 private val pending = ArrayBlockingQueue<Packet>(4)
24 private val running = AtomicBoolean(true)
25 private val closed = AtomicBoolean(false)
26 private val failure = AtomicReference<String?>(null)
27 private val manager = context.getSystemService(AudioManager::class.java)
28 private val attributes = AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
29 .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build()
30 private val focus = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
31 .setAudioAttributes(attributes).setOnAudioFocusChangeListener({ change ->
32 if (change != AudioManager.AUDIOFOCUS_GAIN) { close(); onFocusLost() }
33 }, Handler(Looper.getMainLooper())).build()
34 private lateinit var writer: Thread
35
36 init {
37 check(manager.requestAudioFocus(focus) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { "Another app is using audio. Try Sound again." }
38 writer = thread(name = "codewhale-pet-audio", isDaemon = true) {
39 var track: AudioTrack? = null
40 try {
41 val minimum = AudioTrack.getMinBufferSize(48_000, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_FLOAT)
42 check(minimum > 0) { "Stereo float audio is unavailable." }
43 track = AudioTrack.Builder().setAudioAttributes(attributes).setAudioFormat(AudioFormat.Builder()
44 .setSampleRate(48_000).setEncoding(AudioFormat.ENCODING_PCM_FLOAT)
45 .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO).build())
46 .setTransferMode(AudioTrack.MODE_STREAM).setBufferSizeInBytes(max(minimum, 4_800 * 8)).build()
47 check(track.state == AudioTrack.STATE_INITIALIZED) { "Audio output could not start." }
48 track.play()
49 while (running.get()) {
50 val packet = pending.poll(100, TimeUnit.MILLISECONDS) ?: continue
51 var offset = 0
52 while (running.get() && offset < packet.samples.size && SystemClock.elapsedRealtime() - packet.createdMs <= 500) {
53 val n = track.write(packet.samples, offset, packet.samples.size - offset, AudioTrack.WRITE_NON_BLOCKING)
54 check(n >= 0) { "Audio output stopped ($n)." }
55 offset += n
56 if (n == 0) Thread.sleep(4)
57 }
58 }
59 } catch (_: InterruptedException) {
60 // Closing wakes a waiting writer; the device is always released below.
61 } catch (e: Exception) {
62 failure.set(e.message ?: "Audio output stopped.")
63 } finally {
64 running.set(false); pending.clear()
65 track?.let { runCatching { it.pause(); it.flush() }; it.release() }
66 }
67 }
68 }
69
70 fun offer(samples: FloatArray) {
71 failure.get()?.let { error(it) }
72 check(running.get()) { "Audio output stopped." }
73 require(samples.size in 2..48_000 && samples.size % 2 == 0 && samples.all { it.isFinite() && it in -1f..1f })
74 pending.offer(Packet(samples))
75 }
76 override fun close() {
77 if (closed.getAndSet(true)) return
78 running.set(false); pending.clear()
79 // This may also be called by the focus listener during construction.
80 if (this::writer.isInitialized) writer.interrupt()
81 manager.abandonAudioFocusRequest(focus)
82 }
83 }
84
84 lines Plain Text