Pepper.SFX
All Guidance pages

Guidance / Manual

Advanced Features

10.1 Occlusion System

How It Works:

  1. Raycast from AudioListener to each voice
  2. If blocked, reduce volume + apply low-pass filter
  3. Smooth transitions via lerp

Implementation:

private IEnumerator OcclusionUpdate()
{
    while (true)
    {
        if (Time.unscaledTime - lastOcclusionUpdate > occlusionUpdateInterval)
        {
            lastOcclusionUpdate = Time.unscaledTime;
            // The listening point (§9.3), or the scene's AudioListener when none is registered
            if (!ListenerUtil.TryGetPosition(out Vector3 listenerPosition)) { yield return null; continue; }

            foreach (var voice in realVoices)
            {
                if (voice?.source == null) continue;

                Vector3 a = listenerPosition;
                Vector3 b = voice.source.transform.position;
                bool occluded = Physics.Linecast(a, b, occlusionMask);

                float targetGain = occluded ? 0.3f : 1f;
                float lerpSpeed = Mathf.Clamp01(occlusionUpdateInterval * 5f);
                voice.GainStack.OcclusionGain = Mathf.Lerp(
                    voice.GainStack.OcclusionGain,
                    targetGain,
                    lerpSpeed);

                voice.UpdateFinalVolume();

                // Low-pass cutoff is now written through the voice's OcclusionSlot mixer
                // group (see the Occlusion Mixer Slot Pool chapter), not via a per-voice
                // AudioLowPassFilter component.
            }
        }
        yield return null;
    }
}

Performance Cost:

Cost per frame = (realVoiceCount / updateInterval) raycasts

Example:
20 real voices, 0.2s interval = 100 raycasts/second = ~2 raycasts/frame @ 60fps

Optimization:

✓ Use coarse occlusionMask (only large geometry)
✓ Increase update interval on mobile
✓ Disable for non-critical sounds
✓ Disable entirely for 2D games

10.2 Ducking

Automatic Volume Reduction:

Setup on Trigger Bus (e.g., Dialogue):

enableDucking = true;
duckingTargets = [
    { targetBus: Music_Bus, duckAmount: -10.5f },  // dB: about 30% of the level
    { targetBus: SFX_Bus, duckAmount: -6f }        // dB: about 50%
];
duckingAttack = 0.05f;   // Quick duck
duckingRelease = 0.8f;   // Slow return

Behavior:

Duck triggered (TriggerDucking action, hold = its Fade Duration) → Music/SFX duck over 0.05s
Hold elapses → Music/SFX return over 0.8s

Ducking is triggered, not detected from the bus’s output; see §6.4.

Common Patterns:

Dialogue ducks Music: 70-80%
Dialogue ducks SFX: 40-60%
Important SFX ducks Music: 30-50%
UI sounds duck nothing: 0%

10.3 Crossfading

Types:

For both types, the outgoing sound fades along the incoming sound’s curve run backwards in time.

Linear:

float fadeIn  = x;        // Straight line
float fadeOut = 1f - x;
// Results in slight volume dip in middle

Equal-Power (Recommended):

float fadeIn  = Mathf.Sin(x * Mathf.PI * 0.5f);
float fadeOut = Mathf.Cos(x * Mathf.PI * 0.5f);   // fadeIn² + fadeOut² = 1
// Maintains perceived loudness throughout

Usage:

AudioManager.Instance.CrossFade(
    from: oldMusicContainer,
    to: newMusicContainer,
    duration: 2f,
    type: AudioEvent.CrossfadeType.EqualPower
);

Event-Based:

Event: "TransitionMusic"
Action:
  - Type: CrossFade
  - From Container: Music_Combat
  - To Container: Music_Calm
  - Target Bus: Music_Bus
  - Duration: 3.0s
  - Type: EqualPower

Set the action’s Target Bus to route the incoming music through a bus, the same as a Play action, so it follows that bus’s volume, mute and ducking. That includes the clips an auto-advancing Sequence starts later. With no Target Bus (and always when you call AudioManager.CrossFade directly), the incoming voices play unbussed, through the container’s own Mixer Group. A 3D container plays at the position, or on the GameObject, you pass to Post. The incoming voices are not attached to the CrossFade event’s handle: that handle finishes straight away. Stop the new music with container.Stop(), StopAllSounds(), or another CrossFade.

A container crossfade fades the voices of the “from” container that are playing at that moment. It doesn’t cancel anything the outgoing sound was still going to start: a delayed Play action (the loop in the intro + loop recipe) or a Sequence’s next clip will still come in, at full volume. When you hold the outgoing sound’s handle, use a handle crossfade instead.

Changing your mind mid-crossfade is smooth: crossfading back while a sound is still fading in fades it out from the level it had reached. Note that the target always starts again from the top; a crossfade never resumes a sound that is fading out.

Handle crossfade (recommended for music started by an event):

AudioHandle music = startMainTheme.Post();           // intro now, loop after a delay

// Later — fade the whole main theme out, including the loop it hasn't started yet,
// and fade the combat event in:
music = AudioManager.Instance.CrossFade(music, combatMusicEvent, 2f);

CrossFade(AudioHandle from, AudioEvent to, float duration, CrossfadeType type = EqualPower, GameObject source = null, Vector3 position = default) fades out every voice of from and cancels what it was still going to start (delayed Play actions, a Sequence’s next clip). It then posts to normally, so its bus, priority, cooldown and Max Instances apply, and fades in the voices that post started. It returns the new handle. The old handle’s OnFinished fires when its fade-out ends. OnStopping doesn’t fire, as for every crossfade. Either side may be null. Voices to starts later (its own delayed Plays, a Sequence’s next clip) come in at their normal level. If to is the same event as from and its Max Instances is reached, the instance limit ends the old play at once instead of fading it.

Crossfades and the time scale. A crossfade runs on its event’s Time (§5.4): the CrossFade action’s own event, or for a handle crossfade to’s event (else from’s). Scaled, the default, is game time, so a crossfade started while the game is paused (time scale 0) waits for the game to resume. Set the pause menu’s music event to Unscaled and its crossfade completes during the pause. A container crossfade called from code has no event and runs on game time.


10.4 Beat-Scheduled SFX (BeatScheduler)

Purpose: Fire an AudioEvent on a tempo grid (BPM) whose rate can be changed at runtime without affecting pitch.

Unity’s AudioSource.pitch scales both speed and pitch together — doubling it lifts the clip an octave. For rhythmic SFX (heartbeats, metronomes, ticking clocks, pulsing UI, sonar pings) you usually want the opposite: faster cadence, same timbre. BeatScheduler solves this by retriggering a one-shot event on an interval of 60 / BPM seconds instead of time-stretching a continuous clip. Pitch never changes because the clip itself never changes — only how often it is re-posted.

Class: AudioSystem.BeatScheduler (MonoBehaviour, RunTime/Utilities/BeatScheduler.cs).

When to use it

Use case Fit
Heartbeat whose BPM rises with stress ✅ ideal
Metronome / rhythm game tick ✅ ideal
Clock “tick-tock” ✅ ideal
Pulse ambience (reactor hum, sonar) ✅ good
Musical content that must stay on-grid ⚠️ works for single-hit beats; for layered music use a dedicated music system
Continuous loop that should play slower without pitch drop ❌ use a mixer Pitch Shifter effect with source.pitch inverse instead

Timing model

Beats are scheduled against AudioSettings.dspTime, not Time.time. Each Update() checks whether the DSP clock has reached the next grid time; if so, it posts the AudioEvent with that grid time as the scheduled DSP start (AudioEvent.Post(scheduledDspTime) → AudioSource.PlayScheduled), so each beat’s audible onset is grid-anchored rather than firing-frame-anchored. Spacing between beats is exactly BeatInterval regardless of frame-rate jitter.

