Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.content.pm.PackageManager
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import androidx.annotation.RequiresPermission
import androidx.core.app.ActivityCompat
import com.github.familyvault.AppConfig
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -35,6 +36,7 @@ class AudioRecorderService(
private val SAMPLE_RATE = AppConfig.AUDIO_SAMPLE_RATE
}

@RequiresPermission(Manifest.permission.RECORD_AUDIO)
override fun start() {
if (isRecording || !haveRecordingPermission())
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,63 @@
package com.github.familyvault.services

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
import platform.AVFAudio.AVAudioPlayer
import platform.AVFAudio.AVAudioPlayerDelegateProtocol
import platform.AVFAudio.AVAudioSession
import platform.AVFAudio.AVAudioSessionCategoryOptionDefaultToSpeaker
import platform.AVFAudio.AVAudioSessionCategoryPlayAndRecord
import platform.AVFAudio.setActive
import platform.Foundation.NSData
import platform.Foundation.dataWithBytes
import platform.darwin.NSObject

class AudioPlayerService : IAudioPlayerService {
private var audioPlayer: AVAudioPlayer? = null
private var listener: AudioPlayerListener? = null

private var playingJob: Job? = null

private val audioPlayerScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

@OptIn(ExperimentalForeignApi::class)
override fun play(audioData: ByteArray, onCompletion: (() -> Unit)?) {
TODO("Method not yet implemented")
prepareAudioSession()
audioPlayer = AVAudioPlayer(data = audioData.asData(), null)
audioPlayer?.prepareToPlay()
audioPlayer?.play()
listener = AudioPlayerListener {
onCompletion?.invoke()
}
audioPlayer?.delegate = listener
}

override fun stop() {
TODO("Method not yet implemented")
audioPlayer?.stop()
audioPlayer = null
listener = null
}

@OptIn(ExperimentalForeignApi::class)
private fun prepareAudioSession() {
AVAudioSession.sharedInstance().apply {
setCategory(
AVAudioSessionCategoryPlayAndRecord,
AVAudioSessionCategoryOptionDefaultToSpeaker,
null
)
setActive(true, null)
}
}

}

@OptIn(ExperimentalForeignApi::class)
fun ByteArray.asData(): NSData = usePinned {
NSData.dataWithBytes(it.addressOf(0), size.toULong())
}

class AudioPlayerListener(
private val didFinish: () -> Unit = {}
) : AVAudioPlayerDelegateProtocol, NSObject() {
override fun audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully: Boolean) {
didFinish()
}
}
Original file line number Diff line number Diff line change
@@ -1,37 +1,98 @@
package com.github.familyvault.services

import com.github.familyvault.AppConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

class AudioRecorderService(
) : IAudioRecorderService {
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.usePinned
import platform.AVFAudio.AVAudioRecorder
import platform.AVFAudio.AVAudioSession
import platform.AVFAudio.AVAudioSessionCategoryPlayAndRecord
import platform.AVFAudio.AVAudioSessionRecordPermissionGranted
import platform.AVFAudio.AVFormatIDKey
import platform.AVFAudio.AVNumberOfChannelsKey
import platform.AVFAudio.AVSampleRateKey
import platform.AVFAudio.setActive
import platform.CoreAudioTypes.kAudioFormatOpus
import platform.Foundation.NSApplicationSupportDirectory
import platform.Foundation.NSData
import platform.Foundation.NSFileManager
import platform.Foundation.NSUserDomainMask
import platform.Foundation.dataWithContentsOfFile
import platform.posix.memcpy
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

@OptIn(ExperimentalForeignApi::class)
class AudioRecorderService() : IAudioRecorderService{
private var recorder: AVAudioRecorder? = null
private var isRecording = false
private var recordingJob: Job? = null

private val bufferSize = 0

//TODO: This should be available from common source
companion object {
private const val RECORDING_PERMISSION_REQUEST_CODE = 1002
private val SAMPLE_RATE = AppConfig.AUDIO_SAMPLE_RATE
private val AUDIO_SETTINGS = mapOf<Any?,Any?>(
AVFormatIDKey to kAudioFormatOpus,
AVSampleRateKey to SAMPLE_RATE,
AVNumberOfChannelsKey to 1.toUInt(),
)
}


@OptIn(ExperimentalUuidApi::class)
override fun start() {
TODO("Method not yet implemented")
if (isRecording || !haveRecordingPermission()) return

//TODO: This file name should be correct prepared after implement local cache for audio files
val fileName = Uuid.random()
val file = NSFileManager.defaultManager.URLForDirectory(
NSApplicationSupportDirectory,
NSUserDomainMask,
null,
true,
null
)?.URLByAppendingPathComponent("files/audio/recorded/$fileName.opus")
NSFileManager.defaultManager.createDirectoryAtURL(file!!.URLByDeletingLastPathComponent!!,true,null,null)

prepareSession()
recorder = AVAudioRecorder(file, AUDIO_SETTINGS, null)
recorder?.let { recorder ->
recorder.prepareToRecord()
recorder.record()
isRecording = true
}
}

override fun stop() : ByteArray {
TODO("Method not yet implemented")
recorder?.stop()
return NSData.dataWithContentsOfFile(recorder!!.url.path!!)!!.toByteArray()
}

override fun requestRecordingPermission() {
TODO("Method not yet implemented")
AVAudioSession.sharedInstance().requestRecordPermission {}
}

override fun haveRecordingPermission(): Boolean {
TODO("Method not yet implemented")
return AVAudioSession.sharedInstance().recordPermission == AVAudioSessionRecordPermissionGranted
}

private fun prepareSession(){
AVAudioSession.sharedInstance().apply {
setCategory(
AVAudioSessionCategoryPlayAndRecord,
null
)
setActive(true,null)
}
}
}

@OptIn(ExperimentalForeignApi::class)
fun NSData.toByteArray(): ByteArray = memScoped {
ByteArray(length.toInt()).apply {
usePinned {
memcpy(it.addressOf(0), bytes, length)
}
}
}
2 changes: 2 additions & 0 deletions iosApp/iosApp/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSMicrophoneUsageDescription</key>
<string>This app use microphone for record audio files in chat.</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
Expand Down