Linux: volume control

2018-5-18 : Now available on SourceForge: https://sourceforge.net/projects/volume-controls-cmdline/files/

bll 2016-12-19 Builds a stand-alone executable or loadable Tcl stub for Linux. Uses the pulse audio library.

bll 2017-6-12 Rewrote to use the threaded model; made more efficient.

bll 2017-6-17 Do not keep the pulseaudio connection open.

load [file join [pwd] pulseaudio64[info sharedlibextension]]
set sinkdict [pulseaudio getsinklist] ; # get the list of sinks
# the sink dictionary has:
#   sinkname - string, sink name used in pulseaudio.
#      default - boolean, if true, is the default sink
#      index - int, index number
#      description - string, human readable description of the sink
dict for {sink info} $sinkdict {
  if { [dict get $info default] } {
    set sinkname $sink
    break
  }
}
set vol [pulseaudio $sinkname] ; # get the volume for the specified sink
incr vol -10
set newvol [pulseaudio $sinkname $vol] ; # set the volume, always returns the current volume.
pulseaudio close

pamkvol.sh

#!/bin/sh

slext=.so
tv=8.6
arch=`uname -m`
bits=64
case $arch in
  i?86*)
    bits=32
    ;;
esac
inc=/usr/include/tcl${tv}
lv=$tv

${CC:-cc} -O -m${bits} -o pavolume${bits} \
    -UPA_TCL_INTERFACE pulseaudio.c \
    -lpulse -lm
${CC:-cc} -O -m${bits} -shared -fPIC -o pulseaudio${bits}${slext} \
    -DPA_TCL_INTERFACE -I$inc -DUSE_TCL_STUBS pulseaudio.c \
    -ltclstub${lv} -lpulse -lm

pulseaudio.c

/*
 * Copyright 2016 Brad Lanam Walnut Creek CA USA
 * MIT License
 *
 * pulse audio
 *   getsinklist
 *   getvolume <sink-name>
 *   setvolume <sink-name> <volume-perc>
 *   close
 *
 * References:
 *   https://github.com/cdemoulins/pamixer/blob/master/pulseaudio.cc
 *   https://freedesktop.org/software/pulseaudio/doxygen/
 */
#ifdef PA_TCL_INTERFACE
# include <tcl.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <memory.h>
#include <math.h>
#include <pulse/pulseaudio.h>

typedef struct {
  char      *defname;
  void      *tcldata;
} getSinkData_t;

#define STATE_OK    0
#define STATE_WAIT  1
#define STATE_FAIL  -1
typedef struct {
  pa_threaded_mainloop  *pamainloop;
  pa_context            *pacontext;
  pa_context_state_t    pastate;
  int                   state;
} state_t;

static state_t      gstate;
static int          ginit = 0;

typedef union {
  char            *defname;
  pa_cvolume      *vol;
  getSinkData_t   *sinkdata;
} callback_t;

void
serverInfoCallback (
  pa_context            *context,
  const pa_server_info  *i,
  void                  *userdata)
{
  callback_t    *cbdata = (callback_t *) userdata;

  cbdata->defname = strdup (i->default_sink_name);
  pa_threaded_mainloop_signal (gstate.pamainloop, 0);
}

void
sinkVolCallback (
  pa_context *context,
  const pa_sink_info *i,
  int eol,
  void *userdata)
{
  callback_t    *cbdata = (callback_t *) userdata;

  if (eol != 0) {
    pa_threaded_mainloop_signal (gstate.pamainloop, 0);
    return;
  }
  memcpy (cbdata->vol, &(i->volume), sizeof (pa_cvolume));
  pa_threaded_mainloop_signal (gstate.pamainloop, 0);
}

