Joel Brown
Joel Brown

Reputation: 14388

Combine NAudio Pan and JSNet SuperPitch while playing an mp3 or wav

I have been working on a specialized mp3/wav/(wma later?) player using the excellent NAudio library in a C#/WPF desktop application. I've also added in the JSNet library as used in the Skype Voice Changer.

My objective is to be able to modify the Pan and the Pitch of the sound file while it is playing.

I have succeeded in modifying the Pan property during playback. I've also figured out how to use the JSNet SuperPitch object to modify the pitch. However, I've only managed to modify the pitch prior to playback. Once playback starts, the controls on the SuperPitch object seem to have no effect on the sound.

Has anyone succeeded in combining pitch control using SuperPitch and pan control with WaveChannel32.Pan?

The problem stems from the fact that the effect is applied to a WaveStream which is then converted to a WaveChannel32 to expose the .Pan property. However, once the conversion to WaveChannel32 occurs the EffectChain doesn't seem to be connected any more.

Here is a stripped down WPF project that illustrates my basic approach:

Simple WPF Window:

<Window x:Class="NAudioDebug.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Slider x:Name="sldPan" TickFrequency="0.2" Minimum="-1.0" Maximum="1.0" ToolTip="Balance" 
                Grid.Row="0" TickPlacement="Both" IsSnapToTickEnabled="True" 
                Value="{Binding Path=Pan, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <Slider x:Name="sldPitch" TickFrequency="1" Minimum="-12" Maximum="12.0" ToolTip="Pitch" 
                Grid.Row="1" TickPlacement="Both" IsSnapToTickEnabled="True" 
                Value="{Binding Path=Pitch, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <Button x:Name="btnPlayStop" Content="Play/Stop" Grid.Row="2" Click="btnPlayStop_Click" />
    </Grid>
</Window>

And here is the code-behind for the XAML, above...

using System.Windows;

namespace NAudioDebug
{
    public partial class MainWindow : Window
    {
        private Player _Player;

        public MainWindow()
        {
            InitializeComponent();
            _Player = new Player();
            this.DataContext = _Player;
        }

        private void btnPlayStop_Click(object sender, RoutedEventArgs e)
        {
            _Player.PlayStop();
        }
    }
}

Sound File Playback Object

The important part of the code is this object which controls the playback and provides properties that the WPF controls can bind to. The PlayStop() method is where the issue lies, I think.

using System;
using System.ComponentModel;
using JSNet;
using NAudio.Wave;

namespace NAudioDebug
{
    public class Player : INotifyPropertyChanged, IDisposable
    {
        private IWavePlayer _WaveOutDevice;
        private WaveChannel32 _MainOutputStream;
        private EffectStream _EffectStream;
        private EffectChain _Effects;
        private SuperPitch _PitchEffect;
        private bool _bDisposed = false;

        public Player()
        {
            Pan = 0.0f;
            Pitch = 0.0f;
        }

        private float _Pan;
        public float Pan
        {
            get { return _Pan; }
            set
            {
                _Pan = value;
                if (_MainOutputStream != null)
                {
                    _MainOutputStream.Pan = _Pan;
                }
                OnPropertyChanged("Pan");
            }
        }

        private float _Pitch;
        public float Pitch
        {
            get { return _Pitch; }
            set
            {
                _Pitch = value;
                if (_PitchEffect != null)
                {
                    _PitchEffect.Sliders[1].Value = value;  // Slider 1 is the pitch bend in semitones from -12 to +12;
                }
                OnPropertyChanged("Pitch");
            }
        }

        public void PlayStop()
        {
            if (_WaveOutDevice != null && _WaveOutDevice.PlaybackState == PlaybackState.Playing)
            {
                _WaveOutDevice.Stop();
                DisposeAudioResources();
            }
            else
            {   // Starting a new stream...
                DisposeAudioResources();

                _Effects = new EffectChain();
                _PitchEffect = new SuperPitch();
                _PitchEffect.Sliders[1].Value = _Pitch;      // Slider 1 is the pitch bend in semitones from -12 to +12;
                _PitchEffect.Sliders[2].Value = 0.0F;        // Slider 2 is the pitch bend in octaves.
                _Effects.Add(_PitchEffect);

                WaveStream inputStream;     // NOTE: Use a WaveStream here because the input might be .mp3 or .wav (and later .wma?)
                WaveStream mp3Reader = new Mp3FileReader(@"C:\Temp\Test.mp3");
                if (mp3Reader.WaveFormat.Encoding != WaveFormatEncoding.Pcm)
                {
                    mp3Reader = WaveFormatConversionStream.CreatePcmStream(mp3Reader);
                }
                inputStream = mp3Reader;

                _EffectStream = new EffectStream(_Effects, inputStream);

                _MainOutputStream = new WaveChannel32(_EffectStream);
                _MainOutputStream.PadWithZeroes = false;
                _MainOutputStream.Pan = Pan;

                _WaveOutDevice = new WaveOut();
                _WaveOutDevice.Init(_MainOutputStream);
                _WaveOutDevice.Play();

                this._WaveOutDevice.PlaybackStopped += OnPlaybackStopped;
            }
        }

        private void OnPlaybackStopped(object sender, EventArgs e)
        {   // Clean up the audio stream resources...
            DisposeAudioResources();
        }

        private void DisposeAudioResources()
        {
            if (_WaveOutDevice != null) { _WaveOutDevice.Stop(); }
            if (_MainOutputStream != null) { _MainOutputStream.Close(); _MainOutputStream = null; }
            if (_PitchEffect != null) { _PitchEffect = null; }
            if (_Effects != null) { _Effects = null; }
            if (_EffectStream != null) { _EffectStream = null; }
            if (_WaveOutDevice != null) { _WaveOutDevice.Dispose(); _WaveOutDevice = null; }
        }

        protected void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) { handler(this, e); }
        }

        protected void OnPropertyChanged(string sPropertyName)
        {
            OnPropertyChanged(new PropertyChangedEventArgs(sPropertyName));
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool bDisposing)
        {   // Check to see if Dispose has already been called...
            if (!_bDisposed)
            {
                DisposeAudioResources();
                _bDisposed = true;
            }
        }
    }
}

Upvotes: 1

Views: 523

Answers (1)

Mark Heath
Mark Heath

Reputation: 49482

You must remember to call effect.Slider() after the value of a slider changes. Many effects have to recalculate parameters whenever a slider changes so performance reasons, you have to notify it after changing the value.

I'm hoping to reimplement a lot of the effects I made for SkypeFx with the ISampleProvider interface, which should make them a lot easier to work with in NAudio.

Upvotes: 1

Related Questions