Mac OS X: volume control

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

bll 2017-2-16: The Swift version does not work with tclkits due to incorrect linkage with the tclStub library.


Example Usage

load [file join [pwd] macvolume[info sharedlibextension]]
set vol [macvolume] ; # get the volume
incr vol -10
set newvol [macvolume $vol] ; # set the volume, always returns the current volume.

Objective-C Version

mkvol.sh

#!/bin/bash
set -x

lpath=/Users/bll/tcl/build/tcl/Deployment
ipath=/Users/bll/tcl/build/tcl/Tcl.framework/Versions/8.6/Headers
tcllib=libtclstub8.6.a
tcllibnm=tclstub8.6

clang \
    -mmacosx-version-min=10.9 \
    -framework Cocoa \
    -framework AudioToolbox \
    -o macvolume \
    main.m volume.m
clang \
    -mmacosx-version-min=10.9 \
    -framework Cocoa \
    -framework AudioToolbox \
    -shared -fPIC \
    -o macvolume.dylib \
    -DUSE_TCL_STUBS \
    -I$ipath \
    volume.m tclmacvol.m \
    -L$lpath -l$tcllibnm

volume.m

#import "AudioToolbox/AudioServices.h"
#import "Foundation/NSObject.h"
#include <stdio.h>
#include <stdlib.h>
#include <MacTypes.h>

/* 
 * has objective-c code to get all output devices...
 * http://stackoverflow.com/questions/11347989/get-built-in-output-from-core-audio
 */

int
macvolume_cmd (int set, int vol)
{
  AudioDeviceID outputDeviceID;
  UInt32 outputDeviceIDSize = sizeof (outputDeviceID);
  OSStatus status;
  AudioObjectPropertyAddress propertyAOPA;
  Float32 volume;
  UInt32 volumeSize = sizeof (volume);
  int ivol;

  propertyAOPA.mSelector = kAudioHardwarePropertyDefaultOutputDevice;
  propertyAOPA.mScope = kAudioObjectPropertyScopeGlobal;
  propertyAOPA.mElement = kAudioObjectPropertyElementMaster;

  status = AudioHardwareServiceGetPropertyData(
      kAudioObjectSystemObject,
      &propertyAOPA,
      0,
      NULL,
      &outputDeviceIDSize,
      &outputDeviceID);

  if (outputDeviceID == kAudioObjectUnknown) {
    return 0;
  }

  propertyAOPA.mSelector = kAudioHardwareServiceDeviceProperty_VirtualMasterVolume;
  propertyAOPA.mScope = kAudioDevicePropertyScopeOutput;
  propertyAOPA.mElement = kAudioObjectPropertyElementMaster;

  if (set == 1) {
    volume = (Float32) vol / 100.0;
    AudioHardwareServiceSetPropertyData(
        outputDeviceID,
        &propertyAOPA,
        0,
        NULL,
        volumeSize,
        &volume);
  }

  AudioHardwareServiceGetPropertyData(
      outputDeviceID,
      &propertyAOPA,
      0,
      NULL,
      &volumeSize,
      &volume);
  ivol = (int) round(volume*100.0);
  return ivol;
}

main.m

#include <stdio.h>
#include <stdlib.h>

extern int macvolume_cmd (int, int);

int
main (int argc, const char * argv[])
{
  int set = 0;
  int vol = 0;
  if (argc > 1) {
    set = 1;
    vol = atoi (argv[1]);
  }
  printf ("%d\n", macvolume_cmd(set, vol));
}

tclmacvol.m

#include <stdlib.h>
#include <tcl.h>

extern int macvolume_cmd (int, int);

int
Macvolume_Cmd (
  ClientData cd,
  Tcl_Interp *interp,
  int objc,
  Tcl_Obj *const objv[] )
{
  int   vol = 0;
  int   rvol;
  int   rc = 0;
  int   set = 0;

  if (objc != 1 && objc != 2) {
    Tcl_WrongNumArgs(interp, 1, objv, NULL);
    return TCL_ERROR;
  }
  if (objc == 2) {
    rc = Tcl_GetIntFromObj (interp, objv[1], &vol);
    if (rc != TCL_OK) {
      return TCL_ERROR;
    }
    set = 1;
  }

  rvol = macvolume_cmd (set, vol);
  Tcl_SetObjResult(interp, Tcl_NewIntObj(rvol));
  return TCL_OK;
}