void
connCallback (pa_context *pacontext, void* userdata)
{
  state_t   *stdata = (state_t *) userdata;

  if (pacontext == NULL) {
    stdata->pastate = PA_CONTEXT_FAILED;
    stdata->state = STATE_FAIL;
    pa_threaded_mainloop_signal (stdata->pamainloop, 0);
    return;
  }

  stdata->pastate = pa_context_get_state (pacontext);
  switch (stdata->pastate)
  {
    case PA_CONTEXT_READY:
      stdata->state = STATE_OK;
      break;
    case PA_CONTEXT_FAILED:
      stdata->state = STATE_FAIL;
      break;
    case PA_CONTEXT_UNCONNECTED:
    case PA_CONTEXT_AUTHORIZING:
    case PA_CONTEXT_SETTING_NAME:
    case PA_CONTEXT_CONNECTING:
    case PA_CONTEXT_TERMINATED:
      stdata->state = STATE_WAIT;
      break;
  }
  pa_threaded_mainloop_signal (stdata->pamainloop, 0);
}

void
nullCallback (
  pa_context* context,
  int success,
  void* userdata)
{
  callback_t    *cbdata = (callback_t *) userdata;
  pa_threaded_mainloop_signal (gstate.pamainloop, 0);
}


void
waitop (pa_operation *op)
{
  int retval;

  while (pa_operation_get_state (op) == PA_OPERATION_RUNNING) {
    pa_threaded_mainloop_wait (gstate.pamainloop);
  }
  pa_operation_unref (op);
}

int
argcheck (int argc, char *action)
{
  int rc;

  rc = 0;
  if (argc < 2 || argc > 4 ||
      (strcmp (action, "close") != 0 &&
       strcmp (action, "getsinklist") != 0 &&
       strcmp (action, "getvolume") != 0 &&
       strcmp (action, "setvolume") != 0) ||
      (argc != 2 && strcmp (action, "close") == 0) ||
      (argc != 2 && strcmp (action, "getsinklist") == 0) ||
      (argc != 3 && strcmp (action, "getvolume") == 0) ||
      (argc != 4 && strcmp (action, "setvolume") == 0)) {
    rc = 1;
  }
  return rc;
}

void
pulse_close (void)
{
  if (ginit) {
    pa_threaded_mainloop_stop (gstate.pamainloop);
    pa_threaded_mainloop_free (gstate.pamainloop);
  }
  ginit = 0;
}

void
pulse_disconnect (void)
{
  if (gstate.pacontext != NULL) {
    pa_threaded_mainloop_lock (gstate.pamainloop);
    pa_context_disconnect (gstate.pacontext);
    pa_context_unref (gstate.pacontext);
    gstate.pacontext = NULL;
    pa_threaded_mainloop_unlock (gstate.pamainloop);
  }
}

void
processfailure (char *name)
{
  int       rc;

  if (gstate.pacontext != NULL) {
    rc = pa_context_errno (gstate.pacontext);
    fprintf (stderr, "%s: err:%d %s\n", name, rc, pa_strerror(rc));
    pulse_disconnect ();
  }
  pulse_close ();
}

void
init_context (void) {
  pa_proplist           *paprop;
  pa_mainloop_api       *paapi;

  pa_threaded_mainloop_lock (gstate.pamainloop);
  paapi = pa_threaded_mainloop_get_api (gstate.pamainloop);
  paprop = pa_proplist_new();
  pa_proplist_sets (paprop, PA_PROP_APPLICATION_NAME, "unknown");
  pa_proplist_sets (paprop, PA_PROP_MEDIA_ROLE, "music");
  gstate.pacontext = pa_context_new_with_proplist (paapi, "unknown", paprop);
  pa_proplist_free (paprop);
  if (gstate.pacontext == NULL) {
    pa_threaded_mainloop_unlock (gstate.pamainloop);
    processfailure ("new context");
    return;
  }

  gstate.pastate = PA_CONTEXT_UNCONNECTED;
  gstate.state = STATE_WAIT;
  pa_context_set_state_callback (gstate.pacontext, &connCallback, &gstate);

  pa_context_connect (gstate.pacontext, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL);
  pa_threaded_mainloop_unlock (gstate.pamainloop);
}

#ifndef PA_TCL_INTERFACE

void
getSinkCallback (
  pa_context            *context,
  const pa_sink_info    *i,
  int                   eol,
  void                  *userdata)
{
  callback_t    *cbdata = (callback_t *) userdata;

  char *defflag = "no";

  if (eol != 0) {
    pa_threaded_mainloop_signal (gstate.pamainloop, 0);
    return;
  }
  if (strcmp (i->name, cbdata->sinkdata->defname) == 0) {
    defflag = "yes";
  }

  printf ("%s %d {%s} {%s}\n", defflag, i->index,
       i->name, i->description);
  pa_threaded_mainloop_signal (gstate.pamainloop, 0);
}

