Plotting time-value pairs

Is there a way to plot time-value pairs in sclang similar to the way signals are plotted, where the x-axis shows time? Let say I have a list a values like

l = [
	[0, 0.23],
	[0.465, 0.98],
	[2.91, 0.3]
	// ...
]

where l[i][0] is the time on the x-axis and l[i][1] is the value on the y-axis.

How can I plot those values in a way similar to?

(
{
	b = { Dust.kr(100) }.asBuffer(0.05);
	s.sync;
	0.05.wait;
	{ b.plot.plotMode_(\bars) }.defer
}.fork
)

I don’t think there’s a direct way.

Plotter expects an Array which values are equally spaced. You might fill your original array with zeroes until it matches this specification, but this is probably overkill.

Manually, this can be a starting point:

(
var values = [
	[0, 0.23],
	[0.465, 0.98],
	[2.91, 0.3],
	[4.52, 0.77],
	[5.66, 0.2],
];

var barWidth = 4;
var xMax = 10;
var yMax = 1;

var view = UserView()
.background_(Color.white())
.drawFunc_({
	Pen.fillColor_(Color.red);
	values.do({ |pair|
		Pen.fillRect(
			Rect(
				pair[0].linlin(
					0, xMax,
					0, view.bounds.width
				) - (barWidth / 2),
				
				view.bounds.height -
				pair[1].linlin(
					0, yMax,
					0, view.bounds.height
				),
				
				barWidth,
				
				pair[1].linlin(
					0, yMax,
					0, view.bounds.height
				)
			);
		);
	});
});

view.front;
)
1 Like

Thanks, that is very useful, thank you.

