Edgar Huckert
 

Setting the audio volume in Linux using parallel threads

The problem

We all know how to set the audio volume in Linux: use a small mixer widget. As many Linux distributions exist the mixer widget may be very different depending on the distribution and on the basic graphic model (GTK, pure X11 tec.).

My typical problem is: I play live music on a wind instrument (clarinet, oboe, bassoon, saxophone) having thus boths hands occupied. But how can I influence the audio volume on the fly, i.e. when a song is already started? I prefer foot switches to do that. Note that a standard foot switch normally acts like a poti (variable resistor). As I do not play with devices connected via normal audio cables this standard solution cannot be applied. My solution presented here can also be used with Bluetooth, i.e. without normal audio cables.

The audio components (utilities, libraries) are very diffent between Windows and Linux. It is difficult (but possible) to write a single version for bot operating systems - the result would be diffuclt to read and understand. I have therefor written two versions: this version here is for Linux (Ubuntu/Debian).

Note: this program is related to the PDF document under A foot switch for musicians. The foot switch mentioned there produces events sent via USB/Serial. These events can replace the key strokes used here so that the play process ist completely hands-free!

The solution

The solution is: use two threads: The audio player in thread1 can be a synchronous player, i.e. a program or an API call working without any interruptions. In my sample code the audio player is the external MP3 player mpg123 - a command line utility.

The fact that we use keyboard events (key strokes) in our sample is not a contradiction to the main problem as described above. If you replace the key strokes by foot switch events or events coming from another application then the structure of the program remains the same. Just replace the application logic in routine threadWorker1 by the reception of signals via TCP, USB etc.

Sone remarks on the sample program: this program uses PThreads - it could also use normal Linux threads or any other type of thread (wxWidgets etc.) PThreads are useful as they can also be used under Windows and other operating systems.

In pure Linux GUI programs one single thread can perhaps be used. The audio player must then work in asynchronous mode (this is more difficult) and the polling of the control events can be done in a Timer callback routine. My experiences with such a single thread solution howewer failed so far.

 

The source code (Standard C under Linux)

This is a standard C program compiled with MingW gcc. The command used to compile and link it can be found in the first comment lines of the source code.

You may download the source code here.

// module WaveThreadsU.c
// Uses portable threads (PThreads) under Windows and Linux
// This is the Linux/Alsa version!!!
// Audio result: works: thread2 influences the volume of 
//               the wave player thread1! Evaluates the keys q, +, -

// This version uses mutexes as signals to indicate the termination 
//    of a thread!!! 
// Written by Dr.E.Huckert 03-2009, 05-2016, 03-2020
// Compile:
// Linux: gcc -o WaveThreadsU WaveThreadsU.c -lpthread -lasound
//                         (evtl. auch -lasound2)

#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <termios.h>
#include <unistd.h>
#include <alsa/asoundlib.h>

// This is a very simple thread administration...not 
// really used in this version
#define NUM_THREADS        10  // max. 10 threads
pthread_t thread_ids[NUM_THREADS];
pthread_mutex_t mutex1, mutex2;

char waveFilename[128] = "./test.wav"; // can be overwritten

// for LINUX/ALSA: we use percent values
#define HIGH_VOLUME   100
#define LOW_VOLUME    20
#define MAX_VOLUME    100
#define MIN_VOLUME    0
#define MEDIUM_VOLUME 50

// ----------------------------------------------------------
// sleep a number of milliseconds
int milliSleep(unsigned millis)
{
  int ret;
  ret = usleep((long)millis * 1000L);
  return ret;
}   // end milliSleep()

// ------------------------------------------------------------
// Get a key stroke under Linux
// Sets the keyboard channel (0=stdin) to RAW mode (unbuffered)
// Returns: < 0 upon error, else the character read (or 0 if nothing read)
int getKeyStroke() 
{
  char buf           = 0;
  int  ret           = 0;
  static struct termios old = {0};
  static unsigned long entryCount = 0L;
  //
  if (entryCount == 0UL)
  {
    if (tcgetattr(0, &old) < 0)
      perror("tcsetattr()");
    old.c_lflag     &= ~ICANON;
    old.c_lflag     &= ~ECHO;
    old.c_cc[VMIN]  = 1;
    old.c_cc[VTIME] = 0;
    if (tcsetattr(0, TCSANOW, &old) < 0)
      perror("tcsetattr ICANON");
    // reset of keyboard must be done via "stty sane"
  }   // end if (entryCount == 0UL)
  //
  ret = read(0, &buf, 1);
  entryCount++;
  if (ret > 0)
    ret = buf & 0xff;
  return ret;
}   // end getKeyStroke()

