Add shaped scene texture sugar
This commit is contained in:
parent
aadb0bf0e5
commit
a3c01523c3
6 changed files with 162 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue