4.3.0 update wrong IO channels number (solved)

Hi everyone!

From the latest update got the prombem with number of IO channels.
Runtime request grants me 2 channels of input and output, but when i try to record some audio with 2 channels, it fails with runtime exception (channel out of range assert). When i’m trying to record for mono, it records nothing at all.

This issue began from my update to 4.3.0 projucer version.

Greetz, Blinkop

Could you please provide some more detail?

  • How are you getting the number of channels?
  • What is the application? Can you reproduce this using any of the JUCE demo code?
  • What operating system?

Thanks for response!

My app is just gui application and it gets audio from mic, do some stuff by AudioProcessor’s objects, writes it to a file and sends it to output (AudioProcessorPlayer). My audio recorder seems like Audio Recorder demo, but i inherited it from AudioProcessor and writes data in processBlock method.

FIrst of all I initialize sharedAudioDeviceManager by runtime request, and it grants me 2 channels of I/O(as I need).
But then troubles begin with AudioFormatWriter::createWiterFor() method. If a pass numberOfChannels argument as 2:

  • 4.3.0: jassert fail
  • prev. version: it’s ok

If I pass numberOFChannels as 1, I’m getting my next trouble in fisrt processBlock() method in processros chain:

  • 4.3.0: AudioSampleBuffer::numOfChannels is 1
  • prev. version: AudioSampleBuffer::numOfChannels is 2

So if I try to get an array of 2nd channel from buffer, it fails with jassert.

I’m using windows 10 and sorry for my bad english :slight_smile:

Is it possible for you to share the important parts your code?

Sure,

Here is device manager initialize:

ScopedPointer<AudioDeviceManager> MainScene::sharedAudioDeviceManager = nullptr;
MainScene::MainScene() : deviceManager(MainScene::getSharedAudioDeviceManager())
						
{
	//...
}
//...
AudioDeviceManager & MainScene::getSharedAudioDeviceManager()
{
	if (sharedAudioDeviceManager == nullptr)
	{
		sharedAudioDeviceManager = new AudioDeviceManager();
		RuntimePermissions::request(RuntimePermissions::recordAudio,
			[](bool wasGranted) {
			int numInputChannels = wasGranted ? 2 : 0;
		        sharedAudioDeviceManager->initialise(numInputChannels, 2, 0, true, String(), 0);
		});
	}

	return *sharedAudioDeviceManager;
}

Here is audio recorder:

RecordingProcessor::RecordingProcessor(AudioThumbnail& TN)
							: thumbnail(TN),
							  backGroundThread("Audio Recording processor"),
							  sampleRate(0),
							  nextSampleNum(0),
							  activeWriter(nullptr)
{
	backGroundThread.startThread();
}

RecordingProcessor::~RecordingProcessor()
{
	stop();
}

void RecordingProcessor::startRecording(const File & file)
{
	stop();

	if (sampleRate > 0)
	{
		file.deleteFile();
		ScopedPointer<FileOutputStream> fileStream(file.createOutputStream());

		if (fileStream != nullptr)
		{
			WavAudioFormat wavFormat;
			AudioFormatWriter* writer = wavFormat.createWriterFor(fileStream, sampleRate, 2, 16, StringPairArray(), 0);//first jassert fail

			if (writer != nullptr)
			{
				fileStream.release();
				threadedWriter = new AudioFormatWriter::ThreadedWriter(writer, backGroundThread, 32768);
				thumbnail.reset(writer->getNumChannels(), writer->getSampleRate());
				nextSampleNum = 0;

				const ScopedLock sl(this->getCallbackLock());
				activeWriter = threadedWriter;
			}
		}
	}
}

void RecordingProcessor::processBlock(AudioBuffer<float>& buffer, MidiBuffer & midiMessages)
{
	if (activeWriter != nullptr)
	{
	        activeWriter->write((const float**)buffer.getArrayOfWritePointers(), buffer.getNumSamples());
		thumbnail.addBlock(nextSampleNum, buffer, 0, buffer.getNumSamples());
		nextSampleNum += buffer.getNumSamples();
	}
}
//...

And this is a part of processBlock method, where I need 2 input channels:

void SampleTransform::processBlock(AudioBuffer<float>& buffer/*numOfChannels is 1 instead of 2*/, MidiBuffer & midiMessages)
{
	float* left = buffer.getWritePointer(0);
	float* right = buffer.getWritePointer(1);//second jassert fail
        //...
}

All failures belongs to 4.3.0 version, but not for early one.

OK, could you give me the file and line numbers of the jasserts which are failing?

I can see that you’re using a ScopedPointer for your fileStream - when you pass a stream to createWriterFor the AudioFormatWriter takes ownership of it and will delete it for you. By using a ScopedPointer you’ll delete it twice which will cause an error later on. I don’t think this is causing the problem you’ve described in this thread though.