int
Macvolume_Init (Tcl_Interp *interp)
{
  if (! Tcl_InitStubs(interp, TCL_VERSION, 0)) {
    return TCL_ERROR;
  }

  Tcl_CreateObjCommand (interp, "macvolume", Macvolume_Cmd, NULL, NULL);
  Tcl_PkgProvide (interp, "macvolume", "0.1");
  return TCL_OK;
}

Swift Version

This version does not work with a tclkit due to incorrect linkage with the stub library.

mkvol.sh

#!/bin/bash
set -x

lpath=/Users/bll/tcl/build/tcl/Deployment
ipath=/Users/bll/tcl/build/tcl/Tcl.framework/Versions/8.6/Headers
tcllib=libtclstub8.6.a
tcllibnm=tclstub8.6
swiftc -O -o macvolume volume.swift main.swift
swiftc -O -emit-library \
    -Xcc -DUSE_TCL_STUBS \
    -I$ipath -import-objc-header tclbridge.h \
    volume.swift tclmacvol.swift \
    -Xlinker -undefined -Xlinker dynamic_lookup \
    -Xlinker -L$lpath \
    -l$tcllibnm \
    -o macvolume.dylib

volume.swift

import AudioToolbox

func macvolume_cmd (set: Int32, vol: Int32) -> Int32 
{
  var defaultOutputDeviceID = AudioDeviceID(0)
  var defaultOutputDeviceIDSize = UInt32(MemoryLayout.size(ofValue:defaultOutputDeviceID))

  var getDefaultOutputDevicePropertyAddress = AudioObjectPropertyAddress(
      mSelector: AudioObjectPropertySelector(kAudioHardwarePropertyDefaultOutputDevice),
      mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal),
      mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster))

  AudioObjectGetPropertyData(
      AudioObjectID(kAudioObjectSystemObject),
      &getDefaultOutputDevicePropertyAddress,
      0,
      nil,
      &defaultOutputDeviceIDSize,
      &defaultOutputDeviceID)

  var volume = Float32()
  var volumeSize = UInt32(MemoryLayout.size(ofValue:volume))

  var volumePropertyAddress = AudioObjectPropertyAddress(
      mSelector: AudioObjectPropertySelector(kAudioHardwareServiceDeviceProperty_VirtualMasterVolume),
      mScope: AudioObjectPropertyScope(kAudioDevicePropertyScopeOutput),
      mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster))

  if (set == 1) {
    volume = Float(vol) / 100.0
    AudioHardwareServiceSetPropertyData(
        defaultOutputDeviceID,
        &volumePropertyAddress,
        0,
        nil,
        volumeSize,
        &volume)
  }

  AudioHardwareServiceGetPropertyData(
      defaultOutputDeviceID,
      &volumePropertyAddress,
      0,
      nil,
      &volumeSize,
      &volume)
  let ivol = Int32(round(volume*100.0))
  return ivol
}

main.swift

import Foundation

var vol = Int32(0)
var set = Int32(0)

if (CommandLine.argc > 1) {
  vol = Int32((CommandLine.arguments[1] as NSString).integerValue)
  set = 1
}
var rvol = Int32()
rvol = macvolume_cmd (set:set, vol:vol)
print ("\(rvol)");

tclbridge.h

#include "tcl.h"

tclmacvol.swift

func Macvolume_Cmd(cdata: ClientData?,
    interp: UnsafeMutablePointer<Tcl_Interp>?,
    objc: Int32,
    objv: UnsafePointer<UnsafeMutablePointer<Tcl_Obj>?>?) -> Int32 
{
  var vol = Int32(0)
  var rc = Int32(0)
  var set = Int32(0)
  if (objc != 1 && objc != 2) {
    Tcl_WrongNumArgs(interp, 1, objv, nil)
    return TCL_ERROR
  }
  if (objc == 2) {
    rc = Tcl_GetIntFromObj (interp, objv![1], &vol);
    if (rc == 0) { } // clear compiler warning
    set = 1
  }
  var rvol = Int32()link problems.
  rvol = macvolume_cmd (set:set, vol:vol)
  Tcl_SetObjResult(interp, Tcl_NewIntObj(rvol))
  return TCL_OK
}

@_cdecl("Macvolume_Init")
public func Macvolume_Init(
    interp: UnsafeMutablePointer<Tcl_Interp>) -> Int32 
{
  if Tcl_InitStubs(interp, TCL_VERSION, 0) == nil {
      return TCL_ERROR;
  }

  Tcl_CreateObjCommand(interp, "::macvolume", Macvolume_Cmd, nil, nil);
  Tcl_PkgProvide(interp, "macvolume", "0.1");
  return TCL_OK
}