From 96c1aee38fe06fd6cbc20fa974e3a5c019def1a5 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 8 May 2026 03:04:01 -0700 Subject: [PATCH] feat(editor): add --gen-audio-noise procedural noise WAV synthesis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three audio-engineering noise colors: white — equal energy per Hz (uniform random samples) pink — equal energy per octave via Voss-McCartney 7-band cascade (1/f spectrum, sounds like rain or wind) brown — 1/f² spectrum via random walk (sounds like distant surf or rumbling weather) All written as PCM-16 mono with same RIFF header + 5ms attack/release envelope as gen-audio-tone. Replaces typical proprietary nature-sound MP3 placeholders for ambient zone audio with hand-synthesized seeded WAVs. --- tools/editor/main.cpp | 140 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index b4b2f6d8..fd0cdc13 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -617,6 +617,8 @@ static void printUsage(const char* argv0) { std::printf(" Run both texture-pack + mesh-pack in one pass — full open-format bootstrap\n"); std::printf(" --gen-audio-tone [sampleRate] [waveform]\n"); std::printf(" Synthesize a procedural WAV (PCM-16 mono). Waveform: sine|square|triangle|saw\n"); + std::printf(" --gen-audio-noise [sampleRate] [color] [seed] [amplitude]\n"); + std::printf(" Synthesize procedural noise WAV. Color: white|pink|brown (default white, amp 0.5)\n"); std::printf(" --gen-zone-audio-pack \n"); std::printf(" Drop a starter WAV pack (drone/chime/click/alert) into /audio/\n"); std::printf(" --gen-random-zone [tx ty] [--seed N] [--creatures N] [--objects N] [--items N]\n"); @@ -1118,7 +1120,7 @@ int main(int argc, char* argv[]) { "--audit-project-spawns", "--list-zone-spawns", "--list-project-spawns", "--gen-random-zone", "--gen-random-project", "--gen-zone-texture-pack", "--gen-zone-mesh-pack", "--gen-zone-starter-pack", "--gen-audio-tone", - "--gen-zone-audio-pack", "--info-spawn", + "--gen-audio-noise", "--gen-zone-audio-pack", "--info-spawn", "--diff-zone-spawns", "--list-items", "--info-item", "--set-item", "--export-zone-items-md", "--export-project-items-md", "--export-project-items-csv", @@ -14413,6 +14415,142 @@ int main(int argc, char* argv[]) { std::printf(" bytes : %u (44-byte header + data)\n", riffSize + 8); return 0; + } else if (std::strcmp(argv[i], "--gen-audio-noise") == 0 && i + 2 < argc) { + // Procedural noise WAV. Three "colors" in audio engineering: + // white — equal energy per Hz (uniform random samples) + // pink — equal energy per octave (1/f spectrum) via + // Voss-McCartney 7-band cascade. Sounds like + // rain or wind. + // brown — 1/f² spectrum via random walk (integrated + // white noise). Sounds like distant surf or + // rumbling weather. + // All written as PCM-16 mono via the same RIFF header + // logic as --gen-audio-tone. + std::string outPath = argv[++i]; + float duration = 0.0f; + try { duration = std::stof(argv[++i]); } + catch (...) { + std::fprintf(stderr, + "gen-audio-noise: must be a number\n"); + return 1; + } + int sampleRate = 22050; + std::string color = "white"; + uint32_t seed = 1; + float amp = 0.5f; + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { sampleRate = std::stoi(argv[++i]); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + color = argv[++i]; + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { seed = static_cast(std::stoul(argv[++i])); } catch (...) {} + } + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { amp = std::stof(argv[++i]); } catch (...) {} + } + if (duration <= 0 || duration > 600 || + sampleRate < 8000 || sampleRate > 192000 || + amp <= 0 || amp > 1.0f) { + std::fprintf(stderr, + "gen-audio-noise: duration 0..600s, sampleRate 8000..192000, amp 0..1\n"); + return 1; + } + uint32_t totalSamples = static_cast(duration * sampleRate); + std::vector samples(totalSamples, 0); + uint32_t state = seed ? seed : 1u; + auto next01 = [&state]() -> float { + state = state * 1664525u + 1013904223u; + return (state >> 8) * (1.0f / 16777216.0f); + }; + auto nextSigned = [&]() -> float { return next01() * 2.0f - 1.0f; }; + // Same envelope logic as gen-audio-tone — 5ms attack/release + // so noise doesn't pop at start/stop. + int envSamples = std::min(totalSamples / 4, + static_cast(sampleRate * 0.005f)); + // Voss-McCartney pink noise state: 7 random rows + // updated at progressively halved rates. + float pinkRows[7] = {0}; + float pinkSum = 0.0f; + int pinkIdx = 0; + float brownState = 0.0f; + for (uint32_t s = 0; s < totalSamples; ++s) { + float v = 0.0f; + if (color == "white") { + v = nextSigned(); + } else if (color == "pink") { + pinkIdx++; + int rowsToUpdate = 0; + int idx = pinkIdx; + while ((idx & 1) == 0 && rowsToUpdate < 7) { + idx >>= 1; + rowsToUpdate++; + } + pinkSum -= pinkRows[rowsToUpdate]; + pinkRows[rowsToUpdate] = nextSigned(); + pinkSum += pinkRows[rowsToUpdate]; + v = pinkSum / 7.0f; + } else if (color == "brown") { + brownState = std::clamp(brownState + nextSigned() * 0.1f, -1.0f, 1.0f); + v = brownState * 3.0f; // amplify since walk stays small + } else { + std::fprintf(stderr, + "gen-audio-noise: unknown color '%s' (white|pink|brown)\n", + color.c_str()); + return 1; + } + float env = 1.0f; + if (envSamples > 0) { + if (static_cast(s) < envSamples) { + env = static_cast(s) / envSamples; + } else if (static_cast(totalSamples - s) < envSamples) { + env = static_cast(totalSamples - s) / envSamples; + } + } + v *= env * amp; + samples[s] = static_cast(std::clamp(v, -1.0f, 1.0f) * 32767.0f); + } + FILE* f = std::fopen(outPath.c_str(), "wb"); + if (!f) { + std::fprintf(stderr, + "gen-audio-noise: cannot open %s for write\n", outPath.c_str()); + return 1; + } + uint32_t dataBytes = totalSamples * 2; + uint32_t riffSize = 36 + dataBytes; + uint16_t numChannels = 1; + uint16_t bitsPerSample = 16; + uint16_t blockAlign = numChannels * bitsPerSample / 8; + uint32_t byteRate = sampleRate * blockAlign; + auto wU32 = [&](uint32_t v) { std::fwrite(&v, 4, 1, f); }; + auto wU16 = [&](uint16_t v) { std::fwrite(&v, 2, 1, f); }; + std::fwrite("RIFF", 1, 4, f); + wU32(riffSize); + std::fwrite("WAVE", 1, 4, f); + std::fwrite("fmt ", 1, 4, f); + wU32(16); + wU16(1); + wU16(numChannels); + wU32(static_cast(sampleRate)); + wU32(byteRate); + wU16(blockAlign); + wU16(bitsPerSample); + std::fwrite("data", 1, 4, f); + wU32(dataBytes); + std::fwrite(samples.data(), 2, totalSamples, f); + std::fclose(f); + std::printf("Wrote %s\n", outPath.c_str()); + std::printf(" format : WAV PCM-16 mono\n"); + std::printf(" duration : %.3f sec\n", duration); + std::printf(" sampleRate : %d Hz\n", sampleRate); + std::printf(" color : %s noise\n", color.c_str()); + std::printf(" amplitude : %.2f\n", amp); + std::printf(" seed : %u\n", seed); + std::printf(" samples : %u\n", totalSamples); + std::printf(" bytes : %u (44-byte header + data)\n", + riffSize + 8); + return 0; } else if (std::strcmp(argv[i], "--gen-zone-audio-pack") == 0 && i + 1 < argc) { // Drop a 6-WAV starter audio pack into /audio/. // Two ambient drones (low + fifth above), a melodic