6.播放音频(第一部分)
Posted junguo
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了6.播放音频(第一部分)相关的知识,希望对你有一定的参考价值。
6.1 从tinyalsa开始
这一章将对播放音频的具体内容做讲解。我的想法是按照tinyalsa中的例子作为讲解的范本,因为tinyalsa足够简单,很多时候都忽略了它的细节。趁着这个机会再整理一下tinyalsa的内容。我使用的tinyalsa从https://github.com/tinyalsa/tinyalsa下载,从examples/writei.c开始。
int main(void)
void *frames;
size_t size;
size = read_file(&frames);
if (size == 0)
return EXIT_FAILURE;
if (write_frames(frames, size) < 0)
return EXIT_FAILURE;
free(frames);
return EXIT_SUCCESS;
其中函数read_file从指定的文件中读取pcm数据到frames。
static int write_frames(const void * frames, size_t byte_count)
unsigned int card = 0;
unsigned int device = 0;
int flags = PCM_OUT;
const struct pcm_config config =
.channels = 2,
.rate = 48000,
.format = PCM_FORMAT_S32_LE,
.period_size = 1024,
.period_count = 2,
.start_threshold = 1024,
.silence_threshold = 1024 * 2,
.stop_threshold = 1024 * 2
;
struct pcm * pcm = pcm_open(card, device, flags, &config);
if (pcm == NULL)
fprintf(stderr, "failed to allocate memory for PCM\\n");
return -1;
else if (!pcm_is_ready(pcm))
pcm_close(pcm);
fprintf(stderr, "failed to open PCM\\n");
return -1;
unsigned int frame_count = pcm_bytes_to_frames(pcm, byte_count);
int err = pcm_writei(pcm, frames, frame_count);
if (err < 0)
printf("error: %s\\n", pcm_get_error(pcm));
pcm_close(pcm);
return 0;
这个函数里通过pcm_open打开设备,后面通过pcm_writei去写数据。这里先注意一下pcm_config中的内容,包括声道数、采样数、数据格式等内容。period_size表示的内核中DMA块的大小,period_count表示这样的块有几个,period_count的大小应该大于2,这样才能保证音频播放过程中无卡顿。start_threshold用来表示有多少帧数据的时候,才开始播放声音。silence_threshold和stop_threshold表示静音和停止播放时候需要的帧数,帧数太少的时候进行操作,可能导致破音的产生。
struct pcm *pcm_open(unsigned int card, unsigned int device,
unsigned int flags, const struct pcm_config *config)
struct pcm *pcm;
struct snd_pcm_info info;
int rc;
pcm = calloc(1, sizeof(struct pcm));
if (!pcm)
oops(&bad_pcm, ENOMEM, "can't allocate PCM object");
return &bad_pcm;
/* Default to hw_ops, attemp plugin open only if hw (/dev/snd/pcm*) open fails */
pcm->ops = &hw_ops;
pcm->fd = pcm->ops->open(card, device, flags, &pcm->data, NULL);
#ifdef TINYALSA_USES_PLUGINS
if (pcm->fd < 0)
int pcm_type;
pcm->snd_node = snd_utils_open_pcm(card, device);
pcm_type = snd_utils_get_node_type(pcm->snd_node);
if (!pcm->snd_node || pcm_type != SND_NODE_TYPE_PLUGIN)
oops(&bad_pcm, ENODEV, "no device (hw/plugin) for card(%u), device(%u)",
card, device);
goto fail_close_dev_node;
pcm->ops = &plug_ops;
pcm->fd = pcm->ops->open(card, device, flags, &pcm->data, pcm->snd_node);
#endif
if (pcm->fd < 0)
oops(&bad_pcm, errno, "cannot open device (%u) for card (%u)",
device, card);
goto fail_close_dev_node;
pcm->flags = flags;
if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_INFO, &info))
oops(&bad_pcm, errno, "cannot get info");
goto fail_close;
pcm->subdevice = info.subdevice;
if (pcm_set_config(pcm, config) != 0)
goto fail_close;
rc = pcm_hw_mmap_status(pcm);
if (rc < 0)
oops(&bad_pcm, errno, "mmap status failed");
goto fail;
#ifdef SNDRV_PCM_IOCTL_TTSTAMP
if (pcm->flags & PCM_MONOTONIC)
int arg = SNDRV_PCM_TSTAMP_TYPE_MONOTONIC;
rc = pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_TTSTAMP, &arg);
if (rc < 0)
oops(&bad_pcm, errno, "cannot set timestamp type");
goto fail;
#endif
pcm->xruns = 0;
return pcm;
fail:
pcm_hw_munmap_status(pcm);
if (flags & PCM_MMAP)
pcm->ops->munmap(pcm->data, pcm->mmap_buffer, pcm_frames_to_bytes(pcm, pcm->buffer_size));
fail_close:
pcm->ops->close(pcm->data);
fail_close_dev_node:
#ifdef TINYALSA_USES_PLUGINS
if (pcm->snd_node)
snd_utils_close_dev_node(pcm->snd_node);
#endif
free(pcm);
return &bad_pcm;
这段代码中关于函数open部分的内容,已经都展开过。其后可以看到通过使用ioctl发送SNDRV_PCM_IOCTL_INFO来获取pcm信息,通过pcm_set_config设置参数以及尝试mmap操作。后续的几节将围绕这几个内容展开。
6.2 获取设置pcm信息
6.2.1 获取信息
通过ioctl发送SNDRV_PCM_IOCTL_INFO就可以得到设置信息。来看一下具体的过程。
之前已经介绍过snd_pcm_f_ops,其中的unlocked_ioctl指向snd_pcm_ioctl,对pcm设备的ioctl操作,都会调用到它。
static long snd_pcm_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
struct snd_pcm_file *pcm_file;
pcm_file = file->private_data;
// pcm设备的命令由以下的格式命名
// #define SNDRV_PCM_IOCTL_PVERSION _IOR('A', 0x00, int)
// 第3字节都被定义成了'A',所以这里做个判断
if (((cmd >> 8) & 0xff) != 'A')
return -ENOTTY;
return snd_pcm_common_ioctl(file, pcm_file->substream, cmd,
(void __user *)arg);
打开文件的过程中,snd_pcm_file被设置到了file的private_data,而大多数操作要处理的是snd_pcm_substream对象,所以pcm_file->substream被传入下一个函数。
而snd_pcm_common_ioctl中基本是一批case语句,选择要处理的具体命令:
case SNDRV_PCM_IOCTL_INFO:
return snd_pcm_info_user(substream, arg);
获取pcm信息,需要用到snd_pcm_info_user。
int snd_pcm_info_user(struct snd_pcm_substream *substream,
struct snd_pcm_info __user * _info)
struct snd_pcm_info *info;
int err;
info = kmalloc(sizeof(*info), GFP_KERNEL);
if (! info)
return -ENOMEM;
err = snd_pcm_info(substream, info);
if (err >= 0)
if (copy_to_user(_info, info, sizeof(*info)))
err = -EFAULT;
kfree(info);
return err;
通过snd_pcm_info获取信息,然后通过copy_to_user拷贝到用户空间。
int snd_pcm_info(struct snd_pcm_substream *substream, struct snd_pcm_info *info)
struct snd_pcm *pcm = substream->pcm;
struct snd_pcm_str *pstr = substream->pstr;
memset(info, 0, sizeof(*info));
info->card = pcm->card->number;
info->device = pcm->device;
info->stream = substream->stream;
info->subdevice = substream->number;
strscpy(info->id, pcm->id, sizeof(info->id));
strscpy(info->name, pcm->name, sizeof(info->name));
info->dev_class = pcm->dev_class;
info->dev_subclass = pcm->dev_subclass;
info->subdevices_count = pstr->substream_count;
info->subdevices_avail = pstr->substream_count - pstr->substream_opened;
strscpy(info->subname, substream->name, sizeof(info->subname));
return 0;
6.2.2 设置信息
6.2.2.1 tinyalsa中的实现
int pcm_set_config(struct pcm *pcm, const struct pcm_config *config)
if (pcm == NULL)
return -EFAULT;
else if (config == NULL)
config = &pcm->config;
pcm->config.channels = 2;
pcm->config.rate = 48000;
pcm->config.period_size = 1024;
pcm->config.period_count = 4;
pcm->config.format = PCM_FORMAT_S16_LE;
pcm->config.start_threshold = config->period_count * config->period_size;
pcm->config.stop_threshold = config->period_count * config->period_size;
pcm->config.silence_threshold = 0;
pcm->config.silence_size = 0;
else
pcm->config = *config;
//设置硬件参数
struct snd_pcm_hw_params params;
param_init(¶ms);
param_set_mask(¶ms, SNDRV_PCM_HW_PARAM_FORMAT,
pcm_format_to_alsa(config->format));
param_set_min(¶ms, SNDRV_PCM_HW_PARAM_PERIOD_SIZE, config->period_size);
param_set_int(¶ms, SNDRV_PCM_HW_PARAM_CHANNELS,
config->channels);
param_set_int(¶ms, SNDRV_PCM_HW_PARAM_PERIODS, config->period_count);
param_set_int(¶ms, SNDRV_PCM_HW_PARAM_RATE, config->rate);
// 非中断模式的处理
if (pcm->flags & PCM_NOIRQ)
if (!(pcm->flags & PCM_MMAP))
oops(pcm, EINVAL, "noirq only currently supported with mmap().");
return -EINVAL;
params.flags |= SNDRV_PCM_HW_PARAMS_NO_PERIOD_WAKEUP;
pcm->noirq_frames_per_msec = config->rate / 1000;
// 是否使用mmap
if (pcm->flags & PCM_MMAP)
param_set_mask(¶ms, SNDRV_PCM_HW_PARAM_ACCESS,
SNDRV_PCM_ACCESS_MMAP_INTERLEAVED);
else
param_set_mask(¶ms, SNDRV_PCM_HW_PARAM_ACCESS,
SNDRV_PCM_ACCESS_RW_INTERLEAVED);
if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_HW_PARAMS, ¶ms))
int errno_copy = errno;
oops(pcm, errno, "cannot set hw params");
return -errno_copy;
/* get our refined hw_params */
pcm->config.period_size = param_get_int(¶ms, SNDRV_PCM_HW_PARAM_PERIOD_SIZE);
pcm->config.period_count = param_get_int(¶ms, SNDRV_PCM_HW_PARAM_PERIODS);
pcm->buffer_size = config->period_count * config->period_size;
// 处理mmap
if (pcm->flags & PCM_MMAP)
pcm->mmap_buffer = pcm->ops->mmap(pcm->data, NULL, pcm_frames_to_bytes(pcm, pcm->buffer_size),
PROT_READ | PROT_WRITE, MAP_SHARED, 0);
if (pcm->mmap_buffer == MAP_FAILED)
int errno_copy = errno;
oops(pcm, errno, "failed to mmap buffer %d bytes\\n",
pcm_frames_to_bytes(pcm, pcm->buffer_size));
return -errno_copy;
// 处理软件参数
struct snd_pcm_sw_params sparams;
memset(&sparams, 0, sizeof(sparams));
sparams.tstamp_mode = SNDRV_PCM_TSTAMP_ENABLE;
sparams.period_step = 1;
sparams.avail_min = config->period_size;
if (!config->start_threshold)
if (pcm->flags & PCM_IN)
pcm->config.start_threshold = sparams.start_threshold = 1;
else
pcm->config.start_threshold = sparams.start_threshold =
config->period_count * config->period_size / 2;
else
sparams.start_threshold = config->start_threshold;
/* pick a high stop threshold - todo: does this need further tuning */
if (!config->stop_threshold)
if (pcm->flags & PCM_IN)
pcm->config.stop_threshold = sparams.stop_threshold =
config->period_count * config->period_size * 10;
else
pcm->config.stop_threshold = sparams.stop_threshold =
config->period_count * config->period_size;
else
sparams.stop_threshold = config->stop_threshold;
sparams.xfer_align = config->period_size / 2; /* needed for old kernels */
sparams.silence_size = config->silence_size;
sparams.silence_threshold = config->silence_threshold;
pcm->boundary = sparams.boundary = pcm->buffer_size;
while (pcm->boundary * 2 <= INT_MAX - pcm->buffer_size)
pcm->boundary *= 2;
if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_SW_PARAMS, &sparams))
int errno_copy = errno;
oops(pcm, errno, "cannot set sw params");
return -errno_copy;
return 0;
pcm_config的内容,前面介绍过了。
snd_pcm_hw_params前面看到过,但没有展开,这里看一下:
struct snd_pcm_hw_params
unsigned int flags;
struct snd_mask masks[SNDRV_PCM_HW_PARAM_LAST_MASK -
SNDRV_PCM_HW_PARAM_FIRST_MASK + 1];
struct snd_mask mres[5]; /* reserved masks */
struct snd_interval intervals[SNDRV_PCM_HW_PARAM_LAST_INTERVAL -
SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1];
struct snd_interval ires[9]; /* reserved intervals */
unsigned int rmask; /* W: requested masks */
unsigned int cmask; /* R: changed masks */
unsigned int info; /* R: Info flags for returned setup */
unsigned int msbits; /* R: used most significant bits */
unsigned int rate_num; /* R: rate numerator */
unsigned int rate_den; /* R: rate denominator */
snd_pcm_uframes_t fifo_size; /* R: chip FIFO size in frames */
unsigned char reserved[64]; /* reserved for future */
;
这里的snd_mask与snd_interval在介绍runtime的时候介绍过了。
flags目前看到用到的只有SNDRV_PCM_HW_PARAMS_NO_PERIOD_WAKEUP。
6.2.2.2 SNDRV_PCM_IOCTL_HW_PARAMS
SNDRV_PCM_IOCTL_HW_PARAMS命令用来设置硬件参数,这一节的目的是看看它的具体过程。
前面的过程与ioctl的其它操作类似,在snd_pcm_common_ioctl中,有以下分支:
case SNDRV_PCM_IOCTL_HW_PARAMS:
return snd_pcm_hw_params_user(substream, arg);
再来看看snd_pcm_hw_params_user的实现:
static int snd_pcm_hw_params_user(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params __user * _params)
struct snd_pcm_hw_params *params;
int err;
// 从用户空间拷贝到内核空间
params = memdup_user(_params, sizeof(*params));
if (IS_ERR(params))
return PTR_ERR(params);
err = snd_pcm_hw_params(substream, params);
if (err < 0)
goto end;
// 返回参数再拷贝回用户空间
if (copy_to_user(_params, params, sizeof(*params)))
err = -EFAULT;
end:
kfree(params);
return err;
还需要继续看snd_pcm_hw_params函数:
static int snd_pcm_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
struct snd_pcm_runtime *runtime;
int err = 0, usecs;
unsigned int bits;
snd_pcm_uframes_t frames;
if (PCM_RUNTIME_CHECK(substream))
return -ENXIO;
runtime = substream->runtime;
mutex_lock(&runtime->buffer_mutex);
snd_pcm_stream_lock_irq(substream);
switch (runtime->status->state)
case SNDRV_PCM_STATE_OPEN:
case SNDRV_PCM_STATE_SETUP:
case SNDRV_PCM_STATE_PREPARED:
if (!is_oss_stream(substream) &&
atomic_read(&substream->mmap_count))
err = -EBADFD;
break;
default:
err = -EBADFD;
break;
snd_pcm_stream_unlock_irq(substream);
if (err)
goto unlock;
// 等待sync_irq结束,这里调用不太理解为什么
snd_pcm_sync_stop(substream, true);
params->rmask = ~0U;
// 设置runtime中的snd_pcm_hw_constraints对象
err = snd_pcm_hw_refine(substream, params);
if (err < 0)
goto _error;
// 检查并设置params中参数的值
err = snd_pcm_hw_params_choose(substream, params);
if (err < 0)
goto _error;
// 帮一些没有赋值的参数赋值
err = fixup_unreferenced_params(substream, params);
if (err < 0)
goto _error;
// 分配dma内存
if (substream->managed_buffer_alloc)
err = snd_pcm_lib_malloc_pages(substream,
params_buffer_bytes(params));
if (err < 0)
goto _error;
runtime->buffer_changed = err > 0;
// 调用驱动层函数
if (substream->ops->hw_params != NULL)
err = substream->ops->hw_params(substream, params);
if (err < 0)
goto _error;
// 给runtime中的各个参数赋值
runtime->access = params_access(params);
runtime->format = params_format(params);
runtime->subformat = params_subformat(params);
runtime->channels = params_channels(params);
runtime->rate = params_rate(params);
runtime->period_size = params_period_size(params);
runtime->periods = params_periods(params);
runtime->buffer_size = params_buffer_size(params);
runtime->info = params->info;
runtime->rate_num = params->rate_num;
runtime->rate_den = params->rate_den;
runtime->no_period_wakeup =
(params->info & SNDRV_PCM_INFO_NO_PERIOD_WAKEUP) &&
(params->flags & SNDRV_PCM_HW_PARAMS_NO_PERIOD_WAKEUP);
bits = snd_pcm_format_physical_width(runtime->format);
runtime->sample_bits = bits;
bits *= runtime->channels;
runtime->frame_bits = bits;
frames = 1;
while (bits % 8 != 0)
bits *= 2;
frames *= 2;
runtime->byte_align = bits / 8;
runtime->min_align = frames;
/* Default sw params */
runtime->tstamp_mode = SNDRV_PCM_TSTAMP_NONE;
runtime->period_step = 1;
runtime->control->avail_min = runtime->period_size;
runtime->start_threshold = 1;
runtime->stop_threshold = runtime->buffer_size;
runtime->silence_threshold = 0;
runtime->silence_size = 0;
runtime->boundary = runtime->buffer_size;
while (runtime->boundary * 2 <= LONG_MAX - runtime->buffer_size)
runtime->boundary *= 2;
/* clear the buffer for avoiding possible kernel info leaks */
if (runtime->dma_area && !substream->ops->copy_user)
size_t size = runtime->dma_bytes;
if (runtime->info & SNDRV_PCM_INFO_MMAP)
size = PAGE_ALIGN(size);
memset(runtime->dma_area, 0, size);
snd_pcm_timer_resolution_change(substream);
snd_pcm_set_state(substream, SNDRV_PCM_STATE_SETUP);
if (cpu_latency_qos_request_active(&substream->latency_pm_qos_req))
cpu_latency_qos_remove_request(&substream->latency_pm_qos_req);
usecs = period_to_usecs(runtime);
if (usecs >= 0)
cpu_latency_qos_add_request(&substream->latency_pm_qos_req,
usecs);
err = 0;
_error:
if (err)
/* hardware might be unusable from this time,
* so we force application to retry to set
* the correct hardware parameter settings
*/
snd_pcm_set_state(substream, SNDRV_PCM_STATE_OPEN);
if (substream->ops->hw_free != NULL)
substream->ops->hw_free(substream);
if (substream->managed_buffer_alloc)
snd_pcm_lib_free_pages(substream);
unlock:
mutex_unlock(&runtime->buffer_mutex);
return err;
函数有点长,依次来看。
snd_pcm_hw_refine ,在runtime中讲过了,根据规则来设置snd_pcm_hw_constraints对象。
snd_pcm_hw_params_choose用来检查并设置用户空间传入的参数,它会去设置以下参数的值:
static const int vars[] =
SNDRV_PCM_HW_PARAM_ACCESS,
SNDRV_PCM_HW_PARAM_FORMAT,
SNDRV_PCM_HW_PARAM_SUBFORMAT,
SNDRV_PCM_HW_PARAM_CHANNELS,
SNDRV_PCM_HW_PARAM_RATE,
SNDRV_PCM_HW_PARAM_PERIOD_TIME,
SNDRV_PCM_HW_PARAM_BUFFER_SIZE,
SNDRV_PCM_HW_PARAM_TICK_TIME,
-1
;
原则是除了SNDRV_PCM_HW_PARAM_BUFFER_SIZE设置为参数里的最大值外,其它的都会设置为最小值。有这么一个操作的原因是用户空间传入的值可能为多个,不过一般程序都不会这么做,比如在tinyalsa中,传入的参数一般是:
static void param_set_int(struct snd_pcm_hw_params *p, int n, unsigned int val)
if (param_is_interval(n))
struct snd_interval *i = param_to_interval(p, n);
i->min = val;
i->max = val;
i->integer = 1;
以单个整数的形式传入。
fixup_unreferenced_params帮一些没有赋值的参数赋默认值。
substream->managed_buffer_alloc用来判断是否由框架代码代为管理dma buffer。HDA选择是,在snd_hda_attach_pcm_stream中函数中,调用了snd_pcm_set_managed_buffer_all,通过它,managed_buffer_alloc已经被设置成了1。DMA数据的处理将放到一起统一讲解。
然后调用 substream->ops->hw_params,hda中对应的函数是:
static int azx_pcm_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *hw_params)
struct azx_pcm *apcm = snd_pcm_substream_chip(substream);
struct azx *chip = apcm->chip;
struct azx_dev *azx_dev = get_azx_dev(substream);
int ret = 0;
trace_azx_pcm_hw_params(chip, azx_dev);
dsp_lock(azx_dev);
if (dsp_is_locked(azx_dev))
ret = -EBUSY;
goto unlock;
azx_dev->core.bufsize = 0;
azx_dev->core.period_bytes = 0;
azx_dev->core.format_val = 0;
unlock:
dsp_unlock(azx_dev);
return ret;
这时候其实并没有做什么设置性的处理,简单的帮几个值清零。
snd_pcm_hw_params中其它的代码不需要太多的解释了,可以参看runtime中对各个成员的介绍。
如何桥接字节数组和音频流?
【中文标题】如何桥接字节数组和音频流?【英文标题】:How to bridge byte array and audio streaming? 【发布时间】:2015-08-14 01:02:20 【问题描述】:我正在为我的流媒体应用创建中继服务器。基本上,它应该是这样工作的:
-
客户端 A 通过套接字将麦克风音频流式传输到服务器
服务器获取流并可能将其临时存储在某个地方?(不确定)
客户端 B 从服务器获取流并播放它。
基本上,我已经完成了第一部分(将麦克风音频发送到服务器):
while(isStreaming)
minBufSize = recorder.read(buffer, 0, buffer.length);
mSocket.emit("stream", Arrays.toString(buffer));
第三部分完成,只是播放音频:
mediaplayer.reset();
mediaplayer.setDataSource("http://192.168.1.2:1337/stream");
mediaplayer.prepare();
mediaplayer.start();
现在我不确定如何桥接传入的字节数组和流。这是我当前的服务器代码:
var ms = require('mediaserver');
// from server to Client B
exports.letsStream = function(req, res, next)
ms.pipe(req, res, "sample_song_music_file.mp3");
;
// from Client A to server
exports.handleSocketConnection = function(socket)
console.log("connected");
socket.on('stream', function(data)
var bytes = JSON.parse(data);
console.log("GETTING STREAM:" + bytes);
);
有什么建议吗?如何直接流式传输该字节数组?
【问题讨论】:
您好,您找到解决方案了吗? @BackPacker 不,很久以前就停止寻找它了。 好的,谢谢,我想我必须使用 webrtc 或类似的东西 @BackPacker 既然你问了,你找到解决方案了吗?哈哈 @Gintas_ 你找到解决方案了吗?我有完全相同的情况,我正在苦苦挣扎.. 【参考方案1】:mediaserver 模块仅支持流式传输现有音频,而不支持“实时”流式传输。这行不通。
实现这三项任务的一种方法是:
-
https://www.npmjs.com/package/microphone 将麦克风音频读取为字节流。
http://binaryjs.com/ 处理通过 websockets 将字节流传输到服务器,然后发送到客户端。如果您设置了两条单独的路径,一条用于发送数据,一条用于接收。将数据从一个流发送到另一个。
使用https://github.com/TooTallNate/node-speaker在客户端B上播放传入的PCM数据流
【讨论】:
但是我已经完成了阅读麦克风输入部分。而且我不必使用mediaserver
模块。我希望有一个专门用于此的模块。此外,第 1 部分和第 2 部分是 android,而不是 nodejs。以上是关于6.播放音频(第一部分)的主要内容,如果未能解决你的问题,请参考以下文章