PCM data flow - 7 - Frame & Period

http://blog.csdn.net/azloong/article/details/51044893


后面章节将分析 dma buffer 的管理,其中细节需要对音频数据相关概念有一定的了解。因此本章说明下音频数据中的几个重要概念:

  • Sample:样本长度,音频数据最基本的单位,常见的有 8 位和 16 位;
  • Channel:声道数,分为单声道 mono 和立体声 stereo;
  • Frame:帧,构成一个完整的声音单元,Frame = Sample * channel
  • Rate:又称 sample rate,采样率,即每秒的采样次数,针对帧而言;
  • Period size:周期,每次硬件中断处理音频数据的帧数,对于音频设备的数据读写,以此为单位;
  • Buffer size:数据缓冲区大小,这里指 runtime 的 buffer size,而不是结构图 snd_pcm_hardware 中定义的 buffer_bytes_max;一般来说 buffer_size = period_size * period_count, period_count 相当于处理完一个 buffer 数据所需的硬件中断次数。

下面一张图直观的表示 buffer/period/frame/sample 之间的关系:
pcm_data_concept
这个 buffer 中有 16 个 period,每当 DMA 搬运完一个 period 的数据就会出生一次中断,因此搬运这个 buffer中的数据将产生 16 次中断。ALSA 为什么这样做?因为数据缓存区可能很大,一次传输可能会导致不可接受的延迟;为了解决这个问题,所以将缓存区拆分成多个周期 period,以周期 period 为单元传输数据。

7.1. Frames & Periods

敏感的读者会察觉到 period 和 buffer size 在 PCM 数据搬运中扮演着非常重要角色。下面引用两段来自 alsa 官网对 Period 的详细解释:
Period

The interval between interrupts from the hardware. This defines the input latency, since the CPU will not have any idea that there is data waiting until the audio interface interrupts it.
The audio interface has a “pointer” that marks the current position for read/write in its h/w buffer. The pointer circles around the buffer as long as the interface is running.
Typically, there are an integral number of periods per traversal of the h/w buffer, but not always. There is at least one card (ymfpci) that generates interrupts at a fixed rate indepedent of the buffer size (which can be changed), resulting in some “odd” effects compared to more traditional designs.
Note: h/w generally defines the interrupt in frames, though not always.
Alsa’s period size setting will affect how much work the CPU does. if you set the period size low, there will be more interrupts and the work that is done every interrupt will be done more often. So, if you don’t care about low latency, set the period size large as possible and you’ll have more CPU cycles for other things. The defaults that ALSA provides are in the middle of the range, typically.
(from an old AlsaDevel thread[1], quoting Paul Davis)
Retrieved from “http://alsa.opensrc.org/Period

FramesPeriods

A frame is equivalent of one sample being played, irrespective of the number of channels or the number of bits. e.g.
- 1 frame of a Stereo 48khz 16bit PCM stream is 4 bytes.
- 1 frame of a 5.1 48khz 16bit PCM stream is 12 bytes.
A period is the number of frames in between each hardware interrupt. The poll() will return once a period.
The buffer is a ring buffer. The buffer size always has to be greater than one period size. Commonly this is 2*period size, but some hardware can do 8 periods per buffer. It is also possible for the buffer size to not be an integer multiple of the period size.
Now, if the hardware has been set to 48000Hz , 2 periods, of 1024 frames each, making a buffer size of 2048 frames. The hardware will interrupt 2 times per buffer. ALSA will endeavor to keep the buffer as full as possible. Once the first period of samples has been played, the third period of samples is transfered into the space the first one occupied while the second period of samples is being played. (normal ring buffer behaviour).

