【问题标题】:Ring buffer in C tail pointer issue (for audio streaming)C 尾指针问题中的环形缓冲区(用于音频流)
【发布时间】:2012-05-16 03:33:02
【问题描述】:

我目前正在编写一个用于音频流的嵌入式应用程序。嵌入式应用程序将接收通过 wifi 发送的音频数据包,缓冲数据包,然后将音频数据发送到解码器芯片。我编写了一个环形缓冲区实现(在 stackoverflow 上的一篇优秀帖子的帮助下),但有时会出现一些奇怪的行为。在音频方面,我在播放过程中听到歌曲的某些部分重复。我发现这是由于尾指针被两次设置到缓冲区的开头。

(在我的实现中,头指针标记了有效数据的结尾,而尾指针标记了有效数据的开头)

例如,我看到:

  • 头指针复位到缓冲区的开始
  • 尾部指针重置为缓冲区的开头
  • 尾部指针重置为缓冲区的开头
  • 头指针复位到缓冲区的开始

这里是环形缓冲区的实现:

typedef struct ring_buffer
{
    UINT8 *buffer;      /* data buffer */
    UINT8 *buffer_end;  /* end of data buffer */
    size_t capacity;    /* maximum number of mp3Bytes in the buffer */
    size_t count;       /* number of mp3Bytes in the buffer */
    size_t typesize;    /* size of each mp3Byte in the buffer */
    UINT8 *head;        /* ring buffer head pointer */
    UINT8 *tail;        /* ring buffer tail pointer */
} ring_buffer;

PUBLIC UINT8
AppAudioStream_RingBufInit(ring_buffer *rb, size_t capacity, size_t typesize)
{   
    /* alloc buffer of size capacity * typesize */
    rb->buffer = malloc(capacity * typesize);
    if(rb->buffer == NULL)
    {
        printf("ring buffer init fail\r\n");
        return RING_BUF_INIT_FAIL;
    }

    /* init rb buffer to 0 */
    memset(rb->buffer, 0, capacity * typesize);

    /* rb struct element init */
    rb->capacity = capacity;
    rb->buffer_end = rb->buffer + capacity * typesize;
    rb->count = 0;
    rb->typesize = typesize;
    rb->head = rb->buffer;
    rb->tail = rb->buffer;

    return RING_BUF_INIT_DONE;
}

PUBLIC VOID
AppAudioStream_RingBufWrite(ring_buffer *rb, UINT8 *mp3Byte)
{   
    /* default: allow overwriting if ring buffer is full */
    memcpy(rb->head, mp3Byte, rb->typesize);
    rb->head = rb->head + rb->typesize;
    if(rb->head == rb->buffer_end) {
        printf("head back to start\r\n");
        rb->head = rb->buffer;
    }

    if(rb->count == rb->capacity) {
        printf("buffer full\r\n");
        if (rb->head > rb->tail)
            rb->tail = rb->tail + rb->typesize;
    } else { 
        rb->count++;
    }
}

PUBLIC VOID
AppAudioStream_RingBufRead(ring_buffer *rb, UINT8 *mp3Byte)
{
    /* insert 'comfort noise' if the ring buffer is empty */
    if(rb->count == 0){
        printf("buffer empty\r\n");
        *mp3Byte = NOISE_BYTE;
    } else {
        /* copy data to mp3Byte and increase tail pointer */
        memcpy(mp3Byte, rb->tail, rb->typesize);
        rb->tail = rb->tail + rb->typesize;
        if(rb->tail == rb->buffer_end) {
            printf("TAIL back to start\r\n");
            printf("Tbuffer count: %i\r\n", rb->count);
            rb->tail = rb->buffer;
        }   
        rb->count--;
    }
}

下面是调用环形缓冲区写入函数的方法:

while (1)
{
    AppAudioStream_BufRecv(sd, dataLen, &addr);
}

PUBLIC VOID 
AppAudioStream_BufRecv(int sd, INT32 dataLen, struct sockaddr_in *addr)
{
    INT32 addrlen = sizeof(struct sockaddr_in);
    UINT8 j, i = 0;
    UINT8 *audioByte;

    /* listen to incoming audio data packets */
    dataLen = recvfrom(sd, (char *) appRxBuf, sizeof(appRxBuf), 0, 
                       (struct sockaddr *)&addr, &addrlen);

    /* set pointer to first element in recieve buffer */
    audioByte = appRxBuf;

    /* buffer received packets into FIFO */
    while (dataLen > 0)
    {
        /* write 1 byte into audio FIFO */
        AppAudioStream_RingBufWrite(&audioFIFO, audioByte);

        /* increase pointer index and update # of bytes left to write */
        audioByte++;
        dataLen--;
    }

    /* wait until buffer is 2/3 full to start decoding */
    if (audioFIFO.count >= FIFO_TWO_THIRD_FULL 
        && audioStreamStatus == GSN_STREAM_BUFFERING) {
        audioStreamStatus = GSN_STREAM_START; 
        //printf("stream start\r\n");
    }
}

在每 2 毫秒发生一次的回调中调用环形缓冲区读取函数(这基本上是一个 ISR):

PRIVATE VOID 
AppAudioStream_DecoderCb(UINT32* pDummy, UINT32 TimerHandle)
{
    UINT8 spiWriteCount = 0;
    UINT8 mp3Byte;
    int i = 0;
    GSN_SPI_NUM_T spiPortNumber = GSN_SPI_NUM_0;

    /* read 32 bytes of data from FIFO and write to SPI */
    while (spiWriteCount < DATA_WRITE_AMT)
    {
        /* set stream status to decoding */
        audioStreamStatus = GSN_STREAM_DECODING;

        /* read 1 byte of audio data from FIFO */
        AppAudioStream_RingBufRead(&audioFIFO, &mp3Byte);

        /* write 1 byte of audio data out to VS1053 */
        AppSpi_SdiByteWrite(spiPortNumber, &mp3Byte);

        /* increase byte written count */
        spiWriteCount++; 
    }
}

非常感谢任何帮助/见解。我确信我现在只是忽略了一些非常明显的东西。

谢谢!

【问题讨论】:

  • 让陌生人通过检查发现代码中的错误是没有效率的。您应该使用调试器隔离一个更简单的test-case,然后再返回。
  • 看起来像一个多线程应用程序,对吧?如果是这样,您需要保护环形缓冲区。
  • @Suma 不,它没有。阅读Code review FAQ
  • if (rb-&gt;head &gt; rb-&gt;tail) rb-&gt;tail = rb-&gt;tail + rb-&gt;typesize; 没有多大意义。您需要始终将尾部向前,如果超过尾部则将其包裹到开头。否则你的不变量(head-tail)%capacity==count,或者不管它是什么,都会中断。
  • @Lundin 我已经阅读了它,但我看不出这个问题不满足第 1-5 点中的哪一点。 1 2 3 5 肯定满足。是 4 的问题吗?

标签: c audio-streaming


【解决方案1】:

如果你从回调中读取这个环形缓冲区并从其他地方写入它,那么显然你会遇到各种奇怪的错误,因为环形缓冲区缺乏保护。您需要添加互斥锁、信号量、中断禁用或适用于您的特定系统的任何内容。

此外,回调和应用程序其余部分之间共享的所有变量都应声明为 volatile,以防止危险的优化器错误 (*)。除此之外,这些变量可能还需要使用互斥锁进行保护,以防止出现竞争条件。

如果是上述任何错误的原因,进行压力测试的一个好方法是创建一个多线程应用程序。创建 10+ 个写入缓冲区的线程,以及 10+ 个从缓冲区读取和打印的线程。记录输出,看看是否打印出奇数或垃圾。


(*)(这句话与线程安全无关,所以请不要来张贴关于 volatile 不足以实现线程安全的 cmets,因为我从来没有说过,我不会再进行那种愚蠢的辩论了。)

【讨论】:

  • @n.m.确实,关于多线程的部分不适用于单线程程序。除了这个明显的声明之外,实现 ISR 的单线程程序确实与多线程程序有相同的问题。回调的处理方式取决于操作系统,但它们通常不需要任何互斥锁。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多