Hitch policy. If Update() runs while one or more grid times are already overdue (frame stall, very high BPM, scene-load slowness), the scheduler fires audio at most once per Update — anchored to the most-recent overdue grid time — and drops earlier missed beats. This prevents voice-stacking on recovery, which used to surface as a +6 to +12 dB thump when multiple overdue voices landed on the same DSP buffer boundary. The OnBeat callback still fires once per missed beat up to maxCatchUpBeatsPerFrame, so beat-counting game logic (e.g. “every 4th beat”) stays synchronized with the grid even across hitches. The grid phase is preserved across stalls: post-recovery, beats land at the original T₀ + N × BeatInterval rather than at dspTime + interval.

Changing Bpm at runtime (inspector field, property setter, or keystroke into the BPM number field) re-anchors the next beat to dspTime + new BeatInterval so the new tempo is audible within one beat. Earlier authoring did not handle intermediate keystroke values (typing “120” passed through bpm=1 which would make the scheduler appear silent until a 60-second stale tick elapsed); this is now handled automatically.

Inspector fields

Field Purpose
Audio Event The AudioEvent asset posted on every beat (drag it in; there’s no name lookup). From code: the Event property.
Emitter Optional GameObject used as the sound source and spatial position. Defaults to the BeatScheduler’s GameObject.
Bpm Beats per minute. Also writable at runtime via the Bpm property.
Max Catch-Up Beats Per Frame Cap on OnBeat callbacks per Update if the scheduler falls behind. Audio always fires at most ONCE per Update regardless — only OnBeat consumers (beat-counters driving game logic) see catch-up, keeping their count synced to the grid. Grid phase is preserved across stalls.
Play On Enable Start scheduling when the component is enabled.
Fire Immediately On Start If true, the first beat fires on the next frame. If false, it waits one full interval.

Public API

public float Bpm { get; set; }          // Clamp-guarded, min 1
public bool IsRunning { get; }          // True while scheduling
public double BeatInterval { get; }     // Seconds per beat, 60 / Bpm

public void StartScheduling();
public void StopScheduling();
public void ResyncPhase();              // Reset: next beat = now + BeatInterval

public event System.Action OnBeat;      // Main-thread callback per beat

Minimal example — stress-driven heartbeat

using UnityEngine;
using AudioSystem;

public class StressHeartbeat : MonoBehaviour
{
    [SerializeField] private BeatScheduler heartbeat;
    [SerializeField] private float calmBpm = 60f;
    [SerializeField] private float panicBpm = 160f;

    [Range(0f, 1f)] public float stress;

    private void Update()
    {
        heartbeat.Bpm = Mathf.Lerp(calmBpm, panicBpm, stress);
    }
}

Drag the Heartbeat_Thump event asset into the BeatScheduler’s Audio Event (a normal AudioEvent with a RandomContainer of 2–3 thump variations). No other plumbing required.

Common patterns

Quantize gameplay to beats. Subscribe to OnBeat to drive visual pulses, damage ticks, flashlight flicker, etc. The event fires on the main thread so Unity API calls are safe.

Pause on gameplay pause. Toggle the component’s enabled or call StopScheduling() / StartScheduling(). The phase resets on restart unless you snapshot BeatInterval yourself.

Multiple tempos simultaneously. Add multiple BeatScheduler components to a GameObject, each pointing at a different event. They run independently and don’t share a phase.

