I have an android application written in Kotlin & jetpack compose. The application is running on a device with physical keyboard.
My goal is to play a sound every time I click a button.
Ideally, I want to play a sound of small duration so when I am making fast typing I want to play one sound for each button click. Sounds of large duration, work but sometimes they are not as frequent as the frequency of typing.
Initially, I tried to use MediaPlayer. The problem with this approach is that when I use specific sounds, the application crashes and I get null pointer exception. The application does not even start. This happens mostly with small sounds.
class MainActivity : ComponentActivity() {
private lateinit var mediaPlayer: MediaPlayer
@OptIn(ExperimentalComposeUiApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mediaPlayer = MediaPlayer.create(this, R.raw.keypress33)
setContent {
DemoTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
val cursorPosition = remember { mutableStateOf(1) }
var textFieldValue by remember { mutableStateOf(TextFieldValue(text = "1", selection = TextRange(cursorPosition.value)) ) }
Box(
contentAlignment = Alignment.Center
) {
OutlinedTextField(
value = textFieldValue,
onValueChange = { newValue ->
textFieldValue = newValue
},
modifier = Modifier
.focusRequester(focusRequester)
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
keyboardController?.hide()
}
}
}
}
}
}
override fun onDestroy() {
mediaPlayer.release()
super.onDestroy()
}
@SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (event?.action == KeyEvent.ACTION_DOWN) {
playSound()
}
return super.dispatchKeyEvent(event)
}
private fun playSound() {
if (::mediaPlayer.isInitialized) {
mediaPlayer.start()
}
}
}
On the other hand, I have tried the SoundPool. There are other kind of issues here. The application does not crash, but it does not play any sound. If I change the sound and use something with little longer duration, it works but after a few clicks, it stops working.
class MainActivity : ComponentActivity() {
private lateinit var soundPool: SoundPool
private var soundId: Int = 0
private var isSoundLoaded = false
@OptIn(ExperimentalComposeUiApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
DemoTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
val attributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
soundPool = SoundPool.Builder()
.setAudioAttributes(attributes)
.setMaxStreams(2)
.build()
soundPool.setOnLoadCompleteListener { _, _, status ->
if (status == 0) {
isSoundLoaded = true
}
}
soundId = soundPool.load(this, R.raw.keypress, 1)
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
val cursorPosition = remember { mutableStateOf(1) }
var textFieldValue by remember { mutableStateOf(TextFieldValue(text = "1", selection = TextRange(cursorPosition.value)) ) }
Box(
contentAlignment = Alignment.Center
) {
OutlinedTextField(
value = textFieldValue,
onValueChange = { newValue ->
textFieldValue = newValue
},
modifier = Modifier
.focusRequester(focusRequester)
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
keyboardController?.hide()
}
}
}
}
}
}
override fun onDestroy() {
soundPool.release()
super.onDestroy()
}
@SuppressLint("RestrictedApi")
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (event?.action == KeyEvent.ACTION_DOWN) {
playSound()
}
return super.dispatchKeyEvent(event)
}
private fun playSound() {
if (isSoundLoaded) {
soundPool.play(
soundId,
1.0f,
1.0f,
1,
0,
1.0f
)
}
}
}