Additional example
Here is an alternative example for the above discussion.
Say we want to work with a stereo, 16-bit, 44.1 KHz stream, one-way (meaning, either in playback or in capture direction). Then we have:
- ‘stereo’ = number of channels: 2
- 1 analog sample is represented with 16 bits = 2 bytes
- 1 frame represents 1 analog sample from all channels; here we have 2 channels, and so: 1 frame = (num_channels) * (1 sample in bytes) = (2 channels) * (2 bytes (16 bits) per sample) = 4 bytes (32 bits)
- To sustain 2x 44.1 KHz analog rate - the system must be capable of data transfer rate, in Bytes/sec: Bps_rate = (num_channels) * (1 sample in bytes) * (analog_rate) = (1 frame) * (analog_rate) = ( 2 channels ) * (2 bytes/sample) * (44100 samples/sec) = 2*2*44100 = 176400 Bytes/sec

Now, if ALSA would interrupt each second, asking for bytes - we’d need to have 176400 bytes ready for it (at end of each second), in order to sustain analog 16-bit stereo @ 44.1Khz.
- If it would interrupt each half a second, correspondingly for the same stream we’d need 176400/2 = 88200 bytes ready, at each interrupt;
- if the interrupt hits each 100 ms, we’d need to have 176400*(0.1/1) = 17640 bytes ready, at each interrupt.

We can control when this PCM interrupt is generated, by setting a period size, which is set in frames.
- Thus, if we set 16-bit stereo @ 44.1Khz, and the period_size to 4410 frames => (for 16-bit stereo @ 44.1Khz, 1 frame equals 4 bytes - so 4410 frames equal 4410*4 = 17640 bytes) => an interrupt will be generated each 17640 bytes - that is, each 100 ms.
- Correspondingly, buffer_size should be at least 2*period_size = 2*4410 = 8820 frames (or 8820*4 = 35280 bytes).

It seems (writing-an-alsa-driver.pdf), however, that it is the ALSA runtime that decides on the actual buffer_size and period_size, depending on: the requested number of channels, and their respective properties (rate and sampling resolution) - as well as the parameters set in the snd_pcm_hardware structure (in the driver).
Also, the following quote may be relevant, from “(alsa-devel) Questions about writing a new ALSA driver for a very limitted device”:

The “frame” represents the unit, 1 frame = # channels x sample_bytes.
In your case, 1 frame corresponds to 2 channels x 16 bits = 4 bytes.

The periods is the number of periods in a ring-buffer. In OSS, called
as “fragments”.

So,
- buffer_size = period_size * periods
- period_bytes = period_size * bytes_per_frame
- bytes_per_frame = channels * bytes_per_sample

I still don’t understand what ‘period_size’ and a ‘period’ is?

The “period” defines the frequency to update the status, usually via the invokation of interrupts. The “period_size” defines the frame sizes corresponding to the “period time”. This term corresponds to the “fragment size” on OSS. On major sound hardwares, a ring-buffer is divided to several parts and an irq is issued on each boundary. The period_size defines the size of this chunk.

On some hardwares, the irq is controlled on the basis of a timer. In this case, the period is defined as the timer frequency to invoke an irq.

这里不做翻译了,简单说下 Frame 和 Period 要点:

  • Frame:帧,构成一个完整的声音单元,它的大小等于 sample_bits * channels;
  • Peroid:周期大小,即每次 dma 硬件中断处理音频数据的帧数。如果周期大小设定得较大,则单次处理的数据较多,这意味着单位时间内硬件中断的次数较少,CPU 也就有更多时间处理其他任务,功耗也更低,但这样也带来一个显著的弊端——数据处理的时延会增大。

再说说 period bytes,对于 dma 处理来说,它关心的是数据大小,而不管 period size 和 period count,因此有个转换关系:

period_bytes = period_size * sample_bits * channels / 8

由于 I2S 总线采样率是稳定的,我们可以计算 I2S 传输一个周期的数据所需的时间:

transfer_time = 1 * period_size / sample_rate, in second

例如 period_size = 1024,sample_rate = 48KHz 时,那么传输时间为 1 * 1024 / 48000 = 21.3 (ms)。

7.2. hrtimer 模拟 PCM 周期中断