int
main (int argc, char *argv[])
{
  char              *sinkname;
  int               vol;
  int               rc;

  if (argcheck (argc, argv[1]) != 0) {
    fprintf (stderr,
        "Usage: %s {close|getsinklist|getvolume <sinkname>|setvolume <sinkname> <vol>}\n", argv[0]);
    return 1;
  }

  vol = 0;
  sinkname = "";
  if (argc > 2) {
    sinkname = argv[2];
  }
  if (argc > 3) {
    vol = atoi (argv[3]);
  }
  rc = process (argv[1], sinkname, &vol, NULL);
  if (strcmp (argv[1], "getvolume") == 0 ||
      strcmp (argv[1], "setvolume") == 0) {
    printf ("%d\n", vol);
  }
  fflush (stdout);
  pulse_disconnect ();
  pulse_close ();
  return rc;
}

#endif /* ! PA_TCL_INTERFACE */

#ifdef PA_TCL_INTERFACE

typedef struct {
  Tcl_Interp    *interp;
  Tcl_Obj       *dictObj;
} tclSinkData_t;

static void
addStringToDict (Tcl_Interp *interp, Tcl_Obj *dict,
        const char *nm, const char *val)
{
  Tcl_Obj           *tempObj1;
  Tcl_Obj           *tempObj2;

  tempObj1 = Tcl_NewStringObj (nm, -1);
  tempObj2 = Tcl_NewStringObj (val, -1);
  Tcl_DictObjPut (interp, dict, tempObj1, tempObj2);
}

static void
addIntToDict (Tcl_Interp *interp, Tcl_Obj *dict,
        const char *nm, int val)
{
  Tcl_Obj           *tempObj1;
  Tcl_Obj           *tempObj2;


  tempObj1 = Tcl_NewStringObj (nm, -1);
  tempObj2 = Tcl_NewIntObj (val);
  Tcl_DictObjPut (interp, dict, tempObj1, tempObj2);
}

static void
addBooleanToDict (Tcl_Interp *interp, Tcl_Obj *dict,
        const char *nm, int val)
{
  Tcl_Obj           *tempObj1;
  Tcl_Obj           *tempObj2;


  tempObj1 = Tcl_NewStringObj (nm, -1);
  tempObj2 = Tcl_NewBooleanObj (val);
  Tcl_DictObjPut (interp, dict, tempObj1, tempObj2);
}

void
getSinkCallback (
  pa_context            *context,
  const pa_sink_info    *i,
  int                   eol,
  void                  *userdata)
{
  callback_t    *cbdata = (callback_t *) userdata;

  tclSinkData_t *tcldata;
  char          *defflag = "no";
  Tcl_Obj       *dictObj;
  Tcl_Obj       *tempDictObj;
  Tcl_Obj       *nameKey;

  if (eol != 0) {
    pa_threaded_mainloop_signal (gstate.pamainloop, 0);
    return;
  }
  if (strcmp (i->name, cbdata->sinkdata->defname) == 0) {
    defflag = "yes";
  }

  tcldata = cbdata->sinkdata->tcldata;
  tempDictObj = Tcl_NewDictObj();
  addBooleanToDict (tcldata->interp, tempDictObj, "default",
      (defflag == "yes" ? 1 : 0));
  addIntToDict (tcldata->interp, tempDictObj, "index", i->index);
  addStringToDict (tcldata->interp, tempDictObj, "description", i->description);
  nameKey = Tcl_NewStringObj (i->name, -1);
  Tcl_DictObjPut (tcldata->interp, tcldata->dictObj, nameKey, tempDictObj);

/*  printf ("%s %d {%s} {%s}\n", defflag, i->index,
       i->name, i->description); */
  pa_threaded_mainloop_signal (gstate.pamainloop, 0);
}

