DEV Community

Anup Jayant Dharangutti
Anup Jayant Dharangutti

Posted on

Generate Background Music from JSON in .NET

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Create a matching model:

record AudioCue(
    string Name,
    int Tempo,
    string Dynamic,
    string Notes);
Enter fullscreen mode Exit fullscreen mode

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.");
Enter fullscreen mode Exit fullscreen mode

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}
    }}
    """;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

That's the entire pipeline.

cue.json
    ↓
Deserialize
    ↓
Application Model
    ↓
Map to SoundScript
    ↓
Compile
    ↓
checkout-success.wav
checkout-success.mid
Enter fullscreen mode Exit fullscreen mode

Why Not Just Store a WAV File?

A reasonable question.

For a single fixed cue:

checkout-success.wav
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Instead of exposing raw audio controls, define a simple business model:

record StatusCue(
    string State,
    int Severity);
Enter fullscreen mode Exit fullscreen mode

Map severity into known dynamics:

var dynamic =
    cue.Severity switch
    {
        1 => "mp",
        2 => "mf",
        3 => "f",
        _ => "ff"
    };
Enter fullscreen mode Exit fullscreen mode

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"
    };
Enter fullscreen mode Exit fullscreen mode

Create SoundScript:

var source = $"""
    tempo 110

    track status {{
        instrument piano

        {dynamic}
        {motif}
    }}
    """;
Enter fullscreen mode Exit fullscreen mode

And render:

var wav =
    SoundScriptEngine
        .Compile(source)
        .RenderWave();
Enter fullscreen mode Exit fullscreen mode

Notice the separation of responsibilities:

State + Severity
         ↓
Application Rules
         ↓
Musical Intent
         ↓
SoundScript
         ↓
Audio
Enter fullscreen mode Exit fullscreen mode

Keep Business Logic Outside the Audio Engine

This is an important architectural boundary.

You probably don't want SoundScript deciding:

Severity 4 = Emergency
Enter fullscreen mode Exit fullscreen mode

That's an application concern.

Likewise, you probably don't want your business configuration describing:

MIDI Channel 1
Velocity 112
Tick Position 480
Enter fullscreen mode Exit fullscreen mode

That's audio implementation detail.

A cleaner architecture is:

Business State
      ↓
Application Rules
      ↓
Musical Intent
      ↓
SoundScript
      ↓
Audio
Enter fullscreen mode Exit fullscreen mode

Each layer has a single responsibility.


Avoid Passing Arbitrary Source from JSON

A tempting approach is:

{
  "source": "tempo 120 track x { ... }"
}
Enter fullscreen mode Exit fullscreen mode

followed by:

SoundScriptEngine.Compile(
    config.Source);
Enter fullscreen mode Exit fullscreen mode

For trusted developer-authored configuration, that's perfectly valid.

For user data or external input, a better pattern is:

{
  "state": "warning",
  "intensity": 2
}
Enter fullscreen mode Exit fullscreen mode

Then map it explicitly:

Known Input
      ↓
Validation
      ↓
Application Rules
      ↓
Known Musical Structures
Enter fullscreen mode Exit fullscreen mode

Rather than:

Arbitrary String
        ↓
Compiler
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Create a model:

record DeploymentEvent(
    string Event,
    string Result,
    int Severity,
    string Environment);
Enter fullscreen mode Exit fullscreen mode

Now define rules.

Tempo

var tempo =
    data.Environment switch
    {
        "production" => 116,
        "staging" => 104,
        _ => 96
    };
Enter fullscreen mode Exit fullscreen mode

Dynamic

var dynamic =
    data.Severity switch
    {
        <= 1 => "mp",
        2 => "mf",
        3 => "f",
        _ => "ff"
    };
Enter fullscreen mode Exit fullscreen mode

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"
    };
Enter fullscreen mode Exit fullscreen mode

Assemble the cue:

var source = $"""
    tempo {tempo}

    track deployment {{
        instrument piano

        {dynamic}
        {motif}
    }}
    """;
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Musical intent:

tempo 116

track deployment {
    instrument piano
    ff

    C3 e
    C3 e
    G2 q
    C3 e
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Advantages

  • Simple
  • Fast
  • Professional audio quality

Trade-off

Every variation usually requires another asset.


Generative Music Service

State
  ↓
Prompt
  ↓
Model
  ↓
Generated Audio
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Advantages

  • Maximum control

Trade-off

  • Significant audio-engine complexity

SoundScript

State
  ↓
Application Rules
  ↓
Musical Intent
  ↓
SoundScript
  ↓
WAV / MIDI
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Your application maps:

gentle
   ↓
piano
mp
Enter fullscreen mode Exit fullscreen mode

and:

normal
   ↓
tempo 100
Enter fullscreen mode Exit fullscreen mode

Another profile might be:

{
  "notificationStyle": "urgent",
  "speed": "fast"
}
Enter fullscreen mode Exit fullscreen mode

Mapping to:

urgent
  ↓
stronger dynamic
different motif

fast
  ↓
tempo 132
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Application rules derive:

danger = true
      ↓
warning motif

health < 30
      ↓
stronger dynamic

level = 7
      ↓
tempo adjustment
Enter fullscreen mode Exit fullscreen mode

Result:

Game State
     ↓
Application Rules
     ↓
Musical State
     ↓
Audio
Enter fullscreen mode Exit fullscreen mode

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}";
Enter fullscreen mode Exit fullscreen mode

Then:

if (cache.TryGetValue(
        key,
        out var existing))
{
    return existing;
}
Enter fullscreen mode Exit fullscreen mode

Otherwise:

var wav =
    SoundScriptEngine
        .Compile(source)
        .RenderWave();

cache[key] = wav;

return wav;
Enter fullscreen mode Exit fullscreen mode

The architecture becomes:

Application State
         ↓
      Key
    ↙     ↘
 Hit     Miss
  ↓        ↓
Reuse   Render
           ↓
        Cache
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

That's only:

12 Outputs
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

Or use hashes:

using System.Security.Cryptography;

var hash =
    Convert.ToHexString(
        SHA256.HashData(first));

Console.WriteLine(hash);
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Usage:

var wav =
    RenderCue(
        "warning",
        3);

File.WriteAllBytes(
    "warning.wav",
    wav);
Enter fullscreen mode Exit fullscreen mode

Your application stays focused on:

State
Severity
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Programmable audio adds another possibility:

JSON
  ↓
Application Rules
  ↓
Musical Intent
  ↓
Audio
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Map it to SoundScript:

var source = $"""
    tempo {cue.Tempo}

    track cue {{
        instrument piano

        {cue.Dynamic}
        {cue.Notes}
    }}
    """;
Enter fullscreen mode Exit fullscreen mode

Render it:

var wav =
    SoundScriptEngine
        .Compile(source)
        .RenderWave();

File.WriteAllBytes(
    $"{cue.Name}.wav",
    wav);
Enter fullscreen mode Exit fullscreen mode

Then experiment.

Change:

"tempo": 120
Enter fullscreen mode Exit fullscreen mode

to:

"tempo": 80
Enter fullscreen mode Exit fullscreen mode

Or:

"dynamic": "mf"
Enter fullscreen mode Exit fullscreen mode

to:

"dynamic": "ff"
Enter fullscreen mode Exit fullscreen mode

Render again and compare the result.

The workflow is straightforward:

Configure
     ↓
Map
     ↓
Compile
     ↓
Render
     ↓
Listen
Enter fullscreen mode Exit fullscreen mode

Try SoundScript

dotnet add package SoundScript --version 13.0.0
Enter fullscreen mode Exit fullscreen mode

Resources


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.

📧 info@dharangutti.in

🌐 https://www.dharangutti.in/

Top comments (0)