4.2.1. pcm operations 章节中,我们提到:每次 dma 传输完成一个周期的数据传输后,都要调用 snd_pcm_period_elapsed() 告知 pcm native 一个周期的数据已经传送到 FIFO 上了,然后再次调用 dma 传输音频数据…如此循环。
但有些 Platform 可能由于设计如此或设计缺陷,dma 传输完一个周期的数据不会产生硬件中断。这样系统如何知道什么时候传输完一个周期的数据了呢?在上个章节的最后,我们提到 I2S 总线传输一个周期的数据所需的时间,这其实也是 dma 搬运一个周期的数据所需的时间,这很容易理解:I2S FIFO 消耗完一个周期的数据,dma 才接着搬运一个周期的数据到 I2S FIFO。
因此我们可以用定时器来模拟这种硬件中断:

  • 1). 触发dma搬运数据时,启动定时器开始计时;
  • 2). 当定时到 1 * period_size / sample_rate,这时 I2S 已传输完一个周期的音频数据了,进入定时器中断处理:调用 snd_pcm_period_elapsed() 告知 pcm native 一个周期的数据已经处理完毕了,同时准备下一次的数据搬运;
  • 3). 继续执行步骤 1)…

为了更好保证数据传输的实时性,建议采用高精度定时器 hrtimer。

作者见过至少两家芯片在传输音频数据时需要用定时器模拟周期中断,一是 MTK 的智能手机处理器,二是 Freescale 的 i.MX 系列处理器。后者已经合入 Linux 内核代码,具体见:sound/soc/imx/imx-pcm-fiq.c,这里简略分析:

// 定时器中断处理例程
static enum hrtimer_restart snd_hrtimer_callback(struct hrtimer *hrt)
{
    ...

    /* If we've transferred at least a period then report it and
     * reset our poll time */
    if (delta >= iprtd->period) {
        snd_pcm_period_elapsed(substream); // 告知 pcm native 一个周期的数据已经处理完毕
        iprtd->last_offset = iprtd->offset;
    }

    hrtimer_forward_now(hrt, ns_to_ktime(iprtd->poll_time_ns)); // 重新计时,poll_time_ns:I2S 传输一个周期的数据所需的时间
    return HRTIMER_RESTART;
}

// hw_params 回调,数据传输开始前,先设置 dma 传输参数
static int snd_imx_pcm_hw_params(struct snd_pcm_substream *substream,
                struct snd_pcm_hw_params *params)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    struct imx_pcm_runtime_data *iprtd = runtime->private_data;

    iprtd->size = params_buffer_bytes(params);    // dma 缓冲区大小
    iprtd->periods = params_periods(params);      // 周期数
    iprtd->period = params_period_bytes(params) ; // 周期大小
    iprtd->offset = 0;
    iprtd->last_offset = 0;
    iprtd->poll_time_ns = 1000000000 / params_rate(params) *
                params_period_size(params);       // 计算 I2S 传输一个周期的数据所需的时间
    snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer); // 设置 dma 缓冲区

    return 0;
}