Limitations

  • Sample-accurate spacing, frame-bounded latency. Each beat is scheduled to its grid DSP time via PlayScheduled, so spacing between consecutive beats is exactly BeatInterval regardless of frame-rate jitter. The audible onset of each beat can lag its grid time by up to one DSP buffer (Unity rounds past-time PlayScheduled to the next buffer boundary), but that lag is approximately constant across beats and doesn’t affect perceived tempo. Suitable for rhythmic gameplay SFX and single-track musical-grid scheduling; for sample-aligned multi-track musical content, schedule PlayScheduled directly with explicit DSP times.
  • No musical phase alignment across schedulers. Two schedulers at the same BPM are not guaranteed to fire on the same DSP frame.
  • Does not track segment/bar/measure structure. It emits an unbroken stream of beats. For song-form authoring (intro / loop / outro), use an interactive-music layer above the SFX system.

10.5 Audition (outside Play mode)

Window > Audio System > Audition plays a container tree, or an event, while you edit it, so an engine can be tuned by ear without driving the car.

  1. Select a container or an event in the Project window (with Follow Selection on, the window picks it up), or drag one into Sound. Press Play.
  2. Move the sliders: one for every parameter the tree reads (Blends, pitch follow, Parameter Curves on containers and layers, Switch Group drivers), and for each one’s Bypass Slew While parameter (an engine cut), in each parameter’s range and unit. Values go through each parameter’s Smoothing, as in the game; a “heard” line shows where the smoothing has got to. A slider shows the value written: where a sweep or a replay has it, and where a sweep ended.
  3. Pick switch values from the dropdowns. A Switch set to Next Play Only plays again to pick up the new value; one set to Crossfade or Restart changes as it would in the game.
  4. Sweeps: press Add Sweep for each parameter to move, and set its range, its seconds and how it carries on: Once (stops at the end), Repeat (starts again from the start) or Back And Forth. Any number run at once, one per parameter (RPM 900 → 7000 over 4 s while load goes 0 → 1 over 3 s). Moving a slider stops only that parameter’s sweep.
  5. Watch Layers: each sound playing, its level (dB), pitch (semitones), low-pass, whether it is real or virtual (silent, keeping its place), and for an event the bus it plays through. Keep the Inspector open beside the window: edits to curves, regions and root values are heard at once, and a curve or parameter added there gets its slider without selecting the sound again.
  • An event plays its Play actions as a post would: each through its Target Bus (the bus’s volume and mute), with the event’s priority and Persistent flag, after its Delay. Its Set RTPC and Set Switch actions set the audition’s values. Its other actions (Stop, Pause, Resume, states, ducking, crossfades) aren’t auditioned; the window says how many it skipped. Max Instances and cooldowns don’t apply, and the event itself is never changed.
  • Replay a Recording (§10.6): pick a recording and press Replay. Every recorded value moves at once, as the game wrote it, through each parameter’s Smoothing, so a smoothing change is heard on the replay; the recorded switch values are set as the game set them. Loop goes round (all of it, or the range you set) and Time scrubs: the values where you drag to are heard at once. Moving a recorded parameter’s slider takes that one parameter off the recording; Replay again puts it back.
  • Distance and Angle place the sound around the listener: they drive Distance, Approach Speed and Angle parameters and an Attenuation asset’s filters.
  • Heard 2D. Outside Play mode there may be no listener for Unity to pan and fade from, so a 3D sound is heard straight, at full level; check the distance fade and panning in Play mode.
  • Outside Play mode a Sequence plays only its first clip, and Stop is immediate (both run on Unity coroutines, which don’t advance there).
  • Generated engines (Granular and Additive Engines, Unity 6.3 and later, §4.7) play here too: their Rpm and Load get sliders.
  • The audition runs on its own hidden Audio Manager. It stops when you enter Play mode, when scripts reload and when the window closes, and it adds nothing to your scene.
  • From code (editor tools): AudioAudition.Begin(out whyNot), then Play(container) or Play(audioEvent), SetParameter, Sweep(parameter, from, to, seconds, SweepMode), SetSwitch, Replay(recording), Tick(dt) every editor update, GetLayers, and Dispose().

10.6 Live View and recording a drive (Play mode)

Window > Audio System > Live View shows, while the game runs, what each Audio Emitter (each car) is doing:

  • Its values: every parameter set on the emitter, as written and as heard after smoothing, with “moving” while it is still on its way; and its switch values.
  • Its sounds: each play posted on it, with its event and container, bus, whether it is Persistent, and whether it is real or virtual; the play’s own values (set on its handle, such as each axle’s slide speed); and a row per voice: its level (dB), pitch (semitones), its region for a layer of a Regions Blend (“1780–2510 RPM at 2300: in”, “fading” or “out”), and, when virtual, why: over the cap, silent layer, past Max Distance, held by code (MakeVirtual), bus paused, emitter paused, or stopping (”, fading” while it fades down).
  • No emitter lists the sounds posted without one.

The Voice Pool Debug window’s “Why virtual” column gives the same reasons.

Recording. Choose an emitter and press Record; drive; press Stop and Save and pick where the recording goes. It keeps what the game wrote to that emitter, every value (Bypass While ones included) and its switch values, from each frame, and only when they change. Replay it in the Audition window (§10.5) to tune a mix on a loop of a real lap. It doesn’t keep values set on one sound’s handle (a tyre’s per-axle values) or global values. A recording still running when Play mode ends is offered for saving; one whose emitter is destroyed (a car reset or removed) stops there, and the Live View offers it for Save or Discard.

The Gain dB column (here and in the Audition window) is Pepper’s gain to the AudioSource: volume, curves, bus, offsets, ducking and, in Pepper spatial mode, distance. Unity’s own 3D distance fade (Spatial Mode Unity) and the mixer come after it, so it isn’t what you hear; it reads -inf while a voice is parked virtual.

From code: AudioEmitterRecorder.Begin(emitter, out whyNot) and End() give an AudioParameterRecording; AudioManager.GetPlaysDebug(list) and AudioEmitter.AllEnabled are what the Live View reads, for a game’s own debug overlay.

10.7 Loop Analysis

Window > Audio System > Loop Analysis measures engine loops (any loop, really). Select clips, or containers or a Blend playing them, press Use Selection, then Analyse:

  • Root: the loop’s fundamental, and the RPM it means for the Cylinders you set: a four-stroke engine fires each cylinder once every two turns, so RPM = fundamental × 120 ÷ cylinders (an inline-4 idling at 900 RPM sounds at 30 Hz, a V8 at 60 Hz). If a recording fools it by an octave, ÷2 and ×2 fix the reading. When the clip’s name holds an RPM (the last number between 300 and 20000, as the Engine Builder reads it), a reading more than half an octave from it is flagged under the row: the pitch read an octave off, or a name that isn’t the loop’s RPM.
  • Cylinders is a scale. Start from the engine’s count, but go by the loops: their strongest pitch isn’t always the firing rate, and a library tuned to notes has a scale of its own (one tuned to notes at RPM ÷ 20 Hz reads on 6, whatever the engine). When most readings sit 2 semitones or more from the RPMs in their names, the window names the count that fits, with a Use N button: a V8 library read at 6 is 5 semitones high, and gets 8. Cylinders is one setting for every engine, so set it again for each library: libraries of different engines, or tuned differently, read on different counts.
  • Loop Point: whether the loop clicks where it goes round: the first sample against where the end was heading, compared with how much the sound moves sample to sample. “Clean”, or “clicks”.
  • Level Across It: the first 50 ms against the last 50 ms; more than 3 dB is a jump to re-cut or crossfade.
  • Level: RMS and peak in dBFS, and integrated loudness in LUFS (ITU-R BS.1770; a mono clip counts as one channel).
  • Seam ▶ plays the end and the start of the loop round and round, so you hear only the seam.

Clips must be Decompress On Load to be read (the right setting for loop layers anyway).