Hi, this is not an exact answer to your question. But I felt like sharing, and it does match your title (:

I made the following class, which allows you to record from a multichannel control or audio rate bus, and store it as a csv file. I use Octave (an opensource alternative to matlab) to read the data and make plots.

I guess its not perfect, just notice that I do not free the synth. But anyhow helps me out.

TimeTrace {
    // send the values you want to record to a bus, specify the path were you want to store it, and the record time, recording directly starts
    // example: TimeTrace.new(~bScopeAr, "C:/Users/piert/MakingMusic/SuperCollider/Data Analysis", 10);
    var <>bus, <>buffer, <>synth, <>file;
    *new { |bus_, path_, time_ = 10|
        ^super.new.init(bus_, path_, time_);
    }

    init { |bus_, folderPath_, time_|
        var path, synthDef;
        bus = bus_;
        path = folderPath_ ++ "/" ++ "data_" ++ Date.localtime.stamp ++ ".csv";
        file = File(path, "w");
        if(file.isOpen){
            "Data is being collected".log(this);
            if(bus.rate == \audio){
                synthDef = SynthDef(\timeTraceAr, { |busIndex, bufnum|
                    var input;
                    input = In.ar(busIndex, bus.numChannels);
                    RecordBuf.ar(input, bufnum: bufnum, offset: 0.0, recLevel: 1.0, preLevel: 0.0, run: 1.0, loop: 0.0, trigger: 1.0, doneAction: Done.freeSelf);
                });
                Server.default.makeBundle(nil, { // makeBundle in combination with sync can be used to make sure actions at the server are executed in the right order (not sure why it does not work with Routine, I then get on error about synth.onFree, so apparantly it does not wait with executing)
                    synthDef.send(Server.default);
                    buffer = Buffer.alloc(Server.default, Server.default.sampleRate * time_, bus.numChannels);
                    Server.default.sync;
                    synth = Synth.new(\timeTraceAr, [\busIndex, bus.index, \bufnum, buffer.bufnum], addAction: 'addToTail');
                });
                synth.onFree({ this.export }); 
            }{
                synthDef = SynthDef(\timeTraceKr, { |busIndex, bufnum|
                    var input;
                    input = In.kr(busIndex, bus.numChannels);
                    RecordBuf.kr(input, bufnum: bufnum, offset: 0.0, recLevel: 1.0, preLevel: 0.0, run: 1.0, loop: 0.0, trigger: 1.0, doneAction: Done.freeSelf);
                });
                Server.default.makeBundle(nil, {
                    synthDef.send(Server.default);
                    buffer = Buffer.alloc(Server.default, Server.default.sampleRate / Server.default.options.blockSize * time_, bus.numChannels);
                    Server.default.sync;
                    synth = Synth.new(\timeTraceKr, [\busIndex, bus.index, \bufnum, buffer.bufnum], addAction: 'addToTail');
                });
                synth.onFree({ this.export }); 
            };
        } {
            Error("File % could not be opened.".format(path)).throw;
        };       
    } 

    stop {
        synth.free;
    }
 
    export {
        buffer.loadToFloatArray(action: { |data|
            var sampleRate;
            if(bus.rate == \audio){
                sampleRate = Server.default.sampleRate;
            }{
                sampleRate = Server.default.sampleRate / Server.default.options.blockSize;
            };
            
            // Write CSV header
            file.write("time (s),amplitude\n");

            // Write each sample with timestamp
            for(0, data.size/bus.numChannels-1){ |i| 
                // since it is a multichannel unlaced array, we have a one dimensional array which consists of the values at one time stance, and the after the values at the next timestance
                // index i corresponds to the timestep, index j to the channel
                var time = i / sampleRate;  // Convert index to time in seconds
                var string = time.asFloat.asStringPrecF;
                for(0, bus.numChannels-1){ |j|
                    string = string + "," + data[i*bus.numChannels+j].asFloat.asStringPrecF;
                };
                string = string + "\n";
                file.write(string);
            };
            /* data.do { |sample, i| 
                var time = i / sampleRate;  // Convert index to time in seconds
                 file.write(time.asFloat.asStringPrecF + "," + sample.asFloat.asStringPrecF + "\n");
            }; */
            
            file.close;
            "Buffer data saved to CSV with timestamps!".log(this);
        });
    }
}

This is already answered, but how about the following way?

(
var time_value_pairs = [[0, 0.23], [0.465, 0.98], [2.91, 0.3]];
var start = time_value_pairs[0][0];
var step = 0.005;
var end = time_value_pairs[time_value_pairs.size - 1][0] + step;
var result = [];
var resultSize = (end / step).ceil + 1;
var timePoints = Array.series(resultSize, start, step);

timePoints.do({ |thisTimePoint|
	var found = false;
	
	time_value_pairs.do { |time_value_pair|
		if(time_value_pair[0] == thisTimePoint) {
			result = result.add([thisTimePoint, time_value_pair[1]]);
			found = true;
		};
	};
	
	if(found.not) {
		result = result.add([thisTimePoint, 0]);
	};
});

result.flop[1].plot.plotMode_(\bars);
)

Env has the .xyc method, which takes triplets of x, y, and curve values.

Indeed, Env also includes the .pairs method. The array l mentioned in the original post can be directly utilised as the first argument of Env.pairs. However, the resulting output differs from my proposal, which aligns with the result suggested by @Thor_Madsen, if my understanding is correct.


(
Env.pairs([[0, 0.23],
	[0.465, 0.98],
	[2.91, 0.3]], \step).plot.plotMode_(\bars)
)

returns:

(
Env.pairs([[0, 0.23],
	[0.465, 0.98],
	[2.91, 0.3]], \hold).plot.plotMode_(\bars)
)

returns:

The following is the result of my code:

1 Like

Hi, this may be too late to be helpful, but you can explicitly set the x/domain values, such that they can be non-uniformly spaced:

(
l = [
	[0.1, 0.23],
	[0.465, 0.98],
	[2.91, 0.3]
	// ...
];

// group time (x, domain) values and level (y) values into channels
m = l.flop;

p = m[1].plot.plotMode_(\dstems); // plot the levels
p.domain_(m[0]); // set x values for each level
p.domainSpecs_([0, m[0].maxItem.ceil].asSpec); // set plot range on x axis
p.axisLabelX_("Time (sec)");
p.axisLabelY_("Magnitude");
)

3 Likes