Peaking During SC Runtime Playback Sounds Different Compared to `Recorder` Output

When recording sound, for example the following:

s.waitForBoot({
	var def = SynthDef.new(\intentionalPeak, { arg out;
		var signal = Ringz.ar(EnvGen.ar(Env.perc()));
		Out.ar(out, signal.dup);
		DetectSilence.ar(signal, doneAction: Done.freeSelf);
	});

	fork {
		def.play;
		s.record;
		1.wait;
		s.stopRecording;
	};
});

In the SC IDE, the signal the def.play outputs does not peak for me (windows). However, the recorded .wav file has significant peaking and clipping.
I am aware of how to prevent this in a SynthDef and how Ringz should not be recorded raw this way, my question is more about workflow. When doing sound design, how can I make sure the recorded output and the output I hear in the SC IDE are the same? Ideally the def.play here would also peak in the IDE, this way I notice it before pressing record and can ameliorate it using Compander or a similar tool.

The server volume is at ±0db. I tried setting the server-wide Volumes lag to 0 (or 1.0) but this did not change the IDE applying dynamics control, the above still does not peak.

How can I make sure the signal outputted by the IDE and the .wav recorded using record sound the same during live coding playback?

Thank you.

The volume you hear depends both on the SC volume setting and on the system volume. If the system volume is low, then you could have a chain like this:

  1. Synth produces amplitude > 1.0 (peaking) – this is what gets recorded. If you’re using an integer sample format, out of range samples get clipped.
  2. Sound goes out to the audio system in floating point format.
  3. System volume control scales the out of range signal down so that it’s in range.

Then the system output sound is free of distortion (because of the downscaling), but the recorded sound is distorted because the system volume control didn’t affect it.

I’d verify using s.meter – because the system volume control is involved, you can’t trust your ears alone to tell you if there’s distortion.

Also, I always scale everything down in every SynthDef. I never ever pass a full scale signal through to the audio hardware, ever. It’s a good habit to get into: always have an amp parameter, whose default is never higher than 0.5.

hjh

1 Like

Thank you for the reply

s.meter does report 100%, obviously it does peak in reality I’m just not sure why my system results in audible distortion depending on which app the sound is coming from.

As a test I recorded the clipping audio to a .wav with sample type double, put the system volume to 100%, then instead of using the above synth def I played the recorded file in both Audacity, and using the raw sclang.exe (not the IDE) using a PlayBuf this time. So both ways it read the same .wav from the disk, it didn’t generate the ring in real time.
Again, only audacity sounded distorted, not SC, so it definitely has to do with how SC pushes samples to the operating system. I tried it on Linux ALSA with the same behavior.

If someone has any other advice I would be thankful, but I think the real solution is to use amp and keep an eye on the meter or create a class that warns me anytime the audio starts clipping, so I am being notified using objective data, not my subjective experience

The most likely explanation is that the other app clips the audio before sending it to the hardware, and SC doesn’t.

Compare:

(
{
	var sig = SinOsc.ar(440) * 3;
	var pair = [
		sig,              // like SC's output
		sig.clip(-1, 1)   // like other app
	];
	pair * 0.1  // like system volume control = -20 dB
}.plot;
)

As a test I recorded the clipping audio to a .wav with sample type double

float maybe? AFAICS double is not a supported sample format.

Could you share the file (or a short edit), and the SC PlayBuf code?

(I’ll be offline about 12 hours later, for the better part of a day or more, so I might not answer quickly.)

hjh

I tested your code on Windows 11 via Parallels Desktop on macOS 15.7.7 and natively on macOS 15.7.7.
I hear

  • clipped sound on macOS
  • no clipped sound on Windows

On Ubuntu and Windows 11 (via Parallels Desktop on macOS 15.7.7) and on macOS 15.7.7, I tested the following code:

(
s.waitForBoot {
	SynthDef(\intentionalPeak, { |out = 0|
		var env = Env.sine.ar(Done.freeSelf) * 2 ! 2;
		var signal = SinOsc.ar * env;		
		Out.ar(out, signal);
	}).add;
	
	s.sync;
	
	s.record;
	
	s.sync;
	
	Synth(\intentionalPeak);
	
	2.wait;
	
	s.stopRecording;
}
)
  • On macOS, I hear clipped sound.
  • On Windows, I do not hear clipped sound.
  • On Ubuntu, I cannot judge exactly due to sound quality, but the sound seems to be clipped.

Windows seems to do extra processing against clipping…?

Thank you to the others for testing this on different OS. It appears to be OS-dependent (or potentially audio driver) related behavior, so I don’t think I can do anything about it.

I wrote this watcher class that monitors a bus at all times and prints a warning when peaking occurs. This solves the issue for me, I no long need to rely on just my ears.

Code review welcome, I am still relatively new to SC. I changed the title of the topic so someone googling a similar issue could come across it. Thank you

PeakSentinel {
	classvar <allSentinels; // static list of all active sentinels

	var <monitoredBus;
	var <oscFunc, <oscID;
	var <>synth, <synthDef;

	*initClass {
		allSentinels = Dictionary.new;
	}

	init { arg server, bus, hash;
		var genSynthDef = { arg numChannels;
			var name = ("peakSentinelMonitor_" ++ numChannels.asString).asSymbol;
			SynthDef(name, { arg bus = 0, oscID = -1;
				var signal = In.ar(bus, numChannels);

				// send an OSC message anytime a new all-time peaked is encountered
				var runningMax = RunningMax.ar(
					signal.reduce('max').abs,
					0 // never resets
				);
				var newPeak = (HPZ1.ar(runningMax) > 0) * (runningMax >= 1.0);

				SendReply.ar(
					Changed.ar(newPeak),
					'/peak_sentinel_print_warning',
					[ bus, runningMax ],
					oscID
				);
			});
		};

		synthDef = genSynthDef.(bus.numChannels);
		synthDef.add;

		if (bus.rate != \audio) {
			Error("In PeakSentinel.init: bus is not an audio bus").throw;
		};

		oscID = UniqueID.next;
		oscFunc = OSCdef.new(("peakSentinelWarn_" ++ hash).asSymbol, { arg msg, time, addr, recvPort;
			var bus = msg[3].asInteger;
			var peakValue = msg[4];
			"In PeakSentinel: Clipping detected on Bus `%` (Peak Value: `%`)".format(
				bus,
				peakValue
			).warn;
		}, '/peak_sentinel_print_warning', server.addr);

		// add to singleton instance list
		allSentinels.put(hash, this);

		// register with the server tree so it stays global
		ServerTree.add(this, server);

		monitoredBus = bus;
	}

	*new { arg server, bus;
		var hash;

		server = server ?? Server.default;
		bus = bus ?? server.outputBus;

		if (server.isKindOf(Server).not) {
			Error("In PeakSentinel.new: expected `Server` for argument #1, got `%`".format(bus.class)).throw;
		};

		if (bus.isKindOf(Bus).not) {
			Error("In PeakSentinel.new: expected `Bus` for argument #2, got `%`".format(bus.class)).throw;
		};

		// generate unique hash for this server/bus combination
		hash = "%_%".format(server.hash, bus.hash);

		// if a sentinel already exists for this bus, do not create a new one
		if (allSentinels.at(hash).notNil) {
			^allSentinels.at(hash);
		};

		^super.new.init(server, bus, hash);
	}

	doOnServerTree { arg server;
		if (synth.isNil.not) {
			synth.free;
		};

		synth = Synth.tail(
			RootNode(server), // add to the end of all groups
			synthDef.name,
			[
				\bus, monitoredBus,
				\oscID, oscID,
				\numChannels, monitoredBus.numChannels
			]
		);
	}
}