From 2c37e7fa62ceb79020f9f3821762c8850a837183 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Mon, 25 May 2026 21:31:09 +0100 Subject: [PATCH] Non-cringify --- .forgejo/workflows/deploy.yml | 7 ++ .../posts/ad-astra-attiny85-game-engine.md | 57 ++++----- src/content/posts/avoid-early-web-game.md | 8 +- .../posts/city-simulation-unity-traffic.md | 18 +-- .../posts/declared-shared-simulation-code.md | 54 ++++----- .../posts/fleeting-garden-webgpu-drawing.md | 94 ++++++--------- .../foreign-exchange-prediction-experiment.md | 20 ++-- .../graph-editor-javafx-simulation-input.md | 12 +- .../posts/greatai-ai-deployment-api.md | 60 ++++------ .../posts/life-towers-immutable-tries.md | 48 ++++---- .../posts/lights-synchronized-to-music.md | 18 +-- .../posts/my-notes-android-markdown-app.md | 12 +- .../posts/nuclear-cooling-simulation.md | 50 ++++---- src/content/posts/photo-colour-grader.md | 12 +- src/content/posts/photo-site-generator.md | 12 +- src/content/posts/platform-game-c-sdl.md | 15 ++- .../posts/reconcile-text-3-way-merge.md | 64 +++++----- src/content/posts/sdf-2d-ray-tracing.md | 61 +++++----- src/content/projects/ad-astra.md | 2 +- src/content/projects/avoid.md | 2 +- src/content/projects/city-simulation.md | 2 +- src/content/projects/colors.md | 2 +- src/content/projects/declared.md | 2 +- src/content/projects/fleeting-garden.md | 4 +- src/content/projects/forex.md | 2 +- src/content/projects/great-ai.md | 2 +- src/content/projects/leds.md | 4 +- src/content/projects/my-notes.md | 4 +- src/content/projects/nuclear-editor.md | 2 +- src/content/projects/nuclear-simulation.md | 2 +- src/content/projects/photos.md | 2 +- src/content/projects/platform-game.md | 2 +- src/content/projects/reconcile.md | 4 +- src/content/projects/sdf-2d.md | 4 +- src/content/projects/towers.md | 4 +- src/lib/site.ts | 2 +- src/pages/about.astro | 112 ++++++++++++------ src/pages/index.astro | 15 ++- src/styles/global.css | 10 ++ 39 files changed, 410 insertions(+), 397 deletions(-) diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 07a7185..d32842b 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -49,3 +49,10 @@ jobs: apt update && apt install -y rsync mkdir -p /pages rsync -a --delete dist/ /pages/schmelczer-dev + + - name: Copy build to staging pages mount + if: github.event_name == 'pull_request' + run: | + apt update && apt install -y rsync + mkdir -p /pages + rsync -a --delete dist/ /pages/schmelczer-dev-staging diff --git a/src/content/posts/ad-astra-attiny85-game-engine.md b/src/content/posts/ad-astra-attiny85-game-engine.md index 0570b2d..733f354 100644 --- a/src/content/posts/ad-astra-attiny85-game-engine.md +++ b/src/content/posts/ad-astra-attiny85-game-engine.md @@ -1,6 +1,6 @@ --- -title: A 50 FPS Game Engine on an ATtiny85 -description: Building a tiny embedded game engine around an ATtiny85V, OLED display, IR input, EEPROM persistence, and a custom PCB. +title: A 50 FPS Game Engine on an 8-Bit Microcontroller +description: A handheld game built from the PCB up — ATtiny85V, OLED, IR receiver, EEPROM, 8 MHz 8-bit ALU. 50 FPS floor. date: 2026-05-06 projectPeriod: 'Spring 2020' thumbnail: @@ -8,9 +8,9 @@ thumbnail: alt: The Ad Astra game running on a small OLED display. tags: ['embedded', 'games', 'systems'] role: Hardware and firmware author -stack: ['C', 'ATtiny85V', 'OLED', 'EEPROM', 'PCB design'] -scale: 8-bit microcontroller, 8 MHz clock, 15-20 ms maximum frame times during gameplay -outcome: A working low-power handheld game engine and game built from the circuit board up +stack: ['C', 'ATtiny85V', 'SPI OLED', 'IR receiver', 'EEPROM', 'KiCad'] +scale: 8 MHz, 8-bit ALU, ~31 mW at full brightness, ~1.5 mA standby, 15–20 ms frame budget +outcome: A handheld built from schematic to firmware, with a 50 FPS game on it audience: technical links: - label: Source @@ -22,42 +22,31 @@ media: mp4: /media/video/ad_astra.mp4 captions: /media/video/ad_astra.vtt alt: Video demonstration of the embedded game running on a small OLED display. - caption: The game engine ran on an ATtiny85V with an OLED display and IR input. - transcript: No spoken dialogue. The demonstration shows the Ad Astra handheld board running its OLED game, with the player moving through the small display while the IR input controls gameplay. + caption: The whole thing — board, firmware, sprites, game loop — runs on a single ATtiny85V at 8 MHz. + transcript: No spoken dialogue. The handheld board runs its OLED game; the player moves through the small display while the IR input controls gameplay. --- -Ad Astra came from wanting to combine graphics and microcontrollers without hiding behind a large development board. The result was a small embedded game engine and game built around an ATtiny85V, an OLED display, IR input, EEPROM persistence, and a custom PCB. +**The short version:** -The fun part was that every layer mattered. The circuit, display driver, memory layout, object model, sprite tooling, and game loop all had to fit inside a tiny system. +- A handheld game built from the PCB up: ATtiny85V, 0.96" SPI OLED, TSOP4838 IR receiver, 3.3V regulator, battery. +- 8-bit ALU at 8 MHz. Every byte and every cycle has a price tag, and that's the whole point. +- 15–20 ms peak frame time, so gameplay never drops below 50 FPS. Power draw ~31 mW peak, ~1.5 mA standby. -## The Problem +## Why the PCB changed the project -The hardware setup was intentionally constrained: an ATtiny85V, a D096-12864-SPI7 OLED display, a TSOP4838 IR receiver, and a 3.3V regulator. The system was low power, with peak consumption around 31 mW at full brightness and a standby mode around 1.5 mA. +I'd done microcontroller work on dev boards before and it always felt like I was renting the hardware. As soon as I had a real board with my soldering on it, bugs stopped feeling like software inconveniences and started feeling like consequences of choices I'd made in KiCad. That shift was most of the value of doing it this way. -Those numbers made the project feel physical. Performance was not an abstract target. Every frame and every byte had a cost. +The constraint that mattered: an 8-bit ALU at 8 MHz, with no FPU, no SIMD instruction set, and 8 KB of flash. Anything I built had to fit inside that, or I'd be staring at a brick. -## Constraints +## The bits worth showing -The engine ran at 8 MHz on an 8-bit ALU. That meant the display driver and game loop had to avoid expensive generality. +- **SIMD-on-an-8-bit-ALU display driver.** The OLED is 128×64 monochrome, 1024 bytes per frame. The driver packs four pixels into a byte and processes them with bit-parallel tricks. That's how the frame budget stayed under 20 ms with room for game logic. +- **Prototype-based inheritance, in C.** Entities share behaviour by pointing at a struct of function pointers. No vtable, no class, no allocator. Cheap dispatch and the whole object model fits on one screen. +- **Atomic EEPROM commits.** Sprite data and save state both live in EEPROM. The commit path writes a new region, then swaps a tiny header pointer. Pull the battery mid-write and the previous version is intact. +- **PNG-to-C sprite pipeline.** A Python script turns PNG artwork into static C arrays the firmware can include directly. Asset workflow without ever leaving the source tree. -Even the programming model needed restraint. I wrote the firmware in C, but used a balance of structured and object-oriented ideas to keep game object behaviour manageable without paying for a runtime that did not exist. +## What I'd change -## Design - -The display driver was the most performance-sensitive layer. I used SIMD-like techniques on the 8-bit ALU to process four pixels at once. That helped keep maximum frame times between 15 and 20 milliseconds during gameplay, so the lowest gameplay frame rate stayed above 50 FPS. - -For game objects, I used prototype-based inheritance. It was a pragmatic way to reuse behaviour while keeping the implementation simple enough for the target. - -Persistent state used the built-in EEPROM with an atomic commit approach. Sprite data also lived in EEPROM, and I wrote scripts to convert PNG sprites into C array definitions so assets could move into firmware cleanly. - -## What Worked - -The project worked because the abstraction level stayed close to the hardware. The engine had reusable pieces, but none of them pretended the platform was larger than it was. - -The custom PCB also changed the project. Once the system had a real board, bugs felt less like software inconveniences and more like design consequences. That made the final result much more satisfying. - -## What I Would Change - -Today I would write a more explicit development log around the display driver and persistence layer. Those are the parts that still feel technically interesting, and they deserve diagrams and measurements. - -I would also add a small emulator or host-side harness. Debugging firmware directly on constrained hardware is useful, but a fast feedback loop would have made the engine easier to evolve. +- **A host-side emulator.** Debugging firmware directly on hardware was character-building and slow. A small SDL-based simulator linking the same C code would have shortened the iteration loop from "reflash and hope" to "rebuild and run." +- **Power numbers I'd actually trust.** I have peak and standby draw. I don't have a curve over a real gameplay session, so I honestly can't say how long the battery lasts under load. I can only say it outlasted my patience. +- **A development log for the driver.** The display driver and the EEPROM commit protocol are the parts I'd still defend. They deserved diagrams and measurements at the time, not the half page of comments I left them with. diff --git a/src/content/posts/avoid-early-web-game.md b/src/content/posts/avoid-early-web-game.md index d556533..01a608e 100644 --- a/src/content/posts/avoid-early-web-game.md +++ b/src/content/posts/avoid-early-web-game.md @@ -1,6 +1,6 @@ --- -title: Avoid, an Early Web Game -description: A tiny archived web game from my first experiments with browser-based interaction. +title: Avoid +description: My first browser game. Tiny, archived for honesty. date: 2026-04-29 projectPeriod: 'January 2018' thumbnail: @@ -9,8 +9,8 @@ thumbnail: tags: ['games', 'web'] role: Game author stack: ['JavaScript', 'Canvas'] -outcome: A small playable web game kept as an archive of early browser work +outcome: My first browser game; kept for the timeline audience: general --- -I recently found my first web game. It is very simple, but I killed some time with it. +The first browser game I wrote. It's not good, but it was the moment a `` element stopped being mysterious. Keeping it here because pretending the older work didn't happen would be dishonest. diff --git a/src/content/posts/city-simulation-unity-traffic.md b/src/content/posts/city-simulation-unity-traffic.md index 51356fc..2fbcfd2 100644 --- a/src/content/posts/city-simulation-unity-traffic.md +++ b/src/content/posts/city-simulation-unity-traffic.md @@ -1,6 +1,6 @@ --- -title: A Unity City Simulation for a Cybersecurity Challenge -description: A client-server Unity simulation where REST-controlled traffic lights made mistakes immediately visible through crashes. +title: A Unity City Where Bad PLC Code Made Cars Crash +description: A REST-controlled traffic-light sim for a cybersecurity event. Bad PLC code showed up as car crashes — the most honest feedback loop I've shipped. date: 2026-05-01 projectPeriod: 'July-August 2018' thumbnail: @@ -9,17 +9,17 @@ thumbnail: tags: ['simulation', 'systems'] role: Simulation author stack: ['Unity', 'C#', 'REST API', 'Blender'] -outcome: A visual context for a PLC-focused cybersecurity challenge +outcome: Visible consequences for an otherwise abstract PLC challenge audience: technical links: [] --- -I simulated a city where car crashes were more frequent than usual. +A small city in Unity where the traffic lights were driven by a REST API. Contestants in a PLC cybersecurity event would write control logic; bad logic made cars crash, and they'd watch it happen. -The state of the traffic lights could be changed through a REST API. Drivers followed the instructions of those lights, so if a mistake was made, collisions appeared in the simulation. There was also support for displaying tweets on a HUD. +Three things are worth saying about it: -The project was the context for a cybersecurity challenge about PLCs. Contestants could see the effect of their work immediately, as crashes. +- **Visual feedback was the whole point.** Most security challenges punish wrong answers with a red "incorrect." This one punished them with car wrecks, and people learned faster. +- **Server-client, all decisions on the server.** Every agent's behaviour was computed centrally and broadcast. The harder problem wasn't simulation — it was getting the broadcast to be fault-tolerant on the conference Wi-Fi without flooding it. +- **Built it solo, including the models and animations in Blender.** Not a flex, just context for why everything's a little janky. -The architecture was server-client. Every decision of the agents was calculated server-side, and the real challenge was broadcasting those decisions in a fault-tolerant way on minimal bandwidth. - -It was built in Unity with C# as the scripting language. I also made the models and animations in Blender. +There was also a HUD overlay for tweets, which felt clever at the time and dated horribly. Skip that part. diff --git a/src/content/posts/declared-shared-simulation-code.md b/src/content/posts/declared-shared-simulation-code.md index f26512c..98f7701 100644 --- a/src/content/posts/declared-shared-simulation-code.md +++ b/src/content/posts/declared-shared-simulation-code.md @@ -1,6 +1,6 @@ --- -title: Shared Simulation Code in a Mobile Multiplayer Browser Game -description: How decla.red used shared TypeScript game logic, WebSockets, client prediction, and spatial indexing for a team-based browser game. +title: One Game Library, Imported by Both the Client and the Server +description: A mobile multiplayer browser game where client and server linked the same TypeScript module. One source of truth, one fewer class of bug. date: 2026-05-07 projectPeriod: 'Autumn-Winter 2020' thumbnail: @@ -8,9 +8,9 @@ thumbnail: alt: The decla.red browser game interface showing a space scene. tags: ['games', 'web', 'systems'] role: Game and backend systems author -stack: ['TypeScript', 'Node.js', 'WebSockets', 'Firebase', 'WebGL'] -scale: Multiple servers, each communicating with 16-32 clients -outcome: A mobile-capable online browser game built on top of SDF-2D +stack: ['TypeScript', 'Node.js', 'WebSockets', 'Firebase', 'WebGL', 'SDF-2D'] +scale: Multiple game servers, each talking to 16–32 clients, browser and mobile +outcome: A multiplayer browser game that proved SDF-2D survived a real game loop audience: technical links: - label: Source @@ -22,41 +22,31 @@ media: - type: image src: ./_assets/decla-red.jpg alt: The decla.red browser game interface showing a space scene with team controls and planets. - caption: decla.red used the SDF-2D renderer in a real-time multiplayer game. + caption: A real game loop is a worse audience than a tech demo. That's the point. --- -`decla.red` was a conquest-style online multiplayer browser game set in space. Two teams fought over small planets, gained points based on control, and could shoot at the other team while moving through a ray-traced 2D scene. +**The short version:** -The rendering made the game look interesting, but the architecture was the more useful lesson. The game needed to run on phones, talk to multiple servers, keep clients responsive, and avoid duplicating game rules between frontend and backend. +- Conquest-style space shooter. Two teams, small planets, points for control, ray-traced 2D rendering. +- Built on top of [SDF-2D](/articles/sdf-2d-ray-tracing/), partly to prove the renderer worked outside a thesis demo. +- The architecture decision worth remembering: one TypeScript module containing the game rules, linked by both the Node server and the browser client. Same code, both sides of the wire. -## The Problem +## The split that usually goes wrong -Real-time multiplayer games have an awkward split. The server should be authoritative, but the client has to feel immediate. If every meaningful interaction waits for a round trip, the game feels broken. If the client is trusted too much, the game becomes inconsistent or easy to abuse. +Real-time multiplayer has an awkward two-machine problem. The server has to be authoritative or the game is cheatable; the client has to feel immediate or the game is unplayable. If you write the rules twice — once on each side — they will drift. Eventually a player's screen will say one thing and the server will think another. -For this project, I wanted the same game rules to be used by the server and the client. The server would calculate the actual next state. The client could predict locally with the same code and later reconcile with the server. +I wanted the server's "compute the next state" function and the client's "predict the next state locally" function to be literally the same function. So I put the rules in a shared TypeScript library, published nothing, and had both `package.json` files link to it. -## Constraints +The win wasn't elegance, it was the bugs that didn't happen. Client prediction stopped being an approximation of the server — it _was_ the server, run optimistically and reconciled when the authoritative update came back. -The project used TypeScript on both sides: browser code for the client and Node.js for the server. WebSockets carried real-time updates. Firebase helped the servers reach consensus about the active server set. +## Other choices worth a sentence -Each server communicated with 16-32 clients. That is not large by industry standards, but it was enough to make careless spatial operations and state updates visible. +- **k-d trees for spatial queries.** Once the world held more than a few dozen objects, naive collision and proximity checks dominated the server tick. A k-d tree dropped them out of the profile. +- **Message-passing object model.** Lifted from Smalltalk's `doesNotUnderstand:` idea. Entities respond to messages they care about and ignore the rest. Easier to extend than the inheritance tree I tried first, and less brittle. +- **Firebase only for server discovery.** Not for game state — for "which servers are currently in the pool." Tiny consistent store, didn't need to write one. -## Design +## What I'd change -The key decision was a shared library for game logic. Both the client and server linked to it, so the transition rules lived in one place. - -That reduced a common source of bugs: the client and server disagreeing about the meaning of an action. It also made client-side prediction more realistic, because the client was not approximating a different system. - -As the game logic became heavier, spatial operations needed attention. I implemented k-d trees to reduce the cost of queries over objects in the world. For the object model, I borrowed ideas from message passing, including a version of the Smalltalk-style `messageNotUnderstood` pattern, to keep behaviour extensible without pushing every entity into a brittle inheritance tree. - -## What Worked - -Sharing simulation code was the most important architecture choice. It let the project stay coherent as the client and server evolved. - -The project also validated SDF-2D outside a toy environment. A rendering library is more convincing when it survives a game loop, input, network updates, and mobile constraints. - -## What I Would Change - -I would now spend more effort on observability for synchronisation and prediction errors. Multiplayer systems need good visibility into divergence. Without that, debugging becomes a sequence of guesses. - -I would also separate the story of rendering and networking more clearly in the codebase. Both were interesting, but they put different kinds of pressure on the architecture. +- **Observability for desync.** Multiplayer systems live or die by visibility into divergence. I had logs; I needed dashboards showing the rate, the shape, and the triggering interaction for every prediction miss. Without those, debugging was guessing. +- **Don't tangle rendering and networking in the same tree.** Both were interesting, both put different kinds of pressure on the architecture, and the directories grew into each other. Separate top-level folders from day one next time. +- **Skip multi-server until the math demands it.** I wired up multi-server early because it sounded right. With 16–32 clients per server I was nowhere near needing it; the complexity wasn't free. diff --git a/src/content/posts/fleeting-garden-webgpu-drawing.md b/src/content/posts/fleeting-garden-webgpu-drawing.md index 3685455..d1cbd9c 100644 --- a/src/content/posts/fleeting-garden-webgpu-drawing.md +++ b/src/content/posts/fleeting-garden-webgpu-drawing.md @@ -1,16 +1,16 @@ --- title: A WebGPU Drawing Garden Where Agents Rewrite Your Strokes -description: How Fleeting Garden runs an agent simulation in WebGPU compute shaders, with a 3×3 reaction matrix as the personality of each vibe. +description: A single-file WebGPU drawing toy. You stroke a colour, agents follow it, and a 3×3 matrix per vibe gives each preset its personality. date: 2026-05-22 projectPeriod: '2026' thumbnail: src: ./_assets/fleeting-garden.jpg alt: A kaleidoscopic Fleeting Garden snapshot of cyan, violet, and yellow agent trails radiating from a central knot. tags: ['graphics', 'simulation', 'web'] -role: Author +role: Graphics and shader author stack: ['TypeScript', 'WebGPU', 'WGSL', 'Compute shaders', 'Vite', 'Tweakpane'] -scale: A single-file WebGPU bundle, ~10 WGSL shaders, six vibe presets, runs entirely client-side -outcome: A browser drawing toy where user input seeds an agent simulation that rewrites the canvas in real time +scale: One HTML file, ~10 WGSL shaders, 6 vibe presets, 60 FPS target on consumer hardware +outcome: A browser drawing toy where user strokes seed an agent simulation that overwrites them audience: technical links: - label: Demo @@ -20,75 +20,55 @@ links: media: - type: image src: ./_assets/fleeting-garden.jpg - alt: Close-up of intertwining cyan, violet, and yellow agent trails radiating into a kaleidoscopic central knot, with a fine grain over the whole image. - caption: A snapshot of one Fleeting Garden session. The trail texture is what you see; the agents that drew it are no longer visible. + alt: Close-up of intertwining cyan, violet, and yellow agent trails radiating into a kaleidoscopic central knot. + caption: A snapshot from one session. What you see is the trail texture; the agents that drew it are already gone. --- -Fleeting Garden began as a chance to spend a few weeks inside WebGPU compute. The first constraint I set for myself was that user input should steer the simulation, not just seed it. The second was that the same engine should produce visibly different behaviour under different presets, without growing a fork per preset. +**The three-bullet pitch:** -The shape that emerged is a single-page drawing toy. You pick a palette, drag a colour onto the canvas, and a swarm of agents follows the stroke, branches off, and slowly rewrites the patch you laid down. The strokes themselves vanish immediately. What remains is a trail texture that the agents both read from and write to, blurred and faded a little every frame. +- Draw a stroke; agents spawn along it, follow the trail, and slowly overwrite the patch you laid down. You shape the gesture, the garden owns the response. +- Six "vibes" share the same simulation. The personality of each one lives in a 3×3 matrix of nine numbers in `{-1, 0, 1}`. That's the entire behaviour rule. +- One static HTML file. `vite build`, `rsync`, done. Runs offline, can be emailed, no account, no save state. -## The Problem +## Why physarum needed a knob -Physarum-style agent simulations are a well-trodden idea. Sense the surrounding trail, turn toward what you like, deposit a bit of your own colour, repeat. Drop a million of these on a texture and you get the familiar branching networks that look biological from a distance. +Physarum-style agent sims are everywhere and most of them stop being interesting after thirty seconds, because they converge to the same family of branching shapes no matter what you feed them. Seeding the initial condition isn't enough — the input has to keep being a force inside the loop, otherwise you're just watching the attractor settle. -The interesting question is not how to make one run. It is how to make one feel like something specific. A generic physarum visual converges to the same family of structures regardless of input, which is why so many of them stop being interesting after the first thirty seconds. User input has to do more than seed the initial condition; it has to remain a force inside the system. +My second self-imposed constraint was that one engine had to produce six visibly different presets without forking. The first prototype had a `switch (preset)` with one behaviour function per vibe and it was already painful at vibe two. I needed the personality to live in data, not code. -The second part of the problem is variety. The same engine had to produce visibly different behaviour under different presets, so that switching vibes felt like changing seasons rather than nudging one slider. That ruled out separate behaviour code per preset, which had been the obvious shape of the first prototype and had not survived contact with the second one. +## The reaction matrix -## Constraints +Each vibe is a 3×3 table of colour-to-colour affinities. When an agent of colour `i` looks at the trail in front of it, it weights the three channels of that sample by row `i` of the matrix, then uses the sign to pick left, right, or straight. That's it. The whole behaviour rule. -The toy had to be a single static file. No server, no account, no save state. Open the URL, draw, close the tab. That is the deal the metaphor makes with the user, and the deployment story falls out of it: `vite build` produces one HTML file, which a CI job rsyncs to a static host. +Three examples of what nine numbers can do: -It had to be WebGPU only. Compute shaders are the right tool for this kind of simulation, and writing a Canvas2D or WebGL fallback would have meant either a second implementation or a watered-down primary one. The browserslist is literally `supports webgpu and last 2 years`, and anything older gets a clear message instead of a degraded experience. +- **Aurora Mycelium** — cyclic, each colour chases the next. Agents wind into ribbons. +- **Velvet Observatory** — every off-diagonal entry negative. Colours repel into separate islands. +- **Paper Lantern Fog** — matrix filled with ones. Colours collapse into one cooperative blob. -It had to run on consumer hardware at sixty frames per second. The number of agents is the obvious lever, so it had to be adaptive. The number of WGSL pipelines is the less obvious one, so the architecture had to keep each frame's compute work split across a small number of focused shaders rather than one fat kernel. +Adding a tenth number to the matrix would tax every existing vibe. Tuning the nine I have is a text edit. Six presets in, I haven't extended it. -## Design +## The compute work, broken into small jobs -The simulation is split into six compute stages, written across ten WGSL files. Each stage has one job: +Six stages, ten WGSL files, each one short enough that I can hold it in my head when something breaks: -1. **Agent step** advances every agent by one frame. It samples the trail texture at a sensor offset, picks a turn direction, moves, and deposits a small amount of colour into the next frame's trail texture. -2. **Diffusion** blurs and decays the trail texture, so old marks soften and disappear. -3. **Brush** writes user strokes into the trail texture and a separate "source" texture that the agent shader can read. -4. **Eraser** has two variants. One clears a region of the trail texture, the other kills agents inside the eraser radius. -5. **Agent generation** handles spawning new agents along a stroke, resizing the agent buffer when the cap changes, and compacting the buffer after erasure so dead slots do not waste GPU time. -6. **Render** reads the final trail texture and produces the canvas image, with the palette and grain applied at the last moment. +1. **Agent step** — sample the trail at a sensor offset, pick a turn, move, deposit colour. ~300 lines, the longest one. +2. **Diffusion** — blur and decay so old marks soften. +3. **Brush** — write user strokes into both the trail texture and a separate "source" texture the agents can read. +4. **Eraser** — two variants: one clears a region of the trail, the other kills agents in a radius. +5. **Agent generation** — spawn along strokes, resize the buffer when the cap changes, compact after erasure so dead slots don't waste GPU time. +6. **Render** — read the trail, apply palette and grain. -Each of these is around a few dozen lines of WGSL, and the longest one (agent step) is under 300. Keeping them small is what made the simulation tunable; once they grew tangled, the tuning loop slowed to a crawl. +The bind-group setup overhead from running more pipelines was lost in the noise next to the simulation cost. The win was that when the eraser shader started killing the wrong agents, I opened one file and reasoned about it without touching anything else. -### The Reaction Matrix +## Smaller calls -The piece of the design I would defend hardest is the reaction matrix. Each vibe carries a 3×3 table of colour-to-colour affinities. When an agent of colour `i` senses the trail in front of it, the three channels of that sample are weighted by row `i` of the matrix to decide whether to turn left, turn right, or hold course. That is the entire behaviour rule. +- **Adaptive cap, circular buffer.** If FPS drops, the cap shrinks; if there's headroom, it grows. When the cap is hit, new agents overwrite older ones. The decay you see — a stroke vanishing thirty seconds after you drew it — isn't an explicit eraser, it's the buffer wrapping around. +- **URL is the share format.** The chosen vibe is in the query string. The "send your friend this preset" link is just a URL with `?vibe=tidepool-lantern` on it. The parser is tolerant about accents and casing because people retype these. +- **One HTML file.** All CSS and JS inline. The piano samples sit beside it. Self-contained enough to email or drop on a USB stick. -The matrix is nine numbers in `{-1, 0, 1}`, and it captures most of what makes the six vibes feel different. _Aurora Mycelium_ has a cyclic preference where each colour chases the next, so its agents wind into ribbons. _Velvet Observatory_ has every off-diagonal entry negative, so the colours repel each other and settle into separate islands. _Paper Lantern Fog_ has the matrix filled with ones, which collapses the three colours into one cooperative blob. +## What I'd change -Putting the personality of a vibe in a small, legible matrix was deliberate. The earlier prototype had a behaviour function per preset, and that route did not survive the second vibe — every new mood became a new branch in a switch statement. A 3×3 matrix is small enough that I can read it and predict the rough shape of the result, which made tuning new vibes a matter of editing a table rather than writing code. - -### Input and Mirroring - -The drawing pipeline is intentionally simple. A pointer event becomes a series of stroke segments, each segment spawns agents along its length, and the agents' initial angle points along the stroke with a small amount of jitter. The mirror slider folds each stroke into N copies rotated around the centre, which is the cheapest way I could think of to give the user a sense of composition without a layers panel. - -Spawning competes with an adaptive cap. If the framerate drops below the target, the cap shrinks; if there is headroom, it grows. When the cap is hit, new agents overwrite older ones in a circular buffer. That overwrite is what gives the garden its decay: a stroke you drew thirty seconds ago is gone not because anything erased it, but because its agents have been replaced. - -### Vibes as URLs - -Switching vibes is the only stateful action in the app, and the chosen vibe is encoded in the URL query string. That makes the link itself the share format. A snapshot is a PNG you download; a "send your friend this preset" is a URL with `?vibe=tidepool-lantern` on the end. The URL parser is tolerant about accents, casing, and whitespace, because the names are the kind of thing people retype rather than copy. - -## What Worked - -The reaction matrix earned its place. Six presets later, I have not had to extend it. Every new vibe so far has been a recolouring plus a different table, sometimes with tweaks to the diffusion or sensor parameters, and the underlying simulation has not changed. At this scale, configuration is cheaper to evolve than code. Adding a tenth number to the matrix would be a tax on every existing vibe; tuning the nine I have is a few minutes of editing a file. - -Splitting the compute work across small WGSL stages held up for the same reason in a different form. When the agent-erase shader started killing the wrong agents, I could open one short file and reason about it without touching anything else. The cost of running more pipelines is the bind-group setup, and that was lost in the noise compared to the simulation work itself. - -The single-file build is the part I underestimated. The whole app, including all CSS and JavaScript, is one HTML file; the piano samples sit beside it and are preloaded at startup. That makes deployment trivial — `rsync` and done — but the part that actually matters is that the file is self-contained enough to hand around. I can attach it to an email or drop it on a USB stick and it runs offline, which is the closest a web app gets to feeling like an object. - -## What I Would Change - -The intro animation cost more than it should have. Agents fly in from off-screen to spell out the title, then transition to steady-state behaviour. The choreography is tied to a single `progress: 0 → 1` value that bleeds into timing, easing, and target positions across three different shaders, and that coupling is what makes the intro the part of the code I would least want to refactor today. If I rebuilt this, I would model the intro as its own dispatch with its own agent buffer and hand off to the steady-state pipeline at the boundary. - -Property tests would help more than I expected. The simulation has invariants that hand-written unit tests are bad at finding — agent count stays under the cap, every drawn stroke produces a positive-coloured deposit on the next frame, the eraser does not leak agents past its radius — and these are exactly the shape of claim a generator-based test would falsify quickly. - -The mobile experience is good enough rather than good. Pointer events behave, but small screens make the toolbar fight the canvas for space, and the agent cap has to shrink hard to keep the framerate up. A real fix means rethinking the toolbar layout and probably making the cap-versus-resolution tradeoff a user-visible choice. - -The part I would keep is the asymmetry. You shape the gesture; the garden owns the response. The trail decay and the refusal of save state both look like missing features in isolation, and both stop looking that way the moment the garden is allowed to be fleeting. Most of the rest of the design is what fell out of taking that idea seriously. +- The intro animation (agents fly in to spell the title, then transition to steady state) couples three shaders through a single `progress: 0 → 1` value. It's the bit I'd least want to refactor today. Next time I'd model the intro as its own dispatch with its own buffer and hand off cleanly. +- Mobile works, but the toolbar fights the canvas for screen and the agent cap has to shrink hard to keep frame time down. A proper fix means rethinking the toolbar and exposing the cap-vs-resolution tradeoff to the user. +- The simulation has invariants — agent count under the cap, every stroke produces a positive-coloured deposit on the next frame, the eraser doesn't leak agents past its radius — that proptest would falsify in minutes. Snapshot tests aren't the right tool here. diff --git a/src/content/posts/foreign-exchange-prediction-experiment.md b/src/content/posts/foreign-exchange-prediction-experiment.md index 42f15bc..43e7b3f 100644 --- a/src/content/posts/foreign-exchange-prediction-experiment.md +++ b/src/content/posts/foreign-exchange-prediction-experiment.md @@ -1,6 +1,6 @@ --- -title: A Frequency-Domain Foreign Exchange Prediction Experiment -description: An older EUR/USD prediction experiment built from smoothing, short-time Fourier transforms, extrapolation, and a Python prediction server. +title: Predicting EUR/USD With Hanning Windows +description: A weekend frequency-domain experiment that did a passable job on EUR/USD. I would not have trusted it with my money, and I didn't. date: 2026-05-03 projectPeriod: 'Autumn 2019' thumbnail: @@ -9,15 +9,21 @@ thumbnail: tags: ['systems', 'tools'] role: Experiment author stack: ['Python', 'NumPy', 'SciPy', 'Flask', 'MQL4'] -outcome: A working prediction server connected to an MQL4 client for trading experiments +outcome: A prediction server, an MQL4 trading client, and a clearer view of how far my edge wasn't audience: technical links: [] --- -This was an experiment in predicting EUR/USD rates. The animation from the old portfolio showed the implementation doing a passable job: the prediction was the blue graph and the actual values were the green one. I would not have trusted it with my money. +A linear regression in the frequency domain, dressed up. Old portfolio screenshots showed the prediction (blue) tracking the actual rate (green) closely enough to be flattering. I did not trade real money with it. -The algorithm was a fancy linear regression in the frequency domain. The steps were: smoothing the input values, differentiating, applying a short-time Fourier transformation with overlapped and Hanning-windowed windows, extrapolating, and then applying the inverse of these transformations to the resulting values. +The pipeline: -The prediction server was written in Python using NumPy, SciPy, and Flask. It communicated with an MQL4 client that was responsible for handling financial transactions based on the generated data. +- Smooth the input series. +- Differentiate. +- Short-time Fourier transform with overlapped, Hanning-windowed frames. +- Extrapolate the frequency-domain coefficients. +- Invert everything back to a predicted price series. -There was still plenty of room for improvement, but even with this simple algorithm, a sometimes profitable strategy was viable. The project was mostly a look into trading algorithms, their complexity, and the competition around them. +A Python server (NumPy, SciPy, Flask) ran the model. An MQL4 client on a broker terminal called the server and would have placed trades if I'd dared. + +What I actually learned: even a naive model can show a sometimes-profitable backtest, and that's the trap. The real game is built by people with co-located servers, microsecond ticks, and millions in infrastructure. This project taught me how far my edge wasn't. diff --git a/src/content/posts/graph-editor-javafx-simulation-input.md b/src/content/posts/graph-editor-javafx-simulation-input.md index 2c2582b..13f4cf0 100644 --- a/src/content/posts/graph-editor-javafx-simulation-input.md +++ b/src/content/posts/graph-editor-javafx-simulation-input.md @@ -1,6 +1,6 @@ --- -title: A JavaFX Graph Editor for Simulation Input -description: A small JavaFX editor for creating and uploading graph input for the cooling system simulator. +title: A JavaFX Editor for the Cooling Simulator +description: The companion editor for the cooling-system sim. Drag-and-drop graph layout, JSON export, upload-to-backend. Small tool, mattered more than I expected. date: 2026-04-25 projectPeriod: 'October-November 2018' thumbnail: @@ -9,13 +9,13 @@ thumbnail: tags: ['simulation', 'tools'] role: Editor author stack: ['JavaFX', 'JSON', 'REST API'] -outcome: An editor for building input graphs and sending them to the simulation backend +outcome: A drag-and-drop graph editor that let non-developers feed the simulator audience: technical links: [] --- -This was a small editor for building input graphs for the cooling system simulator. +The input-side companion to the [cooling system sim](/articles/nuclear-cooling-simulation/). A JavaFX desktop tool where you'd lay out the plant as a graph, edit each element's parameters in a side panel, export JSON, or upload directly to the simulator backend. -Nodes could be moved with drag-and-drop gestures. Element parameters were edited on the right panel. +It was small but it punched above its weight at the actual event. The simulator's value depended entirely on the organisers being able to change the plant without me sitting next to them. The editor was what made that possible. -The UI was built with JavaFX. The output could be exported as JSON or uploaded directly to the simulation backend. +If I built it again I'd skip JavaFX and put the editor in the browser — same place the monitoring clients lived. One install fewer for everyone. diff --git a/src/content/posts/greatai-ai-deployment-api.md b/src/content/posts/greatai-ai-deployment-api.md index ad34d7b..3487747 100644 --- a/src/content/posts/greatai-ai-deployment-api.md +++ b/src/content/posts/greatai-ai-deployment-api.md @@ -1,6 +1,6 @@ --- -title: Designing an ML Deployment API Around Best Practices -description: How GreatAI tried to make stronger ML deployment habits accessible through a small Python API. +title: A Python Framework Where Doing the Right Thing Is the Default +description: My MSc thesis. 33 catalogued ML deployment habits, a decorator-shaped Python API, and a survey of working engineers on which actually got adopted. date: 2026-05-09 projectPeriod: '2022' thumbnail: @@ -9,9 +9,9 @@ thumbnail: tags: ['ai', 'systems', 'tools'] featuredOrder: 1 role: Researcher and framework author -stack: ['Python', 'ML deployment', 'API design'] -scale: 33 deployment best practices, six proposed additions, evaluated with professional data scientists and software engineers -outcome: A Python framework, thesis, and research-backed API design for production-oriented AI deployments +stack: ['Python', 'decorators', 'FastAPI', 'survey design'] +scale: 33 deployment habits surveyed, 6 proposed additions, framework evaluated by working data scientists and engineers +outcome: A pip-installable framework, an MSc thesis, and one strong opinion about API surface area audience: recruiter-relevant links: - label: PyPI @@ -25,47 +25,33 @@ media: - type: image src: ./_assets/great-ai.png alt: Example Python code using GreatAI decorators and prediction helpers. - caption: GreatAI's public surface was designed to keep deployment best practices close to the application code. + caption: A working GreatAI service is about ten lines on top of a plain prediction function. --- -GreatAI started from a practical frustration: applying machine learning was becoming easier, but deploying it well was still easy to get wrong. Many failures were not about model architecture. They were about missing metadata, weak versioning, poor reproducibility, untracked inputs, or interfaces that made the right behaviour too cumbersome to use. +**The short version:** -My thesis work looked at that gap from two sides. First, I collected and organised AI/ML deployment best practices, including 33 practices and six additions proposed through the research. Then I designed a Python framework that tried to make those practices feel like the natural path rather than an enterprise checklist. +- I surveyed working data scientists and engineers on a catalogue of 33 ML deployment habits. The honest finding: people skip habits not because they disagree, but because adopting one means writing five lines of glue, and they have a deadline. +- That's a design problem, not a discipline problem. The thesis built a Python framework around one bet: put the deployment behaviour next to the prediction function with a single decorator, and the right thing becomes the path of least resistance. +- Pip-installable as `great-ai`. Worth a look mainly for the survey methodology behind it. -The result was GreatAI: a deployment-oriented framework with a deliberately small API. The design goal was not to wrap every part of an ML stack. It was to make common deployment concerns visible, automatic where possible, and hard to forget. +## The thing nobody wants to admit -## The Problem +The literature has a long list of habits you should adopt when shipping an ML service: track inputs, version models, expose health, log decisions, keep predictions reproducible. Everyone agrees with the list. Almost nobody implements all of it. -Deployment quality is often treated as something that happens after model development. That separation creates a bad default. A model can be useful in a notebook, but a deployed AI service also needs traceability, stable interfaces, input/output logging, model metadata, and operational behaviour that can be inspected later. +I spent the bulk of the thesis catalogueing 33 such habits, proposing 6 more, and surveying engineers on which actually got applied in their day jobs. The data was pretty clear about the failure mode: it wasn't ignorance, it wasn't laziness, it wasn't budget. It was that the cost of doing the right thing — five lines of glue per habit, multiplied across a stack — was higher than the visible cost of skipping it. So skipping it became the default. -The hard part is not listing those needs. The hard part is getting busy engineers and data scientists to adopt them without making their work feel slower. +So the real research question wasn't "what should engineers do." It was "what API shape makes doing the right thing cheaper than not." -So the core question became: can a framework implement meaningful deployment practices while keeping the API small enough that people would actually use it? +## The framework's bet -## Constraints +- **A decorator on a plain function.** `@GreatAI.create` turns a regular Python function into a deployed service with metadata, request tracing, and a versioned interface. No inheritance, no project layout, no enforced directory structure. The mental cost is one import. +- **Implicit behaviour only for cross-cutting concerns.** Logging, versioning, metadata are implicit. Anything touching business logic stays explicit. The rule: if it would surprise me when I'm debugging, it shouldn't be implicit. +- **Own the contract, leave the storage alone.** Where you persist logs, models, or metrics is your choice; GreatAI defines the shape and provides defaults. The model registry stays somebody else's library. -GreatAI had to satisfy two constraints that usually pull in opposite directions. +The survey backed up the central premise: ease of use and functionality both matter for adoption, and they're independent axes. A framework that ticks every box and is awkward will lose to a smaller one that doesn't. -It needed to encode deployment practices such as metadata handling, model loading, request tracing, and reproducible prediction interfaces. It also had to be approachable enough that the basic use case still looked like ordinary Python. +## What I'd change -That shaped the API. The framework could not demand a new mental model for every project. The deployment behaviour had to sit close to the prediction function, because that is where the developer already has context. - -## Design - -The design leaned on decorators and lightweight conventions. The application author should be able to declare the prediction boundary, attach the relevant model and metadata behaviour, and let the framework handle repeated operational concerns. - -That is a careful tradeoff. Too much implicit behaviour makes systems difficult to debug. Too much explicit setup makes best practices optional in practice, because the path of least resistance is to skip them. GreatAI tried to keep the implicit parts focused on cross-cutting deployment concerns rather than business logic. - -Feedback from professional data scientists and software engineers supported the main premise: ease of use and functionality both matter when people decide whether to adopt deployment tooling. A framework that is technically complete but awkward to use will still fail. - -## What Worked - -The strongest part of the project was treating API design as part of deployment quality. Best practices are not only documentation. They need interface support, defaults, and feedback loops. - -The research also forced the framework to be specific. "Production-ready" is too broad to be useful. A concrete list of deployment practices made it possible to ask which practices can be automated, which ones need explicit developer decisions, and which ones belong outside the framework. - -## What I Would Change - -If I returned to the project now, I would focus more on integration boundaries: how GreatAI should fit into modern observability, model registry, and evaluation workflows without trying to own them. Deployment frameworks age quickly when they become too broad. - -The part I would keep is the central idea: make the right deployment behaviour easy enough that it becomes the default. +- I'd narrow further. Anything GreatAI did that overlapped with MLflow, BentoML, or modern observability stacks would go. The durable bit was always the decorator and the catalogue behind it. +- I'd publish the survey instrument separately. The 33-habit catalogue and the adoption-vs-impact methodology outlive the framework. People still ask about that part. +- I'd stop calling them "best practices." I used that phrase in the thesis and it aged into corporate-speak. The honest name is "things that hurt later if you skip them." diff --git a/src/content/posts/life-towers-immutable-tries.md b/src/content/posts/life-towers-immutable-tries.md index 9a90cf6..82bb3ab 100644 --- a/src/content/posts/life-towers-immutable-tries.md +++ b/src/content/posts/life-towers-immutable-tries.md @@ -1,6 +1,6 @@ --- -title: Syncing State with Immutable Tries -description: How a multi-device life tracking project used trie structure to diff, reconcile, and synchronise goal state. +title: Syncing State with an Immutable Trie +description: A visual goal tracker whose lasting idea was the sync model — an immutable trie so structural diffs are trivial and only deltas cross the wire. date: 2026-05-05 projectPeriod: 'August-September 2019' thumbnail: @@ -9,9 +9,9 @@ thumbnail: tags: ['systems', 'web', 'tools'] featuredOrder: 4 role: Full-stack author -stack: ['Python', 'Angular', 'State synchronisation'] +stack: ['Python', 'Angular', 'TypeScript', 'Custom sync protocol'] scale: Multi-device goal and task state shared between clients and a server -outcome: A working synchronisation model built around immutable trie properties +outcome: A working sync protocol where structural sharing made the delta tiny audience: recruiter-relevant links: - label: Source @@ -20,35 +20,43 @@ media: - type: image src: ./_assets/towers.jpg alt: Screenshot of a life tracking web interface represented with tower-like visual structures. - caption: The visual idea was simple; the useful lesson was the synchronisation model behind it. + caption: The interface was a 2019 weekend experiment. The trie underneath aged better. --- -Life Towers was a multi-device goal and task tracker with an intentionally visual interface. The surface idea was an aesthetic representation of previous and current goals. The more interesting part was synchronising state across clients without sending more data than necessary. +**The short version:** -This was not a large distributed system, but it had a real version of a common problem: clients and server drift apart, and the system needs a compact way to compare, reconcile, and update. +- Built as a personal task tracker with a tower metaphor. The UI was the fun bit; the sync model is the part I'd still write the same way today. +- Clients and server hold the same data as an immutable trie. Comparing two versions is a walk over shared nodes. Sending the delta is sending only the nodes that aren't shared. +- No CRDT, no OT, no clever conflict resolution. The structure does the work, the protocol stays boring. -## The Problem +## The problem in one paragraph -If a task model is stored as an ordinary mutable object graph, synchronising it often becomes a choice between sending too much data or writing complicated ad hoc diff logic. +Pick any non-trivial mutable object graph, sync it across devices, and you end up either sending the whole thing on every change (wasteful) or writing ad-hoc diff logic per shape (brittle). I wanted a representation where the _shape_ of the data made the diff fall out for free. -I wanted a structure where the shape of the data made synchronisation easier. The client should be able to compare its state with the server's state, find a difference, reconcile it, and send only the delta. +## The trie, concretely -## Design +A goal in Life Towers is a path of strings. `Health / Running / 5k`. Tasks under a goal hang off the leaf. A user's whole state is a tree, and a trie is exactly the data structure that makes that tree's _identity_ manipulable. -I used a trie. A trie made the hierarchical shape explicit, and its properties made it easier to reason about differences between stored versions. +Two properties did the heavy lifting: -The immutable nature of the structure simplified much of the logic. Instead of mutating arbitrary branches in place, updates could produce new structure with shared unchanged parts. That made reconciliation easier to reason about and reduced the amount of data that needed to move across the network. +- **Structural sharing.** When you tick off a task under `Health / Running / 5k`, the new root reuses every untouched subtree by reference. The `Career` branch and the `Reading` branch are the same objects they were before. Comparing the old and new roots is mostly pointer equality; only the path that actually changed gets walked. +- **Immutability.** Updates produce new structure instead of mutating. "Where I was" and "where I am" become two pointers, not two snapshots. The diff between them is whatever's not shared — and that walk is O(changes), not O(state). -The project also gave me a reason to deepen my Python and Angular knowledge, but the synchronisation structure was the main lesson. +The sync loop falls out: -## What Worked +1. Client holds the last root the server acknowledged plus its own current root. +2. To send: walk only the unshared paths, emit one op per changed leaf. In practice that's a handful of bytes for a typical edit, no matter how large the rest of the tree is. +3. Server applies, returns its new root. +4. Client rebases any in-flight edits by replaying them on top. -The biggest win was choosing a data structure that matched the problem. Once the state was represented in a way that made comparison natural, the network protocol became simpler. +There's no conflict resolution layer because the operations commute on the structure — two clients adding tasks under different branches produce non-overlapping deltas that compose trivially. The hard cases (two clients editing the same leaf) are tiny and obvious, because they're the _only_ place the deltas touch the same path. -The other useful lesson was that visual products still need a strong internal model. A pleasant interface is fragile if the underlying state is hard to trust. +## What I'd change -## What I Would Change +- **Property tests around the rebase.** The reconcile path is exactly where a generator finds bugs that hand-written tests never think to write. I had hand-written cases; I'd start with `proptest` now. +- **A standalone spec for the wire format.** The part worth lifting out was the protocol, not the goal tracker. A short spec would let me (or anyone) reimplement it in a different stack without re-deriving everything from the Python source. +- **Strip the visual experiment.** The tower visualisation was fun but it bound the storage to a UI metaphor. The sync model should be a library; the towers should be a separate toy. -Today I would document the sync protocol more formally and add property-based tests around reconciliation. Synchronisation code is exactly the kind of code that benefits from generated edge cases. +## If you take one idea from this -I would also separate the visual experiment from the state synchronisation story more explicitly. The latter is the part that aged better. +Most sync problems are diff problems pretending to be transport problems. Pick the data structure that makes the diff free, and the protocol almost writes itself. The corollary: if you're writing a lot of "if this changed, send that" code, you're using the wrong structure. diff --git a/src/content/posts/lights-synchronized-to-music.md b/src/content/posts/lights-synchronized-to-music.md index 4f86e56..799b189 100644 --- a/src/content/posts/lights-synchronized-to-music.md +++ b/src/content/posts/lights-synchronized-to-music.md @@ -1,6 +1,6 @@ --- -title: Lights Synchronized to Music -description: A Raspberry Pi music player that analysed audio output and drove RGB LED strips. +title: My First Real Project — LEDs Driven by an FFT +description: A Raspberry Pi music player that drove RGB strips through MOSFETs. The first thing I started and actually finished. date: 2026-04-26 projectPeriod: 'Spring 2016' thumbnail: @@ -8,14 +8,18 @@ thumbnail: alt: RGB LED strips lit by a music synchronisation project. tags: ['systems', 'tools'] role: Hardware and software author -stack: ['Python', 'NumPy', 'FFT', 'Raspberry Pi', 'Vanilla web'] -outcome: My first finished non-trivial project, combining a web UI, audio processing, and hardware output +stack: ['Python', 'NumPy', 'FFT', 'Raspberry Pi', 'MOSFETs', 'vanilla web'] +outcome: The first non-trivial project I started and finished audience: technical links: [] --- -A Raspberry Pi ran a small music player, and the audio it produced drove the colour of a couple of RGB LED strips through some MOSFETs. +Spring 2016. I had a Raspberry Pi, a couple of 12V RGB LED strips someone had given me, a handful of MOSFETs from an electronics kit, and zero idea what I was doing. The plan was something like: play music, look at it, make the lights match. -It was the first non-trivial project I actually finished. Far from perfect, but I am still proud that I built it on my own. +I got bands wrong first. I tried mapping raw audio amplitude to brightness, which made the lights pulse with anything — clipping, voice, fan noise — and produced a strobing mess that hurt to look at. Reading about Fourier transforms long enough to type `numpy.fft.fft(audio_chunk)` into a REPL was the moment the project started actually behaving like the thing I'd imagined. Bass-heavy frequency bins went to red; mids to green; highs to blue. Smoothing the output over a few frames stopped the seizure-inducing flicker. -The backend was Python, with NumPy doing the FFT. The frontend was a vanilla web page for picking tracks and tweaking settings. +The MOSFETs took longer than they should have. I wired one backwards and it got hot enough to leave a small mark on the breadboard. I learned to read a datasheet, slowly, by needing one. + +The frontend was a vanilla web page on the same Pi: pick a track, tweak the band thresholds, see what changed. No framework. Just a `