// ---------------------------------------------------------
// Play a WAVE file whose name is given
// Uses a system() call to start the aplay command
// Returns: < 0 upon error
int playWaveFile(const char *filename)
{
  int ret;
  char cmd[256];
  //
  printf("playWaveFile() file=%s\n", filename);
  strcpy(cmd, "aplay ");
  strcat(cmd, filename);
  //
  ret = system(cmd);
  //
  printf("playWaveFile() system(%s) ret=%d\n", cmd, ret);
  return ret;
}   // end playWaveFile()

// -----------------------------------------------------------
// symmetric channel values assumed effect
// Linux: no effect
long makeMono(long stereoVol)  // 32 bit, 16 bit per channel
{
  //return stereoVol & 0x0000ffff;  // result=right channel
  return stereoVol;
}  // end makeMono()

// -----------------------------------------------------------
// symmetric channel values assumed
// Linux: no effect
long makeStereo(long monoVol) // right channel value, 16 Bit
{
  //return (monoVol  << 16) | monoVol;
  return monoVol;
}  // end makeStereo()

// -----------------------------------------------------------
// set the volume for a WAVE audio channel
// Returns: < 0 upon error
//          else the last volume set (mono value, not shifted)
// note: internalrange min=0, max=65536 (=0xffff)
// argument given: percent volume, ditto for return value
long getSetVolume(long newVol)  // in percent of max value
{
  int      ret     = 0;
  long     retVol  = 0ul;
  long     min, max;
  snd_mixer_t *handle;
  snd_mixer_selem_id_t *sid;
  const char *card       = "default";
  const char *selem_name = "Master";
  //
  printf("getSetVolume() newVol=0x%lx\n", newVol);
  snd_mixer_open(&handle, 0);
  snd_mixer_attach(handle, card);
  snd_mixer_selem_register(handle, NULL, NULL);
  snd_mixer_load(handle);
  //
  snd_mixer_selem_id_alloca(&sid);
  snd_mixer_selem_id_set_index(sid, 0);
  snd_mixer_selem_id_set_name(sid, selem_name);
  snd_mixer_elem_t* elem = snd_mixer_find_selem(handle, sid);
  //
  snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
  snd_mixer_selem_set_playback_volume_all(elem, newVol * max / 100);
  retVol = newVol;
  //
  snd_mixer_close(handle);
  //
  zurueck:
  printf("getSetVolume() retVol=0x%lx\n", retVol);
  return retVol;
}   // end getSetVolume()

// -------------------------------------------------
// thread 1 worker routine: plays a local wave file
// Assumes that mutex1 is locked at start time
void *threadWorker1(void *pArg)
{
  int n, rc;
  int oldState;
  //
  printf("thread 1: playing file %s\n", waveFilename);
  //
  //pthread_setcancelstate(PTHREAD_CANCEL_DEFERRED, &oldState);
  pthread_testcancel();   // this is necessary!!!
  //
  rc = playWaveFile(waveFilename);
  //
  pthread_testcancel();   // this is necessary!!!
  //
  // send a termination signal: the termination signal is the mutex itself!
  rc = pthread_mutex_unlock(&mutex1);
  if (rc != 0)
  {
   printf("thread 1: unlock ERROR\n");
  }
  else
   printf("thread 1: termination signal = mutex set\n");
  //
  zurueck:
  printf("leaving thread 1\n");
  pthread_exit(NULL);
}   // end threadWorker1()

// ----------------------------------------------------
// thread worker routine: runs 15 secs
// This is the event evaluation thread
// Tries to change the volume of the audio player thread 1
void *threadWorker2(void *pArg)
{
  int           rc;
  char          ch;
  long          lastVol, newVol;
  long          dummy;
  int           key; 
  //
  lastVol = getSetVolume(MEDIUM_VOLUME);
  //
  // wait for key events:
  // q = quit
  // + = increase volume
  // - = decrease volume
  while (1)
  {
    rc = getKeyStroke();
    if (rc <= 0)
      goto go_on;
    key = rc & 0xff;
    printf("thread 2 key=%d\n", key);
    fflush(stdout);
    if (key == 'q')
    {
      // we terminate the complete process (all threads)
      system("stty sane");  // reset the keyboard to normal (=buffered) state
      //pthread_exit((void *)&dummy);
      // kill the player thread
      printf("thread 2: we terminate!\n");
      fflush(stdout);
      exit(1);
      break;  // not reached
    }
    if (key == '-')
    {
      newVol = lastVol - (MAX_VOLUME / 10);
      if (newVol < MIN_VOLUME)
        newVol = MIN_VOLUME;
      lastVol = getSetVolume(newVol);
    }
    if (key == '+')
    {
      newVol = lastVol + (MAX_VOLUME / 10);
      if (newVol > MAX_VOLUME)
        newVol = MAX_VOLUME;
      lastVol = getSetVolume(newVol);
    }
    go_on:
    milliSleep(50);
  }   // end while (1)
  //
  // send a termination signal: the termination signal is the mutex itself!
  rc = pthread_mutex_unlock(&mutex2);
  if (rc != 0)
  {
   printf("thread 2: unlock ERROR\n");
  }
  else
   printf("thread 2: termination signal = mutex set\n");
  //
  zurueck:
  printf("leaving thread 2\n");
  pthread_exit(NULL);
}   // end threadWorker2()

