Tinyalsa PCM API 實現深度剖析

wolfcs發表於2023-10-16

高階 Linux 音訊架構 (ALSA) 用於為 Linux 作業系統提供音訊和 MIDI 功能。它可以高效地支援所有型別的音訊介面,從消費者音效卡到專業的多通道音訊介面。它支援全模組化的音訊驅動。它是 SMP 和執行緒安全的。它提供了使用者空間庫 (alsa-lib) 來簡化應用程式程式設計並提供了更高階的功能。它支援老式的 OSS API。

ALSA 核心介面

ALSA 定義了使用者空間程式和核心音訊子系統之間互動的介面。這些介面主要由幾個部分組成:

  1. 匯出的音訊裝置檔案。Linux 核心 ALSA 子系統以固定的規則在 devtmpfs 檔案系統中特定的位置匯出音訊裝置檔案,這些音訊裝置檔案在檔案系統中的位置,具體來說為 /dev/snd/。如某 Linux 系統中的音訊裝置檔案:
$ ls -sl /dev/snd/
總用量 0
0 crw-rw----+ 1 root audio 116,  5 10月 10 09:39 controlC0
0 crw-rw----+ 1 root audio 116,  3 10月 11 09:32 pcmC0D0c
0 crw-rw----+ 1 root audio 116,  2 10月 11 17:21 pcmC0D0p
0 crw-rw----+ 1 root audio 116,  4 10月 10 09:39 pcmC0D1c
0 crw-rw----+ 1 root audio 116,  1 10月 11 09:29 seq
0 crw-rw----+ 1 root audio 116, 33 10月 11 09:29 timer

所有音訊裝置檔案其檔案型別都是字元裝置,其主裝置號都是 116,次裝置號因各裝置檔案建立的先後而異,但 timer 裝置檔案的從裝置號為 33。ALSA 為特定音效卡匯出多個裝置檔案,controlCX 裝置檔案用於控制音效卡,特別是對於 audio codec 等裝置的內部小部件進行控制;pcmCXDYp/pcmCXDYc 裝置檔案用於資料交換,用於播放的裝置檔案以 p 結尾,p 表示 playout,用於錄製的裝置檔案以 c 結尾,c 表示 capture (音訊裝置檔名中的 X 表示音效卡編號,Y 表示音訊裝置編號,如上面看到的 0、1 等)。controlCXpcmCXDYp/pcmCXDYc 是 ALSA 匯出的最為重要的裝置檔案。

  1. 各個音訊裝置檔案支援的檔案操作。不同的音訊裝置檔案支援的檔案操作集合不同,具體如下:
  • controlCX 裝置檔案:open()close()ioctl()read()lseek()/llseek()poll()fcntl()
  • pcmCXDYp 裝置檔案:open()close()write()ioctl()lseek()/llseek()poll()mmap()fcntl()
  • pcmCXDYc 裝置檔案:open()close()read()ioctl()lseek()/llseek()poll()mmap()fcntl()

核心中,controlCX 裝置檔案的檔案操作定義(位於sound/core/control.c)如下:

static const struct file_operations snd_ctl_f_ops =
{
	.owner =	THIS_MODULE,
	.read =		snd_ctl_read,
	.open =		snd_ctl_open,
	.release =	snd_ctl_release,
	.llseek =	no_llseek,
	.poll =		snd_ctl_poll,
	.unlocked_ioctl =	snd_ctl_ioctl,
	.compat_ioctl =	snd_ctl_ioctl_compat,
	.fasync =	snd_ctl_fasync,
};

核心中,pcmCXDYp/pcmCXDYc 音訊裝置檔案的檔案操作定義 (位於sound/core/pcm_native.c) 如下:

const struct file_operations snd_pcm_f_ops[2] = {
	{
		.owner =		THIS_MODULE,
		.write =		snd_pcm_write,
		.write_iter =		snd_pcm_writev,
		.open =			snd_pcm_playback_open,
		.release =		snd_pcm_release,
		.llseek =		no_llseek,
		.poll =			snd_pcm_poll,
		.unlocked_ioctl =	snd_pcm_ioctl,
		.compat_ioctl = 	snd_pcm_ioctl_compat,
		.mmap =			snd_pcm_mmap,
		.fasync =		snd_pcm_fasync,
		.get_unmapped_area =	snd_pcm_get_unmapped_area,
	},
	{
		.owner =		THIS_MODULE,
		.read =			snd_pcm_read,
		.read_iter =		snd_pcm_readv,
		.open =			snd_pcm_capture_open,
		.release =		snd_pcm_release,
		.llseek =		no_llseek,
		.poll =			snd_pcm_poll,
		.unlocked_ioctl =	snd_pcm_ioctl,
		.compat_ioctl = 	snd_pcm_ioctl_compat,
		.mmap =			snd_pcm_mmap,
		.fasync =		snd_pcm_fasync,
		.get_unmapped_area =	snd_pcm_get_unmapped_area,
	}
};

Linux 核心 ALSA 框架為音訊裝置檔案提供的這些檔案操作不是完全正交的,不同檔案操作的功能有一定的重合,如 pcmCXDYp/pcmCXDYc 音訊裝置檔案,它們的 write()/read() 操作,和 ioctl() 操作的一些命令在功能上是重合的。這些音訊檔案操作不是每個都有意義,如 lseek()/llseek() 操作被實現為空操作,fcntl() 對於音訊裝置檔案沒有意義。

Linux 核心 ALSA 框架的用於方便應用程式開發的使用者空間封裝庫,如 alsa-lib 和 tinyalsa 等,可以再次定義各裝置檔案支援的操作集合。如 tinyalsa 在 tinyalsa/src/pcm_io.h 檔案中定義了 pcm 檔案操作集合:

struct pcm_ops {
    int (*open) (unsigned int card, unsigned int device,
                 unsigned int flags, void **data, struct snd_node *node);
    void (*close) (void *data);
    int (*ioctl) (void *data, unsigned int cmd, ...);
    void *(*mmap) (void *data, void *addr, size_t length, int prot, int flags,
                   off_t offset);
    int (*munmap) (void *data, void *addr, size_t length);
    int (*poll) (void *data, struct pollfd *pfd, nfds_t nfds, int timeout);
};

tinyalsa 在 tinyalsa/src/mixer_io.h 檔案中定義了 control 檔案操作集合:

struct mixer_ops {
    void (*close) (void *data);
    int (*get_poll_fd) (void *data, struct pollfd *pfd, int count);
    ssize_t (*read_event) (void *data, struct snd_ctl_event *ev, size_t size);
    int (*ioctl) (void *data, unsigned int cmd, ...);
};

使用者空間的核心 ALSA 框架封裝庫,使用了 ALSA 框架匯出的檔案操作集合的子集。

  1. 各個音訊裝置檔案的檔案操作支援的命令及引數和返回值中用到的列舉和各種資料結構等。pcm 音訊裝置檔案的 ioctl() 操作支援眾多命令,這些命令如下:
  • SNDRV_PCM_IOCTL_PVERSION
  • SNDRV_PCM_IOCTL_INFO
  • SNDRV_PCM_IOCTL_TSTAMP
  • SNDRV_PCM_IOCTL_TTSTAMP
  • SNDRV_PCM_IOCTL_USER_PVERSION
  • SNDRV_PCM_IOCTL_HW_REFINE
  • SNDRV_PCM_IOCTL_HW_PARAMS
  • SNDRV_PCM_IOCTL_HW_FREE
  • SNDRV_PCM_IOCTL_SW_PARAMS
  • SNDRV_PCM_IOCTL_STATUS
  • SNDRV_PCM_IOCTL_DELAY
  • SNDRV_PCM_IOCTL_HWSYNC
  • SNDRV_PCM_IOCTL_SYNC_PTR
  • SNDRV_PCM_IOCTL_STATUS_EXT
  • SNDRV_PCM_IOCTL_CHANNEL_INFO
  • SNDRV_PCM_IOCTL_PREPARE
  • SNDRV_PCM_IOCTL_RESET
  • SNDRV_PCM_IOCTL_START
  • SNDRV_PCM_IOCTL_DROP
  • SNDRV_PCM_IOCTL_DRAIN
  • SNDRV_PCM_IOCTL_PAUSE
  • SNDRV_PCM_IOCTL_REWIND
  • SNDRV_PCM_IOCTL_RESUME
  • SNDRV_PCM_IOCTL_XRUN
  • SNDRV_PCM_IOCTL_FORWARD
  • SNDRV_PCM_IOCTL_WRITEI_FRAMES
  • SNDRV_PCM_IOCTL_READI_FRAMES
  • SNDRV_PCM_IOCTL_WRITEN_FRAMES
  • SNDRV_PCM_IOCTL_READN_FRAMES
  • SNDRV_PCM_IOCTL_LINK
  • SNDRV_PCM_IOCTL_UNLINK

pcmCXDYp 音訊裝置檔案的 write() 操作與它的 ioctl() 操作的 SNDRV_PCM_IOCTL_WRITEI_FRAMESSNDRV_PCM_IOCTL_WRITEN_FRAMES 命令實現相同的功能。pcmCXDYc 音訊裝置檔案的 read() 操作與它的 ioctl() 操作的 SNDRV_PCM_IOCTL_READI_FRAMESSNDRV_PCM_IOCTL_READN_FRAMES 命令實現相同的功能。

pcm 音訊裝置檔案的 mmap() 操作支援一些特殊的 offset,以獲取一些特別的用於和核心通訊的資料結構,這些特殊的 offset 值如下:

  • SNDRV_PCM_MMAP_OFFSET_DATA - 0x00000000
  • SNDRV_PCM_MMAP_OFFSET_STATUS - 0x80000000
  • SNDRV_PCM_MMAP_OFFSET_CONTROL - 0x81000000

control 音訊裝置檔案的 ioctl() 操作也支援眾多命令,這些命令如下:

  • SNDRV_CTL_IOCTL_PVERSION
  • SNDRV_CTL_IOCTL_CARD_INFO
  • SNDRV_CTL_IOCTL_ELEM_LIST
  • SNDRV_CTL_IOCTL_ELEM_INFO
  • SNDRV_CTL_IOCTL_ELEM_READ
  • SNDRV_CTL_IOCTL_ELEM_WRITE
  • SNDRV_CTL_IOCTL_ELEM_LOCK
  • SNDRV_CTL_IOCTL_ELEM_UNLOCK
  • SNDRV_CTL_IOCTL_SUBSCRIBE_EVENTS
  • SNDRV_CTL_IOCTL_ELEM_ADD
  • SNDRV_CTL_IOCTL_ELEM_REPLACE
  • SNDRV_CTL_IOCTL_ELEM_REMOVE
  • SNDRV_CTL_IOCTL_TLV_READ
  • SNDRV_CTL_IOCTL_TLV_WRITE
  • SNDRV_CTL_IOCTL_TLV_COMMAND
  • SNDRV_CTL_IOCTL_HWDEP_NEXT_DEVICE
  • SNDRV_CTL_IOCTL_HWDEP_INFO
  • SNDRV_CTL_IOCTL_PCM_NEXT_DEVICE
  • SNDRV_CTL_IOCTL_PCM_INFO
  • SNDRV_CTL_IOCTL_PCM_PREFER_SUBDEVICE
  • SNDRV_CTL_IOCTL_RAWMIDI_NEXT_DEVICE
  • SNDRV_CTL_IOCTL_RAWMIDI_INFO
  • SNDRV_CTL_IOCTL_RAWMIDI_PREFER_SUBDEVICE
  • SNDRV_CTL_IOCTL_POWER
  • SNDRV_CTL_IOCTL_POWER_STATE

Linux 核心 ALSA 框架的使用者空間封裝庫,如 alsa-lib 和 tinyalsa 等,封裝 Linux 核心音訊裝置檔案的這些檔案操作,實現內部建立的檔案操作集合。這裡不關心 control 裝置檔案操作的封裝。tinyalsa 在 tinyalsa/src/pcm_hw.c 檔案中,基於系統呼叫封裝器實現各個 pcm 操作,具體如下:

struct pcm_hw_data {
    /** Card number of the pcm device */
    unsigned int card;
    /** Device number for the pcm device */
    unsigned int device;
    /** File descriptor to the pcm device file node */
    int fd;
    /** Pointer to the pcm node from snd card definiton */
    struct snd_node *node;
};

static void pcm_hw_close(void *data)
{
    struct pcm_hw_data *hw_data = data;

    if (hw_data->fd >= 0)
        close(hw_data->fd);

    free(hw_data);
}

static int pcm_hw_ioctl(void *data, unsigned int cmd, ...)
{
    struct pcm_hw_data *hw_data = data;
    va_list ap;
    void *arg;

    va_start(ap, cmd);
    arg = va_arg(ap, void *);
    va_end(ap);

    return ioctl(hw_data->fd, cmd, arg);
}

static int pcm_hw_poll(void *data __attribute__((unused)),
                        struct pollfd *pfd, nfds_t nfds, int timeout)
{
    return poll(pfd, nfds, timeout);
}

static void *pcm_hw_mmap(void *data, void *addr, size_t length, int prot,
                       int flags, off_t offset)
{
    struct pcm_hw_data *hw_data = data;

    return mmap(addr, length, prot, flags, hw_data->fd, offset);
}

static int pcm_hw_munmap(void *data __attribute__((unused)), void *addr, size_t length)
{
    return munmap(addr, length);
}

static int pcm_hw_open(unsigned int card, unsigned int device,
                unsigned int flags, void **data, struct snd_node *node)
{
    struct pcm_hw_data *hw_data;
    char fn[256];
    int fd;

    hw_data = calloc(1, sizeof(*hw_data));
    if (!hw_data) {
        return -ENOMEM;
    }

    snprintf(fn, sizeof(fn), "/dev/snd/pcmC%uD%u%c", card, device,
             flags & PCM_IN ? 'c' : 'p');
    // Open the device with non-blocking flag to avoid to be blocked in kernel when all of the
    //   substreams of this PCM device are opened by others.
    fd = open(fn, O_RDWR | O_NONBLOCK);

    if (fd < 0) {
        free(hw_data);
        return fd;
    }

    if ((flags & PCM_NONBLOCK) == 0) {
        // Set the file descriptor to blocking mode.
        if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK) < 0) {
            fprintf(stderr, "failed to set to blocking mode on %s", fn);
            close(fd);
            free(hw_data);
            return -ENODEV;
        }
    }

    hw_data->card = card;
    hw_data->device = device;
    hw_data->fd = fd;
    hw_data->node = node;

    *data = hw_data;

    return fd;
}

const struct pcm_ops hw_ops = {
    .open = pcm_hw_open,
    .close = pcm_hw_close,
    .ioctl = pcm_hw_ioctl,
    .mmap = pcm_hw_mmap,
    .munmap = pcm_hw_munmap,
    .poll = pcm_hw_poll,
};

tinyalsa 建立了一個結構體 struct pcm_hw_data 來儲存具體的 pcm 裝置檔案相關的資訊,包括開啟的音訊裝置檔案的檔案描述符,音訊裝置檔案所屬的音效卡編號及裝置編號等。這裡各個操作的具體實現沒有什麼特別值得關注的地方。

Tinyalsa 的 API

ALSA 的使用者空間封裝庫,基於 Linux 核心提供的介面,為應用程式開發提供更高階的介面。ALSA 官方的 alsa-lib 庫提供了功能完備且強大的介面,但它的介面顯得有點易用性不足。tinyalsa 提供了一個 alsa-lib 庫的簡化版,其功能介面不如 alsa-lib 完備,但好在方便易用。tinyalsa 已經用於 Android 系統多年,眾多 audio HAL 服務基於這個庫實現。這裡關注 PCM 相關的 API。alsa-lib 的 API 多以 snd_pcm_ 開頭,如 snd_pcm_open()snd_pcm_close()snd_pcm_info()snd_pcm_start()snd_pcm_writei()snd_pcm_readi()snd_pcm_hw_params_set_channels()snd_pcm_wait() 等。tinyalsa 的 API 則多以 pcm_ 開頭,如 pcm_open()pcm_close()pcm_set_config()pcm_writei()pcm_readi()pcm_mmap_write()pcm_mmap_read()pcm_mmap_begin()pcm_mmap_commit()pcm_start()pcm_stop()pcm_wait() 等。alsa-lib 的 API 和 tinyalsa 的比較大的區別在與,alsa-lib 透過 snd_pcm_hw_params_set_format()snd_pcm_hw_params_set_channels() 等眾多介面為開啟的音訊流設定引數,而 tinyalsa 透過 pcm_set_config() 單個介面設定多個音訊流引數。

tinyalsa 的 PCM 相關 API 有如下 (位於 tinyalsa/include/tinyalsa/pcm.h) 這些:

#if defined(__cplusplus)
extern "C" {
#endif

/** Audio sample format of a PCM.
 * The first letter specifiers whether the sample is signed or unsigned.
 * The letter 'S' means signed. The letter 'U' means unsigned.
 * The following number is the amount of bits that the sample occupies in memory.
 * Following the underscore, specifiers whether the sample is big endian or little endian.
 * The letters 'LE' mean little endian.
 * The letters 'BE' mean big endian.
 * This enumeration is used in the @ref pcm_config structure.
 * @ingroup libtinyalsa-pcm
 */
enum pcm_format {

/* Note: This section must stay in the same
 * order for binary compatibility with older
 * versions of TinyALSA. */

    PCM_FORMAT_INVALID = -1,
    /** Signed 16-bit, little endian */
    PCM_FORMAT_S16_LE = 0,
    /** Signed, 32-bit, little endian */
    PCM_FORMAT_S32_LE,
    /** Signed, 8-bit */
    PCM_FORMAT_S8,
    /** Signed, 24-bit (32-bit in memory), little endian */
    PCM_FORMAT_S24_LE,
    /** Signed, 24-bit, little endian */
    PCM_FORMAT_S24_3LE,

/* End of compatibility section. */

    /** Signed, 16-bit, big endian */
    PCM_FORMAT_S16_BE,
    /** Signed, 24-bit (32-bit in memory), big endian */
    PCM_FORMAT_S24_BE,
    /** Signed, 24-bit, big endian */
    PCM_FORMAT_S24_3BE,
    /** Signed, 32-bit, big endian */
    PCM_FORMAT_S32_BE,
    /** 32-bit float, little endian */
    PCM_FORMAT_FLOAT_LE,
    /** 32-bit float, big endian */
    PCM_FORMAT_FLOAT_BE,
    /** Max of the enumeration list, not an actual format. */
    PCM_FORMAT_MAX
};

/** A bit mask of 256 bits (32 bytes) that describes some hardware parameters of a PCM */
struct pcm_mask {
    /** bits of the bit mask */
    unsigned int bits[32 / sizeof(unsigned int)];
};

/** Encapsulates the hardware and software parameters of a PCM.
 * @ingroup libtinyalsa-pcm
 */
struct pcm_config {
    /** The number of channels in a frame */
    unsigned int channels;
    /** The number of frames per second */
    unsigned int rate;
    /** The number of frames in a period */
    unsigned int period_size;
    /** The number of periods in a PCM */
    unsigned int period_count;
    /** The sample format of a PCM */
    enum pcm_format format;
    /* Values to use for the ALSA start, stop and silence thresholds, and
     * silence size.  Setting any one of these values to 0 will cause the
     * default tinyalsa values to be used instead.
     * Tinyalsa defaults are as follows.
     *
     * start_threshold   : period_count * period_size
     * stop_threshold    : period_count * period_size
     * silence_threshold : 0
     * silence_size      : 0
     */
    /** The minimum number of frames required to start the PCM */
    unsigned long start_threshold;
    /** The minimum number of frames required to stop the PCM */
    unsigned long stop_threshold;
    /** The minimum number of frames to silence the PCM */
    unsigned long silence_threshold;
    /** The number of frames to overwrite the playback buffer when the playback underrun is greater
     * than the silence threshold */
    unsigned long silence_size;

    unsigned long avail_min;
};

/** Enumeration of a PCM's hardware parameters.
 * Each of these parameters is either a mask or an interval.
 * @ingroup libtinyalsa-pcm
 */
enum pcm_param
{
    /** A mask that represents the type of read or write method available (e.g. interleaved, mmap). */
    PCM_PARAM_ACCESS,
    /** A mask that represents the @ref pcm_format available (e.g. @ref PCM_FORMAT_S32_LE) */
    PCM_PARAM_FORMAT,
    /** A mask that represents the subformat available */
    PCM_PARAM_SUBFORMAT,
    /** An interval representing the range of sample bits available (e.g. 8 to 32) */
    PCM_PARAM_SAMPLE_BITS,
    /** An interval representing the range of frame bits available (e.g. 8 to 64) */
    PCM_PARAM_FRAME_BITS,
    /** An interval representing the range of channels available (e.g. 1 to 2) */
    PCM_PARAM_CHANNELS,
    /** An interval representing the range of rates available (e.g. 44100 to 192000) */
    PCM_PARAM_RATE,
    PCM_PARAM_PERIOD_TIME,
    /** The number of frames in a period */
    PCM_PARAM_PERIOD_SIZE,
    /** The number of bytes in a period */
    PCM_PARAM_PERIOD_BYTES,
    /** The number of periods for a PCM */
    PCM_PARAM_PERIODS,
    PCM_PARAM_BUFFER_TIME,
    PCM_PARAM_BUFFER_SIZE,
    PCM_PARAM_BUFFER_BYTES,
    PCM_PARAM_TICK_TIME,
}; /* enum pcm_param */

struct pcm_params;

struct pcm_params *pcm_params_get(unsigned int card, unsigned int device,
                                  unsigned int flags);

void pcm_params_free(struct pcm_params *pcm_params);

const struct pcm_mask *pcm_params_get_mask(const struct pcm_params *pcm_params, enum pcm_param param);

unsigned int pcm_params_get_min(const struct pcm_params *pcm_params, enum pcm_param param);

unsigned int pcm_params_get_max(const struct pcm_params *pcm_params, enum pcm_param param);

/* Converts the pcm parameters to a human readable string.
 * The string parameter is a caller allocated buffer of size bytes,
 * which is then filled up to size - 1 and null terminated,
 * if size is greater than zero.
 * The return value is the number of bytes copied to string
 * (not including null termination) if less than size; otherwise,
 * the number of bytes required for the buffer.
 */
int pcm_params_to_string(struct pcm_params *params, char *string, unsigned int size);

/* Returns 1 if the pcm_format is present (format bit set) in
 * the pcm_params structure; 0 otherwise, or upon unrecognized format.
 */
int pcm_params_format_test(struct pcm_params *params, enum pcm_format format);

struct pcm;

struct pcm *pcm_open(unsigned int card,
                     unsigned int device,
                     unsigned int flags,
                     const struct pcm_config *config);

struct pcm *pcm_open_by_name(const char *name,
                             unsigned int flags,
                             const struct pcm_config *config);

int pcm_close(struct pcm *pcm);

int pcm_is_ready(const struct pcm *pcm);

unsigned int pcm_get_channels(const struct pcm *pcm);

const struct pcm_config * pcm_get_config(const struct pcm *pcm);

unsigned int pcm_get_rate(const struct pcm *pcm);

enum pcm_format pcm_get_format(const struct pcm *pcm);

int pcm_get_file_descriptor(const struct pcm *pcm);

const char *pcm_get_error(const struct pcm *pcm);

int pcm_set_config(struct pcm *pcm, const struct pcm_config *config);

unsigned int pcm_format_to_bits(enum pcm_format format);

unsigned int pcm_get_buffer_size(const struct pcm *pcm);

unsigned int pcm_frames_to_bytes(const struct pcm *pcm, unsigned int frames);

unsigned int pcm_bytes_to_frames(const struct pcm *pcm, unsigned int bytes);

int pcm_get_htimestamp(struct pcm *pcm, unsigned int *avail, struct timespec *tstamp);

unsigned int pcm_get_subdevice(const struct pcm *pcm);

int pcm_writei(struct pcm *pcm, const void *data, unsigned int frame_count) TINYALSA_WARN_UNUSED_RESULT;

int pcm_readi(struct pcm *pcm, void *data, unsigned int frame_count) TINYALSA_WARN_UNUSED_RESULT;

int pcm_write(struct pcm *pcm, const void *data, unsigned int count) TINYALSA_DEPRECATED;

int pcm_read(struct pcm *pcm, void *data, unsigned int count) TINYALSA_DEPRECATED;

int pcm_mmap_write(struct pcm *pcm, const void *data, unsigned int count) TINYALSA_DEPRECATED;

int pcm_mmap_read(struct pcm *pcm, void *data, unsigned int count) TINYALSA_DEPRECATED;

int pcm_mmap_begin(struct pcm *pcm, void **areas, unsigned int *offset, unsigned int *frames);

int pcm_mmap_commit(struct pcm *pcm, unsigned int offset, unsigned int frames);

int pcm_mmap_avail(struct pcm *pcm);

int pcm_mmap_get_hw_ptr(struct pcm* pcm, unsigned int *hw_ptr, struct timespec *tstamp);

int pcm_get_poll_fd(struct pcm *pcm);

int pcm_link(struct pcm *pcm1, struct pcm *pcm2);

int pcm_unlink(struct pcm *pcm);

int pcm_prepare(struct pcm *pcm);

int pcm_start(struct pcm *pcm);

int pcm_stop(struct pcm *pcm);

int pcm_wait(struct pcm *pcm, int timeout);

long pcm_get_delay(struct pcm *pcm);

int pcm_ioctl(struct pcm *pcm, int code, ...) TINYALSA_DEPRECATED;

#if defined(__cplusplus)
}  /* extern "C" */
#endif

