Nicolas Séveno
Nicolas Séveno

Reputation: 326

How can a custom audio effect produce some sound after a AudioFileInputNode has finished playing

I'm developping an audio application in C# and UWP using the AudioGraph API. My AudioGraph setup is the following : AudioFileInputNode --> AudioSubmixNode --> AudioDeviceOutputNode.

I attached a custom echo effect on the AudioSubmixNode. If I play the AudioFileInputNode I can hear some echo. But when the AudioFileInputNode playback finishes, the echo sound stops brutally. I would like it to stop gradually after few seconds only. If I use the EchoEffectDefinition from the AudioGraph API, the echo sound is not stopped after the sample playback has finished.

I don't know if the problem comes from my effect implementation or if it's a strange behavior of the AudioGraph API... The behavior is the same in the "AudioCreation" sample in the SDK, scenario 6.

Here is my custom effect implementation :

public sealed class AudioEchoEffect : IBasicAudioEffect
{
    public AudioEchoEffect()
    {
    }

    private readonly AudioEncodingProperties[] _supportedEncodingProperties = new AudioEncodingProperties[]
    {
        AudioEncodingProperties.CreatePcm(44100, 1, 32),
        AudioEncodingProperties.CreatePcm(48000, 1, 32),
    };

    private AudioEncodingProperties _currentEncodingProperties;
    private IPropertySet _propertySet;

    private readonly Queue<float> _echoBuffer = new Queue<float>(100000);
    private int _delaySamplesCount;

    private float Delay
    {
        get
        {
            if (_propertySet != null && _propertySet.TryGetValue("Delay", out object val))
            {
                return (float)val;
            }
            return 500.0f;
        }
    }

    private float Feedback
    {
        get
        {
            if (_propertySet != null && _propertySet.TryGetValue("Feedback", out object val))
            {
                return (float)val;
            }
            return 0.5f;
        }
    }

    private float Mix
    {
        get
        {
            if (_propertySet != null && _propertySet.TryGetValue("Mix", out object val))
            {
                return (float)val;
            }
            return 0.5f;
        }
    }

    public bool UseInputFrameForOutput { get { return true; } }

    public IReadOnlyList<AudioEncodingProperties> SupportedEncodingProperties { get { return _supportedEncodingProperties; } }

    public void SetProperties(IPropertySet configuration)
    {
        _propertySet = configuration;
    }

    public void SetEncodingProperties(AudioEncodingProperties encodingProperties)
    {
        _currentEncodingProperties = encodingProperties;

        // compute the number of samples for the delay
        _delaySamplesCount = (int)MathF.Round((this.Delay / 1000.0f) * encodingProperties.SampleRate);

        // fill empty samples in the buffer according to the delay
        for (int i = 0; i < _delaySamplesCount; i++)
        {
            _echoBuffer.Enqueue(0.0f);
        }
    }

    unsafe public void ProcessFrame(ProcessAudioFrameContext context)
    {
        AudioFrame frame = context.InputFrame;

        using (AudioBuffer buffer = frame.LockBuffer(AudioBufferAccessMode.ReadWrite))
        using (IMemoryBufferReference reference = buffer.CreateReference())
        {
            ((IMemoryBufferByteAccess)reference).GetBuffer(out byte* dataInBytes, out uint capacity);
            float* dataInFloat = (float*)dataInBytes;
            int dataInFloatLength = (int)buffer.Length / sizeof(float);

            // read parameters once
            float currentWet = this.Mix;
            float currentDry = 1.0f - currentWet;
            float currentFeedback = this.Feedback;

            // Process audio data
            float sample, echoSample, outSample;
            for (int i = 0; i < dataInFloatLength; i++)
            {
                // read values
                sample = dataInFloat[i];
                echoSample = _echoBuffer.Dequeue();

                // compute output sample
                outSample = (currentDry * sample) + (currentWet * echoSample);
                dataInFloat[i] = outSample;

                // compute delay sample
                echoSample = sample + (currentFeedback * echoSample);
                _echoBuffer.Enqueue(echoSample);
            }
        }
    }

    public void Close(MediaEffectClosedReason reason)
    {
    }

    public void DiscardQueuedFrames()
    {
        // reset the delay buffer
        _echoBuffer.Clear();
        for (int i = 0; i < _delaySamplesCount; i++)
        {
            _echoBuffer.Enqueue(0.0f);
        }
    }
}

EDIT : I changed my audio effect to mix the input samples with a sine wave. The ProcessFrame effect method runs continuously before and after the sample playback (when the effect is active). So the sine wave should be heared before and after the sample playback. But the AudioGraph API seems to ignore the effect output when there is no active playback...

Here is a screen capture of the audio output : enter image description here

So my question is : How can the built-in EchoEffectDefinition output some sound after the playback finished ? An access to the EchoEffectDefinition source code would be a great help...

Upvotes: 0

Views: 327

Answers (1)

Faywang - MSFT
Faywang - MSFT

Reputation: 5868

By infinitely looping the file input node, then it will always provide an input frame until the audio graph stops. But of course we do not want to hear the file loop, so we can listen the FileCompleted event of AudioFileInputNode. When the file finishes playing, it will trigger the event and we just need to set the OutgoingGain of AudioFileInputNode to zero. So the file playback once, but it continues to silently loop passing input frames that have no audio content to which the echo can be added.

Still using scenario 4 in the AudioCreation sample as an example. In the scenario4, there is a property named fileInputNode1. As mentioned above, please add the following code in fileInputNode1 and test again by using your custom echo effect.

fileInputNode1.LoopCount = null; //Null makes it loop infinitely
fileInputNode1.FileCompleted += FileInputNode1_FileCompleted;          

private void FileInputNode1_FileCompleted(AudioFileInputNode sender, object args)
{
    fileInputNode1.OutgoingGain = 0.0;
}

Upvotes: 1

Related Questions