Cross-Platform Sample Storage Plan#
Key Insight: Samples ARE Paintings#
AC already has pixel-sample.mjs which encodes audio samples as RGB pixel data
in painting bitmaps. stample.mjs uses this today. Paintings have full
infrastructure: upload, CDN, short codes (#k3d), @handle ownership, track-media
API, and cross-platform rendering.
Samples use the same # sigil and PNG storage format as paintings, but live in
a separate samples MongoDB collection with audio-specific metadata.
Architecture Decision: Shared Sigil, Separate Collection#
Why # (not a new sigil like ^)#
- Samples and paintings share the same PNG storage format
- A painting can BE a sample (load any image as audio via stample)
- A sample IS a painting (the pixel-encoded waveform is visible art)
%and&are URL-unfriendly (%is URL escape,&is query separator)- Keeps the sigil set small:
@people,$code,#media,*time
Why separate collection (not a flag on paintings)#
- Clean querying: "list my samples" vs "list my paintings" without filters
- Separate indexes optimized for audio metadata (duration, sampleRate, etc.)
- Separate counts/quotas per media type
- Future-proof: audio-specific features (waveform preview, BPM detection)
Shared code namespace#
#codes must be unique across BOTHpaintingsANDsamplescollectionsgenerateUniqueCode()checks both collections before assigning- Other sigils (
$KidLisp,*clock, tapes) have independent namespaces - The code resolver checks both collections to find which type a
#coderefers to
MongoDB Schema#
Collection: samples#
{
_id: ObjectId,
user: "auth0|63effeeb...", // owner (null for guest)
slug: "kick-drum", // user-friendly name
code: "k3d", // unique short code (shared with paintings)
when: ISODate, // upload timestamp
// Sample-specific metadata
v: 1, // pixel-sample encoding version
sampleRate: 48000,
sampleLength: 240000, // exact sample count
duration: 5.0, // seconds
channels: 1,
source: "native", // or "web"
// Standard media fields
ext: "png", // storage format
width: 256, // image dimensions
height: 313,
}
Indexes#
// Unique code (shared namespace with paintings — enforced at generation time)
await samples.createIndex({ code: 1 }, { unique: true, sparse: true });
// User queries: "list my samples"
await samples.createIndex({ user: 1 });
// Audio-specific queries
await samples.createIndex({ duration: 1 }); // sort by length
await samples.createIndex({ "source": 1 }); // native vs web
await samples.createIndex({ v: 1 }); // encoding version (for migration)
await samples.createIndex({ when: -1 }); // recent first
await samples.createIndex({ user: 1, slug: 1 }, { unique: true }); // per-user slugs
Encoding Version Contract#
v1 (current — pixel-sample.mjs):
- 3 audio samples per pixel (R, G, B channels)
- Float range -1.0..+1.0 mapped to 0..255
- A channel: 255 (opaque)
- Width: 256px (configurable)
- Height: ceil(sampleLength / 3 / width)
- Mono only
- The `v` field MUST be stored on every record so decoders know which algorithm to use
Implementation Plan#
Phase 1: Backend — track-media + code generation#
Files to modify:
-
system/netlify/functions/track-media.mjs- Add
mediaType: "sample"branch for PNG uploads with sample metadata - Route to
samplescollection instead ofpaintings - Store
v,sampleRate,sampleLength,duration,channels,source
- Add
-
system/backend/generate-short-code.mjsgenerateUniqueCode()accepts optionalsiblingCollectionsarray- For
#codes: checks bothpaintingsANDsamplesbefore assigning - Other types unchanged (single collection check)
-
system/netlify/functions/painting-code.mjs(or equivalent resolver)- When resolving
#code, checkpaintingsfirst, thensamples - Return
{ type: "painting" | "sample", ...record }so the client knows
- When resolving
-
New:
system/netlify/functions/list-samples.mjs(or extend existing)GET /api/samples/@handle→ list user's samplesGET /api/samples/@handle/:slug→ get specific sample- Returns CDN URLs + metadata
Phase 2: Native pixel-sample bridge#
Files to create/modify:
-
fedac/native/pieces/lib/pixel-sample-native.mjs- Pure-JS encode/decode (no DOM, no Canvas — works in QuickJS)
- Same algorithm as web
pixel-sample.mjs encodeSampleToBitmap(float32Array, width)→ RGBA pixel arraydecodeBitmapToSample(rgbaArray, width, height, sampleLength)→ float32[]
-
PNG write from native
- Option A: Add
stb_image_write.h(single-header, ~1KB) for PNG encoding in C - Option B: Minimal PNG writer in JS (deflate + PNG header — ~100 lines)
- Option C: BMP format (simpler, no compression, server converts to PNG on upload)
- Option A: Add
Phase 3: Upload from native#
Flow:
Record audio → float32[] → encodeSampleToBitmap → PNG bytes
→ POST /api/track-media { ext:"png", mediaType:"sample", sampleMeta:{v:1,...} }
→ GET presigned URL → PUT PNG to Spaces → #code returned
Files:
fedac/native/pieces/samples.mjs— add upload key (u)fedac/native/src/js-bindings.c—system.uploadMedia()binding if needed- Or use existing
system.fetch()+system.fetchPost()for the API calls
Phase 4: Download to native#
Flow:
Type #code → resolve via /api/painting-code → get CDN URL
→ fetchBinary(url, /tmp/sample.png) → decode PNG → decodeBitmapToSample
→ sound.sample.loadData(float32, rate) → play
Requires: PNG decoding in native
- Option A:
stb_image.h(single-header PNG decoder) - Option B: Decode in JS (pure-JS PNG inflate)
- Option C: Server endpoint that returns raw PCM (avoid client-side PNG decode)
Phase 5: Unified experience#
- notepat.mjs: type
#codeto load a sample from cloud into sample bank - samples.mjs: browse/record/upload/download on web and native
- stample.mjs: already works, becomes the web sample player
- Gallery: shows speaker icon on
#codes that are samples - Profile: separate "samples" tab alongside "paintings"
Data Flow#
Native Record Web Record
| |
float32[] float32[]
| |
encodeSampleToBitmap encodeSampleToBitmap
| |
PNG bytes PNG bytes (via Canvas)
| |
POST /api/track-media POST /api/track-media
{ mediaType:"sample" } { mediaType:"sample" }
| |
+----→ DO Spaces ←------+
+ MongoDB "samples"
+ short code (#abc)
|
+------------+------------+
| |
download PNG download PNG
| |
decodeBitmapToSample decodeBitmapToSample
| |
float32[] float32[]
| |
audio playback audio playback
Storage Math#
| Duration | Samples @48kHz | Pixels (3/px) | Image (256w) | PNG size |
|---|---|---|---|---|
| 1 sec | 48,000 | 16,000 | 256×63 | ~20 KB |
| 5 sec | 240,000 | 80,000 | 256×313 | ~100 KB |
| 10 sec | 480,000 | 160,000 | 256×625 | ~200 KB |
Key Files#
| Existing | Purpose |
|---|---|
system/public/aesthetic.computer/lib/pixel-sample.mjs |
Encode/decode samples↔bitmaps |
system/public/aesthetic.computer/disks/stample.mjs |
Web sample-painting player |
system/netlify/functions/track-media.mjs |
Media upload API |
system/netlify/functions/painting-code.mjs |
Short code → slug resolver |
system/backend/generate-short-code.mjs |
Unique code generation |
system/netlify/functions/presigned-url.js |
CDN upload/download URLs |
| New/Modified | Purpose |
|---|---|
fedac/native/pieces/lib/pixel-sample-native.mjs |
Pure-JS encode/decode for QuickJS |
fedac/native/pieces/samples.mjs |
Native sample browser + upload/download |
system/public/aesthetic.computer/disks/samples.mjs |
Web sample browser piece |
system/netlify/functions/track-media.mjs |
Add sample branch |
system/backend/generate-short-code.mjs |
Cross-collection check for # codes |