Skip to content

Commit

Permalink
V0.2.5 beta02 (#62)
Browse files Browse the repository at this point in the history
* init

* Added `composeResource` constructor for `Audio`

* Updated Audio and AudioByte file names.

* Updated Audio and AudioByte file names.

* Updated Audio and AudioByte file names.
  • Loading branch information
robertjamison authored Feb 7, 2025
1 parent ca5f789 commit cdede8d
Show file tree
Hide file tree
Showing 183 changed files with 733 additions and 368 deletions.
34 changes: 22 additions & 12 deletions basic-sound/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,23 @@ Currently, this library only ingests URLs and local paths. Composable Resources
![badge-nodejs](https://img.shields.io/badge/jsNode-full_support-65c663.svg?style=flat)
![badge-jsBrowser](https://img.shields.io/badge/jsBrowser-full_support-65c663.svg?style=flat)
![badge-wasmJsBrowser](https://img.shields.io/badge/wasmJsBrowser-full_support-65c663.svg?style=flat)
![badge-jvm](http://img.shields.io/badge/jvm-no_support-red.svg?style=flat)
![badge-jvm](http://img.shields.io/badge/jvm-full_support-65c663.svg?style=flat)
![badge-linux](http://img.shields.io/badge/linux-no_support-red.svg?style=flat)
![badge-windows](http://img.shields.io/badge/windows-no_support-red.svg?style=flat)

## Supported Filetypes
| Format | Android | iOS | javascript / wasm | File / Container Types |
|:----------|:------------------:|:------------------:|:------------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| AAC LC | :white_check_mark: | :white_check_mark: | :x: | 3GPP (.3gp) MPEG-4 (.mp4, .m4a) ADTS raw AAC (.aac, decode in Android 3.1+, encode in Android 4.0+, ADIF not supported) MPEG-TS (.ts, not seekable, Android 3.0+) |
| AMR-NB | :white_check_mark: | :x: | :x: | 3GPP (.3gp) AMR (.amr) |
| FLAC | :white_check_mark: | :x: | :x: | FLAC (.flac) MPEG-4 (.mp4, .m4a, Android 10+) |
| MIDI | :white_check_mark: | :x: | :x: | Type 0 and 1 (.mid, .xmf, .mxmf) RTTTL/RTX (.rtttl, .rtx) OTA (.ota) iMelody (.imy) |
| MP3 | :white_check_mark: | :white_check_mark: | :white_check_mark: | MP3 (.mp3) MPEG-4 (.mp4, .m4a, Android 10+) Matroska (.mkv, Android 10+) |
| Opus | :white_check_mark: | :x: | :question: | Ogg (.ogg) Matroska (.mkv) |
| PCM/WAVE | :white_check_mark: | :x: | :white_check_mark: | WAVE (.wav) |
| Vorbis | :white_check_mark: | :x: | :question: | Ogg (.ogg) Matroska (.mkv, Android 4.0+) MPEG-4 (.mp4, .m4a, Android 10+) |

| Format | Android | iOS | javascript / wasm | JVM* | File / Container Types |
|:----------|:------------------:|:------------------:|:------------------:|:------------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| AAC LC | :white_check_mark: | :white_check_mark: | :x: | :question: | 3GPP (.3gp) MPEG-4 (.mp4, .m4a) ADTS raw AAC (.aac, decode in Android 3.1+, encode in Android 4.0+, ADIF not supported) MPEG-TS (.ts, not seekable, Android 3.0+) |
| AMR-NB | :white_check_mark: | :x: | :x: | :question: | 3GPP (.3gp) AMR (.amr) |
| FLAC | :white_check_mark: | :x: | :x: | :question: | FLAC (.flac) MPEG-4 (.mp4, .m4a, Android 10+) |
| MIDI | :white_check_mark: | :x: | :x: | :question: | Type 0 and 1 (.mid, .xmf, .mxmf) RTTTL/RTX (.rtttl, .rtx) OTA (.ota) iMelody (.imy) |
| MP3 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | MP3 (.mp3) MPEG-4 (.mp4, .m4a, Android 10+) Matroska (.mkv, Android 10+) |
| Opus | :white_check_mark: | :x: | :question: | :question: | Ogg (.ogg) Matroska (.mkv) |
| PCM/WAVE | :white_check_mark: | :x: | :white_check_mark: | :question: | WAVE (.wav) |
| Vorbis | :white_check_mark: | :x: | :question: | :question: | Ogg (.ogg) Matroska (.mkv, Android 4.0+) MPEG-4 (.mp4, .m4a, Android 10+) |

* __NOTE: JVM file formats are dependent on the underlying operating system the app is run on.__
## Installation
You'll need to add your maven dependency list
```toml
Expand Down Expand Up @@ -123,6 +124,15 @@ Button(
}
}
```
If you need to load a Compose Resource, you need to use a constructor that includes `Context`.
Make sure you safely [pass your `Context` without memory leaks.](https://medium.com/hakz/contain-your-apps-memory-please-0c62819f8d7f).
```kotlin
val resource = Res.getUri("files/ringtone.wav")
// You can pass your Context
val audio = Audio(context, resource) // loads the audio file
audio.play() // plays the audio immediately upon execution
```

## `AudioByte` Usage
AudioByte allows you to load audio to memory to play multiple times later without reloading -- sort of like a soundboard.
You could make a callable class that is passed throughout the app so the sounds could be access in any context.
Expand Down
5 changes: 5 additions & 0 deletions basic-sound/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
Expand Down Expand Up @@ -38,11 +39,14 @@ kotlin {
wasmJs {
browser()
binaries.executable()
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
freeCompilerArgs.add("-Xwasm-attach-js-exception")
}
}

jvm()

listOf(
iosX64(), // mobile
iosArm64(), // mobile
Expand Down Expand Up @@ -78,6 +82,7 @@ kotlin {
wasmJsMain.dependencies {
implementation(libs.kotlinx.browser)
}
jvmMain.dependencies {}
}

//https://kotlinlang.org/docs/native-objc-interop.html#export-of-kdoc-comments-to-generated-objective-c-headers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

package app.lexilabs.basic.sound

import android.content.Context
import android.media.MediaPlayer
import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -37,6 +38,14 @@ public actual class Audio actual constructor(): AudioBuilder {
load()
}

public actual constructor(context: Any?, composeResource: String, autoPlay: Boolean): this() {
this.resource = composeResource
this.autoPlay = autoPlay
context?.let {
load(it)
} ?: throw NullPointerException("Context is required for Composable Resource to be used with `Audio()`")
}

/**
* Used to load an [Audio] file when [AudioState.NONE].
*
Expand All @@ -48,11 +57,22 @@ public actual class Audio actual constructor(): AudioBuilder {
* audio.play() // plays the sound immediately
* ```
*/
public actual override fun load() {
public actual override fun load(context: Any?) {
try {
_audioState.value = AudioState.LOADING
mediaPlayer = MediaPlayer().apply {
setDataSource(resource)
// if there is a context provided
(context as Context?)?.let {
setDataSource(
// Convert file to File Descriptor before ingesting
it.assets.openFd(
// Try to remove prefix created by Compose Resources first
resource.removePrefix("file:///android_asset/")
)
)
} ?: setDataSource(resource)
// reset context after

prepareAsync()
setOnPreparedListener {
// Ready to play
Expand All @@ -61,7 +81,7 @@ public actual class Audio actual constructor(): AudioBuilder {
play()
}
}
setOnErrorListener { _, what, extra ->
setOnErrorListener { _, _, _ ->
throw Exception("init: MediaPlayer Error: $resource")
}
setOnCompletionListener {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ public actual class Audio actual constructor(): AudioBuilder {
load()
}

public actual override fun load() {
public actual constructor(context: Any?, composeResource: String, autoPlay: Boolean) : this() {
this.resource = composeResource
this.autoPlay = autoPlay
load()
}

public actual override fun load(context: Any?) {
try {
_audioState.value = AudioState.LOADING
val data: NSData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,30 @@ public expect class Audio() {
* Play audio from a url or path ([String]).
*
* @param resource provides the link to the audio file online
* @param autoPlay [play] after [AudioState.READY] is reached
* @param autoPlay [play] after [AudioState.READY] is reached (defaults to "false").
*
* Example:
* ```
* val audioUrl = "https://dare.wisc.edu/wp-content/uploads/sites/1051/2008/11/MS072.mp3"
* val audio = Audio(audioUrl, true) // AutoPlay is marked "true"
* ```
*/
public constructor(resource: String, autoPlay: Boolean)
public constructor(resource: String, autoPlay: Boolean = false)

/**
* Play audio from a url or path ([String]).
*
* @param context provide the Platform Context (this is mostly to accommodate Android)
* @param composeResource provides the link to the audio file online
* @param autoPlay [play] after [AudioState.READY] is reached (defaults to "false").
*
* Example:
* ```
* val audioUrl = "https://dare.wisc.edu/wp-content/uploads/sites/1051/2008/11/MS072.mp3"
* val audio = Audio(audioUrl, true) // AutoPlay is marked "true"
* ```
*/
public constructor(context: Any?, composeResource: String, autoPlay: Boolean = false)

/**
* Provides the state of [Audio] after initialization
Expand All @@ -59,7 +74,7 @@ public expect class Audio() {
* audio.play() // plays the sound immediately
* ```
*/
public fun load()
public fun load(context: Any? = null)

/**
* Used after [Audio] is initialized with [AudioState.READY] to play the sound immediately.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public interface AudioBuilder {
* audio.play() // plays the sound immediately
* ```
*/
public fun load()
public fun load(context: Any?)

/**
* When overridden, used after [Audio] is initialized with [AudioState.READY] to play the sound immediately.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ public actual class Audio public actual constructor() : AudioBuilder {
load()
}

actual override fun load() {
public actual constructor(context: Any?, composeResource: String, autoPlay: Boolean) : this() {
this.resource = composeResource
this.autoPlay = autoPlay
load()
}

actual override fun load(context: Any?) {
try {
_audioState.value = AudioState.LOADING
player = Audio(resource)
Expand Down

This file was deleted.

121 changes: 121 additions & 0 deletions basic-sound/src/jvmMain/kotlin/app/lexilabs/basic/sound/Audio.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package app.lexilabs.basic.sound

import app.lexilabs.basic.logging.Log
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.io.File
import javax.sound.sampled.AudioSystem
import javax.sound.sampled.Clip
import kotlin.io.path.Path

@ExperimentalBasicSound
public actual class Audio actual constructor(): AudioBuilder {

private val tag = "Basic-Sound Audio"

private val _audioState = MutableStateFlow<AudioState>(AudioState.NONE)
public actual override val audioState: StateFlow<AudioState> = _audioState.asStateFlow()

private var clip: Clip = AudioSystem.getClip()
private var cursor: Long = 0L

public actual var resource: String = ""
public actual var autoPlay: Boolean = false

public actual constructor(resource: String, autoPlay: Boolean): this() {
this.resource = resource
this.autoPlay = autoPlay
}

public actual constructor(context: Any?, composeResource: String, autoPlay: Boolean): this() {
this.resource = composeResource
this.autoPlay = autoPlay
}

public actual override fun load(context: Any?) {
try {
_audioState.value = AudioState.LOADING

val stream = if (resource.substring(0, 4) == "http") {
AudioSystem.getAudioInputStream(Path(resource).toUri().toURL())
?: throw IllegalStateException("load:The URL provided was invalid or failed to load")
} else {
AudioSystem.getAudioInputStream(File(resource).absoluteFile)
?: throw IllegalStateException("load:The path provided was invalid")
}
clip.open(stream)
when (clip.isOpen) {
true -> {
_audioState.value = AudioState.READY
if (autoPlay) {
play()
}
}

false -> _audioState.value = AudioState.LOADING
}
} catch (e: Exception) {
Log.e(tag, "load:failure: $e")
_audioState.value = AudioState.ERROR("load:failure: $e")
}
}

public actual override fun play() {
when (audioState.value) {
is AudioState.LOADING,
is AudioState.PLAYING -> { /** do nothing **/ }
is AudioState.NONE -> {
throw Exception ("AudioState.NONE: load() not run yet")
}
is AudioState.ERROR -> {
throw Exception("AudioState.ERROR: ${(audioState.value as AudioState.ERROR).message}")
}
is AudioState.PAUSED -> {
clip.microsecondPosition = cursor
clip.start()
_audioState.value = AudioState.PLAYING
}
is AudioState.READY -> {
clip.start()
_audioState.value = AudioState.PLAYING
}
}
}

public actual override fun pause() {
try {
if (clip.isRunning) {
cursor = clip.microsecondPosition
clip.stop()
_audioState.value = AudioState.PAUSED
}
} catch (e: Exception) {
Log.e(tag, "pause:failure: $e")
_audioState.value = AudioState.ERROR("pause:failure: $e")
}
}

public actual override fun stop() {
try {
if (clip.isRunning) {
clip.stop()
cursor = 0L
_audioState.value = AudioState.READY
}
} catch (e: Exception) {
Log.e(tag, "stop:failure: $e")
_audioState.value = AudioState.ERROR("stop:failure: $e")
}
}

public actual override fun release() {
try {
_audioState.value = AudioState.NONE
clip.flush()
} catch (e: Exception) {
Log.e(tag, "release:failure: $e")
_audioState.value = AudioState.ERROR("release:failure: $e")
}
}
}
Loading

0 comments on commit cdede8d

Please sign in to comment.