tinyalsa 的 PCM 相關 API 主要分為兩部分。一是硬體裝置引數查詢及測試,這主要包括如下這些:

struct pcm_params;

struct pcm_params *pcm_params_get(unsigned int card, unsigned int device,
                                  unsigned int flags);

void pcm_params_free(struct pcm_params *pcm_params);

const struct pcm_mask *pcm_params_get_mask(const struct pcm_params *pcm_params, enum pcm_param param);

unsigned int pcm_params_get_min(const struct pcm_params *pcm_params, enum pcm_param param);

unsigned int pcm_params_get_max(const struct pcm_params *pcm_params, enum pcm_param param);

/* Converts the pcm parameters to a human readable string.
 * The string parameter is a caller allocated buffer of size bytes,
 * which is then filled up to size - 1 and null terminated,
 * if size is greater than zero.
 * The return value is the number of bytes copied to string
 * (not including null termination) if less than size; otherwise,
 * the number of bytes required for the buffer.
 */
int pcm_params_to_string(struct pcm_params *params, char *string, unsigned int size);

/* Returns 1 if the pcm_format is present (format bit set) in
 * the pcm_params structure; 0 otherwise, or upon unrecognized format.
 */
int pcm_params_format_test(struct pcm_params *params, enum pcm_format format);

tinyalsa 用一個沒有實際定義的 struct pcm_params 嚮應用程式表示音訊流的硬體裝置引數。在 tinyalsa 的實現內部,用 struct snd_pcm_hw_params 表示音訊流的硬體裝置引數。struct snd_pcm_hw_params 是 Linux 核心定義的,使用者空間程式和 Linux 核心透過這個結構交換音訊流的硬體裝置引數,這個結構體定義 (位於 include/uapi/sound/asound.h) 如下:

struct snd_interval {
	unsigned int min, max;
	unsigned int openmin:1,
		     openmax:1,
		     integer:1,
		     empty:1;
};

#define SNDRV_MASK_MAX	256

struct snd_mask {
	__u32 bits[(SNDRV_MASK_MAX+31)/32];
};

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 */
};

struct snd_pcm_hw_params 主要用 masksintervals 兩個陣列儲存不同型別的音訊引數,masks 用於儲存 mask 型引數,intervals 用於儲存 interval 型引數。對於每一個引數,Linux 核心都會給它分配一個引數編號,且在這兩個陣列中的一個裡,有一個對應的位置儲存其相關引數值。Linux 核心支援的音訊引數 (位於 include/uapi/sound/asound.h) 有如下這些:

typedef int snd_pcm_hw_param_t;
#define	SNDRV_PCM_HW_PARAM_ACCESS	0	/* Access type */
#define	SNDRV_PCM_HW_PARAM_FORMAT	1	/* Format */
#define	SNDRV_PCM_HW_PARAM_SUBFORMAT	2	/* Subformat */
#define	SNDRV_PCM_HW_PARAM_FIRST_MASK	SNDRV_PCM_HW_PARAM_ACCESS
#define	SNDRV_PCM_HW_PARAM_LAST_MASK	SNDRV_PCM_HW_PARAM_SUBFORMAT

#define	SNDRV_PCM_HW_PARAM_SAMPLE_BITS	8	/* Bits per sample */
#define	SNDRV_PCM_HW_PARAM_FRAME_BITS	9	/* Bits per frame */
#define	SNDRV_PCM_HW_PARAM_CHANNELS	10	/* Channels */
#define	SNDRV_PCM_HW_PARAM_RATE		11	/* Approx rate */
#define	SNDRV_PCM_HW_PARAM_PERIOD_TIME	12	/* Approx distance between
						 * interrupts in us
						 */
#define	SNDRV_PCM_HW_PARAM_PERIOD_SIZE	13	/* Approx frames between
						 * interrupts
						 */
#define	SNDRV_PCM_HW_PARAM_PERIOD_BYTES	14	/* Approx bytes between
						 * interrupts
						 */
#define	SNDRV_PCM_HW_PARAM_PERIODS	15	/* Approx interrupts per
						 * buffer
						 */
#define	SNDRV_PCM_HW_PARAM_BUFFER_TIME	16	/* Approx duration of buffer
						 * in us
						 */
#define	SNDRV_PCM_HW_PARAM_BUFFER_SIZE	17	/* Size of buffer in frames */
#define	SNDRV_PCM_HW_PARAM_BUFFER_BYTES	18	/* Size of buffer in bytes */
#define	SNDRV_PCM_HW_PARAM_TICK_TIME	19	/* Approx tick duration in us */
#define	SNDRV_PCM_HW_PARAM_FIRST_INTERVAL	SNDRV_PCM_HW_PARAM_SAMPLE_BITS
#define	SNDRV_PCM_HW_PARAM_LAST_INTERVAL	SNDRV_PCM_HW_PARAM_TICK_TIME

mask 型音訊引數的值在 masks 陣列中的位置為 (音訊引數編號 - 第一個 mask 型音訊引數編號),interval 型引數在 intervals 陣列中的位置為 (音訊引數編號 - 第一個 interval 型音訊引數編號)。在使用 tinyalsa API 的應用程式中,各個音訊引數列示如下:

/** Enumeration of a PCM's hardware parameters.
 * Each of these parameters is either a mask or an interval.
 * @ingroup libtinyalsa-pcm
 */
enum pcm_param
{
    /** A mask that represents the type of read or write method available (e.g. interleaved, mmap). */
    PCM_PARAM_ACCESS,
    /** A mask that represents the @ref pcm_format available (e.g. @ref PCM_FORMAT_S32_LE) */
    PCM_PARAM_FORMAT,
    /** A mask that represents the subformat available */
    PCM_PARAM_SUBFORMAT,
    /** An interval representing the range of sample bits available (e.g. 8 to 32) */
    PCM_PARAM_SAMPLE_BITS,
    /** An interval representing the range of frame bits available (e.g. 8 to 64) */
    PCM_PARAM_FRAME_BITS,
    /** An interval representing the range of channels available (e.g. 1 to 2) */
    PCM_PARAM_CHANNELS,
    /** An interval representing the range of rates available (e.g. 44100 to 192000) */
    PCM_PARAM_RATE,
    PCM_PARAM_PERIOD_TIME,
    /** The number of frames in a period */
    PCM_PARAM_PERIOD_SIZE,
    /** The number of bytes in a period */
    PCM_PARAM_PERIOD_BYTES,
    /** The number of periods for a PCM */
    PCM_PARAM_PERIODS,
    PCM_PARAM_BUFFER_TIME,
    PCM_PARAM_BUFFER_SIZE,
    PCM_PARAM_BUFFER_BYTES,
    PCM_PARAM_TICK_TIME,
}; /* enum pcm_param */

硬體裝置引數查詢及測試的這些介面實現 (位於 include/uapi/sound/asound.h) 如下:

static void param_init(struct snd_pcm_hw_params *p)
{
    int n;

    memset(p, 0, sizeof(*p));
    for (n = SNDRV_PCM_HW_PARAM_FIRST_MASK;
         n <= SNDRV_PCM_HW_PARAM_LAST_MASK; n++) {
            struct snd_mask *m = param_to_mask(p, n);
            m->bits[0] = ~0;
            m->bits[1] = ~0;
    }
    for (n = SNDRV_PCM_HW_PARAM_FIRST_INTERVAL;
         n <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL; n++) {
            struct snd_interval *i = param_to_interval(p, n);
            i->min = 0;
            i->max = ~0;
    }
    p->rmask = ~0U;
    p->cmask = 0;
    p->info = ~0U;
}
 . . . . . .
/** Gets the hardware parameters of a PCM, without created a PCM handle.
 * @param card The card of the PCM.
 *  The default card is zero.
 * @param device The device of the PCM.
 *  The default device is zero.
 * @param flags Specifies whether the PCM is an input or output.
 *  May be one of the following:
 *   - @ref PCM_IN
 *   - @ref PCM_OUT
 * @return On success, the hardware parameters of the PCM; on failure, NULL.
 * @ingroup libtinyalsa-pcm
 */
struct pcm_params *pcm_params_get(unsigned int card, unsigned int device,
                                  unsigned int flags)
{
    struct snd_pcm_hw_params *params;
    void *snd_node = NULL, *data;
    const struct pcm_ops *ops;
    int fd;

    ops = &hw_ops;
    fd = ops->open(card, device, flags, &data, snd_node);

#ifdef TINYALSA_USES_PLUGINS
    if (fd < 0) {
        int pcm_type;
        snd_node = snd_utils_open_pcm(card, device);
        pcm_type = snd_utils_get_node_type(snd_node);
        if (!snd_node || pcm_type != SND_NODE_TYPE_PLUGIN) {
            fprintf(stderr, "no device (hw/plugin) for card(%u), device(%u)",
                 card, device);
            goto err_open;
        }
        ops = &plug_ops;
        fd = ops->open(card, device, flags, &data, snd_node);
    }
#endif
    if (fd < 0) {
        fprintf(stderr, "cannot open card(%d) device (%d): %s\n",
                card, device, strerror(errno));
        goto err_open;
    }

    params = calloc(1, sizeof(struct snd_pcm_hw_params));
    if (!params)
        goto err_calloc;

    param_init(params);
    if (ops->ioctl(data, SNDRV_PCM_IOCTL_HW_REFINE, params)) {
        fprintf(stderr, "SNDRV_PCM_IOCTL_HW_REFINE error (%d)\n", errno);
        goto err_hw_refine;
    }

#ifdef TINYALSA_USES_PLUGINS
    if (snd_node)
        snd_utils_close_dev_node(snd_node);
#endif
    ops->close(data);

    return (struct pcm_params *)params;

err_hw_refine:
    free(params);
err_calloc:
#ifdef TINYALSA_USES_PLUGINS
    if (snd_node)
        snd_utils_close_dev_node(snd_node);
#endif
    ops->close(data);
err_open:
    return NULL;
}

/** Frees the hardware parameters returned by @ref pcm_params_get.
 * @param pcm_params Hardware parameters of a PCM.
 *  May be NULL.
 * @ingroup libtinyalsa-pcm
 */
void pcm_params_free(struct pcm_params *pcm_params)
{
    struct snd_pcm_hw_params *params = (struct snd_pcm_hw_params *)pcm_params;

    if (params)
        free(params);
}

static int pcm_param_to_alsa(enum pcm_param param)
{
    switch (param) {
    case PCM_PARAM_ACCESS:
        return SNDRV_PCM_HW_PARAM_ACCESS;
    case PCM_PARAM_FORMAT:
        return SNDRV_PCM_HW_PARAM_FORMAT;
    case PCM_PARAM_SUBFORMAT:
        return SNDRV_PCM_HW_PARAM_SUBFORMAT;
    case PCM_PARAM_SAMPLE_BITS:
        return SNDRV_PCM_HW_PARAM_SAMPLE_BITS;
        break;
    case PCM_PARAM_FRAME_BITS:
        return SNDRV_PCM_HW_PARAM_FRAME_BITS;
        break;
    case PCM_PARAM_CHANNELS:
        return SNDRV_PCM_HW_PARAM_CHANNELS;
        break;
    case PCM_PARAM_RATE:
        return SNDRV_PCM_HW_PARAM_RATE;
        break;
    case PCM_PARAM_PERIOD_TIME:
        return SNDRV_PCM_HW_PARAM_PERIOD_TIME;
        break;
    case PCM_PARAM_PERIOD_SIZE:
        return SNDRV_PCM_HW_PARAM_PERIOD_SIZE;
        break;
    case PCM_PARAM_PERIOD_BYTES:
        return SNDRV_PCM_HW_PARAM_PERIOD_BYTES;
        break;
    case PCM_PARAM_PERIODS:
        return SNDRV_PCM_HW_PARAM_PERIODS;
        break;
    case PCM_PARAM_BUFFER_TIME:
        return SNDRV_PCM_HW_PARAM_BUFFER_TIME;
        break;
    case PCM_PARAM_BUFFER_SIZE:
        return SNDRV_PCM_HW_PARAM_BUFFER_SIZE;
        break;
    case PCM_PARAM_BUFFER_BYTES:
        return SNDRV_PCM_HW_PARAM_BUFFER_BYTES;
        break;
    case PCM_PARAM_TICK_TIME:
        return SNDRV_PCM_HW_PARAM_TICK_TIME;
        break;

    default:
        return -1;
    }
}

