ddf / Minim

A Java audio library, designed to be used with Processing.
http://code.compartmental.net/tools/minim
GNU Lesser General Public License v3.0
668 stars 136 forks source link

FilePlayer: Exception when Samplerate does not match #91

Closed MeFisto94 closed 5 years ago

MeFisto94 commented 5 years ago

I've discovered the following Exception for some of my samples:

Exception in thread "Thread-2" java.lang.ArrayIndexOutOfBoundsException: 1
    at ddf.minim.MultiChannelBuffer.getSample(MultiChannelBuffer.java:99)
    at ddf.minim.ugens.FilePlayer.uGenerate(FilePlayer.java:369)
    at ddf.minim.UGen.tick(UGen.java:500)
    at ddf.minim.ugens.Summer.uGenerate(Summer.java:142)
    at ddf.minim.UGen.tick(UGen.java:500)
    at ddf.minim.UGen$UGenInput.tick(UGen.java:280)
    at ddf.minim.UGen.tick(UGen.java:496)
    at ddf.minim.ugens.Summer.uGenerate(Summer.java:142)
    at ddf.minim.UGen.tick(UGen.java:500)
    at ddf.minim.UGen$UGenInput.tick(UGen.java:280)
    at ddf.minim.UGen.tick(UGen.java:496)
    at ddf.minim.ugens.Summer.uGenerate(Summer.java:142)
    at ddf.minim.UGen.tick(UGen.java:500)
    at ddf.minim.AudioOutput$SampleGenerator.generate(AudioOutput.java:111)
    at ddf.minim.javasound.JSAudioOutput.readSignal(JSAudioOutput.java:119)
    at ddf.minim.javasound.JSAudioOutput.run(JSAudioOutput.java:70)

Turns out the sample in question was using a Samplerate of 48kHz were other samples were using 44.1kHz. I don't know what the expected behavior is but a cleaner exception would be nice and maybe even some resampling (if possible)

ddf commented 5 years ago

Ah, no, it looks like this might be a mistake in handling output of an audio file with fewer channels than the output it is asking for. This line should be using buffer.getChannelCount() as the end of the for loop, not channels.length.

MeFisto94 commented 5 years ago

Actually I was able to reproduce that with a stereo sound sample on 44.1kHz, not sure why that happened. The exception was slightly different though:

Exception in thread "Thread-2" java.lang.ArrayIndexOutOfBoundsException: 1024
    at ddf.minim.MultiChannelBuffer.getSample(MultiChannelBuffer.java:99)
        at com.github.MeFisto94.VoiceTool.MVE.FixedFilePlayer.uGenerate(FixedFilePlayer.java:371)
    at ddf.minim.UGen.tick(UGen.java:500)
    at ddf.minim.ugens.Summer.uGenerate(Summer.java:142)
    at ddf.minim.UGen.tick(UGen.java:500)
    at ddf.minim.AudioOutput$SampleGenerator.generate(AudioOutput.java:111)
    at ddf.minim.javasound.JSAudioOutput.readSignal(JSAudioOutput.java:119)
    at ddf.minim.javasound.JSAudioOutput.run(JSAudioOutput.java:70)

The Exception only happens occasionally though and does not happen when using only one output. It also shows the half of the samplerate I experienced, not sure why, with two outputs I would've expected double the framerate as everything is ticked twice implicitly. Minimal Testcase:

package com.github.MeFisto94.VoiceTool.MVE;

import com.github.MeFisto94.VoiceTool.ListenableFilePlayer;
import com.github.MeFisto94.VoiceTool.MinimApplication;
import ddf.minim.AudioOutput;
import ddf.minim.Minim;
import ddf.minim.ugens.FilePlayer;

import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Mixer;
import java.io.File;

public class TwoOutputsExample {
    public static Mixer getMixerByName(String name) {
        javax.sound.sampled.Mixer.Info[] mixerinfo = AudioSystem.getMixerInfo();
        for(int i = 0; i < mixerinfo.length; ++i) {
            if (mixerinfo[i].getName().equals(name)) {
                return AudioSystem.getMixer(mixerinfo[i]);
            }
        }
        return null;
    }

    public static void main(String... args) {
        float latency = 250f;
        int bufSize = Math.round(44100f / (1000f / latency));

        Minim minim = MinimApplication.getMinimInstance();
        AudioOutput stdOut = minim.getLineOut(Minim.STEREO, bufSize);
        minim.setOutputMixer(getMixerByName("Lautsprecher (USB Audio Device)"));
        AudioOutput loopOut = minim.getLineOut();
        File f = new File("samples/sample1.wav");        // 44.1kHz Stereo
        FixedFilePlayer filePlayer = new FixedFilePlayer(MinimApplication.getMinimInstance().loadFileStream(f.getAbsolutePath()));

        filePlayer.patch(loopOut);
        filePlayer.patch(stdOut);
        filePlayer.play();
        try {
            Thread.currentThread().sleep(5000);
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
        }
        MinimApplication.getMinimInstance().stop();
    }
}

That Exception is still present with the patch provided (I copied the FilePlayer as "FixedFilePlayer" into my minimal example). It's definitely the bufferOutIndex which is getting out of bounds, maybe because of the line if ( bufferOutIndex == buffer.getBufferSize() ) which never triggers when it exceeds the size due to being triggered multiple times from different threads?

Edit: It is definitely related to concurrency because if I add a synchronized to uGenerate I can hear the sound at the correct samplerate for a short period of time and the exception vanishes. Maybe I'd need a buffering node like an inverse summer which can handle multiple outputs?

ddf commented 5 years ago

Ah, thanks for the test case!

It is definitely not going to work properly to patch a single UGen to two different audio outputs since they are running separate audio threads.

If your aim is to have the program's audio output mirrored to two different audio devices, there's not a good solution for that built in. I think you would have to create some kind of buffered situation where only one of the audio outputs is ticking your UGen chain and you keep a buffer of the results that you can feed to the second output. That sort of thing is definitely beyond the scope of what I am able to provide within the library at this point in time.

Edit: that said, it's an interesting problem and I will mull it over and see if I can propose some sort of straight-forward solution.

MeFisto94 commented 5 years ago

So I put much thought that until I've done a stupid simple suggestion from stackoverflow. My current workaround is to duplicate the whole chain for every output, that's not processing power friendly but works like a charm.

A side note: Maybe an exception would be helpful if one adds multiple inputs to a gain? I assumed the gain to extend Summer (or actually expected every UGen to support multiple ins) and it lead me to some headache why the sound wasn't playing :D Because when you add multiple UGens only the last one will be patched to the Gain.

The "real" solution would be a ring-buffer with three pointers: two read pointers and one write pointer and then two outputs can read whenever they want and it's up to the designer when to write (e.g. an internal thread, some kind of clock or one of the outputs, everything has it's up and downs, considering output could freeze for one second, for example).

Anyway the problem is that when being ticked, the UGen has no clue who ticked it and that isn't doable by third-party code either, that has to be changed in UGen (at least making some relevant methods protected). But since the workaround was so simple I left it at that for now.

Either way the file player definitely works now as expected when one has fewer channels than the output, so this issue could be closed.

ddf commented 5 years ago

Yes, the ring-buffer solution sounds like it would work to me. I'm going to close this Issue now because I won't be able to provide this kind of functionality within Minim any time soon.