Skip to content
Merged
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
6 changes: 3 additions & 3 deletions SaidIt/src/main/java/eu/mrogalski/saidit/AudioMemory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import java.nio.ByteBuffer
* Modern Kotlin implementation of AudioMemory with Result<T> error handling.
* Manages a ring buffer for audio data with improved error handling and thread safety.
*/
class AudioMemory(private val clock: Clock) {
open class AudioMemory(private val clock: Clock) {

companion object {
// Keep chunk size as allocation granularity (20s @ 48kHz mono 16-bit)
Expand Down Expand Up @@ -54,7 +54,7 @@ class AudioMemory(private val clock: Clock) {
* @return Result<Unit> indicating success or failure
*/
@Synchronized
fun allocate(sizeToEnsure: Long): Result<Unit> {
open fun allocate(sizeToEnsure: Long): Result<Unit> {
return try {
var required = 0
while (required < sizeToEnsure) required += CHUNK_SIZE
Expand Down Expand Up @@ -215,7 +215,7 @@ class AudioMemory(private val clock: Clock) {
* @return Result<Unit> indicating success or failure
*/
@Synchronized
fun dump(consumer: Consumer, bytesToDump: Int): Result<Unit> {
open fun dump(consumer: Consumer, bytesToDump: Int): Result<Unit> {
if (capacity == 0 || ring == null || size == 0 || bytesToDump <= 0) {
return Result.success(Unit)
}
Expand Down
53 changes: 39 additions & 14 deletions SaidIt/src/main/java/eu/mrogalski/saidit/SaidItService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,36 @@ class SaidItService : Service() {

private var mediaFile: File? = null
private var audioRecord: AudioRecord? = null
private var aacWriter: AacMp4Writer? = null
private val audioMemory = AudioMemory(SystemClockWrapper())
private var audioWriter: AudioSampleWriter? = null
private var audioMemory: AudioMemory = AudioMemory(SystemClockWrapper())

// Coroutine scope for audio operations
private val audioScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private var audioDispatcher: CoroutineDispatcher = Dispatchers.Default
private var audioScope = CoroutineScope(audioDispatcher + SupervisorJob())

@VisibleForTesting
internal var audioSampleWriterFactory: (Int, Int, Int, File) -> AudioSampleWriter =
{ sampleRate, channels, bitRate, file ->
AacMp4Writer(sampleRate, channels, bitRate, file)
}

@VisibleForTesting
internal fun overrideAudioMemoryForTest(memory: AudioMemory) {
audioMemory = memory
}

@VisibleForTesting
internal fun audioMemoryForTest(): AudioMemory = audioMemory

@VisibleForTesting
internal fun overrideAudioDispatcherForTest(dispatcher: CoroutineDispatcher) {
audioScope.cancel()
audioDispatcher = dispatcher
audioScope = CoroutineScope(audioDispatcher + SupervisorJob())
}

@VisibleForTesting
internal fun stateForTest(): Int = state
private var audioJob: Job? = null

@Volatile
Expand Down Expand Up @@ -188,8 +213,8 @@ class SaidItService : Service() {
Log.e(TAG, "AUDIO RECORD ERROR: $read")
return@Consumer Result.success(0)
}
if (aacWriter != null && read > 0) {
aacWriter?.write(array, offset, read)
if (audioWriter != null && read > 0) {
audioWriter?.write(array, offset, read)
}
Result.success(read)
} ?: Result.success(0)
Expand Down Expand Up @@ -263,19 +288,19 @@ class SaidItService : Service() {
try {
if (isTestEnvironment) {
mediaFile = null
aacWriter = null
audioWriter = null
return@launch
}

mediaFile = File.createTempFile("saidit", ".m4a", cacheDir)
aacWriter = AacMp4Writer(sampleRate, 1, 96_000, mediaFile!!)
audioWriter = audioSampleWriterFactory(sampleRate, 1, 96_000, mediaFile!!)
Log.d(TAG, "Recording to: ${mediaFile!!.absolutePath}")

if (prependedMemorySeconds > 0) {
val bytesPerSecond = (1f / getBytesToSeconds()).toInt()
val bytesToDump = (prependedMemorySeconds * bytesPerSecond).toInt()
audioMemory.dump(AudioMemory.LegacyConsumer { array, offset, count ->
aacWriter?.write(array, offset, count)
audioWriter?.write(array, offset, count)
count
}, bytesToDump)
}
Expand All @@ -295,18 +320,18 @@ class SaidItService : Service() {

audioScope.launch {
flushAudioRecord()
aacWriter?.let { writer ->
audioWriter?.let { writer ->
try {
writer.close()
} catch (e: IOException) {
Log.e(TAG, "CLOSING ERROR", e)
}
}

if (wavFileReceiver != null && mediaFile != null) {
saveFileToMediaStore(mediaFile!!, mediaFile!!.name, wavFileReceiver)
}
aacWriter = null
audioWriter = null
}
}

Expand All @@ -329,7 +354,7 @@ class SaidItService : Service() {

val fileName = newFileName?.replace("[^a-zA-Z0-9.-]".toRegex(), "_") ?: "SaidIt_dump"
dumpFile = File(cacheDir, "$fileName.m4a")
val dumper = AacMp4Writer(sampleRate, 1, 96_000, dumpFile)
val dumper = audioSampleWriterFactory(sampleRate, 1, 96_000, dumpFile)
Log.d(TAG, "Dumping to: ${dumpFile.absolutePath}")

val bytesPerSecond = (1f / getBytesToSeconds()).toInt()
Expand Down Expand Up @@ -461,7 +486,7 @@ class SaidItService : Service() {
val stats = audioMemory.getStats(fillRate)

var recorded = 0
aacWriter?.let { writer ->
audioWriter?.let { writer ->
recorded += writer.getTotalSampleBytesWritten()
recorded += stats.estimation
}
Expand Down
7 changes: 3 additions & 4 deletions SaidIt/src/main/kotlin/eu/mrogalski/saidit/AacMp4Writer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.media.MediaCodecInfo
import android.media.MediaFormat
import android.media.MediaMuxer
import android.util.Log
import java.io.Closeable
import java.io.File
import java.io.IOException
import java.nio.ByteBuffer
Expand Down Expand Up @@ -67,7 +66,7 @@ class AacMp4Writer(
outFile: File,
private val codecFactory: MediaCodecFactory = DefaultMediaCodecFactory(),
private val muxerFactory: MediaMuxerFactory = DefaultMediaMuxerFactory()
) : Closeable {
) : AudioSampleWriter {

companion object {
private const val TAG = "AacMp4Writer"
Expand Down Expand Up @@ -116,7 +115,7 @@ class AacMp4Writer(
* @throws IOException if encoding or muxing fails
*/
@Throws(IOException::class)
fun write(data: ByteArray, offset: Int, length: Int) {
override fun write(data: ByteArray, offset: Int, length: Int) {
var remaining = length
var off = offset

Expand Down Expand Up @@ -232,7 +231,7 @@ class AacMp4Writer(
* Returns the total number of PCM sample bytes written.
* Safe cast for typical audio file sizes.
*/
fun getTotalSampleBytesWritten(): Int {
override fun getTotalSampleBytesWritten(): Int {
return minOf(Int.MAX_VALUE.toLong(), totalPcmBytesWritten).toInt()
}
}
15 changes: 15 additions & 0 deletions SaidIt/src/main/kotlin/eu/mrogalski/saidit/AudioSampleWriter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package eu.mrogalski.saidit

import java.io.Closeable
import java.io.IOException

/**
* Abstraction for classes that persist PCM audio samples.
* Allows SaidItService to swap implementations during tests.
*/
interface AudioSampleWriter : Closeable {
@Throws(IOException::class)
fun write(data: ByteArray, offset: Int, length: Int)

fun getTotalSampleBytesWritten(): Int
}
17 changes: 0 additions & 17 deletions SaidIt/src/test/java/eu/mrogalski/saidit/ExampleUnitTest.java

This file was deleted.

76 changes: 0 additions & 76 deletions SaidIt/src/test/java/eu/mrogalski/saidit/SaidItServiceTest.kt

This file was deleted.

24 changes: 24 additions & 0 deletions SaidIt/src/test/kotlin/eu/mrogalski/saidit/MainDispatcherRule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package eu.mrogalski.saidit

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description

@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule(
val testDispatcher: TestDispatcher = StandardTestDispatcher()
) : TestWatcher() {

override fun starting(description: Description?) {
Dispatchers.setMain(testDispatcher)
}

override fun finished(description: Description?) {
Dispatchers.resetMain()
}
}
Loading
Loading