Example: Seek a generator
Build a custom generator that supports seeking and send it immediate and scheduled seeks.
Read time 6 minutesLast updated 15 days ago
This example builds a custom generator that supports seeking, and a script that seeks it. The generator plays a 440 Hz tone that fades out over its first 10 seconds of playback, so seeks are audible.
The generator follows the same scheduling rules as an generator.
AudioClipRequired includes
Some of the code in this section requires the following includes:
using Unity.Burst;using Unity.Collections;using Unity.IntegerTime;using UnityEngine;using UnityEngine.Audio;using static UnityEngine.Audio.ProcessorInstance;
Make sure to add these includes at the start of your code for them to compile.
Send seeks from a script
The seeks come from a on the same GameObject as the that plays the generator:
MonoBehaviourAudioSourceprivate AudioSource m_AudioSource; private void Awake() { // Expects an AudioSource on the same GameObject. m_AudioSource = GetComponent<AudioSource>(); }
Get the built-in control context and the generator instance from the audio source. Always guard the handle: the instance may be missing or have been destroyed if the audio source was stopped elsewhere.
// The built-in context controls generator instances created by an AudioSource. var context = ControlContext.builtIn; var instance = m_AudioSource.generatorInstance; // Guard the handle: the instance may be missing or have been destroyed. if (!context.Exists(instance)) return;
Then send the seeks:
// context is the ControlContext and instance is the generator instance to seek. // Seek immediately to second 5. var immediate = new SeekMessage(new DiscreteTime(5.0)); context.SendMessage(instance, ref immediate); // Schedule a seek: when playback reaches the 10th second exactly, jump back to the start. var scheduled = new SeekMessage(destination: DiscreteTime.FromTicks(0), when: new DiscreteTime(10.0)); context.SendMessage(instance, ref scheduled);
Forward the message in the control part
The control part is a struct that implements . Its and methods stay empty in this example:
GeneratorInstance.IControl<Realtime>DisposeUpdatestruct Control : GeneratorInstance.IControl<Realtime> { public void Dispose(ControlContext context, ref Realtime realtime) { } public void Update(ControlContext context, Pipe pipe) { }
A custom generator ignores until handles it. Playback state lives in the realtime part, so forwards the seek through the pipe and returns :
SeekMessageOnMessageOnMessageResponse.Handledpublic Response OnMessage(ControlContext context, Pipe pipe, Message message) { if (message.Is<SeekMessage>()) { ref var seek = ref message.Get<SeekMessage>(); // Reposition your playback state using seek.destination and seek.when. // Playback state lives in the realtime part, so this generator hands // the seek to it through the pipe. pipe.SendData(context, seek); return Response.Handled; } return Response.Unhandled; }
In , capture the sample rate. The realtime part needs it to convert seek positions to frames:
Configurepublic void Configure( ControlContext context, ref Realtime realtime, in AudioFormat format, out GeneratorInstance.Setup setup, ref GeneratorInstance.Properties properties) { realtime.sampleRate = format.sampleRate; // Prefer matching the host's format when possible to avoid conversion. setup = new GeneratorInstance.Setup(format.speakerMode, format.sampleRate); }
Apply seeks in the realtime part
Store pending seeks in frames, so they compare directly against the playback position:
// A SeekMessage converted to frames, queued until playback reaches its position. // A negative whenFrames marks an immediate seek. struct SeekCommand { public long destinationFrames; public long whenFrames; }
The realtime part queues incoming seeks in and applies them in . A fixed-capacity list keeps the audio thread allocation-free. splits the block at a scheduled seek's position, so the seek fires exactly when playback reaches it:
UpdateProcessProcess// A custom generator that supports seeking: a 440 Hz tone that fades out over its // first 10 seconds of playback, so repositioning the playback is audible. Seeks // follow the same scheduling rules as AudioClip generators: strict send order, // applied when playback reaches `when`, dropped when playback has already passed it. [BurstCompile(CompileSynchronously = true)] struct Realtime : GeneratorInstance.IRealtime { private const float k_TwoPi = 2.0f * Mathf.PI; private const float k_Frequency = 440.0f; // A4 private const float k_FadeSeconds = 10.0f; private float phase; // [0, 1) private long positionFrames; // Playback position; seeks reposition it. private FixedList128Bytes<SeekCommand> pendingSeeks; // Seeks wait here in send order. internal float sampleRate; // Hz, set from Configure // Capabilities must match those reported from IAudioGenerator and IRealtime. public bool isFinite => false; public bool isRealtime => false; public DiscreteTime? length => null; public void Update(UpdatedDataContext context, Pipe pipe) { // Queue the seeks the control part passed down through the pipe, in send order. foreach (var element in pipe.GetAvailableData(context)) { if (element.TryGetData(out SeekMessage seek)) { if (pendingSeeks.Length == pendingSeeks.Capacity) continue; // The queue is full; drop the seek. // Convert the content positions to frames. A null `when` seeks immediately. pendingSeeks.Add(new SeekCommand { destinationFrames = (long)((double)seek.destination * sampleRate), whenFrames = seek.when.HasValue ? (long)((double)seek.when.Value * sampleRate) : -1, }); } // Ignore other message types gracefully. } } public GeneratorInstance.Result Process( in RealtimeContext context, Pipe pipe, ChannelBuffer buffer, GeneratorInstance.Arguments args) { float phaseIncrement = k_Frequency / sampleRate; int frame = 0; while (frame < buffer.frameCount) { int segmentEnd = buffer.frameCount; // Only look at the seek at the front of the queue: a seek that isn't due // yet holds back every seek sent after it, including immediate seeks. if (!pendingSeeks.IsEmpty) { var seek = pendingSeeks[0]; if (seek.whenFrames <= positionFrames) // Due now; immediate seeks are negative. { pendingSeeks.RemoveAt(0); // Apply the seek, unless it's a scheduled seek whose position playback // has already passed - drop that one, it can no longer be reached. bool alreadyPassed = seek.whenFrames >= 0 && seek.whenFrames < positionFrames; if (!alreadyPassed) positionFrames = seek.destinationFrames; continue; } // Not due yet: if `when` lands inside this block, process up to it, so // the seek applies exactly when playback reaches its position. long offset = seek.whenFrames - positionFrames; if (offset < segmentEnd - frame) segmentEnd = frame + (int)offset; } for (; frame < segmentEnd; frame++) { // The fade depends on the playback position, so a seek is audible. float seconds = positionFrames / sampleRate; float amplitude = Mathf.Clamp01(1.0f - seconds / k_FadeSeconds); float s = amplitude * Mathf.Sin(phase * k_TwoPi); for (int ch = 0; ch < buffer.channelCount; ch++) buffer[ch, frame] = s; phase += phaseIncrement; if (phase >= 1.0f) phase -= 1.0f; positionFrames++; } } return buffer.frameCount; } }
Process- It only inspects the seek at the front of the queue, so a seek that isn't due yet holds back every seek sent after it.
- It jumps the playback position to a due seek's destination, and drops a scheduled seek that playback has already passed.
- It measures against the playback position, which jumps on every applied seek, so an earlier seek that jumps backward can make a later seek's
whenreachable again.when
Create the generator
Tie everything together in a that implements :
MonoBehaviourIAudioGeneratorpublic class Driver : MonoBehaviour, IAudioGenerator { public bool isFinite => false; public bool isRealtime => false; public DiscreteTime? length => null; public GeneratorInstance CreateInstance( ControlContext context, AudioFormat? nestedConfiguration, CreationParameters creationParameters) { // Allocate a new generator instance pairing the realtime and control structs. return context.AllocateGenerator(new Realtime(), new Control(), nestedConfiguration, creationParameters); } }
To try it out:
- Add the component to a GameObject with an
Driver, and assign it to the Generator field in the Inspector.AudioSource - Add the component to the same GameObject.
SeekDriver - Enter Play Mode and call , for example from a UI button. The tone jumps to second 5 and fades from there. When playback reaches second 10, it jumps back to the start and the fade restarts.
SendSeeks