// ---------------------------------------------------------
// Create and start a thread
// Side effect: fills array thread_ids[]
// Returns 0 if OK, < 0 if error
int createThread(int threadNo,       // 0 .. (NUM_THREADS-1)
                 void *(*worker)(void *),  // address of worker routine
                 void *pArg)
{
  int ret = 0;
  printf("creating thread no. %d\n", threadNo);
  ret = pthread_create(&thread_ids[threadNo], 
                       NULL, 
                       worker,
                       pArg);  // argument passed to the worker routine
  if (ret != 0)
  {
    ret = -1;
    goto zurueck;
  }
  zurueck:
  printf("created  thread no. %d with result=%d\n", threadNo, ret);
  return ret;
}   // end createThread()

// ------------------------------------------------------
void usage()
{
  printf("usage: WaveThreadsU [waveFilename]\n");
  printf("       If no file is given the test.wav is assumed\n");
  printf("       Use the volume keys +,- and q (for quit)\n");
  printf("Copyright Dr.E.Huckert 03-2020\n");
} // end usage()

// ------------------------------------------------------
int main (int argc, const char *argv[])
{
   int rc;
   int ret = 0;
   unsigned long arg1 = 101L;
   unsigned long arg2 = 102L;
   //
   usage();
   if (argc > 1)
     strcpy((char *)waveFilename, argv[1]);
   //
   rc = pthread_mutex_init(&mutex1, NULL);
   rc = pthread_mutex_init(&mutex2, NULL);
   //
   // create two threads and start them
   // create and start the player thread
   rc = createThread(0, threadWorker1, &arg1);
   if (rc != 0)
   {
     ret = -1;
     goto zurueck;
   }
   rc = pthread_mutex_lock(&mutex1);
   if (rc != 0)
   {
     ret = -2;
     goto zurueck;
   }
   printf("createThread() rc=%d\n", rc);
   printf("thread id=0x%p\n", (void *)(thread_ids[0]));
   //
   // create and start the event evaluation thread
   rc = createThread(1, threadWorker2, &arg2);
   if (rc != 0)
   {
     ret = -3;
     goto zurueck;
   }
   rc = pthread_mutex_lock(&mutex2);
   if (rc != 0)
   {
     ret = -4;
     goto zurueck;
   }
   printf("createThread() rc=%d\n", rc);
   printf("thread id=0x%p\n", (void *)(thread_ids[1]));
   //
   // Wait until all threads terminate.
   // The termination signal is the change of a mutex associated with each thread
   sleep(1);  // give the threads time to start
   int noThreads = 2;
   //
   while (noThreads > 0)
   {
     // Note: this can be replaced and simplified by a loop over an array of mutexes
     // try to lock mutex1
     rc = pthread_mutex_trylock(&mutex1);
     if (rc != 0)  // errror handling is not complete (is simplified)
     {
       printf("main(): cannot lock - waiting\n");
     }
     else
     {
       printf("main(): lock 1 OK!!!\n");
       noThreads--;
       if (noThreads <= 0) break;
     }
     // try to lock mutex2
     rc = pthread_mutex_trylock(&mutex2);
     if (rc != 0)  // errror handling is not complete (is simplified)
     {
       printf("main(): cannot lock - waiting\n");
     }
     else
     {
       printf("main(): lock 2 OK!!!\n");
       noThreads--;
       if (noThreads <= 0) break;
     }
     sleep(1);
   }   // end while(noThreads > 0)
   //
   ret = 1;
   printf("main() all threads have terminated\n");
   //
   zurueck:
   printf("main() return code=%d\n", ret);
   return ret;
}   // end main()

Copyright for all images, texts and software on this page: Dr. E. Huckert

Contact

If you want to contact me: this is my
mail address