Julian Rubisch

September 29, 2021

Supercollider Object Oriented Patterns, Part 2 - Pluggable Views

Soon I wanted to have a graphical representation of the buffers I was interacting with. Sadly, Supercollider only provides a class for sound files (SoundFileView). OOP to the rescue - we get to make our own BufferSoundFileView.

Let’s first consider what our objective is here. We’d like to end up with a views property on the VBufferCollection so we can plug it into a vertical layout like so:

( 
y = Window.screenBounds.height - 120;
w = Window.new("buffers", Rect(200, y, 800, 600)).alwaysOnTop_(true);

w.layout_(VLayout(*~collection.views)).front;
)

Subclassing SoundFileView

We'll start by equipping our BufferSoundFileView with a buffer instance variable:

BufferSoundFileView : SoundFileView {
    var buffer;
	
    *new { |parent, bounds, buffer|
        buffer = buffer;
	^super.new(parent, bounds);
    }
}

Now we can ask the buffer collection to provide views for each buffer

VBufferCollection {
    var <buffers, views;
    // ...

    // we want to lazily initialize those
    views { |parent|
        views = this.prMakeViews(parent);

	^views;
    }

    prMakeViews { |parent|
	^buffers.collect { |buffer|
	    var view = BufferSoundFileView.new(parent, nil, buffer);
	    buffer.getToFloatArray(action: { |samples|
	        {
		    view.setData(samples);
		    view.refresh;
		}.defer;
	    });

	    view;
	}
    }
}


Note that I'm lazily loading them here, because loading the samples into the view is a pretty expensive operation which would block the entire application. So when the getter views is called, for each buffer a view is initialized and via getToFloatArray a callback is executed which sets the view's data points to the buffer's samples, and afterwards refreshes it. Now plugging them into a VLayout like above actually works and we get the UI we deserved, but it doesn't yet feel quite right. Notice that in the VBufferCollection we're imperatively constructing the views on the fly?

This is known as something called feature envy: the collection is reaching into the BufferSoundFileView's domain, which is actually far better equipped to perform this task: It already has a reference to a buffer, so let's move that bit of code in there.

VBufferCollection {
    var <buffers, views;
    // ...

    // we want to lazily initialize those
    views { |parent|
        views = this.prMakeViews(parent);

	^views;
    }

    prMakeViews { |parent|
        ^buffers.collect { |buffer|
	    BufferSoundFileView.new(parent, nil, buffer);
	}
    }
}

BufferSoundFileView : SoundFileView {
    var buffer;
	
    *new { |parent, bounds, buffer|
        ^super.new.init(parent, bounds, buffer);
    }

    init { |parent, bounds, buffer|
        buffer = buffer;	

	buffer.getToFloatArray(timeout: 30, action: { |samples|
	    {
	        this.setData(samples);
		this.refresh;
	    }.defer;
	});	
    }
}