On a Regions Blend’s Inspector, Detect Root Values measures each layer’s clip and sets its Root Value (with Undo), then logs what it found; set Cylinders beside it. It writes the raw reading: the ÷2 and ×2 buttons here only change what this window shows, so if a loop reads an octave off, correct its Root Value by hand (halve or double it). The log is a warning when a reading is far from its clip’s name, or when most are (it then names the cylinder count that fits).

Loops tuned to notes? If each loop is pitch-corrected to one note, the most exact roots come from the notes, not from a measurement: Root = the note’s frequency × 120 ÷ Cylinders (× 20 at 6). A loop named 2500 tuned to B2 (123.47 Hz) has its root at 2469.4, not 2500; neighbours rooted by their names can sit tens of cents apart in a crossfade and beat.

Auto Layout puts the layers in Root Value order. Sounds already playing keep the order they started with, so while a sound plays (in Play mode, or in the Audition window) it asks before re-ordering: until they are posted again, layers would play with each other’s regions and pitch. Laying out regions without a new order is heard at once.

10.8 Engine Builder

Full version only. In the free version, build the same tree by hand as the Vehicle Engine RPM recipe describes (§3); Loop Analysis (§10.7) and the Blend inspector’s Detect Root Values are in both.

Window > Audio System > Engine Builder turns a car’s engine loops into the whole tree the engine recipe describes (§3, Vehicle Engine RPM):

  1. Select the loops and press Use Selection. Each row guesses its set (OffLoad when its name says off, decel, coast or overrun; else OnLoad) and its RPM (the last number between 300 and 20000 in its name: Eng_OffLoad_2510, v8_on_4960rpm). Correct them, or press Detect (§10.7), which uses the Cylinders above the rows (the same setting as Loop Analysis’s: a scale, see §10.7). Changing Cylinders re-scales the rows Detect measured and keeps the ones you typed. A row more than half an octave from the RPM in its name is flagged under it; when most rows sit 2 semitones or more from their names, the window names the cylinder count that fits instead, with a Use N button.
  2. Pick the RPM and Load parameters, or leave them empty to have EngineRpm (0–9000 RPM, about 15 ms of smoothing) and EngineLoad01 (0–1, up 50 ms, down 150 ms) made. Pick the Target Bus and, if you like, an Attenuation.
  3. Press Build. It lists the clips whose import settings it changes, then makes, in the folder you chose:
    • one looping container per loop, with Uses Parent Positioning and Uses Parent Output (and Avoid Repeat Last 0: one clip has nothing to avoid);
    • an OnLoad and an OffLoad Blend on RPM: Regions layout, Root Values, Auto Layout, Equal Power, Pitch Follow, Silent Layers Virtualize, Force Loop;
    • the engine Blend on load over the two, crossfading at constant power from 0.3 to 0.7 load, 3D, Doppler Level 0;
    • one Persistent event at Priority High playing it on the Target Bus;
    • the clips set to mono and Decompress On Load (their compression is kept). A clip it folds to mono gets Unity’s Normalize turned off, so the bands keep the levels they were recorded at; its dialog lists every change first.

With loops of one set only, the RPM Blend is the engine and there is no load Blend, so it sounds the same on and off the throttle; the window and its dialog say so. That suits a thin set for other cars; a player’s car wants both sets. It never overwrites an asset. The two sets’ crossfades fall at the same RPMs when their roots match; move one set’s regions by hand to stagger them. Audition the event in §10.5.

10.9 The Container Validator and the other windows