/** Gets a mask from a PCM's hardware parameters.
 * @param pcm_params A PCM's hardware parameters.
 * @param param The parameter to get.
 * @return If @p pcm_params is NULL or @p param is not a mask, NULL is returned.
 *  Otherwise, the mask associated with @p param is returned.
 * @ingroup libtinyalsa-pcm
 */
const struct pcm_mask *pcm_params_get_mask(const struct pcm_params *pcm_params,
                                     enum pcm_param param)
{
    int p;
    struct snd_pcm_hw_params *params = (struct snd_pcm_hw_params *)pcm_params;
    if (params == NULL) {
        return NULL;
    }

    p = pcm_param_to_alsa(param);
    if (p < 0 || !param_is_mask(p)) {
        return NULL;
    }

    return (const struct pcm_mask *)param_to_mask(params, p);
}

/** Get the minimum of a specified PCM parameter.
 * @param pcm_params A PCM parameters structure.
 * @param param The specified parameter to get the minimum of.
 * @returns On success, the parameter minimum.
 *  On failure, zero.
 */
unsigned int pcm_params_get_min(const struct pcm_params *pcm_params,
                                enum pcm_param param)
{
    struct snd_pcm_hw_params *params = (struct snd_pcm_hw_params *)pcm_params;
    int p;

    if (!params)
        return 0;

    p = pcm_param_to_alsa(param);
    if (p < 0)
        return 0;

    return param_get_min(params, p);
}

/** Get the maximum of a specified PCM parameter.
 * @param pcm_params A PCM parameters structure.
 * @param param The specified parameter to get the maximum of.
 * @returns On success, the parameter maximum.
 *  On failure, zero.
 */
unsigned int pcm_params_get_max(const struct pcm_params *pcm_params,
                                enum pcm_param param)
{
    const struct snd_pcm_hw_params *params = (const struct snd_pcm_hw_params *)pcm_params;
    int p;

    if (!params)
        return 0;

    p = pcm_param_to_alsa(param);
    if (p < 0)
        return 0;

    return param_get_max(params, p);
}

static int pcm_mask_test(const struct pcm_mask *m, unsigned int index)
{
    const unsigned int bitshift = 5; /* for 32 bit integer */
    const unsigned int bitmask = (1 << bitshift) - 1;
    unsigned int element;

    element = index >> bitshift;
    if (element >= ARRAY_SIZE(m->bits))
        return 0; /* for safety, but should never occur */
    return (m->bits[element] >> (index & bitmask)) & 1;
}

static int pcm_mask_to_string(const struct pcm_mask *m, char *string, unsigned int size,
                              char *mask_name,
                              const char * const *bit_array_name, size_t bit_array_size)
{
    unsigned int i;
    unsigned int offset = 0;

    if (m == NULL)
        return 0;
    if (bit_array_size < 32) {
        STRLOG(string, offset, size, "%12s:\t%#08x\n", mask_name, m->bits[0]);
    } else { /* spans two or more bitfields, print with an array index */
        for (i = 0; i < (bit_array_size + 31) >> 5; ++i) {
            STRLOG(string, offset, size, "%9s[%d]:\t%#08x\n",
                   mask_name, i, m->bits[i]);
        }
    }
    for (i = 0; i < bit_array_size; ++i) {
        if (pcm_mask_test(m, i)) {
            STRLOG(string, offset, size, "%12s \t%s\n", "", bit_array_name[i]);
        }
    }
    return offset;
}

int pcm_params_to_string(struct pcm_params *params, char *string, unsigned int size)
{
    const struct pcm_mask *m;
    unsigned int min, max;
    unsigned int clipoffset, offset;

    m = pcm_params_get_mask(params, PCM_PARAM_ACCESS);
    offset = pcm_mask_to_string(m, string, size,
                                 "Access", access_lookup, ARRAY_SIZE(access_lookup));
    m = pcm_params_get_mask(params, PCM_PARAM_FORMAT);
    clipoffset = offset > size ? size : offset;
    offset += pcm_mask_to_string(m, string + clipoffset, size - clipoffset,
                                 "Format", format_lookup, ARRAY_SIZE(format_lookup));
    m = pcm_params_get_mask(params, PCM_PARAM_SUBFORMAT);
    clipoffset = offset > size ? size : offset;
    offset += pcm_mask_to_string(m, string + clipoffset, size - clipoffset,
                                 "Subformat", subformat_lookup, ARRAY_SIZE(subformat_lookup));
    min = pcm_params_get_min(params, PCM_PARAM_RATE);
    max = pcm_params_get_max(params, PCM_PARAM_RATE);
    STRLOG(string, offset, size, "        Rate:\tmin=%uHz\tmax=%uHz\n", min, max);
    min = pcm_params_get_min(params, PCM_PARAM_CHANNELS);
    max = pcm_params_get_max(params, PCM_PARAM_CHANNELS);
    STRLOG(string, offset, size, "    Channels:\tmin=%u\t\tmax=%u\n", min, max);
    min = pcm_params_get_min(params, PCM_PARAM_SAMPLE_BITS);
    max = pcm_params_get_max(params, PCM_PARAM_SAMPLE_BITS);
    STRLOG(string, offset, size, " Sample bits:\tmin=%u\t\tmax=%u\n", min, max);
    min = pcm_params_get_min(params, PCM_PARAM_PERIOD_SIZE);
    max = pcm_params_get_max(params, PCM_PARAM_PERIOD_SIZE);
    STRLOG(string, offset, size, " Period size:\tmin=%u\t\tmax=%u\n", min, max);
    min = pcm_params_get_min(params, PCM_PARAM_PERIODS);
    max = pcm_params_get_max(params, PCM_PARAM_PERIODS);
    STRLOG(string, offset, size, "Period count:\tmin=%u\t\tmax=%u\n", min, max);
    return offset;
}

int pcm_params_format_test(struct pcm_params *params, enum pcm_format format)
{
    unsigned int alsa_format = pcm_format_to_alsa(format);

    if (alsa_format == SNDRV_PCM_FORMAT_S16_LE && format != PCM_FORMAT_S16_LE)
        return 0; /* caution: format not recognized is equivalent to S16_LE */
    return pcm_mask_test(pcm_params_get_mask(params, PCM_PARAM_FORMAT), alsa_format);
}

pcm_params_get() 介面用於獲取特定音效卡的特定裝置的特定音訊流方向的硬體裝置引數,其中的音訊流方向透過引數 flags 指定。這個函式開啟音訊裝置檔案,透過 ioctl()SNDRV_PCM_IOCTL_HW_REFINE 命令獲得硬體裝置引數,關閉音訊裝置檔案,並返回獲得的硬體裝置引數。pcm_params_free() 介面用於釋放獲得的硬體裝置引數。

pcm_params_get_mask()pcm_params_get_min()pcm_params_get_max() 等介面分別用於從獲得的硬體裝置引數中提取 mask 型引數和 interval 型引數的引數值。pcm_params_format_test() 介面用於測試獲得的硬體裝置引數中是否包含特定 pcm_format。這些函式的實現中,將傳入的 tinyalsa 引數列示轉為 Linux 核心 ALSA 的引數列示,並從 struct snd_pcm_hw_params 中獲取請求的引數值。

pcm_params_to_string() 介面用於生成硬體裝置引數的字串表示。

tinyalsa 的 PCM 相關 API 的第二部分是音訊流相關 API。

Tinyalsa 的 PCM API 的音訊流相關部分

tinyalsa 的 PCM 相關 API 的音訊流相關部分又可以分為音訊流生命週期管理 API 和音訊流狀態和引數查詢 API 兩部分,其中的音訊流生命週期管理 API 有如下這些:

struct pcm;

struct pcm *pcm_open(unsigned int card,
                     unsigned int device,
                     unsigned int flags,
                     const struct pcm_config *config);

struct pcm *pcm_open_by_name(const char *name,
                             unsigned int flags,
                             const struct pcm_config *config);

int pcm_close(struct pcm *pcm);
 . . . . . .
int pcm_set_config(struct pcm *pcm, const struct pcm_config *config);
 . . . . . .
int pcm_writei(struct pcm *pcm, const void *data, unsigned int frame_count) TINYALSA_WARN_UNUSED_RESULT;

int pcm_readi(struct pcm *pcm, void *data, unsigned int frame_count) TINYALSA_WARN_UNUSED_RESULT;

int pcm_write(struct pcm *pcm, const void *data, unsigned int count) TINYALSA_DEPRECATED;

int pcm_read(struct pcm *pcm, void *data, unsigned int count) TINYALSA_DEPRECATED;

int pcm_mmap_write(struct pcm *pcm, const void *data, unsigned int count) TINYALSA_DEPRECATED;

int pcm_mmap_read(struct pcm *pcm, void *data, unsigned int count) TINYALSA_DEPRECATED;

int pcm_mmap_begin(struct pcm *pcm, void **areas, unsigned int *offset, unsigned int *frames);

int pcm_mmap_commit(struct pcm *pcm, unsigned int offset, unsigned int frames);
 . . . . . .
int pcm_link(struct pcm *pcm1, struct pcm *pcm2);

int pcm_unlink(struct pcm *pcm);

int pcm_prepare(struct pcm *pcm);

int pcm_start(struct pcm *pcm);

int pcm_stop(struct pcm *pcm);

int pcm_wait(struct pcm *pcm, int timeout);
 . . . . . .
int pcm_ioctl(struct pcm *pcm, int code, ...) TINYALSA_DEPRECATED;

tinyalsa 用一個其定義應用程式不可見的結構體 struct pcm 嚮應用程式表示音訊流,struct pcm 結構體的指標可以理解為音訊流的控制程式碼。struct pcm 結構體定義 (位於 tinyalsa/src/pcm.c) 如下:

/** A PCM handle.
 * @ingroup libtinyalsa-pcm
 */
struct pcm {
    /** The PCM's file descriptor */
    int fd;
    /** Flags that were passed to @ref pcm_open */
    unsigned int flags;
    /** The number of (under/over)runs that have occured */
    int xruns;
    /** Size of the buffer */
    unsigned int buffer_size;
    /** The boundary for ring buffer pointers */
    unsigned long boundary;
    /** Description of the last error that occured */
    char error[PCM_ERROR_MAX];
    /** Configuration that was passed to @ref pcm_open */
    struct pcm_config config;
    struct snd_pcm_mmap_status *mmap_status;
    struct snd_pcm_mmap_control *mmap_control;
    struct snd_pcm_sync_ptr *sync_ptr;
    void *mmap_buffer;
    unsigned int noirq_frames_per_msec;
    /** The delay of the PCM, in terms of frames */
    long pcm_delay;
    /** The subdevice corresponding to the PCM */
    unsigned int subdevice;
    /** Pointer to the pcm ops, either hw or plugin */
    const struct pcm_ops *ops;
    /** Private data for pcm_hw or pcm_plugin */
    void *data;
    /** Pointer to the pcm node from snd card definition */
    struct snd_node *snd_node;
};

tinyalsa 的音訊流生命週期管理 API 不是正交的,這些 API 的非正交主要發生在資料傳遞相關的 API 上,pcm_write()pcm_read()pcm_mmap_write()pcm_mmap_read() 這幾個介面已經被標記為廢棄的,它們在功能上,基本上與 pcm_writei()pcm_readi() 相同,都用於傳送或接收資料,在實現上,它們透過 pcm_writei()pcm_readi() 完成其主要職責,相關程式碼 (位於 tinyalsa/src/pcm.c) 具體如下:

int pcm_mmap_write(struct pcm *pcm, const void *data, unsigned int count)
{
    if ((~pcm->flags) & (PCM_OUT | PCM_MMAP))
        return -EINVAL;

    unsigned int frames = pcm_bytes_to_frames(pcm, count);
    int res = pcm_writei(pcm, (void *) data, frames);

    if (res < 0) {
        return res;
    }

    return (unsigned int) res == frames ? 0 : -EIO;
}

int pcm_mmap_read(struct pcm *pcm, void *data, unsigned int count)
{
    if ((~pcm->flags) & (PCM_IN | PCM_MMAP))
        return -EINVAL;

    unsigned int frames = pcm_bytes_to_frames(pcm, count);
    int res = pcm_readi(pcm, data, frames);

    if (res < 0) {
        return res;
    }

    return (unsigned int) res == frames ? 0 : -EIO;
}
 . . . . . .
/** Writes audio samples to PCM.
 * If the PCM has not been started, it is started in this function.
 * This function is only valid for PCMs opened with the @ref PCM_OUT flag.
 * This function is not valid for PCMs opened with the @ref PCM_MMAP flag.
 * @param pcm A PCM handle.
 * @param data The audio sample array
 * @param count The number of bytes occupied by the sample array.
 * @return On success, this function returns zero; otherwise, a negative number.
 * @deprecated
 * @ingroup libtinyalsa-pcm
 */