int pulseaudioObjCmd (
  ClientData cd,
  Tcl_Interp* interp,
  int objc,
  Tcl_Obj * const objv[]
  )
{
  char*         action;
  char*         sinkname;
  int           lt1;
  int           lt2;
  int           rvol;
  int           vol;
  int           rc;
  tclSinkData_t tcldata;
  Tcl_Obj       *dictObj = Tcl_NewDictObj ();

  /* for UTF-8 locales */
  if (objc >= 2) {
    action = Tcl_GetStringFromObj(objv[1], &lt1); /* action */
  }
  if (argcheck (objc, action) != 0) {
    Tcl_WrongNumArgs(interp, 1, objv, "action ?sink-name? ?vol?");
    return TCL_ERROR;
  }
  sinkname = NULL;
  vol = 0;
  rvol = 0;
  dictObj = NULL;
  if (strcmp (action, "getvolume") == 0 ||
      strcmp (action, "setvolume") == 0) {
    sinkname = Tcl_GetStringFromObj(objv[2], &lt2); /* sink-name */
    if (strcmp (action, "setvolume") == 0) {
      rc = Tcl_GetIntFromObj(interp, objv[3], &vol);
      if (rc != TCL_OK) {
        return TCL_ERROR;
      }
      rvol = vol;
    }
  } else if (strcmp (action, "getsinklist") == 0) {
    dictObj = Tcl_NewDictObj ();
    tcldata.interp = interp;
    tcldata.dictObj = dictObj;
  }

  rc = process (action, sinkname, &rvol, &tcldata);
  if (rc != 0) {
    return TCL_ERROR;
  }
  /*
   * If this program is made efficient by keeping a connection open
   * to pulseaudio, then pulseaudio will reset the audio track volume
   * when a new track starts.
   */
  pulse_disconnect ();
  pulse_close ();

  if (strcmp (action, "getvolume") == 0 ||
      strcmp (action, "setvolume") == 0) {
    Tcl_SetObjResult (interp, Tcl_NewIntObj (rvol));
  } else if (strcmp (action, "getsinklist") == 0) {
    Tcl_SetObjResult (interp, dictObj);
  }
  return TCL_OK;
}

int
Pulseaudio_Init (Tcl_Interp *interp)
{
  Tcl_Encoding utf;
#ifdef USE_TCL_STUBS
  if (!Tcl_InitStubs(interp,"8.3",0)) {
    return TCL_ERROR;
  }
#else
  if (!Tcl_PkgRequire(interp,"Tcl","8.3",0)) {
    return TCL_ERROR;
  }
#endif
  Tcl_CreateObjCommand(interp,"pulseaudio", pulseaudioObjCmd, (ClientData) NULL, NULL);
  Tcl_PkgProvide(interp,"pulseaudio","0.1");

  return TCL_OK;
}

#endif /* PA_TCL_INTERFACE */