Where are you getting your sample rate from? What is it’s value when you create your writer?

Creating a writer with the following code works correctly for me:

const double sampleRate = 44100;
auto file = TemporaryFile().getFile();
FileOutputStream* fileStream (file.createOutputStream());
WavAudioFormat wavFormat;
auto writer = std::unique_ptr<AudioFormatWriter> (wavFormat.createWriterFor (fileStream,
                                                                             sampleRate
                                                                             2,
                                                                             16,
                                                                             StringPairArray(),
                                                                             0));

Thanks for note about ScopedPointer! Alright, I looked at the call stack and this is my corrections:

if (fileStream != nullptr)
	{
		WavAudioFormat wavFormat;
		AudioFormatWriter* writer = wavFormat.createWriterFor(fileStream, sampleRate, 2, 16, StringPairArray(), 0);// does not fail

		if (writer != nullptr)
		{
			fileStream.release();
			threadedWriter = new AudioFormatWriter::ThreadedWriter(writer, backGroundThread, 32768);//real reason
			thumbnail.reset(writer->getNumChannels(), writer->getSampleRate());
			nextSampleNum = 0;

			const ScopedLock sl(this->getCallbackLock());
			activeWriter = threadedWriter;
		}
	}
}

Jassert appears in another thread. ThreadedWriter::Buffer calls for write() method, and last one calls AudioBuffer::copyFrom()
This jassert is from juce_AudioSampleBuffer.h file line 813:

void copyFrom (int destChannel,/*1*/
                   int destStartSample,
                   const Type* source,
                   int numSamples) noexcept
    {
        jassert (isPositiveAndBelow (destChannel, numChannels));
        jassert (destStartSample >= 0 && destStartSample + numSamples <= size);
        jassert (source != nullptr);//this one

        if (numSamples > 0)
        {
            isClear = false;
            FloatVectorOperations::copy (channels [destChannel] + destStartSample, source, numSamples);
        }
    }

Next jassert is from this code:

void SampleTransform::processBlock(AudioBuffer<float>& buffer/*numOfChannels is 1 instead of 2*/, MidiBuffer & midiMessages)
{
	float* left = buffer.getWritePointer(0);
	float* right = buffer.getWritePointer(1);//second jassert fail
        //...
}

And has jassert from juce_AudioSampleBuffer.h file line 233:

Type* getWritePointer (int channelNumber/*1*/) noexcept
    {
        jassert (isPositiveAndBelow (channelNumber, numChannels));//this
        isClear = false;
        return channels [channelNumber];
    }

I’m getting sample rate from at this part of this code( idk how it works by the way :smiley: ):

void RecordingProcessor::prepareToPlay(double sampleRate, int maximumExpectedSamplesPerBlock)
{
	this->sampleRate = getSampleRate();
}

And when i create the writer it still has the 48000 value(default by my sound card).

Am I do something wrong?

OK, now the first jassert you hit is telling you that source is nullptr. This means that the copyFrom method is trying to copy data from somewhere invalid.

Can you work backwards from here to find out where the null data came from?

If you look at the wav file you recorded does it have two channels or audio or one?

Thanks for responce!

I found out a strange thing - in first call buffer.copyFrom() the 3rd argument should be pointer, but in juce_AudioFormatWriter.cpp file the argument is data[i], which one of recorded sample value:

bool write (const float* const* data, int numSamples)
    {
        if (numSamples <= 0 || ! isRunning)
            return true;

        jassert (timeSliceThread.isThreadRunning());
        int start1, size1, start2, size2;
        fifo.prepareToWrite (numSamples, start1, size1, start2, size2);

        if (size1 + size2 < numSamples)
            return false;

        for (int i = buffer.getNumChannels(); --i >= 0;)
        {
            buffer.copyFrom (i, start1, data[i], size1);//this one
            buffer.copyFrom (i, start2, data[i] + size1, size2);
        }

        fifo.finishedWrite (size1 + size2);
        timeSliceThread.notify();
        return true;
    }

So the nullptr value cames from value of the first sample in array.
I think data[i] should be pointer to one of the channel’s buffer?

The wav file has one channel, and i can’t record anything but the silence at all :frowning:

OK, we’re getting closer to the problem.

Do the audio buffers you’re receiving in RecordingProcessor::processBlock have two channels?

Yes data[i] should be a pointer to a channel of audio, but it’s missing. If we can find out why it’s missing then I expect we’ll be able to solve your problem.

Buffer i’am receiving has 1 channel. I’ve tested the early version of projucer with the same code and it brings buffer with 2 channels.

Could you send me the source code for your app?

Sended via private message

Well, it’s much easier to find problems when you can see everything :slight_smile:

Sorry for all the ineffective suggestions. The real culprit was nothing we’ve looked at so far - you need to call enableAllBuses() on the audio processors in an AudioProcessorGraph.

For other people who read this post some example code would be:

#ifndef MAINCOMPONENT_H_INCLUDED
#define MAINCOMPONENT_H_INCLUDED

#include "../JuceLibraryCode/JuceHeader.h"
#include "YourAudioProcessor.h"

class MainContentComponent   : public Component
{
public:
    //==============================================================================
    MainContentComponent()
    {
        setSize (600, 400);

        auto input = new AudioProcessorGraph::AudioGraphIOProcessor (AudioProcessorGraph::AudioGraphIOProcessor::audioInputNode);
        auto yourProcessor = new YourAudioProcessor();
        auto output = new AudioProcessorGraph::AudioGraphIOProcessor (AudioProcessorGraph::AudioGraphIOProcessor::audioOutputNode);

        yourProcessor->enableAllBuses();

        graph.addNode(input, 1);
        graph.addNode(yourProcessor, 2);
        graph.addNode(output, 3);

        for (int channel = 0; channel < 2; ++channel)
        {
            graph.addConnection(1, channel, 2, channel);
            graph.addConnection(2, channel, 3, channel);
        }

        deviceManager.initialise (2, 2, nullptr, true);
        deviceManager.addAudioCallback (&player);
        player.setProcessor (&graph);
    }

    ~MainContentComponent() {}

    void paint (Graphics&) override {}
    void resized() override {}

private:
    //==============================================================================
    AudioProcessorGraph graph;
    AudioDeviceManager deviceManager;
    AudioProcessorPlayer player;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

#endif  // MAINCOMPONENT_H_INCLUDED
1 Like

Thanks a lot! But why it worked without enableAllBuses() on prev. versions?
And so now, while buffers in processBlock methods has 2 channels, It still records silence :frowning: Have you any idea why I have this issue? I’ve tested again new code in 4.3.0 and early version, and have same results:

  • 4.3.0: records silnece
  • Prev.: everything workes well

A breaking change in 4.3.0 was the introduction of the new multibus API, which you’ll also need to adapt your code to. If you only care about stereo-in and stereo-out processors then all you need to do is call the base class constructor in each of your AudioProcessors, as shown in the JUCE demo plug-in:

JuceDemoPluginAudioProcessor::JuceDemoPluginAudioProcessor()
    : AudioProcessor (BusesProperties().withInput  ("Input",  AudioChannelSet::stereo(), true)
                                       .withOutput ("Output", AudioChannelSet::stereo(), true))

Apologies for how long this has taken to sort out! With this change to your RecordingProcessor the following is definitely able to record from the microphone on my machine:

#ifndef MAINCOMPONENT_H_INCLUDED
#define MAINCOMPONENT_H_INCLUDED

#include "../JuceLibraryCode/JuceHeader.h"
#include "RecordingProcessor.h"
#include "AudioProcessorSource.h"

class MainContentComponent   : public Component
{
public:
    //==============================================================================
    MainContentComponent()
    {
        setSize (600, 400);

        player.setProcessor (&graph);

        deviceManager.initialise (2, 2, nullptr, true);
        deviceManager.addAudioCallback (&player);

        auto input  = new AudioProcessorGraph::AudioGraphIOProcessor (AudioProcessorGraph::AudioGraphIOProcessor::audioInputNode);
        auto output = new AudioProcessorGraph::AudioGraphIOProcessor (AudioProcessorGraph::AudioGraphIOProcessor::audioOutputNode);
        auto yourProcessor = new RecordingProcessor();

        auto inputNode     = graph.addNode (input);
        auto outputNode    = graph.addNode (output);
        auto processorNode = graph.addNode (yourProcessor);

        for (int channel = 0; channel < 2; ++channel)
        {
            auto inputConnectionSuccess = graph.addConnection (inputNode->nodeId,     channel,
                                                               processorNode->nodeId, channel);
            jassert (inputConnectionSuccess);
            auto outputConnectionSuccess = graph.addConnection (processorNode->nodeId, channel,
                                                                outputNode->nodeId,    channel);
            jassert (outputConnectionSuccess);
        }
    }

    ~MainContentComponent() {}

    void paint (Graphics&) override {}
    void resized() override {}

private:
    //==============================================================================
    AudioProcessorGraph graph;
    AudioDeviceManager deviceManager;
    AudioProcessorPlayer player;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

#endif  // MAINCOMPONENT_H_INCLUDED
1 Like

Oh my god, much Thanks with all help! Firstly, I’ve tried my code with those corrections, and it didn’t work, but then I did all my connections after setProcessor(), and everything is working now! One more time thanks for discovering of new version! :slight_smile: