Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Linux how to record sound in RAM buffer and playback audio with custom delay

I need to send audio from a radio to a secondary system using an embedded linux system.

The secondary system needs to set up a communication channel which takes a few seconds.

So if I don't want to lose the beginning of the audio, I need a way to record the sound and play it back with a custom delay (a few seconds maximum).

It should be possible to start arecord to record the audio in a file in a tmpfs filesystem, and, when a communication is incoming, start aplay. But in this case the beginning is still lost because the signal to record is coming too late.

Is there a program on Linux which is recording sound continuously in a ring buffer in RAM and able to playback with a custom delay on demand ?

If not, what is the best library to code such a program on an embedded system ? alsa or something else ?

like image 945
leszek.hanusz Avatar asked Sep 04 '15 13:09

leszek.hanusz


2 Answers

Here is a simple C program that will maintain a circular buffer between pipe in and out. Use like in | buffer_program | out. Error checking omitted. Robustness not guaranteed. Gives the general idea.

Test script (but actually since its circular buffer the data your piping in needs to be such that its coherent taking just any chunk in the stream. Or just make the buffer bigger than the data):

cat some.wav | ./circular_buffer 100000 | (sleep 1 && aplay)

circular_buffer.c:

 /**
 * This program simply maintains a circular buffer of a given size indefinitely.
 */
#include <stdio.h>
#include <stddef.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdbool.h> /* C99 only */
#include <sys/select.h>
#include <errno.h>
#include <fcntl.h>

int c_read(int fd, char * buf, unsigned int size, unsigned int * head_in, unsigned int * tail_in);
int c_write(int fd, char * buf, unsigned int size, unsigned int * head_in, unsigned int * tail_in);
bool empty_buf(unsigned int head, unsigned int tail);
bool setblock(int fd, bool block);
#define FD_SET_SET(set, fd, max) FD_SET(fd, &set); max = ((fd > max) ? fd : max);
#define FD_SET_UNSET(set, fd, max) FD_CLR(fd, &set); max = ((fd == max) ? max - 1  : max);  //not ideal. Do while ISFDSET...

int main(int argc, char **argv)
{
  char * buf;
  unsigned int buf_size = 0;
  unsigned int buf_head = 0;
  unsigned int buf_tail = 0;

  // Check args.
  if(argc != 2) {
    fprintf(stderr, "Usage: %s <buffer size in bytes>\n", __FILE__);
    exit(EXIT_FAILURE);
  }
  sscanf(argv[1], "%d", &buf_size);
  buf_size = ( buf_size < 2 ) ? 2 : buf_size;

  // Note the usable buffer space is buf_size-1.
  fprintf(stderr, "Allocating %d\n", buf_size);
  buf = (char*)malloc(buf_size);

  bool done_reading = false;
  int maxfd = 0;
  fd_set r_set, w_set, r_tempset, w_tempset;
  setblock(STDIN_FILENO, false);
  setblock(STDOUT_FILENO, false);
  FD_ZERO(&r_set);
  FD_ZERO(&w_set);
  FD_ZERO(&r_tempset);
  FD_ZERO(&w_tempset);
  FD_SET_SET(r_tempset, STDIN_FILENO, maxfd);
  FD_SET_SET(w_tempset, STDOUT_FILENO, maxfd);
  r_set = r_tempset;
  while(true) {
    select((maxfd + 1), &r_set, &w_set, NULL, NULL);
    if(FD_ISSET(STDIN_FILENO, &r_set)) {
      int c = c_read(STDIN_FILENO, buf, buf_size, &buf_head, &buf_tail);
      if(c == -1) { // EOF, disable select on the input.
        fprintf(stderr, "No more bytes to read\n");
        done_reading = true;
        FD_ZERO(&r_set);
      }
    }
    if(!done_reading) {
      r_set = r_tempset;
    }
    if(FD_ISSET(STDOUT_FILENO, &w_set)) {
      c_write(STDOUT_FILENO, buf, buf_size, &buf_head, &buf_tail);
    }
    if(!empty_buf(buf_head, buf_tail)) { // Enable select on write whenever there is bytes.
      w_set = w_tempset;
    }
    else {
      FD_ZERO(&w_set);
      if(done_reading) { // Finish.
        fprintf(stderr, "No more bytes to write\n");
        break;
      }
    }
  }
  fflush(stderr);
  return 0;
}

bool empty_buf(unsigned int head, unsigned int tail) {
  return head == tail;
}

/**
 * Keep reading until we can read no more. Keep on pushing the tail forward as we overflow.
 * Expects fd to be non blocking.
 * @returns number of byte read, 0 on non stopping error, or -1 on error or EOF.
 */
int c_read(int fd, char * buf, unsigned int size, unsigned int * head_in, unsigned int * tail_in) {
  fprintf(stderr, "In c_read()\n");
  unsigned int head = *head_in;
  unsigned int tail = *tail_in;
  bool more_bytes = true;
  int n = 0;
  int c = 0;

  while(more_bytes) {
    bool in_front = tail > head;
    fprintf(stderr, "Read %d %d %d\n", size, head, tail);

    n = read(fd, buf+head, size - head);
    if(n == -1) {
      more_bytes = false;
      if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { // Not EOF but the read would block.
        c = 0;
      }
      else {
        c = -1;
      }
    }
    else if(n == 0) { // EOF. No more bytes possible.
      more_bytes = false;
      c = -1;
    }
    else if(n != (size - head)) { // if not full read adjust pointers and break.
      more_bytes = false;
      c += n;
      head = (head+n)%size;
      if(in_front && (head >= tail || head == 0)) {
        tail = (head+1)%size;
      }
    }
    else {
      c = 0;
      head = 0;
      tail = (tail == 0) ? 1 : tail;
    }
  }
  *head_in = head;
  *tail_in = tail;
  return c;
}

/**
 * Try flush the buffer to fd. fd should be non blocking.
 */
int c_write(int fd, char * buf, unsigned int size, unsigned int * head_in, unsigned int * tail_in) {
  fprintf(stderr, "In c_write()\n");
  unsigned int head = *head_in;
  unsigned int tail = *tail_in;
  int n = 0;
  fprintf(stderr, "Write %d %d %d\n", size, head, tail);

  if(tail < head) {
    n = write(fd, buf+tail, head-tail);
    tail += n;
  }
  else if(head < tail) {
    n = write(fd, buf+tail, size-tail);
    if(n == size-tail) {
      n = write(fd, buf, head);
      tail = n;
    }
  }
  *head_in = head;
  *tail_in = tail;
  return n;
}

bool setblock(int fd, bool block)
{
  int flags;
  flags = fcntl(fd, F_GETFL);
  if (block)
      flags &= ~O_NONBLOCK;
  else
      flags |= O_NONBLOCK;
  fcntl(fd, F_SETFL, flags);
  return true;
}
like image 150
spinkus Avatar answered Oct 08 '22 18:10

spinkus


If all you need is to have a buffer to keep sound output until it is ready to be consumed a variant of this should work:

Start recording:

mkfifo /tmp/f
stdbuf -o256M arecord -i | cat > /tmp/f

Start playing, when yours output device is ready:

aplay /tmp/f

Tweak the output buffer size to fit your needs.

EDIT (given that playing can start anytime):

If you need to continuously record and start playing anytime you could split the output into smaller files using the split command and delete the old files in a helper process.

Something like:

# Garbage collector
( while sleep 1 ; do rm $(ls *.blb 2>/dev/null | sort | head -n-3 ) > /dev/null 2>&1 ; done ) &
# Actual recording
arecord -i | split -a 10 -u -b 24576 --additional-suffix '.blb'

And to play:

{ while true ; do for f in $(find . -name '*.blb' -size 24576c | sort) ; do cat $f ; rm $f ; done ; done } | aplay

This solution is quite dirty, but might work (preferably on tmpfs as you have already mentioned)...

like image 39
vlp Avatar answered Oct 08 '22 18:10

vlp