1

Resolved

Playing Short Audio Files

description

I'm writing a game and I'd like to use NAudio to add sound to it. I followed a few examples and was rather pleased with NAudio's API and performance.

I ran into the following issue: NAudio will not play short audio files. I've used WaveChannel32.Length to get the following numbers.
Files with length 755712 or less will not play.
Files with length 5935104 do play.
I'm not sure where the exact limit is. There are no exceptions thrown. I was hoping to use NAudio to play files of about 1 second long. Is anyone else having this issue? Is there a fix? Do I need to find a different library?

comments

markheath wrote Dec 14, 2012 at 11:00 AM

what output driver are you using? I wonder if it reaches the end of the input stream before it finishes filling the initial buffer. A quick workaround would be to make an IWaveProvider that puts a bit of trailing silence on the end.

vexorum wrote Dec 23, 2012 at 6:42 AM

Here's the NAudio code I'm calling.

Initializing:
WaveMixerStream32 mixer = new WaveMixerStream32();
mixer.AutoStop = false;

IWavePlayer waveOutDevice = new WaveOut();
waveOutDevice.Init(mixer);
waveOutDevice.Play();

WaveChannel32 sample = CreateInputStream(filename);

Loading functions: (From tutorial, really :b)
    private static WaveChannel32 CreateInputStream(string fileName)
    {
        if (!File.Exists(fileName))
        {
            throw new InvalidOperationException("Cannot find audio file: " + fileName);
        }

        WaveChannel32 inputStream;
        if (fileName.EndsWith(".mp3"))
        {
            WaveStream mp3Reader = new Mp3FileReader(fileName);
            inputStream = new WaveChannel32(mp3Reader);
        }
        else if (fileName.EndsWith(".wav"))
        {
            WaveStream mp3Reader = new WaveFileReader(fileName);
            inputStream = new WaveChannel32(mp3Reader);
        }
        else
        {
            throw new InvalidOperationException("Unsupported extension");
        }
        return inputStream;
    }
Play sound:
mixer.AddInputStream(sample);

Stop sound:
mixer.RemoveInputStream(sample);


I don't have an idea how to make an IWaveProvider that adds trailing silence. But instead I've added trailing silence in my audio files and tried out stuff that way. I found some rather curious things.

If I add 30 seconds of trailing silence to a 1 second effect sound, it won't play it.
If I add 10 seconds of silence before the 1 second effect and 30 seconds after, it won't play it.
If I put three 1 second effects in sequence, separated by 5 seconds and add 30 seconds after the last one, I will hear only the last 2 effects in the sequence.

It seems like silence and short sounds aren't enough to be taken serious by the library. It feels like the first effect in the sequence is just lost trying to convince the system that there's actual sound in the file.

Does anyone have a good idea?
I can work around this issue by making a single sound file of all my effects and adding the first effect twice. It's ugly, but it should work.

markheath wrote Dec 23, 2012 at 7:18 AM

there should be no problem with short sounds. I've made a drum machine example in the WPF demo that plays loads of short sounds. I can't see a problem with your code although WaveMixerStream32 is an ancient class that really I'd like to take out of NAudio - it was written for one very specific application a long time ago. If you can, I'd recommend using a similar approach to the WPF demo app. Use AudioFileReader to get your samples as an ISampleProvider, and the MixingSampleProvider to mix them together.

vexorum wrote Dec 23, 2012 at 5:43 PM

Losing the WaveMixerStream32 helped a lot! Thanks. The short files now play nicely. Here's my current rapid-hack code:

WaveOut playbackDevice = new WaveOut();
AudioFileReader samp2 = new AudioFileReader(<FileOne>);
AudioFileReader samp3 = new AudioFileReader(<FileTwo>);
MixingSampleProvider mixy = new MixingSampleProvider(new List<ISampleProvider> { samp2, samp3 });

playbackDevice.Init(new SampleToWaveProvider(mixy));
playbackDevice.PlaybackStopped += Stopped;
playbackDevice.Play();
Thread.Sleep(TimeSpan.FromSeconds(60.0)); // Prevent program from closing before files are done.

Function Stopped simple prints "Stopped" to the terminal.

If I put 1-second files in one and two, it mixes them nicely. It does exactly what I want them to do.
If I put 16-second files in one and two, it mixes them correctly but stops playing them after about 1 second.
How come?

I'd like to be able to play long music files and short effects. I imagine I'd use a MixingSampleProvider to mix it all together. I've seen it has functions: AddMixerInput, RemoveMixerInput. Perfect! But it needs to be able to handle a single 60-second file playing, and then suddenly mixing in a 1-second file. And then another! And then the same one again! Am I on the right track to getting this set up?

markheath wrote Dec 23, 2012 at 6:31 PM

what type of application are you running? A console app? In which case you should be using WaveOutEvent

vexorum wrote Dec 24, 2012 at 7:46 AM

Hehe, yes, it is a console app. But I'm running a 3D engine from there. The WaveOutEvent fixed this issue. Thanks once again.

Now I have my short and long files loaded in, and I've linked them up to events in the game. Every time the mouse is moved over a button, I do this:
sample.Position = 0;
if(!isAdded)
{
isAdded = true;
mixer.AddMixerInput(sample as ISampleProvider);
}
When the mouse is removed from the button it was over (guaranteed to occur), I do this:
if(isAdded)
{
isAdded = false;
mixer.RemoveMixerInput(sample);
}
Where mixer = MixingSampleProvider and sample = AudioFileReader.
It seems that this works only once. The sample cannot be reset and re-added to the mixer. Can you help me?

Additionally, at initialization, I do this:
waveOutDevice = new WaveOutEvent();
ISampleProvider samp = new AudioFileReader(<AudioFile>);
mixer = new MixingSampleProvider(new List<ISampleProvider> { samp });

waveOutDevice.Init(new SampleToWaveProvider(mixer));
waveOutDevice.PlaybackStopped += PlaybackStopped;
waveOutDevice.Play();

I've found that creating a MixingSampleProvider with an empty List<ISampleProvider> causes a null-ref exception. So I've just added a sample at start-up. There's nothing wrong with a start-up jingle. But perhaps this is related to the issue?

markheath wrote Dec 24, 2012 at 9:08 AM

you will need to set samp.Position = 0 before re-adding it to the mixer.
the constructor that takes a list of inputs needs at least one because it needs to know what WaveFormat we are using. I think there is another constructor you use.

vexorum wrote Dec 24, 2012 at 3:21 PM

Setting Position = 0; before re-adding the AudioFileReader to the MixingSampleProvider doesn't fix it. I already did that in my original code. I can only play an AudioFileReader once.

Additionally, I peeked at the WaveFormat that creating a MixingSampleProvider with a sample creates and produced one in code. Like this:
WaveFormat format = WaveFormat.CreateIeeeFloatWaveFormat(44100, 2);
And then I fed format to the MixingSampleProvider constructor. The result is no sound at all.
I've inspected the WaveFormat made with the sample and compared it to the WaveFormat made with CreateIeeeFloatWaveFormat. They appear identical.

I am at a loss on both points. Do you have any ideas?

markheath wrote Dec 25, 2012 at 6:47 PM

It may be that you are running out of input data. If Read ever returns 0 then the playback automatically ends. One thing that I didn't get round to adding to MixingSampleProvider was an automatic way to ensure Read always returns the number of samples requested. I've checked in a new property (ReadFully) that you can set to true to do this. Or you can have create a trivial SilenceSampleProvider which is a mixer input that always returns 0s, and then playback will never end until you explicitly request it to.

vexorum wrote Dec 26, 2012 at 11:56 PM

I've created a SilenceProvider that implements ISampleProvider. Like you said, it's pretty easy. But for potential future visitors:
public class SilenceProvider : ISampleProvider
{
    private readonly WaveFormat format;

    public SilenceProvider(WaveFormat format)
    {
        this.format = format;
    }

    public int Read(float[] buffer, int offset, int count)
    {
        for (int i = 0; i < count; i++)
        {
            buffer[offset + i] = 0;
        }
        return count;
    }

    public WaveFormat WaveFormat
    {
        get { return format; }
    }
}
I just pass it the WaveFormat of an actual sample so that I am certain that the formats match.

NAudio now works exactly as I want it within my application. Thank you very much, markheath, for you time and support. I'd like to add your name and "NAudio" to the the credits list of my free game. (It will one day be finished, I think.) And sorry for messing up this thread... I took it quite off-topic there. But! I've gained a good insight into the workings of NAudio.

markheath wrote Dec 27, 2012 at 7:17 AM

great, glad you got it working in the end. I've improved MixingSampleProvider, to hopefully make things easier for people trying to do the same thing in the future.

markheath wrote Jan 9, 2013 at 9:51 AM

marking as fixed with the changes to MixingSampleProvider