int pcm_write(struct pcm *pcm, const void *data, unsigned int count)
{
    unsigned int requested_frames = pcm_bytes_to_frames(pcm, count);
    int ret = pcm_writei(pcm, data, requested_frames);

    if (ret < 0)
        return ret;

    return ((unsigned int )ret == requested_frames) ? 0 : -EIO;
}

/** Reads audio samples from PCM.
 * If the PCM has not been started, it is started in this function.
 * This function is only valid for PCMs opened with the @ref PCM_IN flag.
 * This function is not valid for PCMs opened with the @ref PCM_MMAP flag.
 * @param pcm A PCM handle.
 * @param data The audio sample array
 * @param count The number of bytes occupied by the sample array.
 * @return On success, this function returns zero; otherwise, a negative number.
 * @deprecated
 * @ingroup libtinyalsa-pcm
 */
int pcm_read(struct pcm *pcm, void *data, unsigned int count)
{
    unsigned int requested_frames = pcm_bytes_to_frames(pcm, count);
    int ret = pcm_readi(pcm, data, requested_frames);

    if (ret < 0)
        return ret;

    return ((unsigned int )ret == requested_frames) ? 0 : -EIO;
}

pcm_write()pcm_read()pcm_mmap_write()pcm_mmap_read()pcm_writei()pcm_readi() 的主要區別在於,前者用位元組數表示要傳遞的資料量,後者則用幀數表示。在介面定義上,pcm_write()pcm_read() 用於以非 mmap 的方式和 Linux 核心交換資料,pcm_mmap_write()pcm_mmap_read() 用於以 mmap 的方式和 Linux 核心交換資料,因而 pcm_mmap_write()pcm_mmap_read() 函式的實現中檢查了 flags 是否包含 PCM_MMAP。但實現上,除了要傳遞的資料量的描述方式外,pcm_write()pcm_read()pcm_writei()pcm_readi() 完全相同。

tinyalsa 用一組介面 pcm_writei()pcm_readi() 支援了 mmap 和非 mmap 兩種方式與 Linux 核心的資料交換,與 Linux 核心的實際的資料交換方式透過 flags 來確定。

另外,pcm_writei()pcm_readi() 以 mmap 方式與 Linux 核心交換資料時,需藉助 pcm_mmap_begin()pcm_mmap_commit()pcm_mmap_avail() 等介面來完成。

音訊流狀態和引數查詢 API 有如下這些:

int pcm_is_ready(const struct pcm *pcm);

unsigned int pcm_get_channels(const struct pcm *pcm);

const struct pcm_config * pcm_get_config(const struct pcm *pcm);

unsigned int pcm_get_rate(const struct pcm *pcm);

enum pcm_format pcm_get_format(const struct pcm *pcm);

int pcm_get_file_descriptor(const struct pcm *pcm);

const char *pcm_get_error(const struct pcm *pcm);
 . . . . . .
unsigned int pcm_format_to_bits(enum pcm_format format);

unsigned int pcm_get_buffer_size(const struct pcm *pcm);

unsigned int pcm_frames_to_bytes(const struct pcm *pcm, unsigned int frames);

unsigned int pcm_bytes_to_frames(const struct pcm *pcm, unsigned int bytes);

int pcm_get_htimestamp(struct pcm *pcm, unsigned int *avail, struct timespec *tstamp);

unsigned int pcm_get_subdevice(const struct pcm *pcm);
 . . . . . .
int pcm_mmap_avail(struct pcm *pcm);

int pcm_mmap_get_hw_ptr(struct pcm* pcm, unsigned int *hw_ptr, struct timespec *tstamp);

int pcm_get_poll_fd(struct pcm *pcm);
 . . . . . .
long pcm_get_delay(struct pcm *pcm);

open 是音訊流的生命週期開始的地方。tinyalsa 提供了 pcm_open()pcm_open_by_name() 兩個介面來開啟一個音訊流,它們以不同的方式指定音訊裝置,前者透過音效卡編號和裝置編號來指定,後者透過一個字串裝置名來指定。pcm_open_by_name() 函式定義 (位於 tinyalsa/src/pcm.c) 如下:

struct pcm *pcm_open_by_name(const char *name,
                             unsigned int flags,
                             const struct pcm_config *config)
{
    unsigned int card, device;
    if (name[0] != 'h' || name[1] != 'w' || name[2] != ':') {
        oops(&bad_pcm, 0, "name format is not matched");
        return &bad_pcm;
    } else if (sscanf(&name[3], "%u,%u", &card, &device) != 2) {
        oops(&bad_pcm, 0, "name format is not matched");
        return &bad_pcm;
    }
    return pcm_open(card, device, flags, config);
}

字串裝置名的格式為 'hw:card,device',pcm_open_by_name() 函式從裝置名引數中解析提取音效卡編號和裝置編號,並呼叫 pcm_open() 函式開啟音訊流。以如下方式開啟音訊流:

 . . . . pcm = pcm_open_by_name("hw:0,0",  flags, config);

等價於如下方式開啟音訊流:

 . . . . pcm = pcm_open(0,  0, flags, config);

pcm_open()pcm_open_by_name() 兩個介面的 flags 引數指定了 pcm 的特性和功能,它是如下這些標記的按位或:

/** A flag that specifies that the PCM is an output.
 * May not be bitwise AND'd with @ref PCM_IN.
 * Used in @ref pcm_open.
 * @ingroup libtinyalsa-pcm
 */
#define PCM_OUT 0x00000000

/** Specifies that the PCM is an input.
 * May not be bitwise AND'd with @ref PCM_OUT.
 * Used in @ref pcm_open.
 * @ingroup libtinyalsa-pcm
 */
#define PCM_IN 0x10000000

/** Specifies that the PCM will use mmap read and write methods.
 * Used in @ref pcm_open.
 * @ingroup libtinyalsa-pcm
 */
#define PCM_MMAP 0x00000001

/** Specifies no interrupt requests.
 * May only be bitwise AND'd with @ref PCM_MMAP.
 * Used in @ref pcm_open.
 * @ingroup libtinyalsa-pcm
 */
#define PCM_NOIRQ 0x00000002

/** When set, calls to @ref pcm_write
 * for a playback stream will not attempt
 * to restart the stream in the case of an
 * underflow, but will return -EPIPE instead.
 * After the first -EPIPE error, the stream
 * is considered to be stopped, and a second
 * call to pcm_write will attempt to restart
 * the stream.
 * Used in @ref pcm_open.
 * @ingroup libtinyalsa-pcm
 */
#define PCM_NORESTART 0x00000004

/** Specifies monotonic timestamps.
 * Used in @ref pcm_open.
 * @ingroup libtinyalsa-pcm
 */
#define PCM_MONOTONIC 0x00000008

/** If used with @ref pcm_open and @ref pcm_params_get,
 * it will not cause the function to block if
 * the PCM is not available. It will also cause
 * the functions @ref pcm_readi and @ref pcm_writei
 * to exit if they would cause the caller to wait.
 * @ingroup libtinyalsa-pcm
 * */
#define PCM_NONBLOCK 0x00000010

pcm_open()pcm_open_by_name() 兩個介面的 config 引數指定了開啟 PCM 所使用的硬體和軟體引數。

pcm_open() 函式定義 (位於 tinyalsa/src/pcm.c) 如下:

/** Sets the PCM configuration.
 * @param pcm A PCM handle.
 * @param config The configuration to use for the
 *  PCM. This parameter may be NULL, in which case
 *  the default configuration is used.
 * @returns Zero on success, a negative errno value
 *  on failure.
 * @ingroup libtinyalsa-pcm
 * */
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;
    }

    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;

    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;

    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;
    }

    pcm->boundary = sparams.boundary;
    return 0;
}
 . . . . . .
static int pcm_hw_mmap_status(struct pcm *pcm)
{
    if (pcm->sync_ptr)
        return 0;

    int page_size = sysconf(_SC_PAGE_SIZE);
    pcm->mmap_status = pcm->ops->mmap(pcm->data, NULL, page_size, PROT_READ, MAP_SHARED,
                            SNDRV_PCM_MMAP_OFFSET_STATUS);
    if (pcm->mmap_status == MAP_FAILED)
        pcm->mmap_status = NULL;
    if (!pcm->mmap_status)
        goto mmap_error;

    pcm->mmap_control = pcm->ops->mmap(pcm->data, NULL, page_size, PROT_READ | PROT_WRITE,
                             MAP_SHARED, SNDRV_PCM_MMAP_OFFSET_CONTROL);
    if (pcm->mmap_control == MAP_FAILED)
        pcm->mmap_control = NULL;
    if (!pcm->mmap_control) {
        pcm->ops->munmap(pcm->data, pcm->mmap_status, page_size);
        pcm->mmap_status = NULL;
        goto mmap_error;
    }

    return 0;

mmap_error:

    pcm->sync_ptr = calloc(1, sizeof(*pcm->sync_ptr));
    if (!pcm->sync_ptr)
        return -ENOMEM;
    pcm->mmap_status = &pcm->sync_ptr->s.status;
    pcm->mmap_control = &pcm->sync_ptr->c.control;

    return 0;
}
 . . . . . .
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) {
        memcpy(bad_pcm.error, pcm->error, sizeof(pcm->error));
        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;
}

開啟 PCM 的主要過程如下:

  1. 開啟 PCM 裝置檔案,PCM 裝置檔案路徑為 /dev/snd/pcmC%uD%u%c,如前面在 pcm_hw_open() 函式中看到的那樣。

  2. 透過 ioctl()SNDRV_PCM_IOCTL_INFO 命令獲得 PCM 資訊,這裡主要是想獲得 PCM 裝置的次裝置號。核心中,這個 ioctl() 命令的實現 (位於 sound/core/pcm_native.c) 如下:

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;
	strlcpy(info->id, pcm->id, sizeof(info->id));
	strlcpy(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;
	strlcpy(info->subname, substream->name, sizeof(info->subname));

	return 0;
}

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;
}
 . . . . . .
