Edgar Huckert
 

Setting the audio volume in Windows using parallel threads

The problem

We all know how to set the audio volume in Windows: use the small mixer widget (for me on the right side of the bottom panel). 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.

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 Windows API call PlaySound(). It could also be an external program started via a system() call - but then stopping this external player is more difficult as it runs then as a detached process (you don't know at first glance the PID of this process - the PID is needed to stop it). The solution may be found in my article A foot switch for musicians mentioned above.

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 windows threads or any other type of thread (wxWidgets etc.) PThreads are useful as they can also be used under Linux/Unix.

In pure Windows 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 Windows)

This is a standard C program compiled with MingW gcc. This version is a CLI program - i.e. it has no real graphic interface and runs in a "black shell window". 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 waveThreads.c
// Uses portable threads (PThreads) under Windows
// 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!!!
// Works under Windows 32 Bit 
// Written by Dr.E.Huckert 03-2009, 05-2016

// Windows 32 Bit: compile: gcc -o waveThreads.exe waveThreads.c -lpthread -lwinmm

// Versions:
// 03-2020 problem with printing thread-ID fixed, then OK

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.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;

#ifdef _WIN32
#include <windows.h>
#include <mmsystem.h>
#include <conio.h>
#define sleep(x) Sleep(1000 * x)
#endif

char waveFilename[128] = "test.wav";

// Note: these are values per channel!
// They must be combined (shifted, masked) for stereo
#define HIGH_VOLUME   0xdddd
#define LOW_VOLUME    0x1111
#define MAX_VOLUME    0xffff
#define MIN_VOLUME    0x0000
#define MEDIUM_VOLUME 0x7fff

// ---------------------------------------------------------
// Play a WAVE file whose name is given
// Returns: < 0 upon error
int playWaveFile(const char *filename)
{
  int ret;
  //
  printf("playWaveFile() file=%s\n", filename);
  // EH: must be mode=SND_SYNC - if not only the first buffer is played and
  //        the rest is skipped as the application terminates!
  //ret = ::sndPlaySound(filename, SND_FILENAME | SND_ASYNC); 
  //ret = sndPlaySound(filename, SND_FILENAME | SND_SYNC); // deprecated
  ret = PlaySound(filename, NULL, SND_FILENAME | SND_SYNC);  
  return ret;
}   // end playWaveFile()

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

// -----------------------------------------------------------
// symmetric channel values assumed
DWORD makeStereo(DWORD monoVol) // right channel value, 16 Bit
{
  return (monoVol  << 16) | monoVol;
}  // end makeStereo()

// -----------------------------------------------------------
// set the volume for a WAVE audio channel
// Returns: < 0 upon error
//          else the last volume set (mono value, not shifted)
DWORD getSetVolume(DWORD newVol)  // mono value, not shifted
{
  int      ret = 0;
  HWND     hwnd;
  MMRESULT mmRet;
  DWORD    dword, retVol;
  //
  static unsigned entryCount = 0;
  static HWAVEOUT     hWaveOut;
  static HWAVEIN      hWaveIn;
  static WAVEFORMATEX waveform ;
  //
  printf("getSetVolume newVol=0x%lx\n", newVol);
  retVol = 0UL;
  if (entryCount == 0)
  {
    // Open a WaveOut device once (static!)
    hwnd                     = GetCurrentThread();
    waveform.wFormatTag      = WAVE_FORMAT_PCM ;
    waveform.nChannels       = 1 ;
    waveform.nSamplesPerSec  = 11025 ;
    waveform.nAvgBytesPerSec = 11025 ;
    waveform.nBlockAlign     = 1 ;
    waveform.wBitsPerSample  = 8 ;
    waveform.cbSize          = 0 ;
    //
    waveOutReset(hWaveOut) ;
    mmRet = waveOutOpen(&hWaveOut, 
                          WAVE_MAPPER, 
                          &waveform,
                          0, 
                          0, 
                          CALLBACK_NULL);
    if (mmRet != MMSYSERR_NOERROR)
    {
      char msg[128];
      waveOutGetErrorText(mmRet, msg, sizeof(msg) - 1);
      printf("waveOutOpen() reports ERROR: %s\n", msg);
      ret = -1;
      goto zurueck;
    }
  }
  entryCount++;
  //
  mmRet = waveOutSetVolume(hWaveOut, makeStereo(newVol));
  printf("volume set=%lu\n", newVol);
  printf("volume mmRet=%lu\n", (long)mmRet);
  //
  mmRet = waveOutGetVolume(hWaveOut, &retVol);
  retVol = makeMono(retVol);
  printf("volume get=0x%lx\n", retVol);
  //
  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;
  long *pLong;
  //
  pLong = (long *)pArg;
  //printf("thread 1 arg=%ld\n", *pLong);
  printf("thread 1: playing file %s\n", waveFilename);
  //
  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
// Tries to change the volume of the audio player thread 1
void *threadWorker2(void *pArg)
{
  int n, rc;
  long *pLong;
  DWORD lastVol, newVol;
  int   key; 
  //
  //pLong = (long *)pArg;
  //printf("thread 2 arg=%ld\n", *pLong);
  lastVol = getSetVolume(MEDIUM_VOLUME);
  //
  // wait for key events:
  // q = quit
  // + = increase volume
  // - = decrease volume
  // Important: instead of polling for key strokes you can
  // poll here for events from foot switches, TCP, UDP etc...
  while (1)
  {
    key = _kbhit();
    if (key == 0)
      goto go_on;
    key = _getch();
    printf("thread 2 key=%d\n", key);
    if (key == 'q')
      break;
    // Note: still probelms here as the volum e values are unsigned,
    //       .i.e. comparison < 0 may not work!
    if (key == '-')
    {
      newVol = lastVol - (lastVol / 5);
      if (newVol < MIN_VOLUME)
        newVol = MIN_VOLUME;
      lastVol = getSetVolume(newVol);
    }
    if (key == '+')
    {
      newVol = lastVol + (lastVol / 5);
      if (newVol > MAX_VOLUME)
        newVol = MAX_VOLUME;
      lastVol = getSetVolume(newVol);
    }
    go_on:
    Sleep(100L);
  }   // 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: waveThreads [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, char *argv[])
{
   int rc;
   int ret = 0;
   long arg1 = 101L;
   long arg2 = 102L;
   //
   usage();
   if (argc > 1)
     strcpy(waveFilename, argv[1]);
   //
   rc = pthread_mutex_init(&mutex1, NULL);
   rc = pthread_mutex_init(&mutex2, NULL);
   //
   // create two threads and start them
   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);
   // worked in year 2009 - now (2020) no longer!
   //printf("thread id=%08lx\n", (long)( thread_ids[0]));
   printf("thread id=0x%p\n", thread_ids[0].p);
   //
   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);
   // worked in year 2009 - now (2020) no longer!
   //printf("thread id=%08lx\n", (long)( thread_ids[]));
   printf("thread id=0x%p\n", thread_ids[1].p);
   //
   // 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