Window > Audio System > Container Validator checks every container and event in the project:

  • empty entries, null entries, zero weights; a Routing container with no clip;
  • a Regions Blend where nothing plays, or the layers dip below -20 dB, somewhere in its parameter’s range, and layers with no region (0–0). Silence from the bottom of the range (a skid at rest, wind at low speed) is a note, not a warning;
  • a layer that follows pitch with no Root Value; a layer pitched past the Blend’s Tuning Warning while it is heard (Pitch Follow over the follow parameter’s own range, plus pitch curves on the Blend, the layer and its container);
  • a 3D loop with Doppler Level above 0 in a Blend set to Virtualize; a stereo clip on a 3D layer; a streamed clip on a layer that goes virtual. Inside a nested Blend, a clip is judged by what it plays with (the outer Blend’s 3D through Uses Parent Positioning, its Virtualize), and reported once;
  • 3D distances, or with an Attenuation asset its curve still loud at its Max Distance (the sound goes virtual there);
  • a Play action with no Target Bus (a warning when its container has no Mixer Group either: it plays straight to Unity’s output); a looping event that isn’t Persistent (a hint).

Fix and Auto-Fix All are undoable. The Blend Inspector shows the same Regions and tuning findings.

  • Audio Profiler: Voice Usage is real voices against Max Real Voices (100% at the cap).
  • Voice Pool Debug: Pool Objects against the pool’s size (Max Real + Max Virtual + Steal Headroom), which should never change during a session; the real and virtual tables scroll separately.
  • Mixer and Profiler faders, mute and solo are undoable and saved with the project, in Play mode too: a mix set while the game runs stays.

10.10 Engine Grains (Unity 6.3 and later)

Window > Audio System > Engine Grains cuts an engine sweep into a Grain Table for a Granular Engine (§4.7).

  1. Drag the sweep into Sweep. It must be Decompress On Load; if it isn’t, the window offers to set it (its compression is kept).

  2. Say where its RPM comes from:

    • Known: type the RPM at the clip’s first sample and at its last (From → To), and how it moves between them (Curve): Straight Line, the same RPM every second, or Even Pitch, the same number of semitones every second (the RPM multiplies by the same factor), as most synthesisers sweep. The number of periods of its pitch in one engine cycle is measured against them (6 for a sweep tuned to notes at RPM ÷ 20 Hz).
    • From Pitch: the RPM is measured from the sweep’s pitch, RPM = pitch × 120 ÷ Cylinders (a scale shared with Loop Analysis, §10.7), looked for between the two RPMs typed. Use it for a recording, or a sweep that follows neither curve.
  3. Press Analyse. Each grain is one engine cycle (two turns: 120 ÷ RPM seconds); every cut is moved, by less than half a period and at most 6% of a cycle, to where the waveform matches the start of the grain before it, so the grains join in phase. Each is tagged with the RPM its own length means.

    The curve typed is checked against the sweep’s own, measured from a strong harmonic followed through it. A curve that doesn’t fit puts every cut where no cycle ends, so when they disagree (the other curve, or the RPM changing more or less over the clip than typed) the window says so and offers buttons: the curve that fits, with the two ranges that keep one end you typed (it can’t know which end is wrong). Each is cut on trial, and the one whose cuts match better comes first, marked fits better. Press it, or the other if you know that end is right; it analyses again.

  4. Read the report: the grains and the RPM range they cover, and:

    • No grain between A and B RPM: the sweep jumps there; the player stretches the nearest grain across the gap.
    • Only N grains per 1000 RPM: a fast sweep; a slower one gives the player more to choose from.
    • Holds about X RPM: the RPM barely moves for a while (a note, not a problem): these grains are what the player varies a held RPM with.
    • The grains are N% off the typed RPMs: the sweep doesn’t follow the curve typed. Check From, To and Curve, or try From Pitch.
    • N% of the cuts found nothing like the cycle before within reach: the same, seen from the cuts: the grains aren’t whole engine cycles, and the engine would sound rough.
  5. One Grain At loops the grain nearest an RPM, unchanged and with no crossfade: a click at its seam is a cut in the wrong place.

  6. Press Save. The table is made beside the sweep as <sweep>_Grains.asset, or, when Grain Table names one (it fills the window with that table’s settings), that table is updated, with Undo. Re-analyse whenever the sweep changes.

From the Pepper.SFX 3.1 guides (USER_GUIDE.md). Something wrong or unclear? Tell me.