static int snd_pcm_common_ioctl(struct file *file,
				 struct snd_pcm_substream *substream,
				 unsigned int cmd, void __user *arg)
{
	struct snd_pcm_file *pcm_file = file->private_data;
	int res;

	if (PCM_RUNTIME_CHECK(substream))
		return -ENXIO;

	res = snd_power_wait(substream->pcm->card, SNDRV_CTL_POWER_D0);
	if (res < 0)
		return res;

	switch (cmd) {
	case SNDRV_PCM_IOCTL_PVERSION:
		return put_user(SNDRV_PCM_VERSION, (int __user *)arg) ? -EFAULT : 0;
	case SNDRV_PCM_IOCTL_INFO:
		return snd_pcm_info_user(substream, arg);

從核心往使用者空間複製資料總是很麻煩,需要先在核心空間分配一個相同型別的資料結構,之後填充該資料結構,隨後透過 copy_to_user() 將填充之後的資料結構複製到使用者空間記憶體,最後釋放分配的核心空間資料結構。

  1. 呼叫 pcm_set_config() 介面設定音訊引數。透過 ioctl()SNDRV_PCM_IOCTL_HW_PARAMS 命令設定硬體引數,如音訊取樣格式,通道數,取樣率等;如果 flags 包含 PCM_MMAP 標記,則將用於存放音訊資料的 DMA 緩衝區 mmap 記憶體對映到使用者空間;過 ioctl()SNDRV_PCM_IOCTL_SW_PARAMS 命令設定軟體引數,如 start_thresholdstop_thresholdsilence_size 等。大多數情況下,pcm_set_config() 介面不單獨使用,而是在 open 中使用。

  2. 呼叫 pcm_hw_mmap_status() 函式為音訊流獲得 struct snd_pcm_mmap_statusstruct snd_pcm_mmap_control。這兩個結構由 Linux 核心定義,具體定義 (位於 include/uapi/sound/asound.h) 如下:

struct __snd_pcm_mmap_status {
	snd_pcm_state_t state;		/* RO: state - SNDRV_PCM_STATE_XXXX */
	int pad1;			/* Needed for 64 bit alignment */
	snd_pcm_uframes_t hw_ptr;	/* RO: hw ptr (0...boundary-1) */
	struct __snd_timespec tstamp;	/* Timestamp */
	snd_pcm_state_t suspended_state; /* RO: suspended stream state */
	struct __snd_timespec audio_tstamp; /* from sample counter or wall clock */
};

struct __snd_pcm_mmap_control {
	snd_pcm_uframes_t appl_ptr;	/* RW: appl ptr (0...boundary-1) */
	snd_pcm_uframes_t avail_min;	/* RW: min available frames for wakeup */
};

#define SNDRV_PCM_SYNC_PTR_HWSYNC	(1<<0)	/* execute hwsync */
#define SNDRV_PCM_SYNC_PTR_APPL		(1<<1)	/* get appl_ptr from driver (r/w op) */
#define SNDRV_PCM_SYNC_PTR_AVAIL_MIN	(1<<2)	/* get avail_min from driver */

struct snd_pcm_sync_ptr {
	unsigned int flags;
	union {
		struct snd_pcm_mmap_status status;
		unsigned char reserved[64];
	} s;
	union {
		struct snd_pcm_mmap_control control;
		unsigned char reserved[64];
	} c;
};

透過 ALSA 播放音訊資料的過程大體為,應用程式將資料複製到 DMA 緩衝區,並更新指向 DMA 緩衝區的應用程式指標,硬體驅動程式將 DMA 緩衝區的資料傳送給硬體,並更新硬體指標,應用程式指標和硬體指標分別為 DMA 緩衝區這個迴圈緩衝區的寫指標和讀指標。無論是否以 mmap 的方式來完成應用程式與 Linux 核心的資料交換,這個過程都是一樣的。

應用程式以 Read/Write 的方式與 Linux 核心交換資料時,先將音訊資料放在一塊使用者空間記憶體緩衝區中,之後透過 ioctl() 命令或 write() 操作將音訊資料送給核心,核心將使用者空間記憶體緩衝區中的資料複製到 DMA 緩衝區,並更新指向 DMA 緩衝區的應用程式指標。以 mmap 的方式與 Linux 核心交換資料時,則是將 DMA 緩衝區記憶體對映到使用者空間,應用程式向 DMA 緩衝區中寫入音訊資料,應用程式更新應用程式指標,應用程式透過 ioctl() 命令將應用程式指標等資訊同步給核心。使用 mmap 時,可以少一次音訊資料在使用者空間記憶體和 DMA 緩衝區之間的記憶體複製。

struct snd_pcm_mmap_statusstruct snd_pcm_mmap_control 用於維護應用程式指標和硬體指標等音訊流相關狀態資訊。

pcm_hw_mmap_status() 函式首先嚐試用 mmap() 操作獲得 struct snd_pcm_mmap_statusstruct snd_pcm_mmap_control,為 mmap() 操作傳入的 offset 分別為 SNDRV_PCM_MMAP_OFFSET_STATUSSNDRV_PCM_MMAP_OFFSET_CONTROL。這種方式直接將核心空間的資料結構對映到使用者空間,但這不是每種硬體架構都支援的,如 Linux 核心中 pcm 裝置檔案 mmap 操作的實現 (位於 sound/core/pcm_native.c) 如下:

/*
 * Only on coherent architectures, we can mmap the status and the control records
 * for effcient data transfer.  On others, we have to use HWSYNC ioctl...
 */
#if defined(CONFIG_X86) || defined(CONFIG_PPC) || defined(CONFIG_ALPHA)
/*
 * mmap status record
 */
static vm_fault_t snd_pcm_mmap_status_fault(struct vm_fault *vmf)
{
	struct snd_pcm_substream *substream = vmf->vma->vm_private_data;
	struct snd_pcm_runtime *runtime;
	
	if (substream == NULL)
		return VM_FAULT_SIGBUS;
	runtime = substream->runtime;
	vmf->page = virt_to_page(runtime->status);
	get_page(vmf->page);
	return 0;
}

static const struct vm_operations_struct snd_pcm_vm_ops_status =
{
	.fault =	snd_pcm_mmap_status_fault,
};

static int snd_pcm_mmap_status(struct snd_pcm_substream *substream, struct file *file,
			       struct vm_area_struct *area)
{
	long size;
	if (!(area->vm_flags & VM_READ))
		return -EINVAL;
	size = area->vm_end - area->vm_start;
	if (size != PAGE_ALIGN(sizeof(struct snd_pcm_mmap_status)))
		return -EINVAL;
	area->vm_ops = &snd_pcm_vm_ops_status;
	area->vm_private_data = substream;
	area->vm_flags |= VM_DONTEXPAND | VM_DONTDUMP;
	return 0;
}

/*
 * mmap control record
 */
static vm_fault_t snd_pcm_mmap_control_fault(struct vm_fault *vmf)
{
	struct snd_pcm_substream *substream = vmf->vma->vm_private_data;
	struct snd_pcm_runtime *runtime;
	
	if (substream == NULL)
		return VM_FAULT_SIGBUS;
	runtime = substream->runtime;
	vmf->page = virt_to_page(runtime->control);
	get_page(vmf->page);
	return 0;
}

static const struct vm_operations_struct snd_pcm_vm_ops_control =
{
	.fault =	snd_pcm_mmap_control_fault,
};

static int snd_pcm_mmap_control(struct snd_pcm_substream *substream, struct file *file,
				struct vm_area_struct *area)
{
	long size;
	if (!(area->vm_flags & VM_READ))
		return -EINVAL;
	size = area->vm_end - area->vm_start;
	if (size != PAGE_ALIGN(sizeof(struct snd_pcm_mmap_control)))
		return -EINVAL;
	area->vm_ops = &snd_pcm_vm_ops_control;
	area->vm_private_data = substream;
	area->vm_flags |= VM_DONTEXPAND | VM_DONTDUMP;
	return 0;
}

static bool pcm_status_mmap_allowed(struct snd_pcm_file *pcm_file)
{
	/* See pcm_control_mmap_allowed() below.
	 * Since older alsa-lib requires both status and control mmaps to be
	 * coupled, we have to disable the status mmap for old alsa-lib, too.
	 */
	if (pcm_file->user_pversion < SNDRV_PROTOCOL_VERSION(2, 0, 14) &&
	    (pcm_file->substream->runtime->hw.info & SNDRV_PCM_INFO_SYNC_APPLPTR))
		return false;
	return true;
}

static bool pcm_control_mmap_allowed(struct snd_pcm_file *pcm_file)
{
	if (pcm_file->no_compat_mmap)
		return false;
	/* Disallow the control mmap when SYNC_APPLPTR flag is set;
	 * it enforces the user-space to fall back to snd_pcm_sync_ptr(),
	 * thus it effectively assures the manual update of appl_ptr.
	 */
	if (pcm_file->substream->runtime->hw.info & SNDRV_PCM_INFO_SYNC_APPLPTR)
		return false;
	return true;
}

#else /* ! coherent mmap */
/*
 * don't support mmap for status and control records.
 */
#define pcm_status_mmap_allowed(pcm_file)	false
#define pcm_control_mmap_allowed(pcm_file)	false

static int snd_pcm_mmap_status(struct snd_pcm_substream *substream, struct file *file,
			       struct vm_area_struct *area)
{
	return -ENXIO;
}
static int snd_pcm_mmap_control(struct snd_pcm_substream *substream, struct file *file,
				struct vm_area_struct *area)
{
	return -ENXIO;
}
#endif /* coherent mmap */
 . . . . . .
static int snd_pcm_mmap(struct file *file, struct vm_area_struct *area)
{
	struct snd_pcm_file * pcm_file;
	struct snd_pcm_substream *substream;	
	unsigned long offset;
	
	pcm_file = file->private_data;
	substream = pcm_file->substream;
	if (PCM_RUNTIME_CHECK(substream))
		return -ENXIO;

	offset = area->vm_pgoff << PAGE_SHIFT;
	switch (offset) {
	case SNDRV_PCM_MMAP_OFFSET_STATUS_OLD:
		if (pcm_file->no_compat_mmap || !IS_ENABLED(CONFIG_64BIT))
			return -ENXIO;
		fallthrough;
	case SNDRV_PCM_MMAP_OFFSET_STATUS_NEW:
		if (!pcm_status_mmap_allowed(pcm_file))
			return -ENXIO;
		return snd_pcm_mmap_status(substream, file, area);
	case SNDRV_PCM_MMAP_OFFSET_CONTROL_OLD:
		if (pcm_file->no_compat_mmap || !IS_ENABLED(CONFIG_64BIT))
			return -ENXIO;
		fallthrough;
	case SNDRV_PCM_MMAP_OFFSET_CONTROL_NEW:
		if (!pcm_control_mmap_allowed(pcm_file))
			return -ENXIO;
		return snd_pcm_mmap_control(substream, file, area);
	default:
		return snd_pcm_mmap_data(substream, file, area);
	}
	return 0;
}

只有在 X86、PPC 和 Alpha 這種 coherent 架構上可以將核心的 struct snd_pcm_mmap_statusstruct snd_pcm_mmap_control 對映到使用者空間。

pcm_hw_mmap_status() 函式嘗試用 mmap() 操作獲得 struct snd_pcm_mmap_statusstruct snd_pcm_mmap_control 失敗時,則在應用程式的堆記憶體上直接分配它們。

和核心同步由 struct snd_pcm_mmap_statusstruct snd_pcm_mmap_control 表示的音訊流相關狀態資訊,是 Read/Write 和 mmap 兩種資料傳遞方式都需要的,因而 pcm_hw_mmap_status() 函式的呼叫是無條件的。

開啟音訊流之後,即可獲取音訊流狀態並查詢引數了,相關的 API 實現 (位於 tinyalsa/src/pcm.c) 如下:

/** Gets the buffer size of the PCM.
 * @param pcm A PCM handle.
 * @return The buffer size of the PCM.
 * @ingroup libtinyalsa-pcm
 */
unsigned int pcm_get_buffer_size(const struct pcm *pcm)
{
    return pcm->buffer_size;
}

/** Gets the channel count of the PCM.
 * @param pcm A PCM handle.
 * @return The channel count of the PCM.
 * @ingroup libtinyalsa-pcm
 */
unsigned int pcm_get_channels(const struct pcm *pcm)
{
    return pcm->config.channels;
}

/** Gets the PCM configuration.
 * @param pcm A PCM handle.
 * @return The PCM configuration.
 *  This function only returns NULL if
 *  @p pcm is NULL.
 * @ingroup libtinyalsa-pcm
 * */
const struct pcm_config * pcm_get_config(const struct pcm *pcm)
{
    if (pcm == NULL)
        return NULL;
    return &pcm->config;
}

/** Gets the rate of the PCM.
 * The rate is given in frames per second.
 * @param pcm A PCM handle.
 * @return The rate of the PCM.
 * @ingroup libtinyalsa-pcm
 */
unsigned int pcm_get_rate(const struct pcm *pcm)
{
    return pcm->config.rate;
}

/** Gets the format of the PCM.
 * @param pcm A PCM handle.
 * @return The format of the PCM.
 * @ingroup libtinyalsa-pcm
 */
enum pcm_format pcm_get_format(const struct pcm *pcm)
{
    return pcm->config.format;
}

/** Gets the file descriptor of the PCM.
 * Useful for extending functionality of the PCM when needed.
 * @param pcm A PCM handle.
 * @return The file descriptor of the PCM.
 * @ingroup libtinyalsa-pcm
 */
int pcm_get_file_descriptor(const struct pcm *pcm)
{
    return pcm->fd;
}

/** Gets the error message for the last error that occured.
 * If no error occured and this function is called, the results are undefined.
 * @param pcm A PCM handle.
 * @return The error message of the last error that occured.
 * @ingroup libtinyalsa-pcm
 */
const char* pcm_get_error(const struct pcm *pcm)
{
    return pcm->error;
}
 . . . . . .
/** Gets the subdevice on which the pcm has been opened.
 * @param pcm A PCM handle.
 * @return The subdevice on which the pcm has been opened */
unsigned int pcm_get_subdevice(const struct pcm *pcm)
{
    return pcm->subdevice;
}

/** Determines the number of bits occupied by a @ref pcm_format.
 * @param format A PCM format.
 * @return The number of bits associated with @p format
 * @ingroup libtinyalsa-pcm
 */
unsigned int pcm_format_to_bits(enum pcm_format format)
{
    switch (format) {
    case PCM_FORMAT_S32_LE:
    case PCM_FORMAT_S32_BE:
    case PCM_FORMAT_S24_LE:
    case PCM_FORMAT_S24_BE:
    case PCM_FORMAT_FLOAT_LE:
    case PCM_FORMAT_FLOAT_BE:
        return 32;
    case PCM_FORMAT_S24_3LE:
    case PCM_FORMAT_S24_3BE:
        return 24;
    default:
    case PCM_FORMAT_S16_LE:
    case PCM_FORMAT_S16_BE:
        return 16;
    case PCM_FORMAT_S8:
        return 8;
    };
}

/** Determines how many frames of a PCM can fit into a number of bytes.
 * @param pcm A PCM handle.
 * @param bytes The number of bytes.
 * @return The number of frames that may fit into @p bytes
 * @ingroup libtinyalsa-pcm
 */
unsigned int pcm_bytes_to_frames(const struct pcm *pcm, unsigned int bytes)
{
    return bytes / (pcm->config.channels *
        (pcm_format_to_bits(pcm->config.format) >> 3));
}

/** Determines how many bytes are occupied by a number of frames of a PCM.
 * @param pcm A PCM handle.
 * @param frames The number of frames of a PCM.
 * @return The bytes occupied by @p frames.
 * @ingroup libtinyalsa-pcm
 */
unsigned int pcm_frames_to_bytes(const struct pcm *pcm, unsigned int frames)
{
    return frames * pcm->config.channels *
        (pcm_format_to_bits(pcm->config.format) >> 3);
}
. . . . . .
/** Checks if a PCM file has been opened without error.
 * @param pcm A PCM handle.
 *  May be NULL.
 * @return If a PCM's file descriptor is not valid or the pointer is NULL, it returns zero.
 *  Otherwise, the function returns one.
 * @ingroup libtinyalsa-pcm
 */
int pcm_is_ready(const struct pcm *pcm)
{
    if (pcm != NULL) {
        return pcm->fd >= 0;
    }
    return 0;
}
. . . . . .
int pcm_get_poll_fd(struct pcm *pcm)
{
    return pcm->fd;
}
. . . . . .
/** Gets the delay of the PCM, in terms of frames.
 * @param pcm A PCM handle.
 * @returns On success, the delay of the PCM.
 *  On failure, a negative number.
 * @ingroup libtinyalsa-pcm
 */
long pcm_get_delay(struct pcm *pcm)
{
    if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_DELAY, &pcm->pcm_delay) < 0)
        return -1;

    return pcm->pcm_delay;
}

