Signalsmith Stretch

repository·main·Indexed 17 days ago

https://github.com/signalsmith-audio/signalsmith-stretch

A C++11 library for high-quality pitch-shifting and time-stretching, suitable for real-time audio processing and offline rendering. It features formant compensation, custom frequency mapping, and a command-line tool for processing 16-bit WAV files. The library supports flexible configuration via presets or manual settings and provides granular control over latency, seeking, and buffer management.

Tokens
5.5K
Snippets
19
Records
21
Agent score
68%

What's inside Signalsmith Stretch

  1. Manage Latency and Automation

    main

    The library reports latency in two parts:

    • inputLatency(): Samples supplied ahead of processing time.
    • outputLatency(): Samples received behind processing time.

    Automation Strategy: To ensure pitch and time automation are accurate, you should:

    1. Provide automation values based on the current processing time (.outputLatency() samples ahead of the output).
    2. Feed input samples from .inputLatency() samples ahead of the current processing time.
    int inputLatency = stretch.inputLatency();
    int outputLatency = stretch.outputLatency();
  2. Initialize and Configure SignalsmithStretch

    main

    To use the library, include signalsmith-stretch.h and instantiate a signalsmith::stretch::SignalsmithStretch<T> object, where T is your sample type (e.g., float or double).

    Configuration can be done via presets or manual configuration:

    • Presets: Use .presetDefault(channels, sampleRate) for standard settings or .presetCheaper(channels, sampleRate) for a lower-complexity version.
    • Manual: Use .configure(channels, blockSamples, intervalSamples) to control block and interval sizes. You can query these values using .blockSamples() and .intervalSamples().

    Both preset and manual configuration methods accept an optional splitComputation flag. When enabled, this spreads computation more evenly by introducing one extra interval of output latency, which helps prevent CPU spikes in strict real-time environments.

    #include "signalsmith-stretch.h"
    
    signalsmith::stretch::SignalsmithStretch<float> stretch;
    
    // Using presets
    stretch.presetDefault(2, 44100);
    
    // Or manual configuration
    stretch.configure(2, 512, 128);
  3. Compile Signalsmith Stretch

    main

    The library is C++11 and has been tested primarily with AppleClang (Mac) and MSVC (Windows).

    Optimization & Performance:

    • Fast Math: Enabling -ffast-math is recommended, except when using Apple Clang 16.0.0, which has a known bug with SIMD code generation.
    • Debug Builds: Debug builds can be up to 10x slower due to heavy computation. It is recommended to enable optimizations specifically for the Stretch module even in Debug builds.
    • FFT Acceleration: The library depends on Signalsmith Linear. You can use flags to enable faster FFT implementations like SIGNALSMITH_USE_ACCELERATE, SIGNALSMITH_USE_IPP, SIGNALSMITH_USE_PFFFT, or SIGNALSMITH_USE_PFFFT_DOUBLE via CMake.
  4. Handle Seeking, Starting, and Ending audio streams

    main

    For precise control over audio playback (especially fixed-length sounds):

    Starting and Seeking: Use .seek(inputBuffers, inputSamples, playbackRateHint) to move to a specific point in the audio. For the best results, provide at least (block_length + interval) samples. Tip: To align processing time with the start of input, call .seek() with inputSamples = stretch.inputLatency() samples of input immediately after .reset().

    Ending and Flushing: When the input stream ends, there may still be pending output in the buffers.

    1. Pass an additional .inputLatency() samples of silence to .process() to ensure the processing time reaches the end.
    2. Call .flush(outputBuffers, outputSamples) to read the final remaining output. It is recommended to read at least .outputLatency() samples.
    // Seeking
    stretch.seek(inputBuffers, inputSamples, playbackRateHint);
    
    // Ending a fixed-length sound
    // 1. Feed silence to reach the end
    float **silence = ...;
    stretch.process(silence, stretch.inputLatency(), outputBuffers, outputSamples);
    
    // 2. Flush remaining output
    stretch.flush(outputBuffers, outputSamples);
  5. Process audio using Seek, Process, and Flush

    main

    The library provides a granular API for processing audio in stages, which is useful for real-time streaming or precise seeking.

    1. Seeking: Use outputSeekLength(1/time) to determine how many output samples are needed for a seek, then call outputSeek(inputWav, seekLength) to position the engine.
    2. Processing: Use process(inputWav, inputOffset, outputWav, outputOffset) to process a chunk of audio. Note that the engine's internal output position is slightly ahead of the samples provided due to latency.
    3. Ending: Use flush(outputWav, numSamples) at the end of a stream to process the remaining samples and avoid introducing clicks.
    // 1. Seek
    auto seekLength = stretch.outputSeekLength(1/time);
    stretch.outputSeek(inWav, seekLength);
    
    // 2. Process a chunk
    stretch.process(inWav, inputIndex - seekLength, outWav, outputIndex);
    
    // 3. Flush the remainder
    stretch.flush(outWav, outputLength - outputIndex);
    // Seek
    auto seekLength = stretch.outputSeekLength(1/time);
    stretch.outputSeek(inWav, seekLength);
    
    // Process
    stretch.process(inWav, inputIndex - seekLength, outWav, outputIndex);
    
    // Flush
    stretch.flush(outWav, outputLength - outputIndex);
  6. Process audio blocks with .process()

    main

    The .process() method is the core loop for audio processing. It takes input and output buffers, along with their respective sample counts.

    Important Requirements:

    • Buffer Types: The buffers can be any type where buffer[channel][index] provides access to a sample (e.g., float **, double **, or custom objects for interleaved data).
    • Buffer Separation: The input and output buffers cannot be the same.
    • Time-Stretching: To perform time-stretching, provide differently-sized input and output buffers. The ratio of inputSamples to outputSamples over time determines the stretch factor.

    To clear internal state and buffers, call .reset().

    float **inputBuffers, **outputBuffers;
    int inputSamples, outputSamples;
    
    // Process a block
    stretch.process(inputBuffers, inputSamples, outputBuffers, outputSamples);
    
    // Clear internal buffers
    stretch.reset();
  7. Apply Pitch-shifting and Formant Compensation

    main

    Pitch-shifting can be controlled via factors or semitones:

    • setTransposeFactor(factor): Sets the pitch ratio.
    • setTransposeSemitones(semitones): Sets the pitch in semitones.

    Advanced Pitch Control:

    • Tonality Limit: Use setTransposeSemitones(semitones, tonalityLimit) to preserve timbre using a non-linear frequency map. The limit is normalized against the sample rate.
    • Custom Frequency Map: Use setFreqMap(std::function<float(float)>) to provide a custom mapping of input frequencies to output frequencies (both normalized against sample rate).

    Formant Compensation: To adjust for pitch-shifting when correcting formants, use setFormantFactor(factor, compensatePitch). Note: Formant correction requires a rough estimate of the fundamental frequency via setFormantBase(freq/sampleRate) (normalized against Nyquist).

    // Simple pitch shift
    stretch.setTransposeSemitones(12); 
    
    // Pitch shift with tonality limit
    stretch.setTransposeSemitones(4, 8000/sampleRate);
    
    // Custom frequency mapping
    stretch.setFreqMap([](float inputFreq) {
    	return inputFreq * 2;
    });
    
    // Formant compensation
    stretch.setFormantBase(200/sampleRate);
    stretch.setFormantFactor(1.2, true); // true enables compensatePitch
  8. Configure the stretch engine

    main

    Use configure(nChannels, blockSamples, intervalSamples, splitComputation) to set the operational parameters of the engine.

    • nChannels: Number of audio channels.
    • blockSamples: The number of samples processed in a single block.
    • intervalSamples: The interval at which processing occurs.
    • splitComputation: A boolean indicating whether to split computation (useful for performance tuning).
    Module._configure(2, 512, 256, true);
  9. Configure the SignalsmithStretch engine

    main

    To use the engine, you must first configure its audio parameters using _configure. This defines how the engine handles sample rates, block sizes, and channel counts.

    MethodParametersDescription
    _configuresampleRate, blockSize, numChannels, numBuffersSets the fundamental audio processing parameters.
    _presetDefaultpresetId, numChannelsApplies a standard preset configuration.
    _presetCheaperpresetId, numChannelsApplies a low-CPU preset configuration.

    Note: numBuffers typically refers to the number of buffers used for latency management.

    // Configure for 44.1kHz, 512 sample blocks, stereo, 4 buffers
    SignalsmithStretch._configure(44100, 512, 2, 4);
  10. Initialize memory buffers with setBuffers

    main

    Before processing audio, you must allocate memory for the input and output buffers. Call setBuffers(channels, length) to allocate a contiguous block of memory large enough to hold both input and output channels. The function returns a pointer to the start of this memory block, which you can then wrap in a TypedArray (e.g., Float32Array) in JavaScript.

    Note that the allocated memory is structured such that input channels are stored first, followed by the output channels. For example, if you request length samples and channels channels, the memory layout is: [Input Channel 0 (length samples)][Input Channel 1 (length samples)]...[Output Channel 0 (length samples)][Output Channel 1 (length samples)]....

    // Example usage in JavaScript
    const channels = 2;
    const length = 1024;
    const bufferPtr = Module._setBuffers(channels, length);
    const audioData = new Float32Array(Module.HEAPF32.buffer, bufferPtr, length * channels * 2);
  11. Configure Pitch and Formant parameters

    main

    After initialization, you can set the transformation parameters:

    • setTransposeSemitones(semitones, tonalityLimitInSamples): Sets the pitch shift. The second parameter is the tonality limit expressed in samples (Hz / sampleRate).
    • setFormantSemitones(semitones, useFormantCompensation): Sets the formant shift and whether to apply compensation.
    • setFormantBase(baseFrequencyInSamples): Sets the formant base frequency expressed in samples (Hz / sampleRate).
    stretch.setTransposeSemitones(semitones, tonality / sampleRate);
    stretch.setFormantSemitones(formants, formantComp);
    stretch.setFormantBase(formantBase / sampleRate);
    stretch.setTransposeSemitones(semitones, tonality/inWav.sampleRate);
    stretch.setFormantSemitones(formants, formantComp);
    stretch.setFormantBase(formantBase/inWav.sampleRate);
  12. Manage audio processing and latency

    main

    The engine operates on a block-based processing model. To ensure smooth playback, you must handle processing, flushing, and latency reporting.

    Processing Workflow

    1. Process: Call _process(inputPtr, outputPtr, numSamples) to transform audio blocks.
    2. Flush: When the stream ends, call _flush(inputPtr) to process any remaining samples in the internal buffers.
    3. Reset: Call _reset() to clear the engine state between different audio files or sessions.

    Latency Reporting

    Because the engine uses internal buffering for time-stretching, there is inherent latency. Use these methods to calculate the delay required for synchronization:

    • _inputLatency(): Returns the number of samples of latency at the input.
    • _outputLatency(): Returns the number of samples of latency at the output.
    // Check latency to align audio playback
    const inLat = SignalsmithStretch._inputLatency();
    const outLat = SignalsmithStretch._outputLatency();
    
    // Process a block
    SignalsmithStretch._process(inPtr, outPtr, 512);
    
    // End of stream
    SignalsmithStretch._flush(inPtr);