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!

45 Likes