Rawer

Under Construction

Voltage Controlled Motor Module

Demo vid : VCMM

Schematic :

schematic

This worked on the 3rd attempt. First try, nothing happened but one transistor got hot. Then I discovered I'd got the collector wires the wrong way around. I thought I must have fried the transistors, but maybe not, after I'd replaced them I experimented a bit more. My estimate for a resistor to go in series with the motor had been way out (120R), it needed something really low. But it occurred to me that an inductance should work there, and I had a old PCB (from a satellite tuner PSU) on the desk next to me with the perfect candidate. I'd got the old board there for the 1000uF 16v caps. I didn't bother breadboarding, went straight to stripboard. It would either work or not. The components that were candidates for tweaking were easy access.

pcb

This time it actually worked. After trying it for a few seconds I felt to see if the transistors were getting warm. Burnt my fingers. Motors really aren't my thing. So I made a couple of little heatsinks and hooked it up to the modular for the vid.

I think what I'll do is try it with a PSU of its own next. With a dummy load first, to see if the circuit is behaving as I'd expected. I really don't get why it should be drawing so much current with such a farty little motor, unless the transistors are biased on when they shouldn't be. Dunno. The motor is the main unknown for me.

Voltage Controlled Motor Module

Distortion Module

I think I already posted this schematic :

distortion schematic

Since then I soldered it up and made a front panel. It worked on the second attempt!

First time I'd labeled the power connector back-to-front. Which was annoying since I knew the red wire on Eurorack cables was perversely the negative. I must have doubled on it. Anyway, it fried the op-amp. Horrible to desolder.

fried op amp

The front panel went well :

fp

One Simple Trick! - print onto glossy paper, laminate.

End result was reasonably smart -

distortion module

I couldn't be bothered tweaking the layout further.

The font is Thrums Serif, btw. I designed it a few years back because I wanted to see how it was done, and wanted one that worked for me on web pages. I didn't really get it to look good on web pages, but serendipity - exactly what I want for front panels.

Distortion Module

Wave Function Collapse for Sounds

An LV2 plugin.

Repo : https://github.com/danja/collapse-lv2

Demo : YouTube (hope that works, I got a copyright warning)

It generates signal based on what it's seen, is a kind of machine learning lite. It's very noisy. Which is what I was hoping for. Aside from the obvious noise use, it may be useful as waveform shaper.

