This project has moved. For the latest updates, please go here.

Try this Reverb ISampleProvider

Aug 5, 2016 at 7:03 PM
Edited Aug 23, 2016 at 12:44 PM
Hi All,
I wrote a Reverb ISampleProvider.
You can down load it here if you want to play with it or modify it for your own use. You should be able to drop this class into any NAudio project and try it out.

Edit: the code at this link has been refactored to address any issues mentioned in the discussion below.
Download link: http://breakthrusoftware.com/downloads/Utils/ReverbSampleProvider.txt

I'm hoping this will inspire others to write some other FX Effects for NAudio and post them here for open source use. My pipe dream is to see a set of NAudio implementations that cover the DirectX FX effects: chorus / compressor / distortion / echo / flanger / gargle / parametric eq / reverb

If you try the code, let me know what you think: ways it can be improved, bugs, etc.

Here is the sample code to run it. Just fill in an MP3 filename.
        string fname = @"MyAudio.mp3";

            WaveOut outputDeviceWaveOut = new WaveOut()
            {
                DesiredLatency = 300,
                NumberOfBuffers = 2,
            };
            outputDeviceWaveOut.DeviceNumber = 0;
            IWavePlayer waveOutDevice = outputDeviceWaveOut;
            AudioFileReader afrFile = new AudioFileReader(fname);  // sampler reader           
            ReverbSampleProvider reverbSamplr = new ReverbSampleProvider(afrFile);
            // change these properties for different effect
            reverbSamplr.ReverbLevel = 0.5f;  
            reverbSamplr.ReverbDepth = 4;
            reverbSamplr.Gain =  1.0f;
            waveOutDevice.Init(reverbSamplr);
            waveOutDevice.Play();
Aug 5, 2016 at 8:32 PM
Edited Aug 14, 2016 at 11:11 AM
Cool, a delay line based reverb using several combs. I´ve also created some effects, e.g. yesterday I completed a pitch shifting sampleprovider with the c# port of stephan m. bernsee´s shifter class (SMBPitchShifter C# Port):
'''
''' Author: Freefall
''' Date: 05.08.16
''' Based on: the port of Stephan M. Bernsee´s pitch shifting class
''' Port site: https://sites.google.com/site/mikescoderama/pitch-shifting
'''
Imports NAudio.Wave

Public Class SMBPitchShiftingSampleProvider
    Implements ISampleProvider

    Private SourceStream As ISampleProvider = Nothing
    Private WFormat As WaveFormat = Nothing
    Private Pitch As Single = 1.0F
    Private _FFTSize As Integer = Nothing
    Private _osamp As Long = Nothing
    Private ShifterLeft As New SMBPitchShifter()
    Private ShifterRight As New SMBPitchShifter()
    Private volscale As Single = 1.0F 'Recommended to scale volume down, as SMB seems to clip with pitching
    Private PitchLock As New Object()

    Sub New(ByVal SourceProvider As ISampleProvider, ByVal FFTSize As Integer, ByVal osamp As Long, Optional ByVal InitialPitch As Single = 1.0F)
        SourceStream = SourceProvider
        WFormat = SourceProvider.WaveFormat
        _FFTSize = FFTSize
        _osamp = osamp
        PitchFactor = InitialPitch
    End Sub

    Public Function Read(ByVal buffer() As Single, ByVal offset As Integer, ByVal count As Integer) As Integer Implements NAudio.Wave.ISampleProvider.Read
        SyncLock PitchLock
            Dim SampRead As Integer = SourceStream.Read(buffer, offset, count)
            If Pitch = 1.0F Then Return SampRead 'Nothing to do.
            If WFormat.Channels = 1 Then
                Dim Mono As Single() = New Single(SampRead - 1) {}
                Dim index As Integer = 0
                For sample As Integer = offset To SampRead - 1
                    Mono(index) = buffer(sample)
                    index += 1
                Next
                ShifterLeft.PitchShift(Pitch, SampRead, _FFTSize, _osamp, WFormat.SampleRate, Mono)
                index = 0
                For sample As Integer = offset To SampRead - 1
                    buffer(sample) = Mono(index) * volscale * 0.707F
                    index += 1
                Next
                Return SampRead
            ElseIf WFormat.Channels = 2 Then
                Dim Left As Single() = New Single((SampRead >> 1) - 1) {}
                Dim Right As Single() = New Single((SampRead >> 1) - 1) {}
                Dim index As Integer = 0
                For sample As Integer = offset To SampRead - 1 Step 2
                    Left(index) = buffer(sample)
                    Right(index) = buffer(sample + 1)
                    index += 1
                Next
                ShifterLeft.PitchShift(Pitch, SampRead >> 1, _FFTSize, _osamp, WFormat.SampleRate, Left)
                ShifterRight.PitchShift(Pitch, SampRead >> 1, _FFTSize, _osamp, WFormat.SampleRate, Right)
                index = 0
                For sample As Integer = offset To SampRead - 1 Step 2
                    buffer(sample) = Left(index) * volscale * 0.707F
                    buffer(sample + 1) = Right(index) * volscale * 0.707F
                    index += 1
                Next
                Return SampRead
            Else
                Throw New Exception("Shifting of more than 2 channels is currently not supported.")
            End If
        End SyncLock
    End Function

    Public ReadOnly Property WaveFormat() As NAudio.Wave.WaveFormat Implements NAudio.Wave.ISampleProvider.WaveFormat
        Get
            Return WFormat
        End Get
    End Property

    Public Property PitchFactor() As Single
        Get
            Return Pitch
        End Get
        Set(ByVal value As Single)
            SyncLock PitchLock
                Pitch = value
                ScaleVolume()
            End SyncLock
        End Set
    End Property

    Private Sub ScaleVolume()
        If Pitch > 1.0F Then
            volscale = 1.0F / Pitch
        Else
            volscale = Pitch
        End If
    End Sub
End Class
The PitchShifter of above link must be changed to non-static (simply remove all "shared"/"static" keywords) and I called the class "SMBPitchShifter" after its author.

The output has a bit "phasy" transposing, but sounds not bad and very interestingly. And, compared to soundtouch, no choppy double samples when beats are missed. As a con, sound output seems a bit compressed and overdrives with shifting distance (I try to face this with a linear volume scaling, but is not ideal).

Usage:
'4096 = FFT Size recommended; 8L = Oversampling Quality; 1.5f = half octave higher shifted;
Dim SMB = New SMBPitchShiftingSampleProvider(new AudioFileReader("<Path to your audio file>"), 4096, 8L, 1.5f)
Dim WaveOut as New WaveOut()
WaveOut.Init(SMB);
WaveOut.Play();
I´ll try your Reverb, thx for posting.


EDIT 1: I forgot to implement, for thread safety you must Lock around the get Node of the "PitchFactor" property and the complete read method.

EDIT 2: I added Lock for thread safety and fixed the missing initial volume scaling.

EDIT 3: I optimzed the Read process using bit shifting for faster calculation. Also added exception handling for more than 2 channel (=stereo) inputs.

EDIT 4: I fixed the offset of the buffer allocation before shifting.

EDIT 5: I added volume scaling for mono input.

EDIT 6: I fixed the mono offset.
Aug 5, 2016 at 8:57 PM
Ok, I tried your reverb. It throws an error here:
            while (offset < count)
            {
                for (int ch = 0; ch < numChannels; ch++)
                {
                    currOffset = offset++; // + sample;
                    inValue = buffer[currOffset];
                    modValue = Process(inValue);
                    //modValue = FxUtils.NormalizeTo1toNeg1(modValue);
                    buffer[currOffset] = modValue; // 
                }
            }
I changed it to:
            while (offset < count - 2)
            {
                     ...
            }
to get it working. The effect itself sounds a bit too hard and overdrives, I recommend at first sight two things:
  • Use prime numbers for delay lengths to avoid overlap
  • Use varying dampening, don´t scale it linearly
A good example is the standford NReverb class, here is a C# port: https://github.com/keijiro/unity-custom-reverb-filter/blob/master/Assets/Standard%20Assets/NReverb.cs
Aug 5, 2016 at 9:24 PM
Hi Freefall,
Thanks I'll take a look at both of those links. The pitch shifter looks very interesting. I'll try that tomorrow.

Thanks for testing the reverb class and reporting that error.
I've been testing with several MP3s and never got that error. I probably should have incremented the offset AFTER the sample is processed ( currOffset = offset++) , not BEFORE.

And thanks for the enhancement suggestions. I admit I'm a novice at effects.
Aug 6, 2016 at 5:43 PM
Edited Aug 6, 2016 at 5:46 PM
I´ve added thread safety and fixed the missing inital volume scaling in above code, see Edit 2.

I also created a SampleProvider that allows to modify speed scaling without altering the pitch. I can post it here if there is an interest in this.

