项目概述

音频处理与播放是现代应用的重要功能。在 KMP 框架下,我们需要实现跨平台的音频播放、录制、处理和音效管理。本文详细介绍了如何在 Kotlin Multiplatform 中实现高效的音频处理系统,包括音频播放、录制、音效处理和鸿蒙系统特定的音频管理方式。

相比视频,音频模块往往更加“无处不在”:

  • 音乐/播客类应用:需要稳定的后台播放、播放列表管理、倍速播放等;
  • 语音通话/会议:需要低延迟、回声消除、麦克风降噪等处理;
  • 系统通知、提示音:强调功耗低、启动快、与系统音量策略一致。

本篇的目标是:

  • commonMain 中抽象出统一的 AudioPlayerAudioRecorder 接口;
  • 在 Android、iOS、鸿蒙等平台下分别基于原生音频 API 做 actual 实现;
  • 预留音效处理(均衡器、预设效果)、播放列表等高级功能的扩展空间。

第一部分:音频处理核心概念

音频播放架构

// commonMain/kotlin/com/example/audio/AudioPlayer.kt
expect class AudioPlayer {
    suspend fun loadAudio(url: String): Boolean
    suspend fun play(): Boolean
    suspend fun pause(): Boolean
    suspend fun stop(): Boolean
    suspend fun seek(positionMs: Long): Boolean
    suspend fun setVolume(volume: Float): Boolean
    suspend fun setPlaybackSpeed(speed: Float): Boolean
    fun getDuration(): Long
    fun getCurrentPosition(): Long
    fun addListener(listener: AudioPlayerListener)
}

interface AudioPlayerListener {
    fun onStateChanged(state: AudioState)
    fun onProgressChanged(position: Long, duration: Long)
    fun onError(error: String)
    fun onCompletion()
}

enum class AudioState {
    IDLE, LOADING, PLAYING, PAUSED, STOPPED, ERROR
}

AudioPlayer 作为跨平台音频播放能力的统一抽象,定义了:

  • 常见操作:加载、播放、暂停、停止、seek、调节音量和倍速播放;
  • 状态获取:getDurationgetCurrentPosition 方便 UI 绘制进度条;
  • 事件回调:AudioPlayerListener 把状态变化、进度更新、错误和播放完成等事件统一封装。

这样的接口设计可以让上层业务完全不关心底层使用的是 MediaPlayerAVAudioPlayer 还是鸿蒙的 Player,从而在多端之间共享更多逻辑(比如播放控制栏、播放历史记录、播放状态管理等)。

音频录制架构

// commonMain/kotlin/com/example/audio/AudioRecorder.kt
expect class AudioRecorder {
    suspend fun startRecording(outputPath: String): Boolean
    suspend fun stopRecording(): Boolean
    suspend fun pauseRecording(): Boolean
    suspend fun resumeRecording(): Boolean
    suspend fun setAudioQuality(quality: AudioQuality): Boolean
    fun isRecording(): Boolean
}

enum class AudioQuality {
    LOW, MEDIUM, HIGH, LOSSLESS
}

data class AudioRecordingConfig(
    val quality: AudioQuality = AudioQuality.HIGH,
    val sampleRate: Int = 44100,
    val bitrate: Int = 128000,
    val channels: Int = 2
)

录音功能在实际场景中应用非常广泛:语音备忘录、语音聊天、在线客服录音等。由于设备和平台差异较大,在公共层提前约定好录音配置尤为重要。

  • AudioRecorder 提供从开始到结束的一整套录音生命周期方法;
  • AudioQuality 划分了从低码率到无损的不同档位,方便在各平台上映射到不同采样率和比特率组合;
  • AudioRecordingConfig 用来集中描述录音参数,例如采样率、码率、声道数等,便于在 Android、iOS、鸿蒙内部进行转换和校验。

实践建议:

  • 在启动录音前统一做权限检查(麦克风、存储等);
  • 对不同设备型号和场景预设合理的采样率和码率,避免高档位导致耗电和空间占用过高;
  • 将录音状态暴露给 UI,避免出现“录音中用户以为没开始”的体验问题。

第二部分:平台特定实现

Android 音频处理

// androidMain/kotlin/com/example/audio/AudioPlayer.kt
import android.media.MediaPlayer
import android.media.AudioAttributes
import android.media.AudioManager

