A sketch to make an image filter similar to the image filter in AudioSculpt

Dear users,

The following is a simple sketch to mimic the image filter in AudioSculpt:

( 
s.waitForBoot {
    var length = 5; // seconds 
    var image = Image.new(
		SCDoc.helpSourceDir +/+ "images" +/+
		// "icon.supercollider.png"
		// "vline.png"
		 "node.png"
		// "lastnode.png"
		// "flowers2.jpg"
		// "duck_alpha.png"
		// "vduck2.jpg"
		// "Swamp.png"
		);
    var lastFreqIndex = image.height - 1;
    var lastTimeDomainIndex = image.width - 1;
    var numSpectrumsInTimeDomain = (0..lastTimeDomainIndex).collect { |indexTimeDomain|
        (lastFreqIndex..0).collect { |indexFreq|
            var pixelColor = Image.pixelToColor(image.getPixel(indexTimeDomain, indexFreq));
            [pixelColor.red, pixelColor.green, pixelColor.blue, pixelColor.alpha]
        }
    };
    var freqsSize = numSpectrumsInTimeDomain[0].size;
    var freqs = (1..freqsSize).linexp(1, freqsSize, 20, s.sampleRate / 2);
    var timeUntilNext = length / (lastTimeDomainIndex + 1);
    
    var synth = { |amp, lagTime|
        var source = WhiteNoise.ar;
        Resonz.ar(
            source,
            freqs,
            0.004,
            (amp * \amps.kr(0 ! freqsSize)).lag(lagTime) 
        ).sum/numSpectrumsInTimeDomain.size
    }.play(args:[amp: 0, lagTime: timeUntilNext]);
    
	image.plot;
	
    s.sync;
    
    numSpectrumsInTimeDomain.size.do { |index|
        var thisSpectrumAmps = numSpectrumsInTimeDomain[index].collect{ |thisRGBA|
            thisRGBA[0..2].sum / 3 * thisRGBA[3]
        };
        synth.set(\amp, 10, \amps, thisSpectrumAmps);
        timeUntilNext.wait
    };
    
    synth.free
}
)

Is there another way to do this? It would be nice if I could control FFT filters or other things instead of Resonz, BPF or DynKlank.

3 Likes

That’s cool!
Here’s a version with Sines using DynKlang, which sounds a bit different:

(
s.waitForBoot {
    var length = 5; // seconds
    var image = Image.new(
		SCDoc.helpSourceDir +/+ "images" +/+
		// "icon.supercollider.png"
		// "vline.png"
		// "node.png"
		// "lastnode.png"
		// "flowers2.jpg"
		"duck_alpha.png"
		// "vduck2.jpg"
		// "Swamp.png"
		);
    var lastFreqIndex = image.height - 1;
    var lastTimeDomainIndex = image.width - 1;
    var numSpectrumsInTimeDomain = (0..lastTimeDomainIndex).collect { |indexTimeDomain|
        (lastFreqIndex..0).collect { |indexFreq|
            var pixelColor = Image.pixelToColor(image.getPixel(indexTimeDomain, indexFreq));
            [pixelColor.red, pixelColor.green, pixelColor.blue, pixelColor.alpha]
        }
    };
    var freqsSize = numSpectrumsInTimeDomain[0].size;
    var freqs = (1..freqsSize).linexp(1, freqsSize, 20, s.sampleRate / 2);
    var timeUntilNext = length / (lastTimeDomainIndex + 1);

    var synth = { |amp, lagTime|
		0.1 * DynKlang.ar(`[
            freqs,
			(amp * \amps.kr(0 ! freqsSize)).lag(lagTime),
			nil
		])
		/ numSpectrumsInTimeDomain.size
    }.play(args:[amp: 0, lagTime: timeUntilNext]);

	image.plot(bounds: Rect(60, 100, 500, 500));

    s.sync;

    numSpectrumsInTimeDomain.size.do { |index|
        var thisSpectrum = numSpectrumsInTimeDomain[index].collect{ |thisSpectrumAmps|
            thisSpectrumAmps[0..2].sum / 3 * thisSpectrumAmps[3]
        };
        synth.set(\amp, 10, \amps, thisSpectrum);
        timeUntilNext.wait
    };

    synth.free
}
)
4 Likes

Just for fun I made a version that draws random shapes onto an image:

(
s.waitForBoot {
	var length = 10; // seconds
	var imageWidth = 256;
	var addTriangleFunc = {
		var point = Point(imageWidth.rand, imageWidth.rand);
		Pen.color = Color.grey((0.25, 0.3..0.75).choose, (0.25, 0.3..0.75).choose);
		Pen.moveTo(Point(point.x + 50.rand2, point.y + 50.rand2));
		Pen.lineTo(Point(point.x + 50.rand2, point.y + 50.rand2));
		Pen.lineTo(Point(point.x + 50.rand2, point.y + 50.rand2));
		Pen.perform([\stroke, \fill].choose);
	};
	var addOvalFunc = {
		Pen.fillOval(Rect.aboutPoint(
			Point(imageWidth.rand, imageWidth.rand),
			5.rrand(50),
			5.rrand(50)
		));
	};

	var image = Image.new(imageWidth, imageWidth)
	.draw({ arg image;
		// black background
		Pen.color = Color.black;
		Pen.fillRect(Rect(0, 0, image.width, image.height));
		// triangles
		40.do {
			[addTriangleFunc, addOvalFunc].choose.value;
		};
		// dust
		300.do {
			Pen.color = Color.grey([0.5, 0.7, 0.8, 1].choose, [0.25, 0.5, 0.75, 1].choose);
			Pen.fillRect(Rect(
				image.width.rand.blend(imageWidth * 0.5, 1.0.rand),
				image.height.rand.blend(imageWidth * 0.5, 1.0.rand),
				1, 1));
		};

	});
	var lastFreqIndex = image.height - 1;
	var lastTimeDomainIndex = image.width - 1;
	var numSpectrumsInTimeDomain = (0..lastTimeDomainIndex).collect { |indexTimeDomain|
		(lastFreqIndex..0).collect { |indexFreq|
			var pixelColor = Image.pixelToColor(image.getPixel(indexTimeDomain, indexFreq));
			[pixelColor.red, pixelColor.green, pixelColor.blue, pixelColor.alpha]
		}
	};
	var freqsSize = numSpectrumsInTimeDomain[0].size;
	var freqs = (1..freqsSize).linexp(1, freqsSize, 20, s.sampleRate / 2);
	var timeUntilNext = length / (lastTimeDomainIndex + 1);

	var synth = { |amp, lagTime|
		0.05 * DynKlang.ar(`[
			freqs,
			(amp * \amps.kr(0 ! freqsSize)).lag(lagTime),
			nil
		]) ! 2
		/ numSpectrumsInTimeDomain.size
	}.play(args:[amp: 0, lagTime: timeUntilNext]);

	w = image.plot(bounds: Rect(60, 100, 500, 500), freeOnClose:true);

	s.sync;

	numSpectrumsInTimeDomain.size.do { |index|
		var thisSpectrum = numSpectrumsInTimeDomain[index].collect{ |thisSpectrumAmps|
			thisSpectrumAmps[0..2].sum / 3 * thisSpectrumAmps[3]
		};
		synth.set(\amp, 10, \amps, thisSpectrum);
		timeUntilNext.wait
	};

	synth.free;
	1.wait;
	w.close;
}
)
3 Likes

Thanks for sharing!
Yes, additive synthesis is one approach!

I was only thinking about subtractive synthesis, influenced by AudioSculpt. However, I feel that my original code is missing something (not due to amplitude) compared to the sound of AudioSculpt. If I remember correctly, AudioSculpt’s sound was richer. With GVerb I can achieve a richer sound, but I still miss AudioSculpt a little.

However, your additive synthesis is excellent and I have no complaints!

The second example is also fantastic. This template could be used to simplify the following two tasks

  • Sonification.
  • Simultaneous sound design and visualisation.

3 posts were split to a new topic: SC learning process: opinions, advice

For a subtractive approach, there’s also DynKlank.
I don’t know AudioSculpt, but for a different sound you could also try other types of noise as a source instead of WhiteNoise - e.g. PinkNoise, Dust or ClipNoise.

@sslew
I also think @TXMod is very competent in using SC and handling sound, and wanted to know how he learned SC. No problem. Thanks for asking instead of me!

@TXMod
In my first post I summed signals to a mono signal, but that was just a test.

Klank, Klang, DynKlank and DynKlang seem to use less system resources than sum (or other) signal arrays. However, they do sum signals on a channel. So I do not prefer them if they do not sum signals.

{ var n = 4; Klang.ar(`[220 * (1 .. n), 0.1, 0], 1, 0)/n }.play; // standard and efficient
{ var n = 4, src = SinOsc.ar(220 * (1 .. n), 0, 0.1/n); Splay.ar(src.scramble) }.play // flexible and interesting

What I want to know to my inital question is if there would be a way to dump the spectrum information driven by arrays of vertical pixels of specific time point of the image to FFT filter.

Has anyone ever seen a gui app made with sc where you can play with the FFT bins and do other graphic stuff to mess with the phase vocoder? I cant find anything out there. Im surprised.

something like GitHub - nhthn/canvas: A visual additive synthesizer ?

Here’s a little test I did a while ago of drawing onto a spectrogram with the mouse… it wasn’t fast enough for real time display so you only see what you’ve drawn after you’ve lifted the mouse button (play it by clicking to move the playhead and pressing space)

~winsize = 2048;
~hop = 0.15;
//~hop = 0.25;
~hop = (~hop * 2048 / 64).round * 64 / 2048;
~wintype = 1;
//~wintype = 0;

~brushHeight = 10;
~brushWidth = 10;

~inbuf.free;~inbuf = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
~buf.free;~buf = Buffer.alloc(s, ~inbuf.duration.calcPVRecSize(~winsize, ~hop, s.sampleRate));


(
// note the window type and overlaps... this is important for resynth parameters
SynthDef("pvrec", { |recbuf, inbuf, winsize = 2048, hop = 0.25, wintype = 1|
  var in, chain, bufnum;
  bufnum = LocalBuf.new(winsize, 1);
  Line.kr(1, 1, BufDur.kr(inbuf), doneAction: 2);
  in = PlayBuf.ar(1, inbuf, BufRateScale.kr(inbuf), loop: 0);
  chain = FFT(bufnum, in, hop, wintype);
  chain = PV_RecordBuf(chain, recbuf, 0, 1, 0, hop, wintype);
  // no ouput
}).add;
)

a = Synth("pvrec", [recbuf: ~buf, inbuf: ~inbuf, winsize: ~winsize, hop: ~hop, wintype: ~wintype]);


(
SynthDef("pvplay", { |out, buf = 1, offset = 0, rate = 1, winsize = 2048, hop = 0.25, wintype = 1|
  var in, chain, bufnum;
  bufnum = LocalBuf.new(winsize);
  chain = PV_PlayBuf(bufnum, buf, rate, offset, 0);
  Out.ar(out, IFFT(chain, 1).dup * hop);
}).add;
)

b = Synth("pvplay", [out: 0, buf: ~buf, rate: 0.75, winsize: ~winsize, hop: ~hop, wintype: ~wintype]);

// stop the synth
b.free;

// load to ~data array
~buf.loadToFloatArray(action: { |data| ~data = data.postln })

// to zero it out
~data = ~data[0..2] ++ (~data.size - 3).collect { 0 }
~data[0] = ~winsize;
~data[1] = ~hop;
~data[2] = ~wintype;

(
var sampleRate = s.sampleRate;
var numFrames = ((~data.size - 3) / ~winsize);

var width = numFrames.asInteger;
var height = (~winsize / 2).asInteger;
var colorFunc = { |mag| Color.gray(1, mag.log10.lincurve(-2, 2, 0, 1, curve: 0)) };
var scaleFunc = { |pixel| pixel.lincurve(0, height, 0, height, curve: 3) }; // curve factor for Y scaleing (0 = linear, 3 =~ mel scale)

~image = Image.new(width, height);

~magAt = { |frame, index|
  ~data[(frame * ~winsize) + (index * 2) + 4];
};
~setMag = { |frame, index, mag, phase|
  var i = (frame * ~winsize) + (index * 2) + 3;
  ~data[i + 1] = mag;
  if (phase.notNil) {
    ~data[i] = phase;
  };
};

~playrate = 0.1;
~synth = nil;
~playhead = 0;

if (~win.notNil) { ~win.close };
~win = Window("", Rect(0, 0, ~image.width, ~image.height)).front.background_(Color.white)
.view.keyDownAction_({ |v, char|
  if (char == $ ) {
    if (~synth.notNil) {
      ~synth.free;
      ~synth = nil;
      ~playrout.stop;
    } {
      ~synth = Synth("pvplay", [
        out: 0,
        buf: ~buf,
        offset: ~playhead / ~inbuf.duration * numFrames,
        rate: ~playrate,
        winsize: ~winsize,
        hop: ~hop,
        wintype: ~wintype
      ]);
      //~playhead = 0;
      ~playrout = fork {
        (~inbuf.duration / ~playrate * 30).do {
          ~playhead = ~playhead + (~playrate / 30);
          defer { ~playheadview.refresh };
          (1/30).wait;
        };
      };
    }
  };
});


~makeView = {
  var mouseDownPoint;
  var refreshImage = {
    ~image.setPixels(Int32Array.fill(width * height, { |i|
      var index = scaleFunc.(height - (i / width).floor).asInteger;
      var frame = i % width;
      var mag = ~magAt.(frame, index);
      Image.colorToPixel(colorFunc.(mag));
    }), Rect(0, 0, ~image.width, ~image.height));
  };
  refreshImage.();

  ~view.remove;
  ~view = UserView(~win, ~win.bounds).drawFunc_({
    Pen.use {
      Pen.scale(~view.bounds.width / ~image.width, ~view.bounds.height / ~image.height);
      Pen.drawImage(0@0, ~image)
    }
  }).resize_(5).background_(Color.black);

  ~playheadview = UserView(~win, ~win.bounds).drawFunc_({ |v|
  Pen.use {
  Pen.addRect(Rect(~playhead / ~inbuf.duration * v.bounds.width, 0, 2, v.bounds.height));
  Pen.color = Color.red;
  Pen.fill;
  }
  }).resize_(5)
  .mouseDownAction_({ |v, x, y|
    [x, y].postln;
    mouseDownPoint = x@y;
  })
  .mouseMoveAction_({ |v, x, y|
    /*
    var frame = x / v.bounds.width * numFrames;
    //var index = scaleFunc.((v.bounds.height - y) / v.bounds.height * height - 1).asInteger;
    var index = ((y.postln / v.bounds.height).lincurve(0.8, 0.0, 0.0, 0.8, 3, nil).postln * (~winsize / 2)).asInteger;
    */
    var index = scaleFunc.(height - y.linlin(0, v.bounds.height, 0, height)).asInteger;
    var frame = x.linlin(0, v.bounds.width, 0, width).asInteger;
    //var mag = ~magAt.(frame, index);
    var halfBW = (~brushWidth / 2).asInteger;
    var halfBH = (~brushHeight / 2).asInteger;
    ~brushWidth.do { |bwi|
      ~brushHeight.do { |bhi|
        var thisFrame = frame - halfBW + bwi;
        var thisIndex = index - halfBH + bhi;
        if (thisFrame.isPositive and: thisIndex.isPositive) {
          var distance = sqrt((thisFrame - frame).squared + (thisIndex - index).squared);
          var mag = ~magAt.(thisFrame, thisIndex);
          ~setMag.(thisFrame, thisIndex, mag + distance.lincurve(0, 10, 1, 0, -5).postln, rrand(0, 2pi));
        }
      }
    };

    //refreshImage.();
  })
  .mouseUpAction_({ |v, x, y|
    if (x@y == mouseDownPoint) {
      ~playhead = x / v.bounds.width * ~inbuf.duration;
    };
    ~buf.loadCollection(~data, action: { "loaded".postln });
    ~makeView.();
  });
};
~makeView.();
)

this generates the image from the PV buffer contents but it wouldn’t be too hard I think to do the reverse, generate the PV buffer from the image instead

@Eric_Sluyter

Do you know why the code runs so slow?

I got a considerable delay here, seconds.

I didn’t stop to read it, but maybe there is something going on there

It’s probably to do with drawing the image from the buffer contents…

var refreshImage = {
    ~image.setPixels(Int32Array.fill(width * height, { |i|
      var index = scaleFunc.(height - (i / width).floor).asInteger;
      var frame = i % width;
      var mag = ~magAt.(frame, index);
      Image.colorToPixel(colorFunc.(mag));
    }), Rect(0, 0, ~image.width, ~image.height));
  };
  refreshImage.();

Going the other way (filling the buffer based on the image) would be faster, I think

1 Like

Maybe dividing the function into different routines with smaller chunks would make sense?

But I think this is not much better either. There must be a way to do this.

I think there is something that is not quite right to take that long.

Some GUI master will help us, hopefully.

Thank you for this!
It is amazing,

I need more time and concentration to combine your code with mine.
One thing is using Lanczos interpolation to adjust the y-axis size to the spectrum bin size… It is not a big deal. It is the next step that confuses me a bit.
I will post when I make progress. However, I cannot devote more time to it now…

1 Like