RT-Thread Audio Virtual Driver

Created at 2020-12-10 22:22:25

Forwarded from RT-Thread Community Developer liu2guang

Today I'm going to introduce using audio virtual drivers to analyze driver programming.

1. How to use Audio Driver

Before writing a driver, we'll first need to know how to test the driver? So let's start learning how to play the music.

#include 
#include 
#include 

#define BUFSZ   1024
#define SOUND_DEVICE_NAME    "sound0"    /* Audio Device Name */
static rt_device_t snd_dev;              /* Audio Device Handle */

struct RIFF_HEADER_DEF
{
    char riff_id[4];     // 'R','I','F','F'
    uint32_t riff_size;
    char riff_format[4]; // 'W','A','V','E'
};

struct WAVE_FORMAT_DEF
{
    uint16_t FormatTag;
    uint16_t Channels;
    uint32_t SamplesPerSec;
    uint32_t AvgBytesPerSec;
    uint16_t BlockAlign;
    uint16_t BitsPerSample;
};

struct FMT_BLOCK_DEF
{
    char fmt_id[4];    // 'f','m','t',' '
    uint32_t fmt_size;
    struct WAVE_FORMAT_DEF wav_format;
};

struct DATA_BLOCK_DEF
{
    char data_id[4];     // 'R','I','F','F'
    uint32_t data_size;
};

struct wav_info
{
    struct RIFF_HEADER_DEF header;
    struct FMT_BLOCK_DEF   fmt_block;
    struct DATA_BLOCK_DEF  data_block;
};

int wavplay_sample(int argc, char **argv)
{
    int fd = -1;
    uint8_t *buffer = NULL;
    struct wav_info *info = NULL;
    struct rt_audio_caps caps = {0};

    if (argc != 2)
    {
        rt_kprintf("Usage:\n");
        rt_kprintf("wavplay_sample song.wav\n");
        return 0;
    }

    fd = open(argv[1], O_WRONLY);
    if (fd < 0)
    {
        rt_kprintf("open file failed!\n");
        goto __exit;
    }

    buffer = rt_malloc(BUFSZ);
    if (buffer == RT_NULL)
        goto __exit;

    info = (struct wav_info *) rt_malloc(sizeof * info);
    if (info == RT_NULL)
        goto __exit;

    if (read(fd, &(info->header), sizeof(struct RIFF_HEADER_DEF)) <= 0)
        goto __exit;
    if (read(fd, &(info->fmt_block),  sizeof(struct FMT_BLOCK_DEF)) <= 0)
        goto __exit;
    if (read(fd, &(info->data_block), sizeof(struct DATA_BLOCK_DEF)) <= 0)
        goto __exit;

    rt_kprintf("wav information:\n");
    rt_kprintf("samplerate %d\n", info->fmt_block.wav_format.SamplesPerSec);
    rt_kprintf("channel %d\n", info->fmt_block.wav_format.Channels);

 /* Find the Audio device by device name and get the device handle */
    snd_dev = rt_device_find(SOUND_DEVICE_NAME);

    /* Turn on the Audio playback device in a write-only manner */
    rt_device_open(snd_dev, RT_DEVICE_OFLAG_WRONLY);

    /* Set audio parameter information such as sample rate, channel, sample number of bits, and so on */
    caps.main_type               = AUDIO_TYPE_OUTPUT;                           /* Output type(Player device)*/
    caps.sub_type                = AUDIO_DSP_PARAM;                             /* Set all audio parameter information */
    caps.udata.config.samplerate = info->fmt_block.wav_format.SamplesPerSec;    /* sample rate */
    caps.udata.config.channels   = info->fmt_block.wav_format.Channels;         /* Sample channel */
    caps.udata.config.samplebits = 16;                                          /* Sample bit */
    rt_device_control(snd_dev, AUDIO_CTL_CONFIGURE, &caps);

    while (1)
    {
        int length;

        /* Read the audio data for the wav file in file system */
        length = read(fd, buffer, BUFSZ);

        if (length <= 0)
            break;

        /* Write audio data to the Audio device */
        rt_device_write(snd_dev, 0, buffer, length);
    }

    /* Turn of Audio device */
    rt_device_close(snd_dev);

__exit:

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

    if (buffer)
        rt_free(buffer);

    if (info)
        rt_free(info);

    return 0;
}

