app端的代码使用
com/example/myapplication/MainActivity.kt
// file: MainActivity.kt
package com.example.myapplication
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import com.example.myapplication.ui.theme.MyApplicationTheme
import kotlin.concurrent.thread
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
private lateinit var pcmPlayer: PcmAudioPlayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
pcmPlayer = PcmAudioPlayer(this)
setContent {
MyApplicationTheme {
Scaffold { padding ->
PlayerUI(
modifier = androidx.compose.ui.Modifier.padding(padding),
onPlayStatic = {
pcmPlayer.playStaticFromAssets("test.pcm")
},
onStopStatic = {
pcmPlayer.stopStatic()
},
onPlayStream = {
thread {
pcmPlayer.playStreamFromAssets("test.pcm")
}
},
onStopStream = {
pcmPlayer.stopStream()
})
}
}
}
}
override fun onDestroy() {
super.onDestroy()
pcmPlayer.release()
}
}
enum class PlayMode {
STATIC, STREAM
}
@Composable
fun PlayerUI(
modifier: Modifier = Modifier,
onPlayStatic: () -> Unit,
onStopStatic: () -> Unit,
onPlayStream: () -> Unit,
onStopStream: () -> Unit
) {
var mode by remember { mutableStateOf(PlayMode.STATIC) }
Column(
modifier = modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "PCM Player", style = MaterialTheme.typography.headlineMedium
)
Spacer(Modifier.height(24.dp))
// ===== 模式选择 =====
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
ModeRadio(
text = "MODE_STATIC", selected = mode == PlayMode.STATIC
) { mode = PlayMode.STATIC }
ModeRadio(
text = "MODE_STREAM", selected = mode == PlayMode.STREAM
) { mode = PlayMode.STREAM }
}
Spacer(Modifier.height(32.dp))
// ===== 控制按钮 =====
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Button(onClick = {
when (mode) {
PlayMode.STATIC -> onPlayStatic()
PlayMode.STREAM -> onPlayStream()
}
}) {
Text("播放")
}
Button(onClick = {
when (mode) {
PlayMode.STATIC -> onStopStatic()
PlayMode.STREAM -> onStopStream()
}
}) {
Text("停止")
}
}
}
}
@Composable
private fun ModeRadio(
text: String, selected: Boolean, onClick: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selected, onClick = onClick
)
Text(text)
}
}
com.example.myapplication.PcmAudioPlayer
// file: PcmAudioPlayer.kt
package com.example.myapplication
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioTrack
import java.io.ByteArrayOutputStream
import java.util.concurrent.atomic.AtomicBoolean
class PcmAudioPlayer(context: Context) {
private val sampleRate = 44100
private val channelConfig = AudioFormat.CHANNEL_OUT_STEREO
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
private val minBufferSize =
AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat)
private val assetManager = context.assets
private val isPlayingStream = AtomicBoolean(false)
private val isPlayingStatic = AtomicBoolean(false)
/* =========================
* MODE_STREAM AudioTrack
* ========================= */
private val streamTrack: AudioTrack = AudioTrack.Builder()
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
.setAudioFormat(
AudioFormat.Builder()
.setSampleRate(sampleRate)
.setEncoding(audioFormat)
.setChannelMask(channelConfig)
.build()
)
.setTransferMode(AudioTrack.MODE_STREAM)
.setBufferSizeInBytes(minBufferSize)
.build()
fun playStreamFromAssets(fileName: String) {
if (isPlayingStream.get()) return
isPlayingStream.set(true)
streamTrack.play()
assetManager.open(fileName).use { input ->
val buffer = ByteArray(minBufferSize)
var len: Int
while (input.read(buffer).also { len = it } > 0) {
streamTrack.write(buffer, 0, len)
}
}
}
fun stopStream() {
if (!isPlayingStream.get()) return
isPlayingStream.set(false)
streamTrack.pause()
streamTrack.flush()
streamTrack.stop()
}
/* =========================
* MODE_STATIC AudioTrack
* ========================= */
private var staticTrack: AudioTrack? = null
fun playStaticFromAssets(fileName: String) {
if (isPlayingStatic.get()) return
val pcmData = loadPcmFromAssets(fileName)
if (pcmData.isEmpty()) return
val frameSize = 4 // 16bit * stereo
val validSize = pcmData.size - (pcmData.size % frameSize)
if (validSize <= 0) return
staticTrack?.release()
val bufferSize = maxOf(validSize, minBufferSize)
staticTrack = AudioTrack.Builder()
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
.setAudioFormat(
AudioFormat.Builder()
.setSampleRate(sampleRate)
.setEncoding(audioFormat)
.setChannelMask(channelConfig)
.build()
)
.setTransferMode(AudioTrack.MODE_STATIC)
.setBufferSizeInBytes(bufferSize)
.build()
staticTrack?.apply {
write(pcmData, 0, validSize)
isPlayingStatic.set(true)
play()
}
}
fun stopStatic() {
if (!isPlayingStatic.get()) return
isPlayingStatic.set(false)
staticTrack?.stop()
staticTrack?.flush()
}
/* =========================
* Common
* ========================= */
private fun loadPcmFromAssets(fileName: String): ByteArray {
assetManager.open(fileName).use { input ->
val output = ByteArrayOutputStream()
val buffer = ByteArray(4096)
var len: Int
while (input.read(buffer).also { len = it } > 0) {
output.write(buffer, 0, len)
}
return output.toByteArray()
}
}
fun release() {
isPlayingStream.set(false)
isPlayingStatic.set(false)
streamTrack.release()
staticTrack?.release()
staticTrack = null
}
}
native版本的使用方式
// pcm_player.cpp
// 一个在 AOSP 源码环境下运行的 native 可执行程序
// 功能:使用 AudioTrack 播放原始 PCM 音频文件
#include <android/log.h> // Android 日志
#include <media/AudioTrack.h> // AudioTrack(native 层)
#include <utils/Errors.h> // NO_ERROR 等错误码
#include <fcntl.h> // open
#include <unistd.h> // read / close
#include <sys/stat.h> // stat
using namespace android;
// ========================
// 音频参数定义(必须与 PCM 文件一致)
// ========================
// 采样率:44100Hz
static constexpr int SAMPLE_RATE = 44100;
// 音频格式:16bit PCM
static constexpr audio_format_t FORMAT = AUDIO_FORMAT_PCM_16_BIT;
// 声道:立体声输出
static constexpr audio_channel_mask_t CHANNEL_MASK = AUDIO_CHANNEL_OUT_STEREO;
#define USE_AUDIO_ATTRIBUTES
int main(int argc, char* argv[]) {
// ------------------------
// 1. 参数检查
// ------------------------
// 期望参数:pcm_player <pcm_file_path>
if (argc < 2) {
__android_log_print(
ANDROID_LOG_ERROR,
"PCM_PLAYER",
"Usage: pcm_player <pcm_file>"
);
return -1;
}
// PCM 文件路径
const char* path = argv[1];
// ------------------------
// 2. 打开 PCM 文件
// ------------------------
int fd = open(path, O_RDONLY);
if (fd < 0) {
__android_log_print(
ANDROID_LOG_ERROR,
"PCM_PLAYER",
"Failed to open %s",
path
);
return -1;
}
// ------------------------
// 3. 计算 AudioTrack 最小 frame 数
// ------------------------
// frame = 一个采样点(包含所有声道)
// AudioTrack 内部 buffer 的最小安全大小
size_t frameCount = 0;
status_t status = AudioTrack::getMinFrameCount(
&frameCount, // 输出参数:最小 frame 数
AUDIO_STREAM_MUSIC, // 音频流类型
SAMPLE_RATE // 采样率
);
sp<AudioTrack> track;
#ifdef USE_AUDIO_ATTRIBUTES
// ------------------------
// 3. 构造 AudioAttributes
// ------------------------
// 告诉系统这是“媒体播放”类型的音频
audio_attributes_t attributes = AUDIO_ATTRIBUTES_INITIALIZER;
attributes.usage = AUDIO_USAGE_MEDIA;
attributes.content_type = AUDIO_CONTENT_TYPE_MUSIC;
// ------------------------
// 6. 使用 AudioAttributes 创建 AudioTrack
// ------------------------
track = new AudioTrack(
AUDIO_STREAM_MUSIC,
SAMPLE_RATE,
FORMAT,
CHANNEL_MASK,
frameCount,
AUDIO_OUTPUT_FLAG_NONE,
nullptr,
0,
AUDIO_SESSION_ALLOCATE,
AudioTrack::TRANSFER_DEFAULT,
nullptr,
AttributionSourceState(),
&attributes
);
// av/media/libaudioclient/include/media/AudioTrack.h
// AudioTrack( audio_stream_type_t streamType,
// uint32_t sampleRate,
// audio_format_t format,
// audio_channel_mask_t channelMask,
// size_t frameCount = 0,
// audio_output_flags_t flags = AUDIO_OUTPUT_FLAG_NONE,
// const wp<IAudioTrackCallback>& callback = nullptr,
// int32_t notificationFrames = 0,
// audio_session_t sessionId = AUDIO_SESSION_ALLOCATE,
// transfer_type transferType = TRANSFER_DEFAULT,
// const audio_offload_info_t *offloadInfo = nullptr,
// const AttributionSourceState& attributionSource =
// AttributionSourceState(),
// const audio_attributes_t* pAttributes = nullptr,
// bool doNotReconnect = false,
// float maxRequiredSpeed = 1.0f,
// audio_port_handle_t selectedDeviceId = AUDIO_PORT_HANDLE_NONE);
#else
// ------------------------
// 4. 创建 AudioTrack 对象
// ------------------------
// 这是 native 层播放音频的核心对象
track = new AudioTrack(
AUDIO_STREAM_MUSIC, // 音频流类型(音乐)
SAMPLE_RATE, // 采样率
FORMAT, // PCM 16bit
CHANNEL_MASK, // 立体声
frameCount // buffer frame 数
);
#endif
// ------------------------
// 5. 检查 AudioTrack 初始化是否成功
// ------------------------
if (track->initCheck() != NO_ERROR) {
__android_log_print(
ANDROID_LOG_ERROR,
"PCM_PLAYER",
"AudioTrack init failed"
);
close(fd);
return -1;
}
// ------------------------
// 6. 启动音频播放
// ------------------------
track->start();
// ------------------------
// 7. 循环读取 PCM 文件并写入 AudioTrack
// ------------------------
// 每次读取一小块,模拟流式播放
constexpr size_t BUF_SIZE = 4096;
uint8_t buffer[BUF_SIZE];
ssize_t readBytes;
while ((readBytes = read(fd, buffer, BUF_SIZE)) > 0) {
// 将 PCM 数据写入 AudioTrack 的内部 buffer
// AudioTrack 会自动送给 AudioFlinger 播放
ssize_t written = track->write(buffer, readBytes);
// 如果写入失败,直接退出
if (written < 0) {
__android_log_print(
ANDROID_LOG_ERROR,
"PCM_PLAYER",
"AudioTrack write error"
);
break;
}
}
// ------------------------
// 8. 停止播放并释放资源
// ------------------------
track->stop(); // 停止 AudioTrack
track.clear(); // 释放强引用,析构 AudioTrack
close(fd); // 关闭 PCM 文件
__android_log_print(
ANDROID_LOG_INFO,
"PCM_PLAYER",
"Playback finished"
);
return 0;
}
1661

被折叠的 条评论
为什么被折叠?



