Skip to content

Render Field music, then analyse it

Write a Pat pattern in a box, render twelve cycles of it, and there is a finite WAV file on disk before you have heard it play. That file is all Field Audio ever sees of the music. Pat, Dirt, Dough and Faust render faster than real time and Sonic Pi records a real-time performance, but every one of them hands back the same RenderedAudioFile. Pass it to FieldAudioClient.analyzeRenderedAudio(...), which uploads the file, starts the analysis jobs, reports their progress and returns the results.

To run the examples on this page, open the audio_render_analysis starting point.

The loop:

Field code (Pat / Dirt / Dough / Faust / Sonic Pi)
    → finite WAV + Field-owned render provenance
    → content-addressed Field Audio asset
    → beat, pYIN and Basic Pitch jobs
    → synchronized waveform and analysis views
    → revise the generating code

The source code stays in its box and the render's provenance map stays in Field; the server only sees the WAV. Two programs that render identical audio therefore share one uploaded asset and its cached analyses.

Rendering a Pat pattern and analysing it

This box renders twelve cycles of a Pat pattern through the Dirt samples, sends the file for analysis and shows each job's progress, then the combined view. Swap renderDirtWav for any renderer in the table below and the rest stays the same.

import java.nio.file.Path

val client = FieldAudioClient()
display("all jobs", fieldAudioJobMonitor(heightPixels = 330))

val music = (pat("{60 64 67 71, bd ~ sd ~, hh*8}") with gain(sine().range(0.22, 0.72)) with pan("0.2 0.8"))

val render = renderDirtWav(
    Path.of(System.getProperty("java.io.tmpdir"), "field-audio-renders", "pat-study.wav"),
    music,
    cycles = 12,
    cps = 1.6,
    provenance = mapOf("field_box" to "pat-study"),
)

client
    .analyzeRenderedAudio(
        render,
        requests = { region ->
            standardAudioAnalyses(region, requestedBy = "field:pat-study")
        },
        onAccepted = { named, accepted ->
            display("${named.label} job", fieldAudioJobStatusHtml(accepted))
        },
        onProgress = { named, accepted, event ->
            display("${named.label} job", fieldAudioJobStatusHtml(accepted, event))
        },
    )
    .thenAccept { completed ->
        display("analysis", completed.visualizationHtml(panelHeight = 230))
    }
    .exceptionally { error ->
        display("analysis error", error.cause?.message ?: error.message ?: error.toString())
        null
    }

This captured drum pattern uses pat("{bd ~ sd ~, hh*8}") with the pinned Dirt samples. Its 7.5-second render goes through the real beat, pYIN and Basic Pitch workers.

Actual Pat Dirt render waveform, beat grid, pYIN and Basic Pitch lanes on a shared cursor

Listen to the Dirt render while comparing the analysis lanes.

The lanes share one cursor: playing the audio moves every lane's cursor, hovering a pitch or beat lane moves the waveform cursor, clicking any lane seeks the player, and zoom and pan apply to all of them.

Renderers

Source in Field Finite-file entry point Execution
Pat's deterministic voice renderPatWav(path, pattern, cycles, cps) Renders offline and returns the completed file synchronously. cycles sets the pattern span; cps sets cycles per second.
Pat through Dirt samples renderDirtWav(path, pattern, cycles, cps) Renders offline using the available Dirt samples and returns synchronously. The render provenance records which samples matched.
Dough mono signal renderDoughWav(path, signal, seconds) Evaluates a mono signal for the requested duration and returns the completed WAV synchronously. Use it for a finite study of a continuous signal.
Dough stereo node renderDoughStereoWav(path, node, seconds) Renders a stereo node offline for the requested duration. Left and right channels remain separate in the returned file.
compiled Faust program renderFaustWav(path, program, seconds) Renders an already compiled program offline and returns synchronously. Reuse the compiled program when varying render parameters.
Faust source text renderFaustWavAsync(path, name, source, seconds) Compiles and renders in the background, returning a future for the completed file. Chain analysis with thenCompose to avoid blocking on compiler warm-up.
Sonic Pi source text recordSonicPiWav(sonicPi(), path, source, seconds) Records a real-time performance and returns a future. Completion waits for recording to stop and the WAV to stabilize.
any existing Field WAV RenderedAudioFile.existing(path, generator, provenance) Wraps an existing WAV with its generator name and provenance. It performs no synthesis; use it to send an earlier render through the same analysis pipeline.

Every renderer writes to the path you give it. The example above uses the system temporary directory, since the uploaded copy is kept by the server; write beside your document instead when you want to keep the WAV yourself.

Choosing analyses and regions

standardAudioAnalyses(region) requests multi-scale beats, monophonic pYIN and polyphonic Basic Pitch. Turn off the ones you do not need to make a run faster. This call analyses twelve seconds starting at 4 s, beats only:

client.analyzeRenderedAudio(
    render,
    startSeconds = 4.0,
    durationSeconds = 12.0,
    requests = { region ->
        standardAudioAnalyses(
            region,
            requestedBy = "field:rhythm-only",
            includeMonophonicPitch = false,
            includePolyphonicPitch = false,
        )
    },
)

To request your own set of analyses, return NamedAudioAnalysis(label, operation) values from requests. The result's analyses list keeps each label with its accepted job and final AnalysisResult. Asking again for the same audio with the same parameters returns the cached result.

Asynchronous renderers

Faust source compilation and Sonic Pi capture return CompletableFuture<RenderedAudioFile>, so they chain straight into analysis without blocking Field. Here a Faust program is compiled, rendered for 18 seconds and analysed:

renderFaustWavAsync(path, "voice", faustSource, seconds = 18.0)
    .thenCompose { render -> client.analyzeRenderedAudio(render) }
    .thenAccept { result -> display("analysis", result.visualizationHtml()) }

Sonic Pi's helper starts the recorder, evaluates the Ruby program, captures for the requested time, stops its jobs, waits for the saved file to stabilize, and only then imports it. It does not close the shared Sonic Pi daemon.

The service waveform

This view requests the same render through Field Audio's waveform-summary endpoint.

Real service waveform envelopes of a Pat Dirt render

See also