(
SynthDef(\wavesetPlayer, { |out=0, bufnum, rate=1, amp=0.3, pan=0|
    var sig = PlayBuf.ar(1, bufnum, rate * BufRateScale.kr(bufnum), doneAction: 2);
    sig = sig * amp;
    Out.ar(out, Pan2.ar(sig, pan));
}).add;
)


(
~processWavesets = { |w, minLength=300, weightLength=5, numClusters=10, groupSize=3|
    var lens, amps, validIndices;
    var lensVec, ampsVec, normLens, normAmps, features;
    var kmeans, results, centers, assignments;
    var reps;

    // Extract lengths and amplitudes
    lens = w.lengths;
    amps = w.amps;

    // Find indices of wavesets >= minLength
    validIndices = (0..(lens.size - 1)).select { |i| lens[i] >= minLength };

    if (validIndices.isEmpty) {
        "No wavesets found above minLength!".postln;
        ^nil;
    };

    // Gather features for valid wavesets
    lensVec = validIndices.collect { |i| lens[i] };
    ampsVec = validIndices.collect { |i| amps[i] };

    // Normalize features to 0..1 range
    normLens = lensVec.normalize(0, 1);
    normAmps = ampsVec.normalize(0, 1);

    // Weight length feature
    features = Array.new(validIndices.size);
    validIndices.size.do { |i|
        features[i] = [normLens[i] * weightLength, normAmps[i]];
    };

    // Define k-means clustering function
    kmeans = { |data, k, maxIter=100|
        var centers, assignments, changed;

        // Initialize centers randomly
        centers = data.chooseN(k);
        assignments = Array.new(data.size, -1);

        maxIter.do {
            changed = false;

            data.size.do { |i|
                var distances = centers.collect { |c| (c - data[i]).norm };
                var minIndex = distances.indexOfMin;
                if (assignments[i] != minIndex) {
                    assignments[i] = minIndex;
                    changed = true;
                };
            };

            if (changed.not) { ^[centers, assignments] };

            // Update centers
            centers = (0..(k - 1)).collect { |cid|
var clusterPoints = data.indices.select { |i| assignments[i] == cid }.collect { |i| data[i] };
                if (clusterPoints.isEmpty) {
                    data.choose;
                } {
                    clusterPoints.reduce({ |a, b| a + b }) / clusterPoints.size;
                };
            };
        };
        [centers, assignments]
    };

    // Run clustering
    results = kmeans.(features, numClusters);
    centers = results[0];
    assignments = results[1];

    // Find representative waveset per cluster (closest to centroid)
    reps = Dictionary.new;
    numClusters.do { |cid|
        var clusterIndices = validIndices.select({ |vi, idx| assignments[idx] == cid });
        if (clusterIndices.notEmpty) {
            var clusterFeatures = clusterIndices.collect { |i| features[validIndices.indexOf(i)] };
            var center = centers[cid];
            var distances = clusterFeatures.collect { |f| (f - center).norm };
            var minIdx = distances.indexOfMin;
            reps[cid] = clusterIndices[minIdx];
        };
    };

    // Playback routine
    fork {
        var total = validIndices.size;
        var pos = 0;
        var localGroupSize = groupSize;

        while { pos < total } {
            var clusterID, repIndex;

            // Adjust groupSize if near end
            if (pos + localGroupSize > total) {
                localGroupSize = total - pos;
            };

            clusterID = assignments[pos];
            repIndex = reps[clusterID] ?? validIndices[pos];  // fallback to original

            // Play groupSize wavesets starting at representative
            var ev = w.eventFor(repIndex, localGroupSize, 1, 1);
            ev.put(\pan, rrand(-0.5, 0.5));
            ev.play;
            ev.sustain.wait;

            pos = pos + localGroupSize;
        };
    };
};
)



// Usage example with your Wavesets instance `w`
// Adjust parameters as you like
~processWavesets.(w, minLength: 400, weightLength: 5, numClusters: 12, groupSize: 4);