這些介面的實現中,除 pcm_get_delay() 使用了 ioctl()SNDRV_PCM_IOCTL_DELAY 命令外,其它的都沒有與 Linux 核心的互動。

Tinyalsa 資料傳遞介面的實現

tinyalsa 主要透過 pcm_writei()pcm_readi() 介面實現音訊資料的傳輸,這兩個函式實現 (位於 tinyalsa/src/pcm.c) 如下:

static int pcm_sync_ptr(struct pcm *pcm, int flags)
{
    if (pcm->sync_ptr == NULL) {
        /* status and control are mmapped */
        if (flags & SNDRV_PCM_SYNC_PTR_HWSYNC) {
            if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_HWSYNC) == -1) {
                return oops(pcm, errno, "failed to sync hardware pointer");
            }
        }
    } else {
        pcm->sync_ptr->flags = flags;
        if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_SYNC_PTR,
                            pcm->sync_ptr) < 0) {
            return oops(pcm, errno, "failed to sync mmap ptr");
        }
    }

    return 0;
}

int pcm_state(struct pcm *pcm)
{
    // Update the state only. Do not sync HW sync.
    int err = pcm_sync_ptr(pcm, SNDRV_PCM_SYNC_PTR_APPL | SNDRV_PCM_SYNC_PTR_AVAIL_MIN);
    if (err < 0)
        return err;

    return pcm->mmap_status->state;
}
 . . . . . .
/** Prepares a PCM, if it has not been prepared already.
 * @param pcm A PCM handle.
 * @return On success, zero; on failure, a negative number.
 * @ingroup libtinyalsa-pcm
 */
int pcm_prepare(struct pcm *pcm)
{
    if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_PREPARE) < 0)
        return oops(pcm, errno, "cannot prepare channel");

    /* get appl_ptr and avail_min from kernel */
    pcm_sync_ptr(pcm, SNDRV_PCM_SYNC_PTR_APPL|SNDRV_PCM_SYNC_PTR_AVAIL_MIN);

    return 0;
}
 . . . . . .
static int pcm_rw_transfer(struct pcm *pcm, void *data, unsigned int frames)
{
    int is_playback;

    struct snd_xferi transfer;
    int res;

    is_playback = !(pcm->flags & PCM_IN);

    transfer.buf = data;
    transfer.frames = frames;
    transfer.result = 0;

    res = pcm->ops->ioctl(pcm->data, is_playback
                          ? SNDRV_PCM_IOCTL_WRITEI_FRAMES
                          : SNDRV_PCM_IOCTL_READI_FRAMES, &transfer);

    return res == 0 ? (int) transfer.result : -1;
}

static int pcm_generic_transfer(struct pcm *pcm, void *data,
                                unsigned int frames)
{
    int res;

#if UINT_MAX > TINYALSA_FRAMES_MAX
    if (frames > TINYALSA_FRAMES_MAX)
        return -EINVAL;
#endif
    if (frames > INT_MAX)
        return -EINVAL;

    if (pcm_state(pcm) == PCM_STATE_SETUP && pcm_prepare(pcm) != 0) {
        return -1;
    }

again:

    if (pcm->flags & PCM_MMAP)
        res = pcm_mmap_transfer(pcm, data, frames);
    else
        res = pcm_rw_transfer(pcm, data, frames);

    if (res < 0) {
        switch (errno) {
        case EPIPE:
            pcm->xruns++;
            /* fallthrough */
        case ESTRPIPE:
            /*
             * Try to restart if we are allowed to do so.
             * Otherwise, return error.
             */
            if (pcm->flags & PCM_NORESTART || pcm_prepare(pcm))
                return -1;
            goto again;
        case EAGAIN:
            if (pcm->flags & PCM_NONBLOCK)
                return -1;
            /* fallthrough */
        default:
            return oops(pcm, errno, "cannot read/write stream data");
        }
    }

    return res;
}

/** Writes audio samples to PCM.
 * If the PCM has not been started, it is started in this function.
 * This function is only valid for PCMs opened with the @ref PCM_OUT flag.
 * @param pcm A PCM handle.
 * @param data The audio sample array
 * @param frame_count The number of frames occupied by the sample array.
 *  This value should not be greater than @ref TINYALSA_FRAMES_MAX
 *  or INT_MAX.
 * @return On success, this function returns the number of frames written; otherwise, a negative number.
 * @ingroup libtinyalsa-pcm
 */
int pcm_writei(struct pcm *pcm, const void *data, unsigned int frame_count)
{
    if (pcm->flags & PCM_IN)
        return -EINVAL;

    return pcm_generic_transfer(pcm, (void*) data, frame_count);
}

/** Reads audio samples from PCM.
 * If the PCM has not been started, it is started in this function.
 * This function is only valid for PCMs opened with the @ref PCM_IN flag.
 * @param pcm A PCM handle.
 * @param data The audio sample array
 * @param frame_count The number of frames occupied by the sample array.
 *  This value should not be greater than @ref TINYALSA_FRAMES_MAX
 *  or INT_MAX.
 * @return On success, this function returns the number of frames written; otherwise, a negative number.
 * @ingroup libtinyalsa-pcm
 */
int pcm_readi(struct pcm *pcm, void *data, unsigned int frame_count)
{
    if (!(pcm->flags & PCM_IN))
        return -EINVAL;

    return pcm_generic_transfer(pcm, data, frame_count);
}

pcm_writei()pcm_readi() 函式都透過相同的 pcm_generic_transfer() 函式來實現。在 pcm_generic_transfer() 函式中,會藉助於 pcm_state() 檢查 PCM 的狀態,如果狀態為 PCM_STATE_SETUP,則會為音訊流執行 Prepare。 之後,pcm_generic_transfer() 函式根據是否要以 mmap 的方式傳輸音訊資料,分別呼叫 pcm_mmap_transfer()pcm_rw_transfer() 函式來完成資料傳輸。

pcm_state() 先呼叫 pcm_sync_ptr() 與核心同步音訊流狀態資訊,這最終透過 ioctl()SNDRV_PCM_IOCTL_HWSYNCSNDRV_PCM_IOCTL_SYNC_PTR 命令實現,前者用於支援從核心空間向使用者空間記憶體對映 mmap struct snd_pcm_mmap_statusstruct snd_pcm_mmap_control 的硬體架構,後者用於不支援的。pcm_state() 與核心同步音訊流狀態資訊之後返回 pcm->mmap_status->state。Prepare 主要透過 ioctl()SNDRV_PCM_IOCTL_PREPARE 命令完成,之後 pcm_prepare() 還會再次與核心同步音訊流狀態資訊。在 Linux 核心中,ioctl()SNDRV_PCM_IOCTL_HWSYNCSNDRV_PCM_IOCTL_SYNC_PTR 命令實現 (位於 sound/core/pcm_native.c) 如下:

/* check and update PCM state; return 0 or a negative error
 * call this inside PCM lock
 */
static int do_pcm_hwsync(struct snd_pcm_substream *substream)
{
	switch (substream->runtime->status->state) {
	case SNDRV_PCM_STATE_DRAINING:
		if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
			return -EBADFD;
		fallthrough;
	case SNDRV_PCM_STATE_RUNNING:
		return snd_pcm_update_hw_ptr(substream);
	case SNDRV_PCM_STATE_PREPARED:
	case SNDRV_PCM_STATE_PAUSED:
		return 0;
	case SNDRV_PCM_STATE_SUSPENDED:
		return -ESTRPIPE;
	case SNDRV_PCM_STATE_XRUN:
		return -EPIPE;
	default:
		return -EBADFD;
	}
}
 . . . . . .
 static int snd_pcm_hwsync(struct snd_pcm_substream *substream)
{
	int err;

	snd_pcm_stream_lock_irq(substream);
	err = do_pcm_hwsync(substream);
	snd_pcm_stream_unlock_irq(substream);
	return err;
}
 . . . . . .
 static int snd_pcm_sync_ptr(struct snd_pcm_substream *substream,
			    struct snd_pcm_sync_ptr __user *_sync_ptr)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct snd_pcm_sync_ptr sync_ptr;
	volatile struct snd_pcm_mmap_status *status;
	volatile struct snd_pcm_mmap_control *control;
	int err;

	memset(&sync_ptr, 0, sizeof(sync_ptr));
	if (get_user(sync_ptr.flags, (unsigned __user *)&(_sync_ptr->flags)))
		return -EFAULT;
	if (copy_from_user(&sync_ptr.c.control, &(_sync_ptr->c.control), sizeof(struct snd_pcm_mmap_control)))
		return -EFAULT;	
	status = runtime->status;
	control = runtime->control;
	if (sync_ptr.flags & SNDRV_PCM_SYNC_PTR_HWSYNC) {
		err = snd_pcm_hwsync(substream);
		if (err < 0)
			return err;
	}
	snd_pcm_stream_lock_irq(substream);
	if (!(sync_ptr.flags & SNDRV_PCM_SYNC_PTR_APPL)) {
		err = pcm_lib_apply_appl_ptr(substream,
					     sync_ptr.c.control.appl_ptr);
		if (err < 0) {
			snd_pcm_stream_unlock_irq(substream);
			return err;
		}
	} else {
		sync_ptr.c.control.appl_ptr = control->appl_ptr;
	}
	if (!(sync_ptr.flags & SNDRV_PCM_SYNC_PTR_AVAIL_MIN))
		control->avail_min = sync_ptr.c.control.avail_min;
	else
		sync_ptr.c.control.avail_min = control->avail_min;
	sync_ptr.s.status.state = status->state;
	sync_ptr.s.status.hw_ptr = status->hw_ptr;
	sync_ptr.s.status.tstamp = status->tstamp;
	sync_ptr.s.status.suspended_state = status->suspended_state;
	sync_ptr.s.status.audio_tstamp = status->audio_tstamp;
	snd_pcm_stream_unlock_irq(substream);
	if (copy_to_user(_sync_ptr, &sync_ptr, sizeof(sync_ptr)))
		return -EFAULT;
	return 0;
}
 . . . . . .