For both pitch and speed I´ve been making two versions, one using the frequency domain phase vocoder of the stephan m. bernsee (smb) implementation (see above) and one using the time domain overlap and add method of soundtouch surina.

Note, that there is a fully managed port of soundtouch available here, so both are possible to include statically. Each of them has its pros and cons, which would lead too far here. The best way to decide is to try them out.
Coordinator
Aug 10, 2016 at 1:19 PM
this is awesome. looking forward to giving it a try. thanks for sharing
Aug 11, 2016 at 2:16 PM
Ok, I reposted the ReverbSampleProvider class to the download link above.

Notes:
  • I think I solved the overdrive issue noted by Freefall. It does result in volume somewhat lower than the original signal, but you can use the Gain property to increase it.
  • New mixing algo may have softened the "effect too hard" issue noted by Freefall. You can play with the mixing in the Process method if you want a different reverb quality.
  • This is not a textbook implementation of reverb sound reflection so purists may scoff, but I kinda like the sound. Your results may vary. ;-)
  • The DirectSignalLevel property was removed for this revision.
  • Usage notes were added to the comments at the top of the ReverbSampleProvider class code.
Aug 13, 2016 at 7:08 PM
Edited Aug 13, 2016 at 7:09 PM
Sorry for my late reply, I´m currently in the process of moving to a new city. I´ll test your Reverb revision.

I also optimized above SMBPitchShiftingSampleProvider, see Edit 3.

If there is an interest in it, I´ll post the speed provider based on the smb shifter (after optimizing).
Aug 13, 2016 at 10:58 PM
Hi Freefall,
I confess I haven't had time to setup and test your PitchShifter.

For these two lines, where is the SMBPitchShifter class?
Private ShifterLeft As New SMBPitchShifter()
Private ShifterRight As New SMBPitchShifter()
Aug 14, 2016 at 9:42 AM
Edited Aug 14, 2016 at 9:50 AM
See post # 2, it can be found at the C# port of SMB´s shifter:

https://sites.google.com/site/mikescoderama/pitch-shifting

The class is too big to post here (restricted to post max. 10.000 characters), that´s why I didn´t do so.

As written in post # 2 you need to do with the c# port:
  • Change "PitchShifter" class name -> "SMBPitchShifter" class name
  • Remove all static attributes
Then it should work - feedback is appreciated.
Aug 14, 2016 at 1:06 PM
Ok, thanks. I got the SMBPitchShifter class to compile. So now I guess I have to make two separate class libraries because one class is in C# and one is in Basic. Is that how you got this to build? Not a VBasic programmer so this is getting 'interesting'. But that's my skills limit problem so I'll figure it out. :-)

Good luck moving. That is such a blast.
Aug 14, 2016 at 4:31 PM
Edited Aug 14, 2016 at 4:33 PM
You can convert my vbasic code to c# using a code converter like this.

Yeah, moving means very much work. I´ll see how much time i can invest in audio programming, today was my first free day this week.
Aug 14, 2016 at 10:30 PM
Edited Aug 15, 2016 at 4:39 PM
Here is a quick C# conversion of my shifter class:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using NAudio.Wave;

public class SMBPitchShiftingSampleProvider : ISampleProvider
{

    private ISampleProvider SourceStream = null;
    private WaveFormat WFormat = null;
    private float Pitch = 1f;
    private int _FFTSize = null;
    private long _osamp = null;
    private SMBPitchShifter ShifterLeft = new SMBPitchShifter();
    private SMBPitchShifter ShifterRight = new SMBPitchShifter();
    private float volscale = 1f; //Recommended to scale volume down, as SMB seems to clip with pitching

    private object PitchLock = new object();
    public SMBPitchShiftingSampleProvider(ISampleProvider SourceProvider, int FFTSize, long osamp, float InitialPitch = 1f)
    {
        SourceStream = SourceProvider;
        WFormat = SourceProvider.WaveFormat;
        _FFTSize = FFTSize;
        _osamp = osamp;
        PitchFactor = InitialPitch;
    }

    public int Read(float[] buffer, int offset, int count)
    {
        lock (PitchLock) {
            int SampRead = SourceStream.Read(buffer, offset, count);
            if (Pitch == 1f)    
                //Nothing to do.
                return SampRead;
            if (WFormat.Channels == 1) {
                float[] Mono = new float[SampRead];
                int index = 0;
                for (int sample = offset; sample <= SampRead - 1; sample++) {
                    Mono[index] = buffer[sample];
                    index += 1;
                }
                ShifterLeft.PitchShift(Pitch, SampRead, _FFTSize, _osamp, WFormat.SampleRate, Mono);
                index = 0;
                for (int sample = offset; sample <= SampRead - 1; sample++) {
                    buffer[sample] = Mono[index] * volscale * 0.707f;
                    index += 1;
                }
                return SampRead;
            } else if (WFormat.Channels == 2) {
                float[] Left = new float[(SampRead >> 1)];
                float[] Right = new float[(SampRead >> 1)];
                int index = 0;
                for (int sample = offset; sample <= SampRead - 1; sample += 2) {
                    Left[index] = buffer[sample];
                    Right[index] = buffer[sample + 1];
                    index += 1;
                }
                ShifterLeft.PitchShift(Pitch, SampRead >> 1, _FFTSize, _osamp, WFormat.SampleRate, Left);
                ShifterRight.PitchShift(Pitch, SampRead >> 1, _FFTSize, _osamp, WFormat.SampleRate, Right);
                index = 0;
                for (int sample = offset; sample <= SampRead - 1; sample += 2) {
                    buffer[sample] = Left[index] * volscale * 0.707f;
                    buffer[sample + 1] = Right[index] * volscale * 0.707f;
                    index += 1;
                }
                return SampRead;
            } else {
                throw new Exception("Shifting of more than 2 channels is currently not supported.");
            }
        }
    }

    public NAudio.Wave.WaveFormat WaveFormat {
        get { return WFormat; }
    }

    public float PitchFactor {
        get { return Pitch; }
        set {
            lock (PitchLock) {
                Pitch = value;
                ScaleVolume();
            }
        }
    }

    private void ScaleVolume()
    {
        if (Pitch > 1f) {
            volscale = 1f / Pitch;
        } else {
            volscale = Pitch;
        }
    }
}
Happy testing!

EDIT: Fixed the converter issue.
Aug 15, 2016 at 1:14 PM
Hey Freefall, I got the PitchShifter working. Thanks for all your help.

Test results:
1) It's pretty cool sounding effect. I played a music file with PitchFactor = 1.9 and the singers voice sounds like Alvin the chipmunk. So it's working. :-)
2) Pitch definitely shifts when I change the PitchFactor in increments of 0.1. It can make a guitar sound like a chello.
3) What is the valid value range for PitchFactor?
4) I hear a regular click sound (like a static click). Probably as often as each Read method call. No idea what causes it.

The calculated value for L/R channels stays within the range of +1 to -1. So I assume the clicking is not from overdriven values.
Novice question: As a general rule, is it true that the output values should stay within this range (+1...-1) to avoid overdrive?


And thanks for that VBasic code converter. It works pretty good. I bookmarked the link for future use. It has a problem with converting array index parens "(" and ")" to C# syntax "[" and "]". Easy enough to fix with a text editor after converting. I only mention that in case anyone grabs your converted code above, "Left(index)" and "Right(index)" should have the square brackets syntax.

It's clear this PitchShifter deserved it's own discussion thread.
PS. And don't feel rushed to reply. I know you are throttled with the move.
Aug 15, 2016 at 2:12 PM
Edited Aug 15, 2016 at 2:13 PM
Thanks for testing. Concerning your questions:

3) Valid range is from between 0.5f (one octave down) and 2f (one octave up) - see the comments on the SMB port.
4) That click sound may be caused from overlapping windows. Try to increase the oversampling factor (=osamp) to 16 or 32. But note, that this will increase CPU usage heavily.

Yes, generally sound samples should stay in the -1 ... +1 range. All above or beyound this would be clipped and would result in crackling sound.

Perhaps Mark can include this Class for the next NAudio release, I´ve seen many people struggling searching for that.
Aug 15, 2016 at 3:26 PM
Edited Aug 15, 2016 at 3:42 PM
Freefall, Thanks for those answers.

I tried it with Osamp = 16L and = 32L
The sound was strange at those values. Sounded like original samples went missing. It got worse with 32L value. The click noise was still there. The source is a 16bit stereo MP3 @44100

I recorded CPU usage with the various Osamp values, My laptop CPU is Intel i5 @ 2.53Ghz

Osamp =8L
CPU 30% avg, spikes to 50%

Osamp =16L
strange sound, like some of original sound is missing
CPU 40% avg spikes to 50%

Osamp = 32L
worse sound, more missing sound
CPU usage about the same as 16L
Aug 15, 2016 at 4:37 PM
I think the missing sound comes from the high CPU usage, when the playback thread got too few resources.