MSH_CMD_EXPORT(wavplay_sample,  play wav file);

The main steps for playing the audio data are as follows:
Let's analyze this code, starting with wavplay_sample function:

(*)#define SOUND_DEVICE_NAME "sound0" First define the play driver
(*)fd = open(argv[1], O_WRONLY); Used for opening audio file
(*)snd_dev = rt_device_find(SOUND_DEVICE_NAME); First look for the Audio device to get the device handle
(*)rt_device_open(snd_dev, RT_DEVICE_OFLAG_WRONLY); Turn on the Audio device in a write-only manner, which is the sound device
(*)rt_device_control(snd_dev, AUDIO_CTL_CONFIGURE, &caps); Set audio parameter information (sample rate, channel, etc.)
(*)length = read(fd, buffer, BUFSZ); Decode the data for the audio file
(*)rt_device_write(snd_dev, 0, buffer, length); Write in audio file data
(*)rt_device_close(snd_dev); Play is complete, turn off the device

Add those codes to your code compile and download it, then it starts playing music, of course, only wwav-format audio. Next, let's add the driver.

2. Write Audio Virtual Driver

In last post, I have analyzed the audio-driven frame, and here we continue to introduce how to implement the driver framework.


#include "drv_sound.h" 
#include "drv_tina.h" 
#include "drivers/audio.h"

#define DBG_TAG "drv_sound"
#define DBG_LVL DBG_LOG
#define DBG_COLOR
#include 

#define TX_DMA_FIFO_SIZE (2048)

struct temp_sound
{
    struct rt_audio_device device; 
    struct rt_audio_configure replay_config;
    int volume;
    rt_uint8_t *tx_fifo;
};

static rt_err_t getcaps(struct rt_audio_device *audio, struct rt_audio_caps *caps)
{
    struct temp_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL); 
    sound = (struct temp_sound *)audio->parent.user_data; (void)sound; 

    return RT_EOK; 
}

static rt_err_t configure(struct rt_audio_device *audio, struct rt_audio_caps *caps)
{
    struct temp_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL); 
    sound = (struct temp_sound *)audio->parent.user_data; (void)sound; 

    return RT_EOK; 
}

static rt_err_t init(struct rt_audio_device *audio)
{
    struct temp_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL); 
    sound = (struct temp_sound *)audio->parent.user_data; (void)sound; 

    return RT_EOK; 
}

static rt_err_t start(struct rt_audio_device *audio, int stream)
{
    struct temp_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL); 
    sound = (struct temp_sound *)audio->parent.user_data; (void)sound; 

    return RT_EOK;
}

static rt_err_t stop(struct rt_audio_device *audio, int stream)
{
    struct temp_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL); 
    sound = (struct temp_sound *)audio->parent.user_data; (void)sound;  

    return RT_EOK;
}

rt_size_t transmit(struct rt_audio_device *audio, const void *writeBuf, void *readBuf, rt_size_t size)
{
    struct temp_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL); 
    sound = (struct temp_sound *)audio->parent.user_data; (void)sound; 

    return size; 
}

static void buffer_info(struct rt_audio_device *audio, struct rt_audio_buf_info *info)
{
    struct temp_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL); 
    sound = (struct temp_sound *)audio->parent.user_data;

    /**
     *               TX_FIFO
     * +----------------+----------------+
     * |     block1     |     block2     |
     * +----------------+----------------+
     *  \  block_size  /
     */
    info->buffer      = sound->tx_fifo;
    info->total_size  = TX_DMA_FIFO_SIZE;
    info->block_size  = TX_DMA_FIFO_SIZE / 2;
    info->block_count = 2;
}

static struct rt_audio_ops ops =
{
    .getcaps     = getcaps,
    .configure   = configure,
    .init        = init,
    .start       = start,
    .stop        = stop,
    .transmit    = transmit, 
    .buffer_info = buffer_info,
};

static int rt_hw_sound_init(void)
{
    rt_uint8_t *tx_fifo = RT_NULL; 
    static struct temp_sound sound = {0};

    /* Assign DMA, Move buffer */ 
    tx_fifo = rt_calloc(1, TX_DMA_FIFO_SIZE); 
    if(tx_fifo == RT_NULL)
    {
        return -RT_ENOMEM;
    }

    sound.tx_fifo = tx_fifo;

    /* Register the sound card playing driver */
    sound.device.ops = &ops;
    rt_audio_register(&sound.device, "sound0", RT_DEVICE_FLAG_WRONLY, &sound);

    return RT_EOK; 
}
INIT_DEVICE_EXPORT(rt_hw_sound_init);

Above is the entire audio-driver framework but without hardware-related code, add it to the project and use the list_device command in the shell can see the sound0 driver. If we combine the code in the last post, it is available to play wav audio, but since there is no hardware-related code so it won't play the sound now.

Let's analyze the code first:

  1. rt_hw_sound_init function is the driver's entry point for registering the audio framework, where we assign the buffer that audio dma needs and register the implemented audio-related ops into the sound0 audio device. After calling this function, you can see sound0 driver in list_device.
  2. You might wonder how those functions work in struct rt_audio_ops ops structure and how to program with them. I'll give an explanation.
  3. Since audio-related configurations and settings have more parameters, here we have divided the configuration and parameters accessing into two ops functions, getcaps and configure. getcaps is used to obtain audio features, such as hardware channel number, current sample rate, sample depth, volume, configure function is used to set the number of channels, current sample rate, sample depth, volume.
  4. Init ops function, mainly used to implement the I2C chips (audio data communication with external codec) I2C (control the sample rate of external codec, mute PIN), then configure dma and dma middle end, and control the gpio pin of mute.
  5. start ops function is mainly used for enabling dma and disabling mute.
  6. stop ops function is mainly used for disabling dma and enabling mute.
  7. transmit is mainly used to trigger the handling of data, why it is used to trigger the handling? Because the upper-level code writes the audio data to the audio device will not be written directly to the driver, so it won't directly call transmit function used to handle buffer data to the dma buffer, then when transmit will be call? We can see that the audio framework has a function rt_audio_tx_complete(&sound->device) that used to notify the handling, let's analyze this:

    • The upper-level app calls the rt_device_write function to write data to the audio, and the framework layer caches the written data to an internal buffer (a node in the static memory pool, configured to 2KB data by default)
    • Writing more than 2KB of data to the upper layer blocks the wait
    • First time to use rt_device_write will call the start ops function to enable dma handling, and the dma interrupt in i2s (in the half-empty and full interrupt service functions) will call the rt_audio_tx_complete function
    • rt_audio_tx_complete indicates that dma data has been handled and needs to be populated with the next audio data, which calls transmit ops, but if it is the data that i2s dma loops, dma will automatically handle the data, so there is no need to use transmit ops to copy the data from the audio buffer into the driver dma, so what's the use of transmit? First of all, on a chip that doesn't have a dma loop handling, we can use this function to trigger the next dma handling or cpu handling. Second, it can be used for cache!
  8. buffer_info is used to tell how big and how many blocks your audio driver buffer is in the audio framework, so that the upper layer knows how much byte data to give you when it passes through the transmit ops function!

After reading the above analysis you get to know its basic principles and writing methods. But this driver still can't make a sound, so we have to find a way to achieve a driver, because the my hardware is special so I find a specific solution.

To cache the audio into the file, and here we make a virtual audio driver , this driver won't make a sound, but it saves the data into pcm file. The parameters of pcm are the same as the wav so we can play them on our computer even if the hardware is different.

3. Audio Virtual Driver

/*
 * File: drv_virtual.c
 * 
 * COPYRIGHT (C) 2012-2019, Shanghai Real-Thread Technology Co., Ltd
 */

#include "drv_virtual.h" 
#include "dfs.h"
#include "dfs_posix.h"

#define DBG_TAG "drv_virtual"
#define DBG_LVL DBG_LOG
#define DBG_COLOR
#include 

#define TX_DMA_FIFO_SIZE (2048)

struct tina_sound
{
    struct rt_audio_device device; 
    struct rt_audio_configure replay_config;
    int volume;
    rt_uint8_t *tx_fifo;
    int fd; 
    struct rt_thread thread; 
    int endflag; 
};

static rt_err_t getcaps(struct rt_audio_device *audio, struct rt_audio_caps *caps)
{
    rt_err_t ret = RT_EOK;
    struct tina_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL); 
    sound = (struct tina_sound *)audio->parent.user_data; (void)sound; 

    switch(caps->main_type)
    {
    case AUDIO_TYPE_QUERY: 
    {
        switch (caps->sub_type)
        {
        case AUDIO_TYPE_QUERY:
            caps->udata.mask = AUDIO_TYPE_OUTPUT | AUDIO_TYPE_MIXER;
            break;

        default:
            ret = -RT_ERROR;
            break;
        }

        break;
    }

    case AUDIO_TYPE_OUTPUT: 
    {
        switch(caps->sub_type)
        {
        case AUDIO_DSP_PARAM:
            caps->udata.config.channels   = sound->replay_config.channels;
            caps->udata.config.samplebits = sound->replay_config.samplebits;
            caps->udata.config.samplerate = sound->replay_config.samplerate;
            break;

        default:
            ret = -RT_ERROR;
            break;
        }

        break;
    }

    case AUDIO_TYPE_MIXER: 
    {
        switch (caps->sub_type)
        {
        case AUDIO_MIXER_QUERY:
            caps->udata.mask = AUDIO_MIXER_VOLUME | AUDIO_MIXER_LINE;
            break;

        case AUDIO_MIXER_VOLUME:
            caps->udata.value = sound->volume;
            break;

        case AUDIO_MIXER_LINE:
            break;

        default:
            ret = -RT_ERROR;
            break;
        }

        break;
    }

    default:
        ret = -RT_ERROR;
        break;
    }

    return ret; 
}

static rt_err_t configure(struct rt_audio_device *audio, struct rt_audio_caps *caps)
{
    rt_err_t ret = RT_EOK;
    struct tina_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL); 
    sound = (struct tina_sound *)audio->parent.user_data; (void)sound; 

    switch(caps->main_type)
    {
    case AUDIO_TYPE_MIXER:
    {
        switch(caps->sub_type)
        {
        case AUDIO_MIXER_VOLUME:
        {
            int volume = caps->udata.value;
            sound->volume = volume;
            break;
        }

        default:
            ret = -RT_ERROR;
            break;
        }

        break;
    }

    case AUDIO_TYPE_OUTPUT:
    {
        switch(caps->sub_type)
        {
        case AUDIO_DSP_PARAM:
        {
            int samplerate;

            samplerate = caps->udata.config.samplerate;
            sound->replay_config.samplerate = samplerate;
            LOG_I("set samplerate = %d", samplerate);
            break;
        }

        case AUDIO_DSP_SAMPLERATE:
        {
            int samplerate;

            samplerate = caps->udata.config.samplerate;
            sound->replay_config.samplerate = samplerate;
            LOG_I("set samplerate = %d", samplerate);
            break;
        }

        case AUDIO_DSP_CHANNELS:
        {
            break;
        }

        default:
            break;
        }

        break;
    }

    default:
        break;
    }

    return ret; 
}

static void virtualplay(void *p)
{
    struct tina_sound *sound = (struct tina_sound *)p; (void)sound; 

    while(1)
    {
        /* tick = TX_DMA_FIFO_SIZE/2 * 1000ms / 44100 / 4 ≈ 5.8 */ 
        rt_thread_mdelay(6); 
        rt_audio_tx_complete(&sound->device); 

        if(sound->endflag == 1)
        {
            break; 
        }
    }
}

static int thread_stack[1024] = {0}; 

static rt_err_t init(struct rt_audio_device *audio)
{
    struct tina_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL); 
    sound = (struct tina_sound *)audio->parent.user_data; (void)sound; 

    LOG_I("sound init"); 

    return RT_EOK; 
}

