dowser (low-level spectral analysis utility)

thought i would post this small command-line tool for spectral analysis. it’s quite basic and low level, but is a kind of thing that i often find useful in computer music practice, and it’s nice to have a portable, self-contained, scriptable and efficient implementation.

the tool accepts a soundfile, performs a short-term fourier transform, and emits the peaks of the power spectrum for each analysis frame, alongside some statistical measures.


usage, examples

(from readme):

dowser <infile> <outfile>

output file is a supercollider script, defining a list of dictionaries.

each dictionary contains data for a single spectral analysis frame.

per-frame measurements includes:

  • peaks: list of events with (hz, mag) keys, listing spectral peaks for the given frame
  • papr: peak-to-average-power ratio, a measure of “tonal-ness”
  • flatness: AKA weiner entropy, geometric mean / arithmetic mean. another measure of “tonalness”
  • centroid: spectral centroid, a measure of “brightness”

example

starting with this short loop of electric viola:
[ http://catfact.net/dowser/loop2.mp3 ]

dowser produces this fairly large (286KB) supercollider script describing the analysis data:
[ http://catfact.net/dowser/dowser-output-loop2.scd.txt ]

which can then be processed in whatever way you find useful. the following is not very well-considered as a musical algorithm - it is sort of a naive and basic resynthesis of the spectral peaks as sine waves. but it gives a good idea of the nature of this data. (and its limitations! e.g. the relative noisiness of low frequencies)

script:

supercollider script
// load the data
(
post("reading data... ");
~data = this.executeFile(PathName(Document.current.path).pathOnly++"dowser-output-loop2.scd");
postln("done.");

AppClock.sched(0, {
	~data.collect({arg frame; frame.papr}).histo.plot;
	~data.collect({arg frame; frame.flatness}).histo.plot;
	~data.collect({arg frame; frame.peaks.collect({ arg peak; peak[\hz].cpsmidi})}).flatten.histo.plot;
	nil
});
)

// play some tones
(
s = Server.default;
s.boot;
s.waitForBoot {
	r = Routine {

		~frame_stretch = 4;
		/// magic numbers here:
		// 2**13 = fft size
		// 2 = overlap factor
		// 48k = original samplerate of analyzed file
		~frame_period = (2**12) / 48000.0;
		~frame_period = ~frame_period * ~frame_stretch;
		~frame_period.postln;

		b = Bus.audio(s, 2);


		SynthDef.new(\sine_1shot, {
			arg out=0, amp=0, hz=110, pan=0, atk=1, sus=0, rel=2;
			var snd, env;
			env = EnvGen.ar(Env.linen(atk, sus, rel), doneAction:2);
			snd = SinOsc.ar(hz) * amp * env;
			Out.ar(out, Pan2.ar(snd, pan));
		}).send(s);

		~out_limit = {
			Out.ar(0, Limiter.ar(In.ar(b, 2), 0.9, 0.2).clip(-1, 1))
		}.play(s);


		s.sync;

		~papr_min = 20;
		~flat_max = 20;
		~mag_min = 1;
		~hz_max = 3000;
		~max_peaks_per_frame = 3;

		~data.do({
			arg frame;
			var flat, papr;
			flat = frame[\flatness];
			papr = frame[\papr];
			if ((flat < ~flat_max) && (papr > ~papr_min), {
				frame[\peaks].do({
					arg peak, i;
					var hz, mag, sineIdx, amp;

					//peak.postln;
					hz = peak[\hz];
					mag = peak[\mag];

					if (i < ~max_peaks_per_frame, {
						if ((mag > ~mag_min) && (hz < ~hz_max), {
							var db;
							amp = (mag / 64);
							db = amp.ampdb;
							postln([hz, db]);

							Synth.new(\sine_1shot, [
								\out, b,
								\hz, hz,
								\amp, amp,
								\pan, i.linlin(0, ~max_peaks_per_frame, 0, 1).rand2,
								\atk, ~frame_period * 4,
								\dur, ~frame_period * db.linlin(-60, 0, 4, 16),
								\rel, ~frame_period * db.linlin(-60, 0, 4, 32)

							], s, \addToHead);
						});
					});
				});
			});

			~frame_period.wait;
		});

	}.play;
}
)

(note that this must be run in two sections.)

resulting audio:
[ http://catfact.net/dowser/loop2-resynth.mp3 ]

[warning: woops, i “mastered” the original loop rather quietly so the resynthesis is quite loud by comparison]


where to get it

github : https://github.com/catfact/dowser

i have not created any binary release packages and am not particularly planning to, because frankly it has become painful to do this for macOS and windows. if there is interest i could make source packages for e.g. homebrew and chocolatey.


roadmap

the readme mentions a few more wish-haves. but the most pressing would be:

  • support for other output formats. (.csv and .npy come to mind.)
  • expose more tuning parameters (especially frequency range.)

happy to accept contributions. i made the app using JUCE and cmake, not because either are necessary (it is a simple project,) but to facilitate cross-platform development (have tested on macOS, windows 10 and ubuntu,) and the potential addition of higher-level features.

and, please use this topic for questions, ideas etc!

56 Likes

if you’re still willing to do this i’d like to try building the app

hey @glia i can certainly look into doing that. but the steps to build on mac are not too hard anyway… just checked that this is working for me on a fresh system running macos 10.15.5 catalina.

first install cmake

brew install cmake

then in the dowser repo:

git submodules update --init --recursive

(this pulls a local copy of JUCE which is stupidly oversized for the task sorry about that)

and build

mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build .

fresh executable then located at <dowser_repo>/build/dowser_artefacts/Release/dowser

(making me think i should ditch JUCE for this. the main reason to keep it would be to build out UI niceties like a drag-and-drop window.)

2 Likes

Sorry I meant the location where you cloned the repo

Sorry I’m on my phone and someone is climbing my hai r, I cannot perform a full illustration

3 Likes

thanks for the additional tips to get this running on macos

my attempt failed while trying to get juce properly set up…i get this error in terminal:

git@github.com: Permission denied (publickey).

fatal: Could not read from remote repository.

hoping you may have a suggestion to help but no rush at all…
i’ll try to troubleshoot myself and can hopefully find a solution in the meantime

i’m pretty sure this is from using an ssh url for the subrepo ? i think you can fix it on either end by switching out the url in the .gitmodulesfile with the regular https:// url of the subrepo

((also on a phone here))
((the culprit i think → dowser/.gitmodules at 146253046ede5964118c1c15e7efeda1b0ff054c · catfact/dowser · GitHub))

that should work and the https path would be https://github.com/juce-framework/JUCE.git (note the change from : to /)

but i would highly recommend getting set up to use ssh with github. (https access and passphrase auth is being phased out, for good reason; you must have ssh to contribute to projects.)

3 Likes

excellent excellent advice

i got my ssh key sorted out and just built dowser following your instructions!
now…perhaps my last questions since i have the app built:

do i need to change directory to where my infile is stored? or specify the path? and then name the .scd.txt outfile?

are there any commands for dowser or it should be one step as shown above?

Give it the path to input file, relative to current working directory. Give path/name for output file or accept the default. No other options

1 Like