Polyphonic synth with ofSoundPlayer: voice stealing?

I’m making a music app on iOS called MelodyMorph

It lets you make musical instruments by dropping objects onto the screen that play notes when you tap them. There are several instrument timbres, all polyphonic, that play different pitches by time-stretching sampled sounds. Some instruments are “multisampled,” meaning they use different sound samples for each pitch range, to minimize timbre distortion. So of course I’m using ofSoundPlayer, with multiplay. this is all in OF 0071 for iOS.

My question is: how does ofSoundPlayer in multiplay mode deal with a situation where you play a large number of notes? can I control the number of voices, and the voice stealing behavior?

Ideally, with “voice stealing,” the oldest notes that are playing would be stopped when new notes arrive, and those voices re-used for the new notes. It seems to not be doing this: instead, when you play a lot of notes really fast, the newest ones just don’t play at all, and no new notes play for a a couple of seconds. Here’s the code for my multisampled sound player wrapper class: gist

Can I hack the ofsoundplayer code (in ofOpenALSoundPlayer.cpp I think) to increase the number of voices, or change the voice stealing behavior? Should I (sigh) try to upgrade everything in my big gangly app to a new of version that improves this? Should I be using some other sound player?

thanks!

1 Like

The maximum amount of openAL sources is 32. I think openframeworks creates a source for each channel so you can only play 16 samples at one time for stereo.
Your Voice Stealing code can be done with a queue: http://www.cplusplus.com/reference/queue/queue/
Right before the user presses a key you want something like this.

const int maxVoices = 8;   
std::queue<ofSoundPlayer*> liveSamples;
ofSoundPlayer newsample;
   
    if (liveSamples.size() == maxVoices){
        ofSoundPlayer *oldestSample = liveSamples.front();
        oldestSample->stop();
        liveSamples.pop();
    }
    liveSamples.push(&newSample);
    newSample.play();

By the way I do not recommend using openAl for instruments. There is too much lag. You should consider ofxMaximilian

Edt: oops ,I forgot about note_off events. Where you have to remove samples in the middle of the queue. You should just use vectors , with erase and push_back functions

1 Like

thanks for the thoughtful response!

re: lag, I’ve been using openAL so far in the app and the latency feels good enough… actually I just measured it at about 75ms (not sure how accurate that is- I just recorded audio from my laptop mic, tapping the ipad so you can both hear my finger hit the screen and the sound playback). anyway, lower latency could be even better, especially if I start using percussion sounds.

ofxMaximilian looks pretty amazing! I got an example running on the ipad. It can do the timestretching thing, so I should be able to make my instruments in a similar way with it. it has a lot of other crazy cool features too. the documentation isn’t exactly there but the examples are helpful.

I’m trying to wrap my head around how I would use it to make a voice stealing system… basically, each instrument has a vector of maxiSample objects, so each one is a voice. to start each new note, I call trigger on a maxiSample that is not currently playing (i.e. if the position is less than the length). Then I combine all voices into a mixer. should work, right?

one complication is the multisampling thing. can maxisamples change which audio sample they’re pointing to (i.e. without reloading from a file)? otherwise I’ll need separate vectors of voices for each pitch range, which seems awkward.

1 Like

You are not limited by voices anymore. here is an example of a sampler with just four sounds. change the wave files to whatever you want. You can play sounds by pressing keys ‘a’ ‘s’ ‘d’ ‘f’

#include "testApp.h"
#include "ofxMaxim.h"

struct Key {
    bool noteOn;
    shared_ptr<ofxMaxiSample> sample;
    char triggerLetter;
    maxiEnv env;
    Key(shared_ptr<ofxMaxiSample> _sample,char _triggerLetter){
        noteOn = false;
        sample = _sample;
        triggerLetter = _triggerLetter;
    }
};

const double attack = .01;
const double release = .9999;
const long holdTime = 1;
vector<Key> keys;

//--------------------------------------------------------------
void testApp::setup(){
    ofxMaxiSettings::setup(44100, 2, 512);
    ofSoundStreamSetup(2, 0, 44100, 512, 4);
    
    shared_ptr<ofxMaxiSample> pianoE(new ofxMaxiSample);
    pianoE->load(ofToDataPath("pianoe.wav"),1);
    shared_ptr<ofxMaxiSample> pianoB(new ofxMaxiSample);
    pianoB->load(ofToDataPath("pianob.wav"),1);
    shared_ptr<ofxMaxiSample> pianoG(new ofxMaxiSample);
    pianoG->load(ofToDataPath("pianog.wav"),1);
    shared_ptr<ofxMaxiSample> pianoF(new ofxMaxiSample);
    pianoF->load(ofToDataPath("pianof.wav"),1);
    
    keys.push_back(Key(pianoE,'a'));
    keys.push_back(Key(pianoB,'s'));
    keys.push_back(Key(pianoF,'d'));
    keys.push_back(Key(pianoG,'f'));
                 
    
}
//--------------------------------------------------------------
void testApp::keyPressed(int key){
    for (auto it = keys.begin(); it!=keys.end(); it++) {
        if (it->triggerLetter == key && it->noteOn == false) {
            it->noteOn = true;
            it->sample->trigger();
            
        }
    }
}

//--------------------------------------------------------------
void testApp::keyReleased(int key){
    for (auto it = keys.begin(); it!=keys.end(); it++) {
        if (it->triggerLetter == key) {
            it->noteOn = false;
        }
    }
}
void testApp::audioOut(float *output, int bufferSize, int nChannnels){
    for (auto it = keys.begin(); it!=keys.end(); it++) {
        for (int i = 0; i < bufferSize; i++) {
            float value = it->env.ar(it->sample->playOnce(),attack,release,holdTime,it->noteOn);
            output[2*i]+=value;
            output[2*i+1]+=value;
        }
    }
}

I actually ran into a bug with ofxMaxim. I posted the issue here: https://github.com/micknoise/Maximilian/issues/22 The fix is to remove the +6.