actual class AudioPlayer(private val context: Context) {
    private val mediaPlayer = MediaPlayer()
    private val listeners = mutableListOf<AudioPlayerListener>()
    
    actual suspend fun loadAudio(url: String): Boolean {
        return try {
            mediaPlayer.apply {
                setAudioAttributes(
                    AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_MEDIA)
                        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                        .build()
                )
                setDataSource(url)
                prepareAsync()
            }
            listeners.forEach { it.onStateChanged(AudioState.LOADING) }
            true
        } catch (e: Exception) {
            listeners.forEach { it.onError(e.message ?: "Failed to load audio") }
            false
        }
    }
    
    actual suspend fun play(): Boolean {
        return try {
            mediaPlayer.start()
            listeners.forEach { it.onStateChanged(AudioState.PLAYING) }
            true
        } catch (e: Exception) {
            false
        }
    }
    
    actual suspend fun pause(): Boolean {
        return try {
            mediaPlayer.pause()
            listeners.forEach { it.onStateChanged(AudioState.PAUSED) }
            true
        } catch (e: Exception) {
            false
        }
    }
    
    actual suspend fun setVolume(volume: Float): Boolean {
        return try {
            mediaPlayer.setVolume(volume, volume)
            true
        } catch (e: Exception) {
            false
        }
    }
    
    actual suspend fun setPlaybackSpeed(speed: Float): Boolean {
        return try {
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
                mediaPlayer.playbackParams = mediaPlayer.playbackParams.setSpeed(speed)
            }
            true
        } catch (e: Exception) {
            false
        }
    }
    
    actual fun getDuration(): Long = mediaPlayer.duration.toLong()
    
    actual fun getCurrentPosition(): Long = mediaPlayer.currentPosition.toLong()
    
    actual fun addListener(listener: AudioPlayerListener) {
        listeners.add(listener)
    }
}

// Android 音频录制
actual class AudioRecorder(private val context: Context) {
    private val mediaRecorder = MediaRecorder()
    private var isRecordingFlag = false
    
    actual suspend fun startRecording(outputPath: String): Boolean {
        return try {
            mediaRecorder.apply {
                setAudioSource(MediaRecorder.AudioSource.MIC)
                setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
                setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
                setAudioSamplingRate(44100)
                setAudioBitRate(128000)
                setOutputFile(outputPath)
                prepare()
                start()
            }
            isRecordingFlag = true
            true
        } catch (e: Exception) {
            false
        }
    }
    
    actual suspend fun stopRecording(): Boolean {
        return try {
            mediaRecorder.stop()
            mediaRecorder.release()
            isRecordingFlag = false
            true
        } catch (e: Exception) {
            false
        }
    }
    
    actual fun isRecording(): Boolean = isRecordingFlag
}

Android 端音频实现的要点:

  • 播放使用 MediaPlayer 并配置 AudioAttributes,确保在不同使用场景(媒体、通话、通知等)下与系统音量、音频焦点交互正常;
  • 通过监听和回调将底层状态转换为 AudioState,让上层以统一的方式感知播放进度与错误;
  • 录音使用 MediaRecorder,展示了最基本的音频源、输出格式、编码器、采样率和比特率配置。

实际项目中可以进一步优化:

  • 使用 AudioFocusRequest 管理音频焦点,处理来电、闹钟等打断场景;
  • 在录音过程中增加波形显示、音量电平监控等 UI 反馈;
  • 对录音文件进行统一命名和清理策略,防止磁盘被占满。

iOS 音频处理

// iosMain/kotlin/com/example/audio/AudioPlayer.kt
import platform.AVFoundation.*
import platform.MediaPlayer.*

actual class AudioPlayer {
    private val audioPlayer = AVAudioPlayer()
    private val listeners = mutableListOf<AudioPlayerListener>()
    
    actual suspend fun loadAudio(url: String): Boolean {
        return try {
            val nsUrl = NSURL(string = url)
            val data = NSData.dataWithContentsOfURL(nsUrl)
            val player = AVAudioPlayer(data = data, fileTypeHint = AVFileTypeMP3)
            audioPlayer = player
            listeners.forEach { it.onStateChanged(AudioState.LOADING) }
            true
        } catch (e: Exception) {
            listeners.forEach { it.onError(e.message ?: "Failed to load audio") }
            false
        }
    }
    
    actual suspend fun play(): Boolean {
        return try {
            audioPlayer.play()
            listeners.forEach { it.onStateChanged(AudioState.PLAYING) }
            true
        } catch (e: Exception) {
            false
        }
    }
    
    actual suspend fun pause(): Boolean {
        return try {
            audioPlayer.pause()
            listeners.forEach { it.onStateChanged(AudioState.PAUSED) }
            true
        } catch (e: Exception) {
            false
        }
    }
    
    actual suspend fun setVolume(volume: Float): Boolean {
        return try {
            audioPlayer.volume = volume.toDouble()
            true
        } catch (e: Exception) {
            false
        }
    }
    
    actual fun getDuration(): Long {
        return (audioPlayer.duration * 1000).toLong()
    }
    
    actual fun getCurrentPosition(): Long {
        return (audioPlayer.currentTime * 1000).toLong()
    }
}

// iOS 音频录制
actual class AudioRecorder {
    private val audioRecorder: AVAudioRecorder? = null
    
    actual suspend fun startRecording(outputPath: String): Boolean {
        return try {
            val fileUrl = NSURL(fileURLWithPath = outputPath)
            val settings = mapOf(
                AVFormatIDKey to NSNumber(value = kAudioFormatMPEG4AAC),
                AVSampleRateKey to NSNumber(value = 44100),
                AVNumberOfChannelsKey to NSNumber(value = 2)
            )
            
            // Create and start recorder
            true
        } catch (e: Exception) {
            false
        }
    }
}

iOS 平台中:

  • 播放通常使用 AVAudioPlayerAVPlayer,在这里通过 AVAudioPlayer 演示本地/内存音频播放;
  • 可以结合 AVAudioSession 配置音频会话类型(播放、录制、播放并录制等),从而控制是否走听筒/扬声器、是否支持后台播放等;
  • 录制侧使用 AVAudioRecorder,通过 settings 配置格式、采样率和声道数等,和公共层的 AudioRecordingConfig 概念相互对应。

通过将这些实现封装在 iosMain 中,上层只需要调用统一的 AudioPlayerAudioRecorder 接口即可,无需理解 AVFoundation 的复杂细节,降低多端开发成本。

第三部分:编译后的代码形式

JavaScript 编译版本

// 音频播放器 (JavaScript/Web)
class AudioPlayer {
    constructor() {
        this.audio = new Audio();
        this.listeners = [];
    }
    
    async loadAudio(url) {
        try {
            this.audio.src = url;
            this.audio.addEventListener('loadedmetadata', () => {
                this.notifyListeners('onStateChanged', 'LOADED');
            });
            this.audio.addEventListener('ended', () => {
                this.notifyListeners('onCompletion');
            });
            return true;
        } catch (error) {
            this.notifyListeners('onError', error.message);
            return false;
        }
    }
    
    async play() {
        try {
            await this.audio.play();
            this.notifyListeners('onStateChanged', 'PLAYING');
            return true;
        } catch (error) {
            return false;
        }
    }
    
    async pause() {
        this.audio.pause();
        this.notifyListeners('onStateChanged', 'PAUSED');
        return true;
    }
    
    async setVolume(volume) {
        this.audio.volume = Math.max(0, Math.min(1, volume));
        return true;
    }
    
    async setPlaybackSpeed(speed) {
        this.audio.playbackRate = speed;
        return true;
    }
    
    getDuration() {
        return this.audio.duration * 1000;
    }
    
    getCurrentPosition() {
        return this.audio.currentTime * 1000;
    }
    
    addListener(listener) {
        this.listeners.push(listener);
    }
    
    notifyListeners(method, ...args) {
        this.listeners.forEach(listener => {
            if (listener[method]) {
                listener[method](...args);
            }
        });
    }
}

// 音频录制器
class AudioRecorder {
    async startRecording() {
        try {
            const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
            this.mediaRecorder = new MediaRecorder(stream);
            this.chunks = [];
            
            this.mediaRecorder.ondataavailable = (e) => {
                this.chunks.push(e.data);
            };
            
            this.mediaRecorder.start();
            return true;
        } catch (error) {
            return false;
        }
    }
    
    async stopRecording() {
        return new Promise((resolve) => {
            this.mediaRecorder.onstop = () => {
                const blob = new Blob(this.chunks, { type: 'audio/mp3' });
                resolve(blob);
            };
            this.mediaRecorder.stop();
        });
    }
}

第四部分:鸿蒙系统实际应用

鸿蒙音频处理

// ohos/kotlin/com/example/audio/AudioPlayer.kt
import ohos.media.player.Player
import ohos.media.player.PlayerCallback
import ohos.app.Context

actual class AudioPlayer(private val context: Context) {
    private val player = Player(context)
    private val listeners = mutableListOf<AudioPlayerListener>()
    
    actual suspend fun loadAudio(url: String): Boolean {
        return try {
            player.source = url
            player.setPlayerCallback(object : PlayerCallback {
                override fun onPlaybackComplete() {
                    listeners.forEach { it.onCompletion() }
                }
                
                override fun onError(errorCode: Int, errorMsg: String) {
                    listeners.forEach { it.onError(errorMsg) }
                }
            })
            listeners.forEach { it.onStateChanged(AudioState.LOADING) }
            true
        } catch (e: Exception) {
            false
        }
    }
    
    actual suspend fun play(): Boolean {
        return try {
            player.play()
            listeners.forEach { it.onStateChanged(AudioState.PLAYING) }
            true
        } catch (e: Exception) {
            false
        }
    }
    
    actual fun getDuration(): Long = player.duration
    
    actual fun getCurrentPosition(): Long = player.currentTime
}

// 鸿蒙音频录制
actual class AudioRecorder(private val context: Context) {
    private val recorder = ohos.media.recorder.AudioRecorder()
    
    actual suspend fun startRecording(outputPath: String): Boolean {
        return try {
            recorder.prepare(AudioRecordingConfig().apply {
                outputPath = outputPath
                sampleRate = 44100
                bitrate = 128000
                channels = 2
            })
            recorder.start()
            true
        } catch (e: Exception) {
            false
        }
    }
    
    actual suspend fun stopRecording(): Boolean {
        return try {
            recorder.stop()
            recorder.release()
            true
        } catch (e: Exception) {
            false
        }
    }
}

这一节重点展示如何在鸿蒙系统中实现与其他平台一致的音频能力:

  • 播放侧同样使用 ohos.media.player.Player,通过 PlayerCallback 将播放完成、错误等事件转换为统一的 AudioState 与回调;
  • 录制侧使用 ohos.media.recorder.AudioRecorder,并基于 AudioRecordingConfig 的思想配置输出路径、采样率、比特率和声道;
  • 所有这些实现都封装在 actual 类中,对上层业务保持透明。

在项目实践中,可以:

  • 为鸿蒙端单独封装一层“设备能力探测”,根据不同机型决定是否开启高质量录制模式;
  • 与权限模块结合,在调用录制前统一申请和校验麦克风权限;
  • 在 UI 层提供录音指示、波形动画、剩余时长提示等,提高用户感知。

第五部分:高级功能

音频效果处理

// commonMain/kotlin/com/example/audio/AudioEffects.kt
class AudioEffectManager(private val audioPlayer: AudioPlayer) {
    suspend fun applyEqualizer(preset: EqualizerPreset): Boolean {
        return try {
            when (preset) {
                EqualizerPreset.BASS_BOOST -> applyBassBoost()
                EqualizerPreset.TREBLE_BOOST -> applyTrebleBoost()
                EqualizerPreset.VOCAL_ENHANCE -> applyVocalEnhance()
                EqualizerPreset.FLAT -> applyFlat()
            }
            true
        } catch (e: Exception) {
            false
        }
    }
}

enum class EqualizerPreset {
    BASS_BOOST, TREBLE_BOOST, VOCAL_ENHANCE, FLAT
}

// 播放列表管理
class PlaylistManager(private val audioPlayer: AudioPlayer) {
    private val playlist = mutableListOf<AudioTrack>()
    private var currentIndex = 0
    
    data class AudioTrack(
        val id: String,
        val title: String,
        val url: String,
        val duration: Long
    )
    
    suspend fun playNext(): Boolean {
        if (currentIndex < playlist.size - 1) {
            currentIndex++
            return audioPlayer.loadAudio(playlist[currentIndex].url)
        }
        return false
    }
    
    suspend fun playPrevious(): Boolean {
        if (currentIndex > 0) {
            currentIndex--
            return audioPlayer.loadAudio(playlist[currentIndex].url)
        }
        return false
    }
}

在这里插入图片描述

总结

音频处理与播放是现代应用的重要功能。通过 KMP 框架,我们可以在共享代码中定义统一的音频播放和录制接口,同时在各平台上利用原生 API 实现高效的处理。鸿蒙系统提供了完整的音频处理 API,使得跨平台实现相对平顺。关键是要合理处理音频格式、采样率和音效处理,以提供良好的用户体验。

在工程实践中,你可以结合本篇内容重点思考:

  • 公共层抽象是否覆盖了业务所需的主要场景(音乐、语音通话、系统提示音等);
  • 不同平台下有哪些音频能力差异,需要通过适配层来屏蔽?
  • 如何在保证功耗和性能的前提下,提供足够丰富的音效和播放体验?

建议的下一步实践是:

  • 在 demo 中实现一个简单的跨平台音乐播放器,包含播放/暂停、进度条、音量调节和播放列表;
  • 尝试为其中一两个场景接入均衡器或音效预设,感受在 KMP 下如何扩展平台能力;
  • 将音频模块与应用其他功能(如通知、下载管理、账号系统)集成,打通完整业务链路。
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