Skip to content

Commit

Permalink
Merge pull request #5110 from elgiano/topic/macos-clip
Browse files Browse the repository at this point in the history
scsynth: macOS: clip values on hw out busses
  • Loading branch information
mossheim authored Nov 28, 2020
2 parents 66dbed5 + 7969bf4 commit f441a41
Show file tree
Hide file tree
Showing 9 changed files with 76 additions and 18 deletions.
4 changes: 4 additions & 0 deletions HelpSource/Classes/ServerOptions.schelp
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ A Boolean indicating whether the server should try to lock its memory into physi
method:: maxLogins
An Integer indicating the maximum number of clients which can simultaneously receive notifications from the server. When using TCP this is also the maximum number of simultaneous connections. This is also used by the language to split ranges of link::Classes/Node##Nodes::, link::Classes/Buffer##Buffers::, or link::Classes/Bus##Busses::. In multi-client situations you will need to set this to at least the number of clients you wish to allow. This must be the same in the Server instances on every client. The default is 1.

method:: safetyClipThreshold
note:: MacOS only::
A Float indicating a safety threshold for output values to be clipped to. This is necessary on macOS because setting a low system volume doesn't prevent output values greater than +/- 1 from sounding extremely loud, which can happen by mistake, e.g. when sending a negative coefficient to a filter. With this threshold, values are clipped just before being written to hardware output busses, which does not affect the recording. However, the signal will be affected if it's above the threshold and the sound is routed to other apps using 3rd-party software.
Defaults to a threshold of 1.26 (ca. 2 dB), to save some ears and still allow some headroom. Setting safetyClipThreshold to code::inf::, code::0::, or a negative value, disables clipping altogether.