// trigger 回调,触发 dma 传输或停止
static int snd_imx_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    struct imx_pcm_runtime_data *iprtd = runtime->private_data;

    switch (cmd) {
    case SNDRV_PCM_TRIGGER_START:
    case SNDRV_PCM_TRIGGER_RESUME:
    case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
        atomic_set(&iprtd->running, 1);
        hrtimer_start(&iprtd->hrt, ns_to_ktime(iprtd->poll_time_ns), // 准备传输数据,启动定时器,开始计时
              HRTIMER_MODE_REL);
        if (++fiq_enable == 1)
            enable_fiq(imx_pcm_fiq); // 开始 dma 传输

        break;

    case SNDRV_PCM_TRIGGER_STOP:
    case SNDRV_PCM_TRIGGER_SUSPEND:
    case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
        atomic_set(&iprtd->running, 0);

        if (--fiq_enable == 0)
            disable_fiq(imx_pcm_fiq); // 停止 dma 传输

        break;
    default:
        return -EINVAL;
    }

    return 0;
}

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
if ((err = snd_pcm_hw_params(capture_handle, hw_params)) < 0) { printf("Error setting parameters: %s\n", snd_strerror(err)); return 1; } snd_pcm_hw_params_free(hw_params); unsigned int frames_per_period = 2048; snd_pcm_uframes_t period_size = frames_per_period * CHANNELS * 2; snd_pcm_uframes_t buffer_size = period_size * 2; if ((err = snd_pcm_set_params(capture_handle, FORMAT, SND_PCM_ACCESS_RW_INTERLEAVED, CHANNELS, rate, 1, frames_per_period)) < 0) { printf("Error setting parameters: %s\n", snd_strerror(err)); return 1; } FILE *file = fopen(argv[1], "wb"); if (!file) { printf("Error opening file for writing!\n"); return 1; } uint32_t chunk_size = 0; uint32_t subchunk_size = 16; uint16_t audio_format = 1; uint16_t num_channels = CHANNELS; uint32_t sample_rate = rate; uint32_t byte_rate = (rate * CHANNELS * 2); uint16_t block_align = (CHANNELS * 2); uint16_t bits_per_sample = 16; uint32_t data_size = 0; fwrite("RIFF", 1, 4, file); fwrite(&chunk_size, 4, 1, file); fwrite("WAVE", 1, 4, file); fwrite("fmt ", 1, 4, file); fwrite(&subchunk_size, 4, 1, file); fwrite(&audio_format, 2, 1, file); fwrite(&num_channels, 2, 1, file); fwrite(&sample_rate, 4, 1, file); fwrite(&byte_rate, 4, 1, file); fwrite(&block_align, 2, 1, file); fwrite(&bits_per_sample, 2, 1, file); fwrite("data", 1, 4, file); fwrite(&data_size, 4, 1, file); uint16_t buffer[frames_per_period * CHANNELS]; while (1) { int n = snd_pcm_readi(capture_handle, buffer, frames_per_period); if (n < 0) { printf("Error reading from PCM device: %s\n", snd_strerror(n)); break; } fwrite(buffer, 2, n * CHANNELS, file); data_size += n * CHANNELS * 2; } uint32_t file_size = data_size + 36; fseek(file, 4, SEEK_SET); fwrite(&file_size, 4, 1, file); fseek(file, 40, SEEK_SET); fwrite(&data_size, 4, 1, file); fclose(file); snd_pcm_close(capture_handle); 加上注释
06-02
/* 这段代码是一个录音程序,使用 ALSA 库进行音频捕获,将捕获到的音频写入到一个 WAV 文件中。 在程序中,首先进行 ALSA 库的初始化,然后设置音频捕获参数,包括采样率、声道数、采样大小等。 然后打开一个 WAV 文件,写入 WAV 文件头部信息,开始捕获音频并将捕获到的音频数据写入到 WAV 文件中。 最后,关闭 WAV 文件和 ALSA 库。 */ // 引入 ALSA 库的头文件 #include <alsa/asoundlib.h> int main(int argc, char **argv) { // 定义 ALSA 相关的变量 snd_pcm_t *capture_handle; snd_pcm_hw_params_t *hw_params; int err; // 打开默认音频捕获设备并进行初始化 if ((err = snd_pcm_open(&capture_handle, "default", SND_PCM_STREAM_CAPTURE, 0)) < 0) { printf("Error opening PCM device: %s\n", snd_strerror(err)); return 1; } // 申请 ALSA 配置参数结构体,进行初始化 if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0) { printf("Error allocating hardware parameter structure: %s\n", snd_strerror(err)); return 1; } if ((err = snd_pcm_hw_params_any(capture_handle, hw_params)) < 0) { printf("Error initializing hardware parameter structure: %s\n", snd_strerror(err)); return 1; } // 设置音频捕获参数 unsigned int rate = 44100; int CHANNELS = 2; if ((err = snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { printf("Error setting access type: %s\n", snd_strerror(err)); return 1; } if ((err = snd_pcm_hw_params_set_format(capture_handle, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) { printf("Error setting sample format: %s\n", snd_strerror(err)); return 1; } if ((err = snd_pcm_hw_params_set_channels(capture_handle, hw_params, CHANNELS)) < 0) { printf("Error setting channel count: %s\n", snd_strerror(err)); return 1; } if ((err = snd_pcm_hw_params_set_rate_near(capture_handle, hw_params, &rate, 0)) < 0) { printf("Error setting sample rate: %s\n", snd_strerror(err)); return 1; } if ((err = snd_pcm_hw_params_set_period_size_near(capture_handle, hw_params, &frames_per_period, 0)) < 0) { printf("Error setting period size: %s\n", snd_strerror(err)); return 1; } if ((err = snd_pcm_hw_params_set_buffer_size_near(capture_handle, hw_params, &buffer_size)) < 0) { printf("Error setting buffer size: %s\n", snd_strerror(err)); return 1; } if ((err = snd_pcm_hw_params(capture_handle, hw_params)) < 0) { printf("Error setting parameters: %s\n", snd_strerror(err)); return 1; } // 释放 ALSA 配置参数结构体 snd_pcm_hw_params_free(hw_params); // 计算每个周期的帧数和缓冲区大小 unsigned int frames_per_period = 2048; snd_pcm_uframes_t period_size = frames_per_period * CHANNELS * 2; snd_pcm_uframes_t buffer_size = period_size * 2; // 设置音频捕获参数 if ((err = snd_pcm_set_params(capture_handle, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED, CHANNELS, rate, 1, frames_per_period)) < 0) { printf("Error setting parameters: %s\n", snd_strerror(err)); return 1; } // 打开要写入的 WAV 文件 FILE *file = fopen(argv[1], "wb"); if (!file) { printf("Error opening file for writing!\n"); return 1; } // 写入 WAV 文件头部信息 uint32_t chunk_size = 0; uint32_t subchunk_size = 16; uint16_t audio_format = 1; uint16_t num_channels = CHANNELS; uint32_t sample_rate = rate; uint32_t byte_rate = (rate * CHANNELS * 2); uint16_t block_align = (CHANNELS * 2); uint16_t bits_per_sample = 16; uint32_t data_size = 0; fwrite("RIFF", 1, 4, file); fwrite(&chunk_size, 4, 1, file); fwrite("WAVE", 1, 4, file); fwrite("fmt ", 1, 4, file); fwrite(&subchunk_size, 4, 1, file); fwrite(&audio_format, 2, 1, file); fwrite(&num_channels, 2, 1, file); fwrite(&sample_rate, 4, 1, file); fwrite(&byte_rate, 4, 1, file); fwrite(&block_align, 2, 1, file); fwrite(&bits_per_sample, 2, 1, file); fwrite("data", 1, 4, file); fwrite(&data_size, 4, 1, file); // 读取音频数据并写入 WAV 文件 uint16_t buffer[frames_per_period * CHANNELS]; while (1) { int n = snd_pcm_readi(capture_handle, buffer, frames_per_period); if (n < 0) { printf("Error reading from PCM device: %s\n", snd_strerror(n)); break; } fwrite(buffer, 2, n * CHANNELS, file); data_size += n * CHANNELS * 2; } // 计算文件大小并更新 WAV 文件头部信息 uint32_t file_size = data_size + 36; fseek(file, 4, SEEK_SET); fwrite(&file_size, 4, 1, file); fseek(file, 40, SEEK_SET); fwrite(&data_size, 4, 1, file); // 关闭 WAV 文件和 ALSA 库 fclose(file); snd_pcm_close(capture_handle); return 0; }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值