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/Capture.kt (2534 bytes)

1 package com.honjimaku.subrep
2 
3 import android.annotation.SuppressLint
4 import android.annotation.TargetApi
5 import android.media.AudioAttributes
6 import android.media.AudioFormat
7 import android.media.AudioPlaybackCaptureConfiguration
8 import android.media.AudioRecord
9 import android.media.MediaRecorder
10 import android.media.projection.MediaProjection
11 import android.os.Build
12 
13 /**
14  * Where the sound comes from: the apps on this device, or the microphone.
15  *
16  * Both give 16 kHz mono 16-bit PCM, which is what speech recognition uses. The engine takes any
17  * rate, but a lower rate is less to send.
18  */
19 object Capture {
20 
21     const val SAMPLE_RATE = 16_000
22 
23     /** 100 ms of sound: one frame to the engine. */
24     const val FRAME_BYTES = SAMPLE_RATE * 2 / 10
25 
26     /**
27      * The sound of the apps on this device, with the consent of the user in [projection].
28      * Android gives the media, game and unknown streams. An app can refuse the capture
29      * (a video app with DRM, some music apps): then there is silence.
30      */
31     @TargetApi(Build.VERSION_CODES.Q)
32     @SuppressLint("MissingPermission") // The screen checks RECORD_AUDIO before the service starts.
33     fun ofApps(projection: MediaProjection): AudioRecord {
34         val config = AudioPlaybackCaptureConfiguration.Builder(projection)
35             .addMatchingUsage(AudioAttributes.USAGE_MEDIA)
36             .addMatchingUsage(AudioAttributes.USAGE_GAME)
37             .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN)
38             .build()
39         return AudioRecord.Builder()
40             .setAudioFormat(format())
41             .setBufferSizeInBytes(bufferBytes())
42             .setAudioPlaybackCaptureConfig(config)
43             .build()
44     }
45 
46     /** The microphone, tuned for speech: for a television or a talk in the room. */
47     @SuppressLint("MissingPermission")
48     fun ofMicrophone(): AudioRecord = AudioRecord.Builder()
49         .setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION)
50         .setAudioFormat(format())
51         .setBufferSizeInBytes(bufferBytes())
52         .build()
53 
54     private fun format() = AudioFormat.Builder()
55         .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
56         .setSampleRate(SAMPLE_RATE)
57         .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
58         .build()
59 
60     /** One second of sound, or more when the device needs more. */
61     private fun bufferBytes(): Int {
62         val least = AudioRecord.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)
63         return maxOf(least, SAMPLE_RATE * 2)
64     }
65 }