Add shaped scene texture sugar

This commit is contained in:
Meta Vens 2026-05-31 08:09:43 +01:00
commit a3c01523c3
6 changed files with 162 additions and 0 deletions

View file

@ -326,6 +326,13 @@ records in the `.cc` CultCache/CultMesh store and references them from each
trial result. Qdrant/Ollama retrieval may derive vectors from those records, but
the `.cc` records remain truth.
Noise is not a scene by itself. Background air, dust, codec grit, tape, vinyl,
and room color need a role owner that shapes spectrum and time. Prefer
`texture`/`noise_texture` sugar or an equivalently gated, band-shaped explicit
voice for scene noise. Full-duration raw `wave=noise` beds with broad filters
are failed evidence: they sound like broken subtractive synthesis because that
is exactly what they are.
Do not confuse high metric agreement with musical success. The game context,
the reference sound, and the user’s ear still outrank the spreadsheet. Annoying,
but civilization has survived worse.

View file

@ -275,6 +275,10 @@ public static class PatchScript
FlushPendingOperatorGraph();
Voices.Add(ParseVoice(ExpandVoiceFields(fields, line), VoicePath(Voices.Count), line));
break;
case "texture":
FlushPendingOperatorGraph();
AddNoiseTexture(fields, line);
break;
case "acoustic_voice":
FlushPendingOperatorGraph();
Voices.Add(ParseAcousticVoice(ExpandVoiceFields(fields, line), VoicePath(Voices.Count), line));
@ -2604,6 +2608,59 @@ public static class PatchScript
("depth", GetAny(fields, ["depth", "amount", "mix"], "1"))), line);
}
private void AddNoiseTexture(IReadOnlyDictionary<string, string> fields, int line)
{
var name = SafeName(GetAny(fields, ["name", "n"], $"texture_{Voices.Count}"));
var role = NoiseTextureRole(GetAny(fields, ["role", "kind", "type"], name));
var duration = GetAny(fields, ["sustain", "s", "gate", "hold", "duration"], "30");
var gainText = GetAny(fields, ["gain", "g"], role.Gain.ToString(CultureInfo.InvariantCulture));
var gatePath = GetAny(fields, ["path", "gate_path"], $"/textures/{name}/gate");
if (!gainText.StartsWith('@'))
{
var gainValue = Math.Clamp(ParseFloat(gainText, line), 0, 1);
if (TryGetAny(fields, ["pattern", "pat", "steps"], out var pattern))
{
AddPatternCurve(SyntheticFields(
("name", $"{name}_gate"),
("path", gatePath),
("pattern", pattern),
("step", GetAny(fields, ["step", "step_seconds", "dt"], role.StepSeconds.ToString(CultureInfo.InvariantCulture))),
("high", GetAny(fields, ["high", "on", "hit"], gainValue.ToString(CultureInfo.InvariantCulture))),
("low", GetAny(fields, ["low", "off", "rest"], (gainValue * role.RestScale).ToString(CultureInfo.InvariantCulture))),
("loop", GetAny(fields, ["loop"], "true"))), line);
}
else
{
EnsureParameter(gatePath, gainValue, 0, Math.Max(gainValue * 2, 0.001f), 0.001f, "", line);
AddControlCurve(SyntheticFields(
("name", $"{name}_gate_motion"),
("path", gatePath),
("points", TextureGatePoints(gainValue, role.MotionScale)),
("interp", "linear"),
("loop", "true")), line);
}
gainText = "@" + gatePath;
}
Voices.Add(ParseVoice(ExpandVoiceFields(SyntheticFields(
("wave", "noise"),
("freq", GetAny(fields, ["freq", "frequency"], role.FrequencyHz.ToString(CultureInfo.InvariantCulture))),
("gain", gainText),
("attack", GetAny(fields, ["attack", "a"], role.AttackSeconds.ToString(CultureInfo.InvariantCulture))),
("sustain", duration),
("decay", GetAny(fields, ["decay", "d", "release", "rel"], role.DecaySeconds.ToString(CultureInfo.InvariantCulture))),
("noise", GetAny(fields, ["noise", "nz"], "1")),
("hpf", GetAny(fields, ["hpf", "h"], role.HighPass.ToString(CultureInfo.InvariantCulture))),
("lpf", GetAny(fields, ["lpf", "l"], role.LowPass.ToString(CultureInfo.InvariantCulture))),
("bpf", GetAny(fields, ["bpf", "bp"], role.BandPass.ToString(CultureInfo.InvariantCulture))),
("bpf_q", GetAny(fields, ["bpf_q", "bpq"], role.BandPassQ.ToString(CultureInfo.InvariantCulture))),
("tremolo", GetAny(fields, ["tremolo", "tr"], role.TremoloDepth.ToString(CultureInfo.InvariantCulture))),
("tremolo_hz", GetAny(fields, ["tremolo_hz", "th"], role.TremoloHz.ToString(CultureInfo.InvariantCulture))),
("drive", GetAny(fields, ["drive"], role.Drive.ToString(CultureInfo.InvariantCulture)))), line), VoicePath(Voices.Count), line));
}
private void EnsureParameter(string path, float defaultValue, float min, float max, float step, string unit, int line)
{
if (_parameters.Any(parameter => parameter.Path.Equals(path, StringComparison.OrdinalIgnoreCase)))
@ -3447,6 +3504,7 @@ public static class PatchScript
"acoustic_network" or "port_network" or "network" => "acoustic_network",
"vocal_network" or "vocalnetwork" => "vocal_network",
"v" or "voice" => "voice",
"texture" or "noise_texture" or "scene_texture" or "dust" or "air" or "air_wash" or "codec_bed" => "texture",
"acoustic" or "acoustic_voice" or "av" => "acoustic_voice",
"vocal" or "vocal_voice" or "vv" => "vocal_voice",
"tract" or "vt" or "tractvoice" or "tract_voice" => "tract",
@ -3674,6 +3732,50 @@ public static class PatchScript
private static Dictionary<string, string> SyntheticFields(params (string Key, string Value)[] fields) =>
fields.ToDictionary(field => field.Key, field => field.Value, StringComparer.OrdinalIgnoreCase);
private static string TextureGatePoints(float gain, float motionScale)
{
var low = Math.Max(0, gain * Math.Max(0, 1 - motionScale));
var high = Math.Min(1, gain * (1 + motionScale));
return string.Join(",", new[]
{
(0f, gain),
(0.37f, high),
(0.91f, low),
(1.43f, gain * 0.85f),
(2.11f, high * 0.9f),
(3.0f, gain)
}.Select(point =>
point.Item1.ToString("0.######", CultureInfo.InvariantCulture) +
":" +
point.Item2.ToString("0.######", CultureInfo.InvariantCulture)));
}
private static NoiseTextureDefaults NoiseTextureRole(string role)
{
var normalized = role.ToLowerInvariant().Replace("_", "-", StringComparison.Ordinal);
if (normalized.Contains("dust", StringComparison.Ordinal) || normalized.Contains("hat", StringComparison.Ordinal))
{
return new(7600, 0.075f, 0.001f, 0.09f, 0.78f, 0.42f, 0.66f, 4.5f, 0.22f, 5.0f, 0.16f, 0.08f, 0.35f, 0.125f);
}
if (normalized.Contains("codec", StringComparison.Ordinal) || normalized.Contains("bit", StringComparison.Ordinal))
{
return new(4200, 0.045f, 0.015f, 0.55f, 0.38f, 0.34f, 0.44f, 5.5f, 0.18f, 1.7f, 0.20f, 0.18f, 0.45f, 0.25f);
}
if (normalized.Contains("room", StringComparison.Ordinal) || normalized.Contains("bed", StringComparison.Ordinal))
{
return new(1800, 0.04f, 0.05f, 0.8f, 0.14f, 0.32f, 0.24f, 0.9f, 0.20f, 0.23f, 0.05f, 0.35f, 0.55f, 0.5f);
}
if (normalized.Contains("tape", StringComparison.Ordinal) || normalized.Contains("vinyl", StringComparison.Ordinal))
{
return new(2600, 0.04f, 0.02f, 0.7f, 0.22f, 0.48f, 0.34f, 1.2f, 0.15f, 0.67f, 0.10f, 0.25f, 0.5f, 0.25f);
}
return new(5800, 0.045f, 0.02f, 0.55f, 0.58f, 0.48f, 0.56f, 1.8f, 0.24f, 0.31f, 0.08f, 0.24f, 0.5f, 0.25f);
}
private static string PointsText(IEnumerable<(float Time, float Value)> points) =>
string.Join(",", points.Select(point =>
point.Time.ToString("0.######", CultureInfo.InvariantCulture) +
@ -3886,4 +3988,20 @@ public static class PatchScript
return fields.Where(pair => !excluded.Contains(pair.Key))
.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);
}
private sealed record NoiseTextureDefaults(
float FrequencyHz,
float Gain,
float AttackSeconds,
float DecaySeconds,
float HighPass,
float LowPass,
float BandPass,
float BandPassQ,
float TremoloDepth,
float TremoloHz,
float Drive,
float RestScale,
float MotionScale,
float StepSeconds);
}

