Version 0 of Linux: volume control

Updated 2016-12-19 17:32:22 by bll

Linux: volume control

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

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.

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>
 *
 * 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;

void
serverInfoCallback (
  pa_context            *context,
  const pa_server_info  *i,
  void                  *userdata)
{
  char **defname = (char **) userdata;
  *defname = strdup (i->default_sink_name);
}

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

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

void
connCallback (pa_context* pacontext, void* state)
{
  int *stateptr = (int *) state;

  if (pacontext == NULL) {
    *stateptr = -1;
    return;
  }

  switch (pa_context_get_state (pacontext))
  {
    case PA_CONTEXT_READY:
      *stateptr = 0;
      break;
    case PA_CONTEXT_FAILED:
      *stateptr = -1;
      break;
    case PA_CONTEXT_UNCONNECTED:
    case PA_CONTEXT_AUTHORIZING:
    case PA_CONTEXT_SETTING_NAME:
    case PA_CONTEXT_CONNECTING:
    case PA_CONTEXT_TERMINATED:
      break;
  }
}

void
volSetCallback (
  pa_context* context,
  int success,
  void* userdata)
{
}


void
waitop (
  pa_mainloop  *pamainloop,
  pa_operation *op)
{
  int retval;

  while (pa_operation_get_state (op) == PA_OPERATION_RUNNING) {
    pa_mainloop_iterate (pamainloop, 1, &retval);
  }
  pa_operation_unref(op);
}

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

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

#ifndef PA_TCL_INTERFACE

void
getSinkCallback (
  pa_context            *context,
  const pa_sink_info    *i,
  int                   eol,
  void                  *userdata)
{
  getSinkData_t *sinkdata = (getSinkData_t *) userdata;
  char *defflag = "no";

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

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

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

  if (argcheck (argc, argv[1]) != 0) {
    fprintf (stderr,
        "Usage: %s {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], "getsinklist") != 0) {
    printf ("%d\n", vol);
  }
  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)
{
  getSinkData_t *sinkdata = (getSinkData_t *) userdata;
  tclSinkData_t *tcldata;
  char          *defflag = "no";
  Tcl_Obj       *dictObj;
  Tcl_Obj       *tempDictObj;
  Tcl_Obj       *nameKey;

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

  tcldata = 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); */
}

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, "getsinklist") != 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 {
    dictObj = Tcl_NewDictObj ();
    tcldata.interp = interp;
    tcldata.dictObj = dictObj;
  }
  rc = process (action, sinkname, &rvol, &tcldata);
  if (rc != 0) {
    return TCL_ERROR;
  }

  if (strcmp (action, "getsinklist") == 0) {
    Tcl_SetObjResult (interp, dictObj);
  } else {
    Tcl_SetObjResult (interp, Tcl_NewIntObj (rvol));
  }
  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_mainloop       *pamainloop;
  pa_mainloop_api   *paapi;
  pa_context        *pacontext;
  pa_operation      *op;
  pa_cvolume        pacvolume;
  int               retval;
  int               state;
  int               tvol;

  pamainloop = pa_mainloop_new();
  paapi = pa_mainloop_get_api (pamainloop);
  pacontext = pa_context_new (paapi, "ballroomdj");
  if (pacontext == NULL) {
    pa_mainloop_free (pamainloop);
    return -1;
  }
  pa_context_set_state_callback (pacontext, &connCallback, &state);

  state = 1;
  pa_context_connect (pacontext, NULL, PA_CONTEXT_NOFLAGS, NULL);
  while (state == 1) {
    pa_mainloop_iterate (pamainloop, 1, &retval);
  }
  if (state < 0) {
    pa_context_disconnect (pacontext);
    pa_context_unref (pacontext);
    pacontext = NULL;
    pa_mainloop_free (pamainloop);
    return -1;
  }

  if (strcmp (action, "getsinklist") == 0) {
    char            *defsinkname;
    getSinkData_t   sinkdata;

    defsinkname = NULL;
    op = pa_context_get_server_info (
        pacontext, &serverInfoCallback, &defsinkname);
    waitop (pamainloop, op);

    sinkdata.defname = defsinkname;
    sinkdata.tcldata = tcldata;
    op = pa_context_get_sink_info_list
        (pacontext, &getSinkCallback, &sinkdata);
    waitop (pamainloop, op);
    if (defsinkname != NULL) {
      free (defsinkname);
    }
  } else {
    pa_volume_t     avgvol;
    pa_cvolume      pavol;

    op = pa_context_get_sink_info_by_name (
        pacontext, sinkname, &sinkVolCallback, &pavol);
    waitop (pamainloop, op);

    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;
      }

      nvol = pa_cvolume_set (&pavol, pavol.channels, tvol);

      op = pa_context_set_sink_volume_by_name (
          pacontext, sinkname, nvol, volSetCallback, NULL);
      waitop (pamainloop, op);
    }

    op = pa_context_get_sink_info_by_name (
        pacontext, sinkname, &sinkVolCallback, &pavol);
    waitop (pamainloop, op);

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

  pa_context_disconnect (pacontext);
  pa_context_unref (pacontext);
  pacontext = NULL;
  pa_mainloop_free (pamainloop);
  return 0;
}