subsection:: Other Instance Methods
method:: asOptionsString
Expand Down
6 changes: 6 additions & 0 deletions SCClassLibrary/Common/Control/Server.sc
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ ServerOptions {

var <>bindAddress;

var <>safetyClipThreshold;

*initClass {
defaultValues = IdentityDictionary.newFrom(
(
Expand Down Expand Up @@ -98,6 +100,7 @@ ServerOptions {
recChannels: 2,
recBufSize: nil,
bindAddress: "127.0.0.1",
safetyClipThreshold: 1.26 // ca. 2 dB
)
)
}
Expand Down Expand Up @@ -224,6 +227,9 @@ ServerOptions {
if (maxLogins.notNil, {
o = o ++ " -l " ++ maxLogins;
});
if (thisProcess.platform.name === \osx && Server.program.asString.endsWith("supernova").not && safetyClipThreshold.notNil, {
o = o ++ " -s " ++ safetyClipThreshold;
});
^o
}

Expand Down
5 changes: 5 additions & 0 deletions include/server/ErrorMessage.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
* 02110-1301 USA
*/

// PLEASE NOTE:
// libscsynth API might change across minor versions.
// Always make sure, when using libscsynth as a shared library, that binary and headers come from the same minor
// version.

#pragma once

#include <string>
Expand Down
4 changes: 4 additions & 0 deletions include/server/SC_OSC_Commands.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

// PLEASE NOTE:
// libscsynth API might change across minor versions.
// Always make sure, when using libscsynth as a shared library, that binary and headers come from the same minor
// version.

#pragma once

Expand Down
5 changes: 5 additions & 0 deletions include/server/SC_WorldOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

// PLEASE NOTE:
// libscsynth API might change across minor versions.
// Always make sure, when using libscsynth as a shared library, that binary and headers come from the same minor
// version.

#pragma once

Expand Down Expand Up @@ -49,6 +53,7 @@ struct WorldOptions {

bool mRealTime = true;
bool mMemoryLocking = false;
float mSafetyClipThreshold = 1.26; // ca. 2 dB

const char* mNonRealTimeCmdFilename = nullptr;
const char* mNonRealTimeInputFilename = nullptr;
Expand Down
44 changes: 28 additions & 16 deletions server/scsynth/SC_CoreAudio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "SC_Lib_Cintf.h"
#include "SC_Lock.h"
#include "SC_Time.hpp"
#include "SC_InlineBinaryOp.h"
#include <stdlib.h>
#include <algorithm>

Expand Down Expand Up @@ -332,9 +333,8 @@ void Free_FromEngine_Msg(FifoMsg* inMsg) { World_Free(inMsg->mWorld, inMsg->mDat
SC_AudioDriver::SC_AudioDriver(struct World* inWorld):
mWorld(inWorld),
mSampleTime(0),
mNumSamplesPerCallback(0)

{}
mNumSamplesPerCallback(0),
mSafetyClipThreshold(1.26) {}

SC_AudioDriver::~SC_AudioDriver() {
mRunThreadFlag = false;
Expand Down Expand Up @@ -1223,6 +1223,7 @@ OSStatus appIOProcSeparateIn(AudioDeviceID device, const AudioTimeStamp* inNow,
return kAudioHardwareNoError;
}

template <bool IsClipping>
OSStatus appIOProc(AudioDeviceID device, const AudioTimeStamp* inNow, const AudioBufferList* inInputData,
const AudioTimeStamp* inInputTime, AudioBufferList* outOutputData,
const AudioTimeStamp* inOutputTime, void* defptr) {
Expand Down Expand Up @@ -1255,15 +1256,16 @@ OSStatus appIOProc(AudioDeviceID device, const AudioTimeStamp* inNow, const Audi
def->mPrevSampleTime = sampleTime;

if (!def->UseSeparateIO()) {
def->Run(inInputData, outOutputData, oscTime);
def->Run<IsClipping>(inInputData, outOutputData, oscTime);
return kAudioHardwareNoError;
}


def->Run(def->mInputBufList, outOutputData, oscTime);
def->Run<IsClipping>(def->mInputBufList, outOutputData, oscTime);
return kAudioHardwareNoError;
}

template <bool IsClipping>
void SC_CoreAudioDriver::Run(const AudioBufferList* inInputData, AudioBufferList* outOutputData, int64 oscTime) {
int64 systemTimeBefore = AudioGetCurrentHostTime();
World* world = mWorld;
Expand Down Expand Up @@ -1375,15 +1377,21 @@ void SC_CoreAudioDriver::Run(const AudioBufferList* inInputData, AudioBufferList
if (nchan == 1) {
if (outputTouched[b] == bufCounter) {
for (int k = 0; k < bufFrames; ++k) {
bufdata[k] = busdata[k];
if (IsClipping)
bufdata[k] = sc_clip2(busdata[k], mSafetyClipThreshold);
else
bufdata[k] = busdata[k];
}
}
} else {
int minchan = sc_min(nchan, numOutputBuses - b);
for (int j = 0; j < minchan; ++j, busdata += bufFrames) {
if (outputTouched[b + j] == bufCounter) {
for (int k = 0, m = j; k < bufFrames; ++k, m += nchan) {
bufdata[m] = busdata[k];
if (IsClipping)
bufdata[m] = sc_clip2(busdata[k], mSafetyClipThreshold);
else
bufdata[m] = busdata[k];
}
}
}
Expand Down Expand Up @@ -1598,6 +1606,8 @@ bool SC_CoreAudioDriver::DriverStart() {
if (mWorld->mVerbosity >= 1) {
scprintf("->SC_CoreAudioDriver::DriverStart\n");
}

auto appIOProcFunc = isClippingEnabled() ? appIOProc<true> : appIOProc<false>;
OSStatus err = kAudioHardwareNoError;
// AudioTimeStamp now;
UInt32 propertySize;
Expand All @@ -1617,7 +1627,7 @@ bool SC_CoreAudioDriver::DriverStart() {
// err = AudioDeviceAddIOProc(mOutputDevice, appIOProc, (void *) this); // setup Out device with an
// IO proc

err = AudioDeviceCreateIOProcID(mOutputDevice, appIOProc, (void*)this, &mOutputID);
err = AudioDeviceCreateIOProcID(mOutputDevice, appIOProcFunc, (void*)this, &mOutputID);
if (err != kAudioHardwareNoError) {
scprintf("AudioDeviceAddIOProc failed %s %d\n", &err, (int)err);
return false;
Expand Down Expand Up @@ -1663,7 +1673,7 @@ bool SC_CoreAudioDriver::DriverStart() {
err = AudioObjectIsPropertySettable(mOutputDevice, &propertyAddress, &writable);

AudioHardwareIOProcStreamUsage* su = (AudioHardwareIOProcStreamUsage*)malloc(propertySize);
su->mIOProc = (void*)appIOProc;
su->mIOProc = (void*)appIOProcFunc;

err = AudioObjectGetPropertyData(mOutputDevice, &propertyAddress, 0, NULL, &propertySize, su);

Expand All @@ -1682,15 +1692,15 @@ bool SC_CoreAudioDriver::DriverStart() {
return false;
}

err = AudioDeviceStart(mOutputDevice, appIOProc); // start playing sound through the device
err = AudioDeviceStart(mOutputDevice, appIOProcFunc); // start playing sound through the device
if (err != kAudioHardwareNoError) {
scprintf("AudioDeviceStart failed %d\n", (int)err);
err = AudioDeviceStop(mInputDevice, appIOProcSeparateIn); // stop playing sound through the device
return false;
}
} else {
// err = AudioDeviceAddIOProc(mOutputDevice, appIOProc, (void *) this); // setup our device with an IO proc
err = AudioDeviceCreateIOProcID(mOutputDevice, appIOProc, (void*)this, &mOutputID);
err = AudioDeviceCreateIOProcID(mOutputDevice, appIOProcFunc, (void*)this, &mOutputID);

if (err != kAudioHardwareNoError) {
scprintf("AudioDeviceAddIOProc failed %d\n", (int)err);
Expand All @@ -1706,7 +1716,7 @@ bool SC_CoreAudioDriver::DriverStart() {
err = AudioObjectIsPropertySettable(mOutputDevice, &propertyAddress, &writable);

AudioHardwareIOProcStreamUsage* su = (AudioHardwareIOProcStreamUsage*)malloc(propertySize);
su->mIOProc = (void*)appIOProc;
su->mIOProc = (void*)appIOProcFunc;

err = AudioObjectGetPropertyData(mOutputDevice, &propertyAddress, 0, NULL, &propertySize, su);

Expand All @@ -1728,7 +1738,7 @@ bool SC_CoreAudioDriver::DriverStart() {
err = AudioObjectIsPropertySettable(mOutputDevice, &propertyAddress, &writable);

AudioHardwareIOProcStreamUsage* su = (AudioHardwareIOProcStreamUsage*)malloc(propertySize);
su->mIOProc = (void*)appIOProc;
su->mIOProc = (void*)appIOProcFunc;

err = AudioObjectGetPropertyData(mOutputDevice, &propertyAddress, 0, NULL, &propertySize, su);

Expand All @@ -1741,7 +1751,7 @@ bool SC_CoreAudioDriver::DriverStart() {
err = AudioObjectSetPropertyData(mOutputDevice, &propertyAddress, 0, NULL, propertySize, su);
}

err = AudioDeviceStart(mOutputDevice, appIOProc); // start playing sound through the device
err = AudioDeviceStart(mOutputDevice, appIOProcFunc); // start playing sound through the device
if (err != kAudioHardwareNoError) {
scprintf("AudioDeviceStart failed %d\n", (int)err);
return false;
Expand Down Expand Up @@ -1784,8 +1794,10 @@ bool SC_CoreAudioDriver::DriverStop() {
}
OSStatus err = kAudioHardwareNoError;

auto appIOProcFunc = isClippingEnabled() ? appIOProc<true> : appIOProc<false>;

if (UseSeparateIO()) {
err = AudioDeviceStop(mOutputDevice, appIOProc);
err = AudioDeviceStop(mOutputDevice, appIOProcFunc);
if (err != kAudioHardwareNoError) {
scprintf("Output AudioDeviceStop failed %p\n", err);
return false;
Expand All @@ -1809,7 +1821,7 @@ bool SC_CoreAudioDriver::DriverStop() {
return false;
}
} else {
err = AudioDeviceStop(mOutputDevice, appIOProc);
err = AudioDeviceStop(mOutputDevice, appIOProcFunc);
if (err != kAudioHardwareNoError) {
scprintf("AudioDeviceStop B failed %p\n", err);
return false;
Expand Down
6 changes: 6 additions & 0 deletions server/scsynth/SC_CoreAudio.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ class SC_AudioDriver {
struct World* mWorld;
double mOSCtoSamples;
int mSampleTime;
float mSafetyClipThreshold;

// Common members
uint32 mHardwareBufferSize; // bufferSize returned by kAudioDevicePropertyBufferSize
Expand Down Expand Up @@ -210,6 +211,7 @@ class SC_AudioDriver {
int NumSamplesPerCallback() const { return mNumSamplesPerCallback; }
void SetPreferredHardwareBufferFrameSize(int inSize) { mPreferredHardwareBufferFrameSize = inSize; }
void SetPreferredSampleRate(int inRate) { mPreferredSampleRate = inRate; }
void SetSafetyClipThreshold(float thr) { mSafetyClipThreshold = thr; }

bool SendMsgToEngine(FifoMsg& inMsg); // called by NRT thread
bool SendMsgFromEngine(FifoMsg& inMsg);
Expand Down Expand Up @@ -237,6 +239,7 @@ class SC_CoreAudioDriver : public SC_AudioDriver {
AudioStreamBasicDescription inputStreamDesc; // info about the default device
AudioStreamBasicDescription outputStreamDesc; // info about the default device

template <bool IsClipping>
friend OSStatus appIOProc(AudioDeviceID inDevice, const AudioTimeStamp* inNow, const AudioBufferList* inInputData,
const AudioTimeStamp* inInputTime, AudioBufferList* outOutputData,
const AudioTimeStamp* inOutputTime, void* defptr);
Expand All @@ -246,6 +249,8 @@ class SC_CoreAudioDriver : public SC_AudioDriver {
AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime,
void* defptr);

bool isClippingEnabled() const { return mSafetyClipThreshold > 0 && mSafetyClipThreshold < INFINITY; }

protected:
// Driver interface methods
virtual bool DriverSetup(int* outNumSamplesPerCallback, double* outSampleRate);
Expand All @@ -263,6 +268,7 @@ class SC_CoreAudioDriver : public SC_AudioDriver {

bool StopStart();

template <bool IsClipping>
void Run(const AudioBufferList* inInputData, AudioBufferList* outOutputData, int64 oscTime);

bool UseInput() { return mInputDevice != kAudioDeviceUnknown; }
Expand Down
3 changes: 3 additions & 0 deletions server/scsynth/SC_World.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,9 @@ World* World_New(WorldOptions* inOptions) {
hw->mAudioDriver = SC_NewAudioDriver(world);
hw->mAudioDriver->SetPreferredHardwareBufferFrameSize(inOptions->mPreferredHardwareBufferFrameSize);
hw->mAudioDriver->SetPreferredSampleRate(inOptions->mPreferredSampleRate);
#ifdef __APPLE__
hw->mAudioDriver->SetSafetyClipThreshold(inOptions->mSafetyClipThreshold);
#endif

if (inOptions->mLoadGraphDefs) {
World_LoadGraphDefs(world);
Expand Down
17 changes: 15 additions & 2 deletions server/scsynth/scsynth_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ void Usage() {
" UDP ports never require passwords, so for security use TCP.\n"
" -N <cmd-filename> <input-filename> <output-filename> <sample-rate> <header-format> <sample-format>\n"
#ifdef __APPLE__
" -s <safety-clip-threshold> (default %d)\n"
" absolute amplitude value output signals will be clipped to.\n"
" Set to <= 0 or inf to completely disable output clipping.\n"
" -I <input-streams-enabled>\n"
" -O <output-streams-enabled>\n"
#endif
Expand Down Expand Up @@ -111,7 +114,12 @@ void Usage() {
defaultOptions.mPreferredHardwareBufferFrameSize, defaultOptions.mPreferredSampleRate,
defaultOptions.mNumBuffers, defaultOptions.mMaxNodes, defaultOptions.mMaxGraphDefs,
defaultOptions.mRealTimeMemorySize, defaultOptions.mMaxWireBufs, defaultOptions.mNumRGens,
defaultOptions.mRendezvous, defaultOptions.mLoadGraphDefs, defaultOptions.mMaxLogins);
defaultOptions.mRendezvous, defaultOptions.mLoadGraphDefs, defaultOptions.mMaxLogins
#ifdef __APPLE__
,
defaultOptions.mSafetyClipThreshold
#endif
);
exit(1);
}

Expand Down Expand Up @@ -140,7 +148,8 @@ int scsynth_main(int argc, char** argv) {
WorldOptions options;

for (int i = 1; i < argc;) {
if (argv[i][0] != '-' || argv[i][1] == 0 || strchr("utBaioczblndpmwZrCNSDIOMHvVRUhPL", argv[i][1]) == nullptr) {
if (argv[i][0] != '-' || argv[i][1] == 0
|| strchr("utBaioczblndpmwZrCNSDIOsMHvVRUhPL", argv[i][1]) == nullptr) {
scprintf("ERROR: Invalid option %s\n", argv[i]);
Usage();
}
Expand Down Expand Up @@ -248,6 +257,10 @@ int scsynth_main(int argc, char** argv) {
break;
case 'M':
#endif
case 's':
checkNumArgs(2);
options.mSafetyClipThreshold = atof(argv[j + 1]);
break;
case 'H':
checkNumArgs(2);
options.mInDeviceName = argv[j + 1];
Expand Down

0 comments on commit f441a41

Please sign in to comment.