What if application data could drive sound without your application owning an entire audio engine?
Modern applications already use JSON to describe behaviour.
- Configuration
- User preferences
- Application state
- Notifications
- Workflow status
- Environment settings
- Feature flags
That data typically controls things like:
JSON
↓
UI
JSON
↓
Feature Behaviour
JSON
↓
API Configuration
JSON
↓
Workflow Logic
But JSON can also drive audio.
With SoundScript, a .NET application can transform ordinary structured data into musical intent and render deterministic WAV or MIDI output.
The key idea is that JSON is not SoundScript's language.
Your application owns the JSON schema.
Your application decides what the data means.
SoundScript simply performs the rendering.
JSON
↓
Application Mapping
↓
SoundScript Source
↓
Compile
↓
WAV / MIDI
That separation turns out to be surprisingly useful.
Business data remains business data.
Audio remains programmable.
Let's build it.
Install SoundScript
SoundScript 13 targets .NET 10.
dotnet add package SoundScript --version 13.0.0
We'll also use the JSON functionality built into .NET.
Start with a Simple Configuration
Imagine an application has a configuration file like this:
{
"name": "checkout-success",
"tempo": 120,
"dynamic": "mf",
"notes": "C4 e E4 e G4 e C5 q"
}
Create a matching model:
record AudioCue(
string Name,
int Tempo,
string Dynamic,
string Notes);
Load the JSON:
using System.Text.Json;
var json =
File.ReadAllText("cue.json");
var cue =
JsonSerializer.Deserialize<AudioCue>(
json)
?? throw new InvalidOperationException(
"Invalid audio configuration.");
At this stage there's nothing audio-specific happening.
The application simply has structured state.
Map JSON into SoundScript
Now transform that state into musical intent.
var source = $"""
tempo {cue.Tempo}
track cue {{
instrument piano
{cue.Dynamic}
{cue.Notes}
}}
""";
Compile and render:
using SoundScript;
var compilation =
SoundScriptEngine.Compile(source);
var wav =
compilation.RenderWave();
var midi =
compilation.RenderMidi();
File.WriteAllBytes(
$"{cue.Name}.wav",
wav);
File.WriteAllBytes(
$"{cue.Name}.mid",
midi);
That's the entire pipeline.
cue.json
↓
Deserialize
↓
Application Model
↓
Map to SoundScript
↓
Compile
↓
checkout-success.wav
checkout-success.mid
Why Not Just Store a WAV File?
A reasonable question.
For a single fixed cue:
checkout-success.wav
is often the best solution.
Static assets are:
- Easy to preview
- Professionally designed
- Simple to deploy
- Simple to maintain
You shouldn't replace static audio just because dynamic generation is possible.
The value appears when application state influences the sound.
Example: Severity-Driven Audio
Suppose the application receives:
{
"state": "warning",
"severity": 3
}
Instead of exposing raw audio controls, define a simple business model:
record StatusCue(
string State,
int Severity);
Map severity into known dynamics:
var dynamic =
cue.Severity switch
{
1 => "mp",
2 => "mf",
3 => "f",
_ => "ff"
};
Map state into known musical motifs:
var motif =
cue.State switch
{
"success" =>
"C4 e E4 e G4 e C5 q",
"warning" =>
"A3 e A3 e E4 q A3 e",
"failure" =>
"C3 e C3 e G2 q C3 e",
_ =>
"C4 q"
};
Create SoundScript:
var source = $"""
tempo 110
track status {{
instrument piano
{dynamic}
{motif}
}}
""";
And render:
var wav =
SoundScriptEngine
.Compile(source)
.RenderWave();
Notice the separation of responsibilities:
State + Severity
↓
Application Rules
↓
Musical Intent
↓
SoundScript
↓
Audio
Keep Business Logic Outside the Audio Engine
This is an important architectural boundary.
You probably don't want SoundScript deciding:
Severity 4 = Emergency
That's an application concern.
Likewise, you probably don't want your business configuration describing:
MIDI Channel 1
Velocity 112
Tick Position 480
That's audio implementation detail.
A cleaner architecture is:
Business State
↓
Application Rules
↓
Musical Intent
↓
SoundScript
↓
Audio
Each layer has a single responsibility.
Avoid Passing Arbitrary Source from JSON
A tempting approach is:
{
"source": "tempo 120 track x { ... }"
}
followed by:
SoundScriptEngine.Compile(
config.Source);
For trusted developer-authored configuration, that's perfectly valid.
For user data or external input, a better pattern is:
{
"state": "warning",
"intensity": 2
}
Then map it explicitly:
Known Input
↓
Validation
↓
Application Rules
↓
Known Musical Structures
Rather than:
Arbitrary String
↓
Compiler
The same principle applies to many configurable systems.
A More Realistic Example
Imagine a deployment monitoring system producing:
{
"event": "deployment",
"result": "failed",
"severity": 4,
"environment": "production"
}
Create a model:
record DeploymentEvent(
string Event,
string Result,
int Severity,
string Environment);
Now define rules.
Tempo
var tempo =
data.Environment switch
{
"production" => 116,
"staging" => 104,
_ => 96
};
Dynamic
var dynamic =
data.Severity switch
{
<= 1 => "mp",
2 => "mf",
3 => "f",
_ => "ff"
};
Motif
var motif =
data.Result switch
{
"success" =>
"C4 e E4 e G4 e C5 q",
"failed" =>
"C3 e C3 e G2 q C3 e",
_ =>
"C4 q"
};
Assemble the cue:
var source = $"""
tempo {tempo}
track deployment {{
instrument piano
{dynamic}
{motif}
}}
""";
The same deployment state always produces the same intended musical behaviour.
That's one of the biggest advantages of deterministic audio.
JSON Is Control Data, Not Music
Notice the difference between these two representations.
Application state:
{
"result": "failed",
"severity": 4
}
Musical intent:
tempo 116
track deployment {
instrument piano
ff
C3 e
C3 e
G2 q
C3 e
}
They serve different purposes.
Trying to make them identical often creates unnecessary coupling.
Keeping them separate tends to produce cleaner designs.
Why Not Use an AI Music API?
Let's compare the goals.
Imagine sending:
{
"state": "warning",
"severity": 3
}
to a generative music system using a prompt like:
Create a short urgent notification sound for severity level 3.
That can work very well if you want creative variation.
But application audio often requires different guarantees.
You may want:
Severity 3
↓
Same Rules
↓
Same Output
You may want to:
- Test it
- Cache it
- Hash it
- Version it
- Reproduce it
Deterministic generation is often a better fit for those requirements.
Comparing Audio Architectures
Static Assets
State
↓
Lookup
↓
Existing WAV
Advantages
- Simple
- Fast
- Professional audio quality
Trade-off
Every variation usually requires another asset.
Generative Music Service
State
↓
Prompt
↓
Model
↓
Generated Audio
Advantages
- Creative
- Rich output
- Large stylistic range
Trade-off
- Probabilistic behaviour
- External dependencies
- Different reproducibility model
DSP / Custom Synthesis
State
↓
DSP Code
↓
Oscillators
Envelopes
Mixers
↓
PCM Audio
Advantages
- Maximum control
Trade-off
- Significant audio-engine complexity
SoundScript
State
↓
Application Rules
↓
Musical Intent
↓
SoundScript
↓
WAV / MIDI
This occupies a useful middle ground.
- More dynamic than static assets
- Higher level than DSP
- More deterministic than prompt generation
User-Configurable Notification Profiles
Imagine users can select:
{
"notificationStyle": "gentle",
"speed": "normal"
}
Your application maps:
gentle
↓
piano
mp
and:
normal
↓
tempo 100
Another profile might be:
{
"notificationStyle": "urgent",
"speed": "fast"
}
Mapping to:
urgent
↓
stronger dynamic
different motif
fast
↓
tempo 132
Notice that you're exposing meaningful product concepts, not audio-engine internals.
That's generally easier for both developers and users.
Game State as Audio
Imagine:
{
"health": 24,
"danger": true,
"level": 7
}
Application rules derive:
danger = true
↓
warning motif
health < 30
↓
stronger dynamic
level = 7
↓
tempo adjustment
Result:
Game State
↓
Application Rules
↓
Musical State
↓
Audio
This pattern is particularly useful during prototyping when sound design is still evolving.
Cache Repeated States
Not every cue needs to be rendered repeatedly.
Generate a cache key:
var key =
$"{state}:{severity}:{tempo}";
Then:
if (cache.TryGetValue(
key,
out var existing))
{
return existing;
}
Otherwise:
var wav =
SoundScriptEngine
.Compile(source)
.RenderWave();
cache[key] = wav;
return wav;
The architecture becomes:
Application State
↓
Key
↙ ↘
Hit Miss
↓ ↓
Reuse Render
↓
Cache
A simple optimisation with substantial benefits.
Generate Assets at Build Time
Dynamic audio doesn't have to mean runtime rendering.
Suppose you support:
3 States
× 4 Severity Levels
That's only:
12 Outputs
A build pipeline could generate:
success-1.wav
success-2.wav
success-3.wav
success-4.wav
warning-1.wav
warning-2.wav
warning-3.wav
warning-4.wav
failure-1.wav
failure-2.wav
failure-3.wav
failure-4.wav
At runtime, the application simply loads files.
You keep deterministic generation while avoiding runtime rendering entirely.
This hybrid model is often very practical.
Audio Mappings Can Be Tested
Because generation is deterministic, it can participate in automated testing.
var first =
BuildCue(config);
var second =
BuildCue(config);
Assert.True(
first.AsSpan()
.SequenceEqual(second));
Or use hashes:
using System.Security.Cryptography;
var hash =
Convert.ToHexString(
SHA256.HashData(first));
Console.WriteLine(hash);
Your configuration-to-sound rules become testable software.
A Simple Helper
You can encapsulate the entire workflow:
using SoundScript;
static byte[] RenderCue(
string state,
int severity)
{
var dynamic =
severity switch
{
1 => "mp",
2 => "mf",
3 => "f",
_ => "ff"
};
var motif =
state switch
{
"success" =>
"C4 e E4 e G4 e C5 q",
"warning" =>
"A3 e A3 e E4 q A3 e",
"failure" =>
"C3 e C3 e G2 q C3 e",
_ =>
"C4 q"
};
var source = $"""
tempo 110
track cue {{
instrument piano
{dynamic}
{motif}
}}
""";
return SoundScriptEngine
.Compile(source)
.RenderWave();
}
Usage:
var wav =
RenderCue(
"warning",
3);
File.WriteAllBytes(
"warning.wav",
wav);
Your application stays focused on:
State
Severity
rather than audio implementation details.
Where This Approach Fits
🔔 Application Notifications
Generate controlled cues from application events.
🎮 Games
Derive audio behaviour from game state.
🚦 Monitoring
Map system status into repeatable cues.
♿ Accessibility Experiments
Create structured non-visual feedback.
🏭 Industrial Applications
Represent machine and process states.
🎓 Education
Demonstrate how structured data can drive music.
🔄 CI/CD
Turn build and deployment status into deterministic audio.
🧪 Automated Testing
Generate reproducible audio fixtures.
When Not to Use It
Don't adopt this architecture simply because your application already uses JSON.
If the requirement is:
Play one professionally designed notification sound.
Then:
notification.wav
may be the ideal solution.
Dynamic generation becomes valuable when audio meaningfully depends on application state.
That's the real decision point.
The Bigger Idea
Modern software already transforms data into many outputs.
JSON
↓
UI
JSON
↓
HTML
JSON
↓
API Behaviour
JSON
↓
Workflow Execution
Programmable audio adds another possibility:
JSON
↓
Application Rules
↓
Musical Intent
↓
Audio
The important part isn't the JSON.
The important part is that sound becomes another output of application logic.
Try It Yourself
Create:
{
"name": "success",
"tempo": 120,
"dynamic": "mf",
"notes": "C4 e E4 e G4 e C5 q"
}
Map it to SoundScript:
var source = $"""
tempo {cue.Tempo}
track cue {{
instrument piano
{cue.Dynamic}
{cue.Notes}
}}
""";
Render it:
var wav =
SoundScriptEngine
.Compile(source)
.RenderWave();
File.WriteAllBytes(
$"{cue.Name}.wav",
wav);
Then experiment.
Change:
"tempo": 120
to:
"tempo": 80
Or:
"dynamic": "mf"
to:
"dynamic": "ff"
Render again and compare the result.
The workflow is straightforward:
Configure
↓
Map
↓
Compile
↓
Render
↓
Listen
Try SoundScript
dotnet add package SoundScript --version 13.0.0
Resources
- 🌐 Website: https://soundscript.net/
- 📦 NuGet: https://www.nuget.org/packages/SoundScript
- 💻 GitHub: https://github.com/dharangutti/sound-script
- 📚 Documentation: https://soundscript.net/doc.html?p=documentation.md
- 🚀 V13 Release: https://github.com/dharangutti/sound-script/releases/tag/v13.0.0
SoundScript Developer Series
Previous
Text-to-Music with SoundScript: Deterministic Composition Instead of Prompting
Coming Next
Procedural Game and Application Audio in .NET
We'll move from configuration-driven cues to event-driven procedural audio and explore when generated sound makes more sense than maintaining large collections of static assets.
SoundScript
Write audio and media like code.
Top comments (0)