View file

@ -251,3 +251,4 @@
{"date":"2026-05-31","area":"song-snippet-experiment-phases","lesson":"Three song-snippet phases across different segments improved best log-mel cosine from `0.308233` to `0.380805`, still pressure rather than close parity. Winning candidates converged on bright FM/formant `alien-lead` plus clocked high-band `dust-hat` noise and a continuous `codec-bed`/air layer; syrinx-heavy proposals scored worse. The durable DSL pressure is named scene-instrument sugar (`alien-lead-chatter`, `dust-hat-grid`, `codec-bed-hum`, `air-wash-hiss`) that lowers into explicit `param`, `curve interp=hold loop=true`, and `voice` records. Role sugar may own brevity/defaults; lowered controls remain DSP authority.","evidence":"artifacts/parity/song-snippet-swarms/20260531T014033022/experiment-report.md; artifacts/parity/song-snippet-swarms/20260531T014033022/song-trial-results.cc; docs/audio-doctrine.md; tools/run-song-snippet-trial-swarm.ps1"}
{"date":"2026-05-31","area":"song-snippet-cultmesh-evidence","lesson":"Song-scene agents need target evidence as first-class database material, not loose analysis files. `song-prepare` now emits a log-mel spectrogram, per-band stats with first and second derivatives, RMS envelope, envelope autocorrelation, and an analysis report for every frozen clip; `song-score` upserts those artifacts as typed `SongChallengeEvidenceDocument` records in the `.cc` CultCache/CultMesh store, adds target metrics and `cultmesh://aquasynth/song-challenge-evidence/...` artifact URIs to every result, and indexes the evidence records into Qdrant-derived semantic chunks.","evidence":"src/AquaSynth.Core/IpaTrialResults.cs; tools/IpaTrialWorker/Program.cs; tools/run-song-snippet-trial-swarm.ps1; tests/AquaSynth.Dsl.Tests/IpaTrialOrchestratorTests.cs; artifacts/parity/song-snippet-smoke/cultmesh-evidence/index.md; artifacts/parity/song-snippet-smoke/cultmesh-evidence/search.md"}
{"date":"2026-05-31","area":"song-snippet-album-swarm","lesson":"A 3-pass album swarm over random 30-second clips produced 15 scored trials and 15 full target evidence bundles in the shared `.cc` store. The agents improved from pass-001 best cosine `0.434277` to pass-002 best `0.590958`, then pass-003 best `0.502573`; aggregate mean cosine was `0.353315`, with 11 pressure verdicts and 4 weak verdicts. The strongest families converged on bright formant/vowel leads plus clocked dust/noise grids, while the semantic index successfully carried 90 bounded Qdrant chunks derived from full CultCache evidence docs.","evidence":"artifacts/parity/song-snippet-swarms/20260531T031003707/experiment-report.md; artifacts/parity/song-snippet-swarms/20260531T031003707/song-trial-results.cc; artifacts/parity/song-snippet-swarms/20260531T031003707/pass-003/vector-index-after.md; artifacts/parity/song-snippet-swarms/20260531T031003707/pass-003/semantic-search-after.md"}
{"date":"2026-05-31","area":"song-scene-texture-owner","lesson":"The album-swarm patches exposed a bad scene-noise owner: agents modeled air/dust/recording condition with full-duration broad `wave=noise` voices, producing static white hiss that sounds like broken subtractive synthesis. The DSL now has `texture`/`noise_texture` sugar with role defaults for dust, air, codec, room, tape/vinyl; it lowers into a gated noise voice with bandpass/highpass/lowpass/tremolo shaping and an owned gate curve or pattern. Future song prompts require texture or an equivalently gated/band-shaped explicit voice for background noise.","evidence":"src/AquaSynth.Core/PatchScript.cs; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; tools/run-song-snippet-trial-swarm.ps1; docs/audio-doctrine.md"}

View file

@ -1744,3 +1744,8 @@ Next likely slice:
0.502573. Best overall was
`agent_04__shard_vowel_lead__bright_dust_grid_v2`; strongest repeated pattern
is bright formant/vowel lead plus clocked dust/noise grid.
- Listening correction: those clocked dust/noise grids still sit on persistent
broad white hiss. Added `texture`/`noise_texture` DSL sugar so air, dust,
codec, room, and tape/vinyl noise are owned as gated, band-shaped scene
roles. Future agents should not use full-duration raw `wave=noise` beds for
recording color.

View file

@ -99,6 +99,31 @@ public sealed class PatchScriptTests
Assert.Equal(622.254f, curve.Points[3].Value, 3);
}
[Fact]
public void ParserSupportsShapedNoiseTextureSugar()
{
var patch = PatchScript.Parse("""
texture name=dust_hat role=dust pattern=x..x step=.125 gain=.12 sustain=30
texture name=air_wash role=air gain=.04 sustain=30
voice wave=saw freq=220 gain=.1
""");
Assert.Equal(3, patch.Voices.Count);
Assert.All(patch.Voices.Take(2), voice => Assert.Equal(Waveform.Noise, voice.Oscillator.Waveform));
Assert.Contains(patch.Parameters, parameter => parameter.Path == "/textures/dust_hat/gate");
Assert.Contains(patch.Parameters, parameter => parameter.Path == "/textures/air_wash/gate");
Assert.Contains(patch.ControlCurves, curve => curve.Name == "dust_hat_gate" && curve.Interpolation == ControlCurveInterpolation.Hold);
Assert.Contains(patch.ControlCurves, curve => curve.Name == "air_wash_gate_motion" && curve.Interpolation == ControlCurveInterpolation.Linear);
var dust = patch.Voices[0];
Assert.True(dust.Filter.BandPass > 0);
Assert.True(dust.Filter.BandPassQ > 1);
Assert.True(dust.Color.TremoloDepth > 0);
var faust = FaustEmitter.Emit(patch).Source;
Assert.Contains("fi.resonbp", faust);
Assert.Contains("/textures/dust_hat/gate", faust);
}
[Fact]
public void ParserSupportsVoiceVowelFrames()
{

View file

@ -278,6 +278,11 @@ Challenge:
Goal:
Write exactly one parseable AquaSynth `.aqua` patch that tries to reproduce the frozen $DurationSeconds second reference clip. This is scene-audio parity, not IPA articulation. You may use ordinary voices, FM, AM/tremolo, filters, formants, noise layers, acoustic vocal/syrinx primitives, curves, and helper voices.
Noise and texture:
- Do not model background or recording color as a full-duration raw `voice wave=noise` bed. That produces static white-noise wash and will be treated as broken subtractive synthesis, not scene modeling.
- Use the shaped texture owner for noise roles: `texture name=dust_hat role=dust pattern=x..x step=<beat_seconds/4> gain=.08 sustain=$DurationSeconds`, `texture name=air_wash role=air gain=.035 sustain=$DurationSeconds`, `texture name=codec_bed role=codec gain=.04 sustain=$DurationSeconds`.
- `texture` lowers to a gated noise voice with role-specific bandpass/highpass/lowpass/tremolo defaults and a control curve or pattern gate. Raw `voice wave=noise` is only acceptable for short transients with explicit gates and narrow filters.
Rhythm and tempo:
- The challenge report includes `tempo_bpm`, `beat_seconds`, and `tempo_confidence`, estimated from spectral/RMS onset autocorrelation.
- The challenge report also points to `analysis_report`, `log_mel_spectrogram_csv`, `log_mel_band_stats_csv`, `rms_envelope_csv`, and `rms_envelope_autocorr_csv`; read them before writing the patch.
@ -315,6 +320,7 @@ Evidence contract:
- Write $agentDir/hypotheses.md with: reference features, cited analysis artifacts, tempo/rhythm plan, register/scale plan, scene voices, synthesis owners, invented instrument roles, expected metric movement, known risks.
- Write exactly one `.aqua` file under $agentPatchDir named `<agent-id>__<family>__<hypothesis>.aqua`.
- Include at least three scene voices or layers in the patch, such as primary alien-voice/body, rhythmic transient/noise, and background/recording color.
- Background/recording-color layers must use `texture` or an equivalently gated and band-shaped explicit voice; a static full-duration white-noise bed is failed evidence.
- Make the patch duration/gates cover the $DurationSeconds second target.
Return a short final summary naming the patch and report.