{"id":49146202,"url":"https://github.com/wmodes/driftconditions","last_synced_at":"2026-04-26T00:01:08.588Z","repository":{"id":222579946,"uuid":"757033364","full_name":"wmodes/driftconditions","owner":"wmodes","description":"a procedurally generated audio stream, blending fragmented stories, ambient sounds, and mysterious crosstalk into an ever-evolving soundscape","archived":false,"fork":false,"pushed_at":"2026-04-22T03:40:04.000Z","size":99632,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-04-22T04:03:52.371Z","etag":null,"topics":["art","creative-coding","ffmpeg","generative-audio","icecast","media-arts","nodejs","procedural-generation","radio-art","sound-art"],"latest_commit_sha":null,"homepage":"https://driftconditions.org","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wmodes.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"MIT-LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":".zenodo.json","notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-02-13T18:50:32.000Z","updated_at":"2026-04-22T03:40:09.000Z","dependencies_parsed_at":"2026-04-22T04:00:57.687Z","dependency_job_id":null,"html_url":"https://github.com/wmodes/driftconditions","commit_stats":null,"previous_names":["wmodes/interference","wmodes/driftconditions"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/wmodes/driftconditions","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wmodes%2Fdriftconditions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wmodes%2Fdriftconditions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wmodes%2Fdriftconditions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wmodes%2Fdriftconditions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wmodes","download_url":"https://codeload.github.com/wmodes/driftconditions/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wmodes%2Fdriftconditions/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32280981,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-25T18:29:39.964Z","status":"ssl_error","status_checked_at":"2026-04-25T18:29:32.149Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["art","creative-coding","ffmpeg","generative-audio","icecast","media-arts","nodejs","procedural-generation","radio-art","sound-art"],"created_at":"2026-04-22T04:00:31.014Z","updated_at":"2026-04-26T00:01:08.553Z","avatar_url":"https://github.com/wmodes.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# DriftConditions\n\n[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.19688234.svg)](https://doi.org/10.5281/zenodo.19688234)\n\n*DriftConditions* is an online audio source that captures the chaos and serendipity of late-night radio tuning — an uncanny audio stream generated entirely on the fly by code. Overlapping fragmented stories, ambient sounds, and mysterious crosstalk weave a vivid sonic tapestry, drawing listeners into an immersive and unpredictable listening experience.\n\nInspired by the unpredictability of real-world radio interference, *DriftConditions* explores the boundaries between intention and happenstance, inviting listeners to eavesdrop on a hidden world of voices and atmospheres unconstrained by traditional narrative structures. Every session is unique — it will never be heard exactly the same way again.\n\n**Listen live:** https://driftconditions.org/\n\n---\n\n## How It Works\n\nThe experience is built from three layers:\n\n1. **Content** — contributors upload audio clips (music, field recordings, spoken word, ambient sound) through a web interface. Editors and moderators review and tag everything.\n\n2. **Recipes** — editors write JSON-like recipes that describe how clips should be combined: how many tracks, what kinds of audio go on each track, how long, what effects to apply. Recipes are the creative blueprints for each soundscape.\n\n3. **The Mix Engine** — a backend server reads the recipes, stochastically selects clips that match each recipe's criteria, and assembles them into a continuous audio stream using FFmpeg filter chains. The stream is broadcast live via Icecast.\n\nSeveral elements are procedurally generated to keep every session fresh:\n\n- **Hero image** — uses a hash to select from AI-generated images, varying by session and week\n- **Descriptive text** — the Tracery grammar library generates new homepage copy on every visit\n- **Recipe selection** — stochastic acceptance weighted toward least-recently-used recipes\n- **Clip selection** — stochastic acceptance weighted toward clips that match the recipe's tags and classification, and toward least-recently-used clips\n- **Audio modulation** — a coherent noise function (a harmonic series of sine waves) modulates volume and effects in real time\n\n---\n\n## Architecture\n\n```\nAdminClient/    React SPA — runs in the browser; talks to AdminServer\nAdminServer/    Express API — users, clips, recipes, email, scheduled jobs\n  └── MySQL         primary data store\nMixEngine/      audio generation — builds FFmpeg filter chains from recipes\n  └── FFmpeg        encodes and processes audio\n  └── Liquidsoap ──► Icecast ──► browser (live audio stream)\nconfig/         shared configuration module (used by AdminServer + MixEngine)\n```\n\nFour parallel services, each running as its own Node.js process:\n\n### Components\n\n**AdminClient** — A React single-page application. Handles authentication, contributor uploads, recipe editing, audio moderation, and admin functions. Built with Redux for state management and a custom Ace editor integration for recipe authoring.\n\n**AdminServer** — An Express API server that manages everything behind the scenes: user accounts, audio uploads, recipe storage, moderation queues, email notifications, and scheduled background jobs. Also serves the built React app via Caddy (a reverse proxy that handles SSL).\n\n**MixEngine** — A separate Express server responsible for generating audio mixes. It reads a recipe, selects matching clips from the database, and builds complex FFmpeg filter chains to mix, normalize, loop, and process the audio. Outputs mix files consumed by Liquidsoap.\n\n**Icecast + Liquidsoap** — Liquidsoap feeds a continuous stream of mix files into Icecast, which serves the audio to listeners. Liquidsoap handles transitions between mixes and falls back to a backup stream if the mix queue runs dry.\n\n**MySQL** — Stores users, audio clips, recipes, mix queue, clip usage history, audit logs, and pending email events.\n\n**Systemd timers** — Two scheduled background jobs run on the server:\n- *Digest runner* — sends contributors a periodic email digest of their clip approvals, disapprovals, and contribution stats\n- *Audio analysis runner* — processes newly uploaded music clips through Essentia.js to automatically suggest BPM, musical key, and danceability tags\n\n---\n\n## User Roles\n\nThe platform has a graduated permission system:\n\n| Role | Can do |\n|------|--------|\n| **User** | Create an account, listen |\n| **Contributor** | Upload audio clips |\n| **Editor** | Upload clips, create and edit recipes |\n| **Mod** | Everything above + moderate audio, manage users |\n| **Admin** | Full access |\n\nRole changes trigger an email notification to the user and adjust their digest frequency automatically.\n\n---\n\n## Authentication\n\nUsers can sign in with a username/password or via OAuth 2.0 with **Google**, **GitHub**, or **Discord**. Sessions are managed with signed JWTs stored in HTTP-only cookies.\n\n---\n\n## Email \u0026 Digest System\n\nAdminServer includes a Handlebars-templated email system backed by Nodemailer. Emails are sent for:\n\n- **Role changes** — when a mod promotes a contributor, the user is notified\n- **Clip approvals/disapprovals** — queued as events, batched into digests\n- **Contribution digests** — sent on a per-user schedule (daily / weekly / monthly / yearly) summarizing recent activity and contribution stats\n- **Welcome / anniversary reminders** — onboarding prompts for inactive new users\n\nThe digest runner is triggered nightly by a systemd timer and processes each user's pending event queue independently.\n\n---\n\n## Audio Analysis Pipeline\n\nWhen a music clip (Instrumental, VocalMusic, or Ambient) is uploaded, it is automatically queued for analysis. A nightly systemd timer runs `audioAnalysisRunner.js`, which:\n\n1. Finds clips tagged `needs-audio-analysis`\n2. Passes each clip through `experiments/essentia/analyze.js` — a Node.js script using [Essentia.js](https://essentia.upf.edu/essentia.js) (compiled to WASM)\n3. Analyzes the middle 180 seconds of the clip for:\n   - **BPM** — e.g. `102-bpm`; clips over 120 BPM also emit a halved tag to catch double-time detection\n   - **Musical key** — e.g. `g-minor-key`, `a-flat-major-key`\n   - **Danceability** — tags as `danceable` if above threshold\n4. Merges the suggested tags into the clip's tag list and swaps `needs-audio-analysis` for `audio-analyzed`\n\n---\n\n## Recipes\n\nRecipes are JSON-like text files (comments allowed) that describe a soundscape as a set of simultaneous tracks, each containing one or more clips.\n\n### Basic structure\n\n```javascript\n{\n  tracks: [\n    {\n      track: 0,          // track index (0–4); tracks play simultaneously\n      label: \"bed\",      // optional name, used for duck(label) references\n      volume: 80,        // track volume 0–100\n      effects: [ ... ],  // track-level effects (see below)\n      clips: [\n        {\n          classification: [ \"Ambient\", \"Atmospheric\" ],\n          tags: [ \"drone\", \"ambient\" ],\n          length: [ \"long\", \"huge\" ],  // tiny | short | medium | long | huge\n          volume: 100,\n          effects: [ ... ],            // clip-level effects\n        }\n      ]\n    }\n  ]\n}\n```\n\n**Length categories:**\n- `tiny` — 0–10 seconds (most sound effects)\n- `short` — 10 seconds – 2 minutes\n- `medium` — 2–5 minutes (most music)\n- `long` — 5–10 minutes\n- `huge` — 10–60 minutes (long soundscapes, environmental recordings)\n\n### Supported effects\n\n**Structural (track-level — control mix duration):**\n- `trim` — this track's duration sets the mix length\n- `first` — the first track with this effect sets the mix length\n- `shortest` — mix ends when the shortest track ends\n- `longest` — mix ends when the longest track ends\n- `crossfade` — crossfade between clips on this track\n- `fadeout` — fade the track out at the end of the mix\n\n**Looping:**\n- `loop` / `repeat` — loop to fill the mix duration\n- `loop(n)` / `repeat(n)` — loop exactly *n* times\n\n**Normalization:**\n- `norm` — normalize to a default level\n- `norm(voice)` — normalize optimized for speech\n- `norm(music)` — normalize optimized for music\n- `norm(bed)` — normalize for use as a background bed\n\n**Color and texture:**\n- `backward` / `reverse` — reverse playback\n- `faraway` / `distant` — low-pass filter + reverb, sounds far away\n- `telephone` — narrow bandpass for a telephone/radio effect\n- `detune` — subtle pitch detune\n\n**Dynamic volume (modulation):**\n- `wave` / `noise` — modulate volume with a coherent noise function\n- `wave(noise)`, `wave(noise2)` — noise variants\n- `wave(inverse)` — inverted wave\n- `wave(subtle)`, `wave(subtle2)` — subtle modulation\n- `wave(liminal)` — slow liminal/transition sweep\n- `duck(label)` — sidechain: this track ducks when the named track is active\n\n### Example recipe\n\nA slow narrative over a drone bed:\n\n```javascript\n{\n  tracks: [\n    {\n      // Ambient drone bed — loops, fades out with the mix\n      track: 0,\n      label: \"bed\",\n      volume: 60,\n      effects: [ \"loop\", \"fadeout\", \"norm(bed)\", \"wave(subtle)\" ],\n      clips: [\n        {\n          classification: [ \"Ambient\" ],\n          tags: [ \"drone\", \"ambient\" ],\n          length: [ \"long\", \"huge\" ],\n        }\n      ]\n    },\n    {\n      // Spoken word — sets the mix duration, duck the bed when active\n      track: 1,\n      volume: 100,\n      effects: [ \"trim\" ],\n      clips: [\n        {\n          classification: \"silence\",\n          length: [ \"short\" ]\n        },\n        {\n          classification: [ \"narrative\", \"spoken\" ],\n          tags: [ \"story\", \"reading\" ],\n          length: [ \"medium\", \"long\" ],\n          effects: [ \"norm(voice)\", \"duck(bed)\" ],\n        },\n        {\n          classification: \"silence\",\n          length: [ \"short\" ]\n        }\n      ]\n    }\n  ]\n}\n```\n\n---\n\n## Technologies\n\n### AdminServer\n- **Node.js + Express** — API server and request routing\n- **MySQL** (via `mysql2`) — primary data store\n- **JWT** (`jsonwebtoken`) + **bcrypt** — session tokens and password hashing\n- **OpenID Client** (`openid-client`) — OAuth 2.0 for Google, GitHub, and Discord sign-in\n- **Nodemailer** + **Handlebars** — transactional and digest email system\n- **Multer** — multipart file upload handling\n- **fluent-ffmpeg + ffprobe-static** — audio duration detection and processing\n- **comment-json** — parses recipe files (JSON with comments)\n- **express-rate-limit** — brute-force protection on auth endpoints\n- **Anthropic SDK** — AI-assisted content features\n\n### AdminClient\n- **React 18** + **Create React App**\n- **Redux Toolkit** + **react-redux** — global state management\n- **react-router-dom** — client-side routing\n- **axios** — HTTP client\n- **react-ace** + **ace-builds** — in-browser code editor for recipes\n- **wavesurfer.js** — audio waveform visualization\n- **feather-icons-react** — UI icons\n- **react-tag-input** — tag field component\n- **tracery-improved** — procedural text generation for homepage copy\n- **TailwindCSS** — utility CSS framework\n\n### MixEngine\n- **Node.js + Express** — mix generation API\n- **fluent-ffmpeg + ffprobe-static** — FFmpeg filter chain construction and audio encoding\n- **comment-json / json5** — recipe parsing\n- **JWT** — internal service authentication\n\n### Shared config module\n- **dotenv** — environment variable loading\n- **mysql2** — database connection pooling\n- **winston** — structured logging\n\n### Infrastructure\n- **Caddy** — reverse proxy and automatic SSL termination\n- **Icecast** — audio streaming server\n- **Liquidsoap** — stream source, feeds mixes into Icecast\n- **systemd** — service management and scheduled timers (digest, audio analysis)\n\n---\n\n## Development Setup\n\n\u003e See `NOTES.md` for port maps, server maintenance notes, and detailed technical reference.\n\n### Prerequisites\n\n- Node.js 18+ (pinned in `.nvmrc`)\n- MySQL running locally\n- FFmpeg installed system-wide\n- Caddy (for local reverse proxy + SSL)\n- Icecast + Liquidsoap (optional, for full stream testing)\n\n### Running locally (dev mode)\n\n```bash\n# Terminal 1 — AdminServer\ncd AdminServer \u0026\u0026 npm start\n\n# Terminal 2 — MixEngine\ncd MixEngine \u0026\u0026 npm start\n\n# Terminal 3 — AdminClient (hot reload)\ncd AdminClient \u0026\u0026 npm start   # http://localhost:3001\n\n# Terminal 4 — Caddy reverse proxy\nsudo caddy run --config setupfiles/Caddyfile.local\n\n# Terminal 5 — Liquidsoap (optional)\nliquidsoap setupfiles/liquidsoap.liq\n\n# Terminal 6 — Icecast (optional)\nicecast -c /usr/local/etc/icecast.xml\n```\n\n### Production deployment\n\nServices are managed by systemd on a Debian server. After pushing changes:\n\n```bash\ncd ~/driftconditions \u0026\u0026 git pull\n\n# Rebuild the React client if frontend files changed\ncd AdminClient \u0026\u0026 npm run build\n\n# Restart the API server\nsudo systemctl restart adminserver\n\n# Restart the mix engine if needed\nsudo systemctl restart mixengine\n```\n\n---\n\n## Contributing Audio\n\nThe station relies on community audio contributions. To contribute:\n\n1. **Sign up** at https://driftconditions.org/\n2. **Request contributor access** — a moderator will promote your account\n3. **Upload clips** through the contributor interface\n\nWe ask that all submissions be original works, public domain, or Creative Commons licensed content for which you have clear rights. No copyrighted material without permission.\n\n---\n\n## Author\n\n**Wesley Modes**  \nUniversity of Cincinnati  \nORCID: [0009-0000-1191-8245](https://orcid.org/0009-0000-1191-8245)\n\n---\n\n## Citation\n\nIf you use or reference *DriftConditions* in academic work, please cite it as:\n\n```bibtex\n@software{modes_2026_driftconditions,\n  author    = {Modes, Wesley},\n  title     = {{DriftConditions}: A Generative Audio Streaming Platform},\n  year      = {2026},\n  publisher = {Zenodo},\n  doi       = {10.5281/zenodo.19688234},\n  url       = {https://doi.org/10.5281/zenodo.19688234},\n  orcid     = {0009-0000-1191-8245}\n}\n```\n\n---\n\n## License\n\nMIT — see `MIT-LICENSE.txt`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwmodes%2Fdriftconditions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwmodes%2Fdriftconditions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwmodes%2Fdriftconditions/lists"}