You could try a FFT Size of 1024 or 2048 (higher size means higher precision although).
Aug 15, 2016 at 4:52 PM
Tried it with both FFT Size of 1024 or 2048.
I still hear a static click with both of these values, but it is a little less noticeable (lower volume click).

Do you hear any static when you play it on your machine? My laptop audio card is Realtek HD audio.
Aug 15, 2016 at 6:03 PM
Edited Aug 15, 2016 at 6:04 PM
No, I can´t find any clicking sounds after pitching. Perhaps your hardware is broken or your input file.

As I said before, the clicking could also be a windowing issue of the algorithm.
Aug 15, 2016 at 6:43 PM
I don't think it is 'broken' hardware, because my other ISampleProviders are not causing this click.
And the input file plays fine with my other ISampleProviders or with no ISampleProvider just playing a standard Naudio waveOut device.

Most of the Pitch Shifting is done by the CPU. So with it running at 30% CPU, maybe it isn't finished processing the previous Read event when the next Read event pops. This definitely a mid-range power laptop -- not a high performance machine.

Not sure what "windowing issue of the algorithm" means.
Aug 15, 2016 at 9:07 PM
Edited Aug 15, 2016 at 9:09 PM
Ok, then I´m almost sure the high CPU usage causes this problem. On my 4 core machine its also clicking (rather stuttering) when CPU usage is >25%.

With windowing issue I mean, that in the process of overlapping sample packets maybe aliasing effects could happen. Not sure about this, I´m not the algorithm designer. But this is probably not the reason, rather the CPU.

I´d try in your situation to decrease CPU usage to a safe performance zone, e.g. about 10% CPU. I´ve optimized the SampleProvider as much as I could and the algorithm itself won´t be much optimizeable; hence I recommend to find the best compromise between FFT Size and osamp value. In my case, it was 4096 for FFT Size and 8 for osamp. That provided a nice sound with ~ 10% CPU usage.
Aug 16, 2016 at 12:30 PM
Ok, thanks Freefall. Yeah, I've tried various combinations of values. I don't think my machine has the power for this one. The effect takes a lot of computations, no way to get around that. But it's still a really cool effect and the click is not all that noticeable. People with a fast machine will love it.
Aug 16, 2016 at 7:36 PM
Edited Aug 16, 2016 at 7:52 PM
Well, consider also the delay of processing. Playback will need to wait until first FFT pack has arrived, so there is a delay of:

DelayInSeconds = FFTSize / SampleRate

With my chosen settings:

DelayInSeconds = 4096 / (44100 1/s) = 0,093 s = 93 ms

It can be heard at the start of playback and is of course an unwanted side effect caused by the algorithm. Nevertheless, it´s not a big deal for me personally.
Aug 17, 2016 at 11:20 AM
Hi Mark, just wondering if you gave the Reverb ISampleProvider a try after I made the fixes? Any thoughts?

markheath wrote:
this is awesome. looking forward to giving it a try. thanks for sharing
Coordinator
Aug 20, 2016 at 4:08 PM
not yet, am on my holidays, but its on the list of things to do when I get back to work!
Aug 20, 2016 at 5:17 PM
Cool. Have a fun vacation.
Aug 22, 2016 at 10:27 PM
Edited Aug 23, 2016 at 11:15 AM
I tested my shifter today in C# and the problem also ocurred what you described as clicking. Finally I realized, that I forgot to remove the "static" keywords also inside the local variables. Then it´s working perfectly, like it did in VB (where I didn´t forget that).

The reason for clicking is, that with static buffers left and right samples are mixed together in a random way.

EDIT: Use the release build, debug execution is too slow and might also click.

I created a new github repistory (https://github.com/Freefall63/NAudio-Pitchshifter), where I show the shifter working in realtime in C#.

Another test would be welcome.

And Mark could include it in NAudio after holiday if he wants to.
Aug 23, 2016 at 12:52 PM
Freefall, I'll give this revised code a retest as soon as time permits. Thanks for posting.
Aug 24, 2016 at 10:32 PM
Thanks. Only thing missing with the shifter is a limiter processing afterwards, I´m not happy with my obligatory linear downscaling. Actually, I use the "FastAttackCompressor1175" Effect for this from the SkypeFx Project by Mark (which isn´t still what I wanted, but better than linear scaling). The best Limiter that is known to me is the FL Studio "Fruity Limiter", which is a genious Multiband Compressor. Too bad that it isn´t open source.

Do you guys know any good open source compressor that could be ported to .NET?