I stumbled on WaveFunctionCollapse, "This program generates bitmaps that are locally similar to the input bitmap.". It occurred to me that the same algorithm might be usable on audio streams. I wrote a long prompt for DeepSeek which had a good long discussion with itself before producing the material below. This I passed to Claude chat together with the instructions for it to make an LV2 plugin for me. I want to use it in Reaper, my DAW of choice. (I've already had a go at vibe coding with amen-chaos and midi-chaos-amen with considerable success, as far as I took them). Claude gave me some patchy files which I stuck up at GitHub before setting Claude Code on them.

So far I've not got anything usable, I ran out of Claude tokens. Apparently Google Gemini for coding is now free, so I may give that a shot later. Gemini was slow. Went back to Claude and now it works!

Anyhow, below is what DeepSeek gave me, it has the basic idea. I've a feeling it won't be any good without taking into account the periodicity of the input. This could potentially operate at the waveform level, or (depending on how it performs) over beats or bars. Dunno.

I added period detection with autocorrelation in at the start. It's very picky about which settings will actually produce a processed output. But it really messes things up, which is exactly what I was hoping for.


To apply the wave function collapse (WFC) technique to audio waveforms in near real-time, we reinterpret the grid-based approach for time-series data. Here's a conceptual adaptation and JavaScript implementation:

Core Concept Adaptation:

  1. Grid Representation:

    • X-axis: Represents time (one cycle or window of the waveform).
    • Y-axis: Represents quantized amplitude levels (discrete buckets for continuous sample values).
    • Z-axis (optional): Encodes local patterns (e.g., sequences of amplitude changes).
  2. Pattern Extraction:

    • Slice the input waveform into overlapping windows (e.g., 10-sample segments).
    • Quantize amplitudes (e.g., 256 levels) to create discrete "tiles".
    • Catalog valid neighbor transitions (e.g., which 10-sample blocks can follow each other).
  3. Real-Time Generation:

    • Maintain a "collapsing" buffer of recent samples (context window).
    • For each new sample, resolve constraints by selecting valid patterns that match the context.
    • Output the next sample from the chosen pattern.

JavaScript Implementation:

class WaveformWFC {
  constructor(inputSamples, patternSize = 10, quantLevels = 256) {
    this.patternSize = patternSize;
    this.quantLevels = quantLevels;
    this.patterns = new Map(); // pattern -> {nextPatterns: Set, count: number}

    // Quantize input [-1, 1] to discrete levels
    this.quantize = (sample) =>
      Math.max(0, Math.min(quantLevels - 1,
        Math.floor((sample + 1) * quantLevels / 2)));

    this.dequantize = (level) =>
      (level / (quantLevels - 1)) * 2 - 1;

    this.extractPatterns(inputSamples);
  }

  extractPatterns(samples) {
    // Build patterns from sliding windows
    for (let i = 0; i < samples.length - this.patternSize; i++) {
      const window = samples.slice(i, i + this.patternSize);
      const quantPattern = window.map(s => this.quantize(s));
      const key = quantPattern.join(',');

      // Track pattern frequency and allowed successors
      if (!this.patterns.has(key)) {
        this.patterns.set(key, { nextPatterns: new Set(), count: 0 });
      }
      const data = this.patterns.get(key);
      data.count++;

      // Record valid next pattern (overlapping window)
      if (i < samples.length - this.patternSize - 1) {
        const nextWindow = samples.slice(i + 1, i + this.patternSize + 1);
        const nextKey = nextWindow.map(s => this.quantize(s)).join(',');
        data.nextPatterns.add(nextKey);
      }
    }
  }

  generateSample(context) {
    // Quantize recent context (patternSize - 1 samples)
    const quantContext = context.map(s => this.quantize(s));
    const contextKey = quantContext.join(',');

    // Find patterns starting with current context
    const candidates = [];
    for (const [pattern, data] of this.patterns) {
      const patternArr = pattern.split(',').map(Number);
      const matchesContext = quantContext.every(
        (val, i) => val === patternArr[i]
      );

      if (matchesContext) {
        // Weight by occurrence frequency
        for (let i = 0; i < data.count; i++) {
          candidates.push(patternArr[patternArr.length - 1]); // Last sample in pattern
        }
      }
    }

    // Fallback: Random sample if no candidates
    if (candidates.length === 0) {
      return Math.random() * 2 - 1; // [-1, 1]
    }

    // Random weighted selection
    const nextLevel = candidates[Math.floor(Math.random() * candidates.length)];
    return this.dequantize(nextLevel);
  }
}

// Usage Example
const audioContext = new AudioContext();
const inputSamples = [...]; // Float32Array from source audio

const wfc = new WaveformWFC(inputSamples);
const outputBuffer = new Float32Array(44100); // 1sec buffer
let context = new Array(wfc.patternSize - 1).fill(0); // Initial silence

for (let i = 0; i < outputBuffer.length; i++) {
  outputBuffer[i] = wfc.generateSample(context);

  // Update context (remove oldest, add new sample)
  context.shift();
  context.push(outputBuffer[i]);
}

// Play generated audio
const buffer = audioContext.createBuffer(1, outputBuffer.length, 44100);
buffer.copyToChannel(outputBuffer, 0);
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start();

Key Optimizations for Real-Time:

  1. Context Hashing:
    • Precompute lookup tables for quantized context keys to candidate patterns.
  2. Approximate Matching:
    • Use Euclidean distance thresholds for noisy inputs.
  3. Pattern Pruning:
    • Discard low-frequency patterns to reduce state space.
  4. Overlap Handling:
    • Generate samples in blocks (e.g., 128 samples) to amortize lookup costs.

How Structure is Preserved:

  1. Local Constraints:
    • Transition rules enforce that amplitude sequences (e.g., attack-decay in notes) replicate input behaviors.
  2. Global Coherence:
    • Weighted pattern selection maintains statistical properties (e.g., amplitude distribution).
  3. Continuity:
    • Overlapping context buffers prevent discontinuities between generated segments.

This approach mirrors WFC's constraint-solving in a temporal domain, transforming input waveforms into novel outputs with similar micro/macro structures.

Wave Function Collapse for Sounds

Journal 2025-06-23

New Domain : tensegrity.it

I've added a new DNS domain to my collection, so I now have :

  1. danny.ayers.name - my personal site (this blog)
  2. hyperdata.it - my quasi-company site
  3. strandz.it - for a project, in planning
  4. tensegrity.it - my current raft of code projects

They are all undeniably Under Construction.

The new one came about because I realised that my the things I'm working on strongly resemble a tensegrity structure. Only after setting up a GitHub repo to record this did I think to check for the domain name. Right now the repo just contains a sketchy overview. I'll use the new domain for now just as a tech blog, keeping the .name domain for non-coding stuff.

Journal 2025-06-23

Simple Distortion Circuit

rough demo video

I was a bit frustrated not having a convenient distortion module on my setup so I designed one. After a handful of sketches and a little bit of breadboarding, this is what I came up with :

Schematic

Its operation is a lot more straightforward than it looks.

The left-hand op amp is just operating as a textbook inverting amp, gain of x1 to about x10. The right-hand hand op amp is having more fun. The core idea is that of a log amplifier, the "transdiode" configuration. It's using the exponential characteristic of a bipolar transistor in the feedback of the op amp to achieve a logarithmic transfer function. In other words, soft clipping. Tube overdrive style, if you're an American guitar head. The magic part of this configuration is that the combination of NPN and PNP transistors, simply one on top of the other, actually works to make the function work on negative-going signals in mirror image. No way that would work! But it does.

I've flipped the op amp inputs so this is non-inverting - I'd forgotten, I was going to make the input amp non-inverting too... Reason being, ideally I wanted it DC-coupled so it could be an interesting function on control voltages too. But when wired up to modules this messed up its use on signals - the your guess is as good as mine Eurorack standard voltage levels. So I AC coupled it, hence the cap on input & output. (The 100p cap in the feedback is my cargo cult attempt at ensuring HF stability. It makes no audible difference).

The variable resistance in the feedback effectively adjusts when the transistors start getting curvy. When it's down to the 10k I couldn't see or hear any difference from the input signal, is effectively unity gain. Wide open, the curve starts early, it sounds like hard clipping.

The pair of diodes was the result of messing about. I initially tried them from the signal path between the first op amp and the second, to ground. I expected a buzzier hard clipping at a lower level, until the log-ish follower was wound up for gain. But it wasn't very interesting. What was interesting is the current config. I'll leave it to someone better at sums than me to figure out properly, but intuitively I would have expected this to have extended the linearish low level region up by the voltage drop until transistor bias starts kicking in, with the effect of raising the curvy roof. Nah. The result in practice is that it gives an audible boost overall. When the bases are direct to ground, the transfer curve seems rather kinky, not as smooth as it should be. With the diodes in circuit, it have a much more elegant curve. My technical explanation is something something op amp compensating something bias something.

Here's it on the breadboard :

Breadboard

I've got all the music room gear in the office while I redecorate, and the desktop machine I usually run a 'scope (Bitscope) on was busy entertaining Claude. So it gave me a chance to use my breadboard. It is literally built on a plastic breadboard, hat tip to the originators. As well as the horrid little Chinese scope and signal generator there's a home-hack audio amp, underneath a +/- 12v, +5 PSU in a tin I got from Penny Market. Got 3.5 mm sockets, various pots, a few switches. The idea is I can take prototypes to the modular in a Mohammed-mountain kind of deal. Only after I'd made it did I hear about the Erica Synths (they who do collabs with the wonderful DIYer Moritz Klein) do a ready-made, neat version : the EDU DIY Labor. Hey ho. Saved meself a few quid.

Oh yeah, back to the circuit. Nothing's critical. For op amps I used a TL072, transistors a BC109 & BC179, generic highish gain silicon small signal I happened to have. To keep it balanced up & down you probably want a complementary pair like that. I plan to slap the circuit on a scrap of stripboard, cut & drill a bit of aluminium sheet for front panel, couple of hours work max, my itch scratched.

Simple Distortion Circuit