static rt_err_t start(struct rt_audio_device *audio, int stream)
{
    struct tina_sound *sound = RT_NULL;
    rt_err_t ret = RT_EOK; 

    RT_ASSERT(audio != RT_NULL); 
    sound = (struct tina_sound *)audio->parent.user_data; (void)sound; 

    LOG_I("sound start"); 

    ret = rt_thread_init(&sound->thread, "virtual", virtualplay, sound, &thread_stack, sizeof(thread_stack), 1, 10); 
    if(ret != RT_EOK)
    {
        LOG_E("virtual play thread init failed"); 
        return (-RT_ERROR); 
    }
    rt_thread_startup(&sound->thread); 

    sound->endflag = 0; 

    sound->fd = open("/tmp/virtual.pcm", O_CREAT | O_RDWR, 0666); 

    return RT_EOK;
}

static rt_err_t stop(struct rt_audio_device *audio, int stream)
{
    struct tina_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL); 
    sound = (struct tina_sound *)audio->parent.user_data; (void)sound; 

    LOG_I("sound stop");  

    sound->endflag = 1; 

    close(sound->fd);
    sound->fd = -1; 

    return RT_EOK;
}

rt_size_t transmit(struct rt_audio_device *audio, const void *wb, void *rb, rt_size_t size)
{
    struct tina_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL); 
    sound = (struct tina_sound *)audio->parent.user_data; (void)sound; 

    return write(sound->fd, wb, size); 
}

static void buffer_info(struct rt_audio_device *audio, struct rt_audio_buf_info *info)
{
    struct tina_sound *sound = RT_NULL;

    RT_ASSERT(audio != RT_NULL); 
    sound = (struct tina_sound *)audio->parent.user_data;

    /**
     *               TX_FIFO
     * +----------------+----------------+
     * |     block1     |     block2     |
     * +----------------+----------------+
     *  \  block_size  /
     */
    info->buffer      = sound->tx_fifo;
    info->total_size  = TX_DMA_FIFO_SIZE;
    info->block_size  = TX_DMA_FIFO_SIZE / 2;
    info->block_count = 2;
}

static struct rt_audio_ops ops =
{
    .getcaps     = getcaps,
    .configure   = configure,
    .init        = init,
    .start       = start,
    .stop        = stop,
    .transmit    = transmit, 
    .buffer_info = buffer_info,
};

static int rt_hw_sound_init(void)
{
    rt_uint8_t *tx_fifo = RT_NULL; 
    static struct tina_sound sound = {0};

    /* Assign DMA move buffer */ 
    tx_fifo = rt_calloc(1, TX_DMA_FIFO_SIZE); 
    if(tx_fifo == RT_NULL)
    {
        return -RT_ENOMEM;
    }

    sound.tx_fifo = tx_fifo;

    /* Configure DSP parameter */ 
    {
        sound.replay_config.samplerate = 44100; 
        sound.replay_config.channels   = 2; 
        sound.replay_config.samplebits = 16; 
        sound.volume                   = 60; 
        sound.fd                       = -1; 
        sound.endflag                  = 0;
    }

    /* Register the sound card playing driver  */
    sound.device.ops = &ops;
    rt_audio_register(&sound.device, "sound0", RT_DEVICE_FLAG_WRONLY, &sound);

    return RT_EOK; 
}
INIT_DEVICE_EXPORT(rt_hw_sound_init);

Based on the second part of the analysis, you might understand the code, this driver is taking the advantage of using the virtualplay thread to simulate i2s dma for automatically handling data.

The final file is saved to /tmp/virtual.pcm, note that the virtualplay function has 6ms delay in order to simulate the time it takes to handle (play) 1KB of data in the dma buffer, tick = TX_DMA_FIFO_SIZE/2 * 1000ms / 44100 / 4 ≈ 5.8ms. So we have to write the file fast, I was using ramfs to implement the file system, it takes time to write sd card or flash, so I would recommended you to use ramfs, and make sure that the size of ramfs has more than 20Mbytes. Also, you can give a try with QEMU.

More

Follower
0
Views
71
0 Answer
There is no answer, come and add the answer

Write Your Answer

Log in to publish your answer.,Click here to log in.

Create
Post

Share