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(&params);
    param_set_mask(&params, SNDRV_PCM_HW_PARAM_FORMAT,
                   pcm_format_to_alsa(config->format));
    param_set_min(&params, SNDRV_PCM_HW_PARAM_PERIOD_SIZE, config->period_size);
    param_set_int(&params, SNDRV_PCM_HW_PARAM_CHANNELS,
                  config->channels);
    param_set_int(&params, SNDRV_PCM_HW_PARAM_PERIODS, config->period_count);
    param_set_int(&params, 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(&params, SNDRV_PCM_HW_PARAM_ACCESS,
                   SNDRV_PCM_ACCESS_MMAP_INTERLEAVED);
    else
        param_set_mask(&params, SNDRV_PCM_HW_PARAM_ACCESS,
                   SNDRV_PCM_ACCESS_RW_INTERLEAVED);

    if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_HW_PARAMS, &params)) 
        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(&params, SNDRV_PCM_HW_PARAM_PERIOD_SIZE);
    pcm->config.period_count = param_get_int(&params, 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.播放音频(第一部分)的主要内容,如果未能解决你的问题,请参考以下文章

FFmpeg学习6:视音频同步

第十章 视频播放器开发之音频播放

第十一章 视频播放器开发之音频播放

iOS开发系列--音频播放录音视频播放拍照视频录制

iOS开发系列--音频播放录音视频播放拍照视频录制

音视频开发之旅(45)-ExoPlayer 音频播放器实践