Migration from Superpowered

Hi everybody,

I just discovered JUCE and really like the audio performance! I used to use the Superpowered SDK, but some pops and clicks led me to look for another solution. I would like to do a multi-track player based on URI tracks. What I doing now is:

MixerAudioSource mixer;

auto *bateria = new MemoryInputStream(
    BinaryData::novemberrainbateria_mp3, 
    BinaryData::novemberrainbateria_mp3Size, false
);
auto *bateriaReader = formatManager.createReaderFor(bateria);
auto *bateriaSource = new AudioFormatReaderSource(bateriaReader, true);
mixer.addInputSource(bateriaSource, true);

auto *vocal = new MemoryInputStream(
    BinaryData::novemberrainvocal_mp3, 
    BinaryData::novemberrainvocal_mp3Size, false
);
auto *vocalReader = formatManager.createReaderFor(vocal);
auto *vocalSource = new AudioFormatReaderSource(vocalReader, true);
mixer.addInputSource(vocalSource, true);

The problem in this code is that I need to download all the tracks and put them into the project as binary data. On Superpowered, I can open an audio player with the URI, but I don’t know how to do the same with JUCE. I would appreciate if someone helps me with that :slightly_smiling_face:

UPDATE:
In the same line of what I already did, I tried this:

  auto *metronome = new URL("https://sourcesite.com/track.mp3");
  auto *metronomeStream = metronome->createInputStream(false);
  auto *metronomeReader = formatManager.createReaderFor(metronomeStream);
  auto *metronomeSource = new AudioFormatReaderSource(metronomeReader, true);
  mixer.addInputSource(metronomeSource, true);

All the code that I showed is written on a function called on pressing the play button. But then, I fall on “Annoyingly, the android HTTP functions will choke on this call if you try to do it on the message thread. You’ll need to move your networking code to a background thread to keep it happy…”.
Am I in the correct way? How to do that?

Have a look at the JUCE Thread class.

Create a class that inherits from it and override the run method. Stick your networking code on there. And then call e.g. myThreadClassInstance.startThread() when you need to grab the file from there server. If you need to wait for it to complete before proceeding call waitForThreadToExit(), otherwise you’ll need to implement some kind of async messaging to notify when the file’s been grabbed.

2 Likes

Awesome! This worked well on Linux, thank you.

Now I have to portable this to Android devices and when I try to execute my app crashes. Debugging it, that is what I see right before the crash:

There is a problem with JNI… some idea?

How are you passing the reference to the file from the network thread to your audio thread? I’d suggest doing some checks to see if you are appropiately receiving it

Have a look on my code:

#pragma once

class MainContentComponent : public AudioAppComponent, Thread {
public:
  MainContentComponent() : Thread("Background sound download") {
    addAndMakeVisible(&playButton);
    playButton.setButtonText("Play");
    playButton.onClick = [this] { playButtonClicked(); };
    playButton.setColour(TextButton::buttonColourId, Colours::green);

    setSize(300, 200);

    formatManager.registerFormat(new MP3AudioFormat(), true);

    setAudioChannels(0, 2);
  }

  ~MainContentComponent() override {
    shutdownAudio();
  }

  void prepareToPlay(int samplesPerBlockExpected, double sampleRate) override {
    mixerAudioSource.prepareToPlay(samplesPerBlockExpected, sampleRate);
  }

  void getNextAudioBlock(const AudioSourceChannelInfo &bufferToFill) override {
    mixerAudioSource.getNextAudioBlock(bufferToFill);
  }

  void releaseResources() override {
    mixerAudioSource.releaseResources();
  }

  void resized() override {
    playButton.setBounds(10, 10, getWidth() - 20, 20);
  }

private:
  void playButtonClicked() {
    startThread();
  }

  void run() override {
    auto *metronome = new URL("https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3");
    auto *metronomeStream = metronome->createInputStream(false);
    auto *metronomeReader = formatManager.createReaderFor(metronomeStream);
    auto *metronomeSource = new AudioFormatReaderSource(metronomeReader, true);
    mixerAudioSource.addInputSource(metronomeSource, true);
  }

  TextButton playButton;

  AudioFormatManager formatManager;
  MixerAudioSource mixerAudioSource;

  JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

I just create a separate threat inside your MainContentComponent (either on startup and leave it running but not spinning, either spawning it on buttonclick like adamski said) instead of inheriting the it into a Thread.

What I do:

  1. Create a separate class YourThreadClass inheriting from JUCE’s Thread with all the thread methods, and instantiate it in your MainContentComponent
  2. Put all the audio downloading code there in your thread, and load it into an AudioBuffer or a simple 2D array doing the memory allocation there (not in your audio thread/main function)
  3. Pass that AudioBuffer/Array reference from the download thread to your audio thread to use it

Basically most of those steps are already explained and coded here, you can even reuse that code to safely load and pass the pointer to that buffer to your audio thread.

2 Likes

So, your help guys led me to study and learn so much, thank you!

I did the Tutorial: Looping audio using the AudioSampleBuffer class (advanced) as @johngalt91 suggested and Tutorial: Looping audio using the AudioSampleBuffer class to know why I’m doing safe thread. Then I tried to put all the audio downloading code in my audio download thread, but although this works, it’s not as expected: the song is speeded. I created a GitHub repo to let a history of that and I would appreciate if someone read it and help me with that.

The last commit when I wrote this, is 720ea7f for those in the future who want to know.

If the song is speeded in most cases it’s because of a mismatch between your playing audio rate and the file audio rate. If the file is sampled, let’s say, at 96Khz that means that you must read 96000 samples in order to play 1 second of audio. If your app/device is configured with, let’s say, sample rate of 48Khz, that means each second of audio will be reading and adding 48000 samples in the buffer. So in this example you will actually be reading half of a second instead of 1 second of your file each time a second passes in your audio thread, thus playing it at half the speed.

In your case the file is probably at 48Khz while your program/system is at 44,1Khz (or viceversa). If that’s actually your problem, you need to first see which SR does the downloaded file have, and then resample it to match your SR, take a look at this

Doesn’t this mismatch between the rates only when I’m using the TransportSource? I mean, the code of getNextAudioBlock is doing the output by itself as below:

void MainComponent::getNextAudioBlock(const AudioSourceChannelInfo &bufferToFill) {
    ReferenceCountedBuffer::Ptr retainedCurrentBuffer(audioDownload.currentBuffer);

    if (retainedCurrentBuffer == nullptr) {
        bufferToFill.clearActiveBufferRegion();
        return;
    }

    auto *currentAudioSampleBuffer = retainedCurrentBuffer->getAudioSampleBuffer();
    auto position = retainedCurrentBuffer->position;

    auto numInputChannels = currentAudioSampleBuffer->getNumChannels();
    auto numOutputChannels = bufferToFill.buffer->getNumChannels();

    auto outputSamplesRemaining = bufferToFill.numSamples;
    auto outputSamplesOffset = 0;

    while (outputSamplesRemaining > 0) {
        auto bufferSamplesRemaining = currentAudioSampleBuffer->getNumSamples() - position;
        auto samplesThisTime = jmin(outputSamplesRemaining, bufferSamplesRemaining);

        for (auto channel = 0; channel < numOutputChannels; ++channel) {
            bufferToFill.buffer->copyFrom(channel,
                                          bufferToFill.startSample + outputSamplesOffset,
                                          *currentAudioSampleBuffer,
                                          channel % numInputChannels,
                                          position,
                                          samplesThisTime);
        }

        outputSamplesRemaining -= samplesThisTime;
        outputSamplesOffset += samplesThisTime;
        position += samplesThisTime;

        if (position == currentAudioSampleBuffer->getNumSamples())
            position = 0;
    }

    retainedCurrentBuffer->position = position;
}

I haven’t used TransportSource but the sample rate determines how fast the audio procesing calls happen. If you have a SR of 48Khz, that’s 48000 samples to be processed in 1 second. If your buffer size is let’s say 128, you have 48000/128 = 375 calls per second.

So getNextAudioBlock is just doing the same task no matter the sample rate, the difference is that with a 48Khz it’s called 375 times, but if your SR was 96KHz it would be called 187.5 times per second
If the file you download is 1 second long, and has a sampling rate of 96Khz, that means you will need 2 seconds in order to play a file that is 1 second long. It’s not your getNextAudioBlock problem but your configuration problem (or more accuartely, the problem is the file you download not having the same SR that your app is running). If you resample each file you download to match your SR, the playback will be at the same speed

Take a look at the audio device manager tutorial to get a grasp on how to change it, even tho you can do it manually