new: add playback speed control
This commit is contained in:
+4
@@ -61,6 +61,10 @@ internal class AndroidAudioPlayer(
|
||||
player.prepare()
|
||||
}
|
||||
|
||||
override fun setSpeed(speed: Float) {
|
||||
player.setPlaybackSpeed(speed)
|
||||
}
|
||||
|
||||
override fun play() {
|
||||
player.play()
|
||||
}
|
||||
|
||||
@@ -22,4 +22,7 @@ interface AudioPlayer {
|
||||
fun release()
|
||||
|
||||
fun load(uri: String)
|
||||
|
||||
/** Sets the playback speed multiplier (e.g. 1.0 for normal speed, 0.5 for half speed). */
|
||||
fun setSpeed(speed: Float)
|
||||
}
|
||||
|
||||
+15
-2
@@ -6,14 +6,16 @@ import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import platform.AVFoundation.AVAudioTimePitchAlgorithmTimeDomain
|
||||
import platform.AVFoundation.AVPlayer
|
||||
import platform.AVFoundation.AVPlayerItem
|
||||
import platform.AVFoundation.AVPlayerItemDidPlayToEndTimeNotification
|
||||
import platform.AVFoundation.audioTimePitchAlgorithm
|
||||
import platform.AVFoundation.currentItem
|
||||
import platform.AVFoundation.currentTime
|
||||
import platform.AVFoundation.duration
|
||||
import platform.AVFoundation.pause
|
||||
import platform.AVFoundation.play
|
||||
import platform.AVFoundation.rate
|
||||
import platform.AVFoundation.replaceCurrentItemWithPlayerItem
|
||||
import platform.AVFoundation.seekToTime
|
||||
import platform.CoreMedia.CMTimeGetSeconds
|
||||
@@ -29,6 +31,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
internal class IosAudioPlayer : AudioPlayer {
|
||||
private var player: AVPlayer? = null
|
||||
private var endObserver: NSObjectProtocol? = null
|
||||
private var speed: Float = 1f
|
||||
|
||||
private val _playbackState = MutableStateFlow(PlaybackState.IDLE)
|
||||
override val playbackState: StateFlow<PlaybackState> = _playbackState.asStateFlow()
|
||||
@@ -63,6 +66,8 @@ internal class IosAudioPlayer : AudioPlayer {
|
||||
|
||||
val nsUrl = NSURL.URLWithString(uri) ?: error("Invalid audio URI: $uri")
|
||||
val item = AVPlayerItem(uRL = nsUrl)
|
||||
// Time-domain stretching preserves the pitch when the rate is below 1.0.
|
||||
item.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithmTimeDomain
|
||||
player = AVPlayer(playerItem = item)
|
||||
|
||||
endObserver = NSNotificationCenter.defaultCenter.addObserverForName(
|
||||
@@ -75,10 +80,18 @@ internal class IosAudioPlayer : AudioPlayer {
|
||||
}
|
||||
|
||||
override fun play() {
|
||||
player?.play()
|
||||
// Setting a positive rate resumes playback; using the stored speed keeps the chosen tempo.
|
||||
player?.rate = speed
|
||||
_playbackState.value = PlaybackState.PLAYING
|
||||
}
|
||||
|
||||
override fun setSpeed(speed: Float) {
|
||||
this.speed = speed
|
||||
if (_playbackState.value == PlaybackState.PLAYING) {
|
||||
player?.rate = speed
|
||||
}
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
player?.pause()
|
||||
_playbackState.value = PlaybackState.PAUSED
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.net.URI
|
||||
import javax.sound.sampled.AudioFormat
|
||||
import javax.sound.sampled.AudioSystem
|
||||
import javax.sound.sampled.Clip
|
||||
import javax.sound.sampled.FloatControl
|
||||
import javax.sound.sampled.LineEvent
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.microseconds
|
||||
@@ -26,6 +27,9 @@ internal class JvmAudioPlayer : AudioPlayer {
|
||||
|
||||
private var stoppedByUser = false
|
||||
|
||||
private var speed: Float = 1f
|
||||
private var baseSampleRate: Float = 0f
|
||||
|
||||
private val _playbackState = MutableStateFlow(PlaybackState.IDLE)
|
||||
override val playbackState: StateFlow<PlaybackState> = _playbackState.asStateFlow()
|
||||
|
||||
@@ -77,6 +81,23 @@ internal class JvmAudioPlayer : AudioPlayer {
|
||||
}
|
||||
open(decodedInput)
|
||||
}
|
||||
baseSampleRate = pcmFormat.sampleRate
|
||||
applySpeed()
|
||||
}
|
||||
|
||||
override fun setSpeed(speed: Float) {
|
||||
this.speed = speed
|
||||
applySpeed()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the tempo through the clip's sample-rate control. Unlike the other platforms this does
|
||||
* not preserve pitch, and it is a no-op when the mixer does not expose the control.
|
||||
*/
|
||||
private fun applySpeed() {
|
||||
val clip = clip ?: return
|
||||
if (baseSampleRate <= 0f || !clip.isControlSupported(FloatControl.Type.SAMPLE_RATE)) return
|
||||
(clip.getControl(FloatControl.Type.SAMPLE_RATE) as FloatControl).value = baseSampleRate * speed
|
||||
}
|
||||
|
||||
override fun play() {
|
||||
|
||||
@@ -12,6 +12,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
internal class WebAudioPlayer : AudioPlayer {
|
||||
private var audio: HTMLAudioElement? = null
|
||||
private var speed: Float = 1f
|
||||
|
||||
private val _playbackState = MutableStateFlow(PlaybackState.IDLE)
|
||||
override val playbackState: StateFlow<PlaybackState> = _playbackState.asStateFlow()
|
||||
@@ -31,12 +32,19 @@ internal class WebAudioPlayer : AudioPlayer {
|
||||
|
||||
audio = (document.createElement("audio") as HTMLAudioElement).apply {
|
||||
src = uri
|
||||
playbackRate = speed.toDouble()
|
||||
addEventListener("play") { _playbackState.value = PlaybackState.PLAYING }
|
||||
addEventListener("ended") { _playbackState.value = PlaybackState.ENDED }
|
||||
addEventListener("pause") { _playbackState.value = PlaybackState.PAUSED }
|
||||
}
|
||||
}
|
||||
|
||||
override fun setSpeed(speed: Float) {
|
||||
// Browsers preserve pitch (preservesPitch defaults to true) when changing playbackRate.
|
||||
this.speed = speed
|
||||
audio?.playbackRate = speed.toDouble()
|
||||
}
|
||||
|
||||
override fun play() {
|
||||
audio?.play()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user