int
process (char *action, char *sinkname, int *vol, void *tcldata)
{
  pa_operation          *op;
  pa_cvolume            pacvolume;
  int                   retval;
  callback_t            cbdata;
  int                   tvol;

  if (! ginit) {
    gstate.pacontext = NULL;
    gstate.pamainloop = pa_threaded_mainloop_new();
    pa_threaded_mainloop_start (gstate.pamainloop);
    ginit = 1;
  }

  if (gstate.pacontext == NULL) {
    init_context ();
  }

  pa_threaded_mainloop_lock (gstate.pamainloop);
  while (gstate.state == STATE_WAIT) {
    pa_threaded_mainloop_wait (gstate.pamainloop);
  }
  pa_threaded_mainloop_unlock (gstate.pamainloop);

  if (gstate.pastate != PA_CONTEXT_READY) {
    processfailure ("init context");
    return -1;
  }

  if (strcmp (action, "close") == 0) {
    pulse_disconnect ();
    pulse_close ();
  } else if (strcmp (action, "getsinklist") == 0) {
    char            *defsinkname;
    getSinkData_t   sinkdata;

    defsinkname = NULL;
    pa_threaded_mainloop_lock (gstate.pamainloop);
    op = pa_context_get_server_info (
        gstate.pacontext, &serverInfoCallback, &cbdata);
    if (! op) {
      pa_threaded_mainloop_unlock (gstate.pamainloop);
      processfailure ("serverinfo");
      return -1;
    }
    waitop (op);
    pa_threaded_mainloop_unlock (gstate.pamainloop);
    defsinkname = cbdata.defname;

    cbdata.sinkdata = &sinkdata;
    sinkdata.defname = defsinkname;
    sinkdata.tcldata = tcldata;
    pa_threaded_mainloop_lock (gstate.pamainloop);
    op = pa_context_get_sink_info_list (
        gstate.pacontext, &getSinkCallback, &cbdata);
    if (! op) {
      pa_threaded_mainloop_unlock (gstate.pamainloop);
      processfailure ("getsink");
      return -1;
    }
    waitop (op);
    pa_threaded_mainloop_unlock (gstate.pamainloop);
    if (defsinkname != NULL) {
      free (defsinkname);
    }
  } else {
    /* getvolume or setvolume */
    pa_volume_t     avgvol;
    pa_cvolume      pavol;

    cbdata.vol = &pavol;
    pa_threaded_mainloop_lock (gstate.pamainloop);
    op = pa_context_get_sink_info_by_name (
        gstate.pacontext, sinkname, &sinkVolCallback, &cbdata);
    if (! op) {
      pa_threaded_mainloop_unlock (gstate.pamainloop);
      processfailure ("getsinkbyname");
      return -1;
    }
    waitop (op);
    pa_threaded_mainloop_unlock (gstate.pamainloop);

    if (strcmp (action, "setvolume") == 0) {
      pa_cvolume  *nvol;

      tvol = (int) round ((double) *vol * (double) PA_VOLUME_NORM / 100.0);
      if (tvol > PA_VOLUME_MAX) {
        tvol = PA_VOLUME_MAX;
      }

      if (pavol.channels <= 0) {
        pavol.channels = 2;
          /* make sure this is set properly       */
          /* otherwise pa_cvolume_set will fail   */
      }
      nvol = pa_cvolume_set (&pavol, pavol.channels, tvol);

      pa_threaded_mainloop_lock (gstate.pamainloop);
      op = pa_context_set_sink_volume_by_name (
          gstate.pacontext, sinkname, nvol, nullCallback, &cbdata);
      if (! op) {
        pa_threaded_mainloop_unlock (gstate.pamainloop);
        processfailure ("setvol");
        return -1;
      }
      waitop (op);
      pa_threaded_mainloop_unlock (gstate.pamainloop);
    }

    pa_threaded_mainloop_lock (gstate.pamainloop);
    op = pa_context_get_sink_info_by_name (
        gstate.pacontext, sinkname, &sinkVolCallback, &cbdata);
    if (! op) {
      pa_threaded_mainloop_unlock (gstate.pamainloop);
      processfailure ("getvol");
      return -1;
    }
    waitop (op);
    pa_threaded_mainloop_unlock (gstate.pamainloop);

    avgvol = pa_cvolume_avg (&pavol);
    *vol = (int) round ((double) avgvol / (double) PA_VOLUME_NORM * 100.0);
  }

  return 0;
}

WJG (20/12/16) Gnocl has a wrapping about the native Gtk Volume Button widget. Although the Gtk offering provides no direct control over the audio output this is still easily obtained by using amixer or pulseaudio. The following snippet shows how to do this with amixer. Adjustments can be made via mouse click and drag or with the mouseWheel button. Feedback on new level settings displayed in the widget tooltip.

proc getVol {} {
set vol [exec amixer sget Master]
set a [string first \[ $vol]
set b [string first \] $vol]
return "0.[string range $vol $a+1 $b-2]"
}

gnocl::window -child [gnocl::volumeButton \
    -prelightBackgroundColor green \
    -value [getVol] \
    -onValueChanged { exec amixer sset 'Master' [expr 100*%v] } ]

bll 2016-12-20 (cleanup: put in some backslashes) Notes: amixer is the ALSA (Advanced Linux Sound Architecture - the base level underneath everything else) mixer volume control and the above changes the ALSA master volume. Pulseaudio lives in the middle between the applications and ALSA.

amixer doesn't see my external sound device :(. Using exec I can run a volume command about every 200 milliseconds without any problem. With the Tcl stub, I have the volume timeslice set to 100 milliseconds and can probably do 50 milliseconds if I want to.