static int snd_pcm_common_ioctl(struct file *file,
				 struct snd_pcm_substream *substream,
				 unsigned int cmd, void __user *arg)
{
	struct snd_pcm_file *pcm_file = file->private_data;
	int res;

	if (PCM_RUNTIME_CHECK(substream))
		return -ENXIO;

	res = snd_power_wait(substream->pcm->card, SNDRV_CTL_POWER_D0);
	if (res < 0)
		return res;

	switch (cmd) {
	case SNDRV_PCM_IOCTL_PVERSION:
		return put_user(SNDRV_PCM_VERSION, (int __user *)arg) ? -EFAULT : 0;
 . . . . . .
	case SNDRV_PCM_IOCTL_XRUN:
		return snd_pcm_xrun(substream);
	case SNDRV_PCM_IOCTL_HWSYNC:
		return snd_pcm_hwsync(substream);
	case SNDRV_PCM_IOCTL_DELAY:
	{
		snd_pcm_sframes_t delay;
		snd_pcm_sframes_t __user *res = arg;
		int err;

		err = snd_pcm_delay(substream, &delay);
		if (err)
			return err;
		if (put_user(delay, res))
			return -EFAULT;
		return 0;
	}
	case __SNDRV_PCM_IOCTL_SYNC_PTR32:
		return snd_pcm_ioctl_sync_ptr_compat(substream, arg);
	case __SNDRV_PCM_IOCTL_SYNC_PTR64:
		return snd_pcm_sync_ptr(substream, arg);

pcm_generic_transfer() 函式透過 pcm_rw_transfer() 函式以 Read/Write 的方式向核心傳遞音訊資料時,簡單地透過 ioctl()SNDRV_PCM_IOCTL_WRITEI_FRAMESSNDRV_PCM_IOCTL_READI_FRAMES 命令實現。在核心中,音訊資料的詳細流動過程可以參考 Linux 核心音訊資料傳遞主要流程 (上)Linux 核心音訊資料傳遞主要流程 (下)

以 mmap 的方式傳輸音訊資料時,與以 Read/Write 的方式傳輸音訊資料的詳細過程不會有特別大的區別,但原本在核心空間執行的許多操作,會改為在使用者空間執行。pcm_mmap_transfer() 函式的定義 (位於 tinyalsa/src/pcm.c) 如下:

/** Starts a PCM.
 * @param pcm A PCM handle.
 * @return On success, zero; on failure, a negative number.
 * @ingroup libtinyalsa-pcm
 */
int pcm_start(struct pcm *pcm)
{
    if (pcm_state(pcm) == PCM_STATE_SETUP && pcm_prepare(pcm) != 0) {
        return -1;
    }

    /* set appl_ptr and avail_min in kernel */
    if (pcm_sync_ptr(pcm, 0) < 0)
        return -1;

    if (pcm->mmap_status->state != PCM_STATE_RUNNING) {
        if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_START) < 0)
            return oops(pcm, errno, "cannot start channel");
    }

    return 0;
}

/** Stops a PCM.
 * @param pcm A PCM handle.
 * @return On success, zero; on failure, a negative number.
 * @ingroup libtinyalsa-pcm
 */
int pcm_stop(struct pcm *pcm)
{
    if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_DROP) < 0)
        return oops(pcm, errno, "cannot stop channel");

    return 0;
}

static inline long pcm_mmap_playback_avail(struct pcm *pcm)
{
    long avail = pcm->mmap_status->hw_ptr + (unsigned long) pcm->buffer_size -
            pcm->mmap_control->appl_ptr;

    if (avail < 0) {
        avail += pcm->boundary;
    } else if ((unsigned long) avail >= pcm->boundary) {
        avail -= pcm->boundary;
    }

    return avail;
}

static inline long pcm_mmap_capture_avail(struct pcm *pcm)
{
    long avail = pcm->mmap_status->hw_ptr - pcm->mmap_control->appl_ptr;
    if (avail < 0) {
        avail += pcm->boundary;
    }

    return avail;
}

int pcm_mmap_avail(struct pcm *pcm)
{
    pcm_sync_ptr(pcm, SNDRV_PCM_SYNC_PTR_HWSYNC);
    if (pcm->flags & PCM_IN) {
        return (int) pcm_mmap_capture_avail(pcm);
    } else {
        return (int) pcm_mmap_playback_avail(pcm);
    }
}

static void pcm_mmap_appl_forward(struct pcm *pcm, int frames)
{
    unsigned long appl_ptr = pcm->mmap_control->appl_ptr;
    appl_ptr += frames;

    /* check for boundary wrap */
    if (appl_ptr >= pcm->boundary) {
        appl_ptr -= pcm->boundary;
    }
    pcm->mmap_control->appl_ptr = appl_ptr;
}

int pcm_mmap_begin(struct pcm *pcm, void **areas, unsigned int *offset,
                   unsigned int *frames)
{
    unsigned int continuous, copy_frames, avail;

    /* return the mmap buffer */
    *areas = pcm->mmap_buffer;

    /* and the application offset in frames */
    *offset = pcm->mmap_control->appl_ptr % pcm->buffer_size;

    avail = pcm_mmap_avail(pcm);
    if (avail > pcm->buffer_size)
        avail = pcm->buffer_size;
    continuous = pcm->buffer_size - *offset;

    /* we can only copy frames if the are available and continuos */
    copy_frames = *frames;
    if (copy_frames > avail)
        copy_frames = avail;
    if (copy_frames > continuous)
        copy_frames = continuous;
    *frames = copy_frames;

    return 0;
}

static int pcm_areas_copy(struct pcm *pcm, unsigned int pcm_offset,
                          char *buf, unsigned int src_offset,
                          unsigned int frames)
{
    int size_bytes = pcm_frames_to_bytes(pcm, frames);
    int pcm_offset_bytes = pcm_frames_to_bytes(pcm, pcm_offset);
    int src_offset_bytes = pcm_frames_to_bytes(pcm, src_offset);

    /* interleaved only atm */
    if (pcm->flags & PCM_IN)
        memcpy(buf + src_offset_bytes,
               (char*)pcm->mmap_buffer + pcm_offset_bytes,
               size_bytes);
    else
        memcpy((char*)pcm->mmap_buffer + pcm_offset_bytes,
               buf + src_offset_bytes,
               size_bytes);
    return 0;
}

int pcm_mmap_commit(struct pcm *pcm, unsigned int offset, unsigned int frames)
{
    int ret;

    /* not used */
    (void) offset;

    /* update the application pointer in userspace and kernel */
    pcm_mmap_appl_forward(pcm, frames);
    ret = pcm_sync_ptr(pcm, 0);
    if (ret != 0){
        printf("%d\n", ret);
        return ret;
    }

    return frames;
}

static int pcm_mmap_transfer_areas(struct pcm *pcm, char *buf,
                                unsigned int offset, unsigned int size)
{
    void *pcm_areas;
    int commit;
    unsigned int pcm_offset, frames, count = 0;

    while (pcm_mmap_avail(pcm) && size) {
        frames = size;
        pcm_mmap_begin(pcm, &pcm_areas, &pcm_offset, &frames);
        pcm_areas_copy(pcm, pcm_offset, buf, offset, frames);
        commit = pcm_mmap_commit(pcm, pcm_offset, frames);
        if (commit < 0) {
            oops(pcm, commit, "failed to commit %d frames\n", frames);
            return commit;
        }

        offset += commit;
        count += commit;
        size -= commit;
    }
    return count;
}
 . . . . . .
/** Waits for frames to be available for read or write operations.
 * @param pcm A PCM handle.
 * @param timeout The maximum amount of time to wait for, in terms of milliseconds.
 * @returns If frames became available, one is returned.
 *  If a timeout occured, zero is returned.
 *  If an error occured, a negative number is returned.
 * @ingroup libtinyalsa-pcm
 */
int pcm_wait(struct pcm *pcm, int timeout)
{
    struct pollfd pfd;
    int err;

    pfd.fd = pcm->fd;
    pfd.events = POLLIN | POLLOUT | POLLERR | POLLNVAL;

    do {
        /* let's wait for avail or timeout */
        err = pcm->ops->poll(pcm->data, &pfd, 1, timeout);
        if (err < 0)
            return -errno;

        /* timeout ? */
        if (err == 0)
            return 0;

        /* have we been interrupted ? */
        if (errno == -EINTR)
            continue;

        /* check for any errors */
        if (pfd.revents & (POLLERR | POLLNVAL)) {
            switch (pcm_state(pcm)) {
            case PCM_STATE_XRUN:
                return -EPIPE;
            case PCM_STATE_SUSPENDED:
                return -ESTRPIPE;
            case PCM_STATE_DISCONNECTED:
                return -ENODEV;
            default:
                return -EIO;
            }
        }
    /* poll again if fd not ready for IO */
    } while (!(pfd.revents & (POLLIN | POLLOUT)));

    return 1;
}

/*
 * Transfer data to/from mmapped buffer. This imitates the
 * behavior of read/write system calls.
 *
 * However, this doesn't seems to offer any advantage over
 * the read/write syscalls. Should it be removed?
 */
static int pcm_mmap_transfer(struct pcm *pcm, void *buffer, unsigned int frames)
{
    int is_playback;

    int state;
    unsigned int avail;
    unsigned int user_offset = 0;

    int err;
    int transferred_frames;

    is_playback = !(pcm->flags & PCM_IN);

    if (frames == 0)
        return 0;

    /* update hardware pointer and get state */
    err = pcm_sync_ptr(pcm, SNDRV_PCM_SYNC_PTR_HWSYNC |
                            SNDRV_PCM_SYNC_PTR_APPL |
                            SNDRV_PCM_SYNC_PTR_AVAIL_MIN);
    if (err == -1)
        return -1;
    state = pcm->mmap_status->state;

    /*
     * If frames < start_threshold, wait indefinitely.
     * Another thread may start capture
     */
    if (!is_playback && state == PCM_STATE_PREPARED &&
            frames >= pcm->config.start_threshold) {
        if (pcm_start(pcm) < 0) {
            return -1;
        }
    }

    while (frames) {
        avail = pcm_mmap_avail(pcm);

        if (!avail) {
            if (pcm->flags & PCM_NONBLOCK) {
                errno = EAGAIN;
                break;
            }

            /* wait for interrupt */
            err = pcm_wait(pcm, -1);
            if (err < 0) {
                errno = -err;
                break;
            }
        }

        transferred_frames = pcm_mmap_transfer_areas(pcm, buffer, user_offset, frames);
        if (transferred_frames < 0) {
            break;
        }

        user_offset += transferred_frames;
        frames -= transferred_frames;

        /* start playback if written >= start_threshold */
        if (is_playback && state == PCM_STATE_PREPARED &&
                pcm->buffer_size - avail >= pcm->config.start_threshold) {
            if (pcm_start(pcm) < 0) {
                break;
            }
        }
    }

    return user_offset ? (int) user_offset : -1;
}

pcm_mmap_transfer() 函式的執行過程如下:

  1. 呼叫 pcm_sync_ptr() 與核心同步音訊流狀態資訊。
  2. 如果音訊流用於錄製/採集音訊資料,音訊流的狀態為 PCM_STATE_PREPARED,且請求的音訊幀數超過閾值,則呼叫 pcm_start(pcm) 函式起動音訊流。在 pcm_start(pcm) 函式中,會再次同步並檢查音訊流狀態資訊,如果狀態為 PCM_STATE_SETUP,則會為音訊流執行 Prepare,這裡與 pcm_generic_transfer() 的操作有重複?之後,pcm_start(pcm) 函式透過 ioctl()SNDRV_PCM_IOCTL_START 命令起動音訊流,這最終將呼叫音訊硬體驅動的 trigger 操作。
  3. 透過一個迴圈傳遞請求的音訊幀:
    (1). 等待 DMA 緩衝區中有足夠的可用空間。DMA 緩衝區的可用空間透過 hw_ptrappl_ptr 等狀態計算,計算方法與 Linux 核心中的類似方法沒有區別。關於這兩個狀態的含義,可以參考 Linux 核心音訊資料傳遞主要流程 (上)Linux 核心音訊資料傳遞主要流程 (下)
    (2). 呼叫 pcm_mmap_transfer_areas() 函式傳遞一塊音訊資料。
    (3). 音訊流用於播放音訊資料,音訊流的狀態為 PCM_STATE_PREPARED,且已經傳遞的音訊幀數超過閾值,則呼叫 pcm_start(pcm) 函式起動音訊流。

pcm_mmap_transfer_areas() 函式中,計算要寫入或者讀出 DMA 緩衝區中音訊資料的具體位值,執行音訊資料的記憶體複製,更新 appl_ptr 指標,並與核心同步音訊流狀態資訊。

pcm_mmap_transfer() 函式的執行過程與核心中 __snd_pcm_lib_xfer() 函式的執行過程十分相似。

不難看出,Tinyalsa 音訊流生命週期管理 API 中的許多的使用場景,不是在應用程式開發中,而是在支援實現以 mmap 方式向核心傳遞音訊資料,這些 API 包括如下這些:

int pcm_mmap_begin(struct pcm *pcm, void **areas, unsigned int *offset, unsigned int *frames);

int pcm_mmap_commit(struct pcm *pcm, unsigned int offset, unsigned int frames);

int pcm_mmap_avail(struct pcm *pcm);
 . . . . . .
int pcm_prepare(struct pcm *pcm);

int pcm_start(struct pcm *pcm);

int pcm_stop(struct pcm *pcm);

int pcm_wait(struct pcm *pcm, int timeout);

Tinyalsa PCM API 實現大體如此。

Done.

相關文章