Hello, we build an instrument to spacialise sound over 8 output with a bella mini and the expander. We use 5 audio input and build a control panel with 5 channel. Each channel have a Lorlin (6 positions) to chosse an effect and 2 potentiometer to modulate the effect (speed and spread). We use C++ but can't figure how to catch each value of the control panel. We are able to see the value of the first channel (or the last).
Sorry for the not so good english and also for the not so good C++ :')
This is the last version of the code for the control panel

#include <Bela.h>
#include <cmath>
#include <libraries/Scope/Scope.h>

Scope scope;

const int selectPins[] = {0, 1, 2};
const int numChannels = 5;
const int numPositions = 6;

void delayMicroseconds(unsigned int microseconds) {
    struct timespec ts;
    ts.tv_sec = microseconds / 1000000;
    ts.tv_nsec = (microseconds % 1000000) * 1000;
    nanosleep(&ts, NULL);
}

void setupMux(BelaContext *context) {
  for (int i = 0; i < 3; i++) {
    pinMode(context, 0, selectPins[i], OUTPUT);
  }
}

void setMux(BelaContext *context, int channel) {
  for (int i = 0; i < 3; i++) {
    digitalWrite(context, 0, selectPins[i], (channel >> i) & 1);
  }
  delayMicroseconds(1000);
}

bool setup(BelaContext *context, void *userData) {
  setupMux(context);
  scope.setup(3, context->audioSampleRate);
  return true;
}

float readMuxValue(BelaContext *context, int channel, int analogPin) {
  setMux(context, channel);
  return analogRead(context, 0, analogPin);
}

int getLorlinIndex(float lorlinValue) {
  int index = 0;
  float step = 1.0 / numPositions;

  for (int i = 0; i < numPositions; i++) {
    if (lorlinValue >= (i * step) && lorlinValue < ((i + 1) * step)) {
      index = i;
      break;
    }
  }

  return index;
}

void render(BelaContext *context, void *userData) {
  for (unsigned int n = 0; n < context->audioFrames; n++) {
    for (int channel = 0; channel < numChannels; channel++) {
      float lorlinValue = readMuxValue(context, channel, 0);
      int lorlinIndex = getLorlinIndex(lorlinValue);
      float pot1Value = readMuxValue(context, channel, 1);
      float pot2Value = readMuxValue(context, channel, 2);

      if (n % (context->audioFrames / 2) == 0) {
        rt_printf("Channel: %d\n", channel);
        rt_printf("Lorlin position: %d\n", lorlinIndex);
        rt_printf("Pot 1 value: %f\n", pot1Value);
        rt_printf("Pot 2 value: %f\n", pot2Value);
      }
    }
  }
}

void cleanup(BelaContext *context, void *userData) {
}

And this is the effect code, not yet connected to the control panel (full of bug...)

#include <Bela.h>
#include <cmath>
#include <cstdlib>
#include <ctime>

const unsigned int gInputChannels = 5;
const unsigned int gOutputChannels = 8;

unsigned int gCurrentSpeaker[gInputChannels] = {0};
unsigned int gCounter[gInputChannels] = {0};
const unsigned int gThreshold = 44100; // Durée en échantillons pour changer d'enceinte
unsigned int gCurrentThreshold[gInputChannels];

unsigned int gRandomSpeaker[gInputChannels] = {0};
unsigned int gRandomCounter[gInputChannels] = {0};
//const unsigned int gRandomThreshold = 22050; // Durée en échantillons pour changer d'enceinte aléatoirement


enum Function {
    ROTATE,
    ROTATE_REVERSE,
    RANDOM,
    ALL,
    PENDULUM,
    LEFT_RIGHT
};

// Choisir la fonction pour chaque entrée ici
Function gFunctions[gInputChannels] = {
    ALL,										// entrée 1
    ALL,										// entrée 2
    ROTATE,										// entrée 3
    RANDOM,										// entrée 4
    ALL											// entrée 5
};

bool gPendulumDirection[gInputChannels] = {false};

float fadeFunction(float distance) {
    float exponent = -1.0f; // Plus la valeur est négative, plus le fondu sera progressif
    return std::pow(distance, exponent);
}

void nextSpeaker(unsigned int inputChannel) {
    if (gFunctions[inputChannel] == ROTATE) {
        gCurrentSpeaker[inputChannel] = (gCurrentSpeaker[inputChannel] + 1) % gOutputChannels;
    } else if (gFunctions[inputChannel] == ROTATE_REVERSE) {
        gCurrentSpeaker[inputChannel] = (gCurrentSpeaker[inputChannel] - 1 + gOutputChannels) % gOutputChannels;
    } else if (gFunctions[inputChannel] == PENDULUM) {
        if (gPendulumDirection[inputChannel]) {
            gCurrentSpeaker[inputChannel]--;
        } else {
            gCurrentSpeaker[inputChannel]++;
        }

        if (gCurrentSpeaker[inputChannel] == 0 || gCurrentSpeaker[inputChannel] == gOutputChannels - 1) {
            gPendulumDirection[inputChannel] = !gPendulumDirection[inputChannel];
        }
    } else if (gFunctions[inputChannel] == LEFT_RIGHT) {
        gCurrentSpeaker[inputChannel]++;
        gCurrentSpeaker[inputChannel] %= (gOutputChannels / 2);
    }
}

bool setup(BelaContext *context, void *userData) {
    if (context->audioInChannels < gInputChannels || context->audioOutChannels < gOutputChannels) {
        rt_printf("Pas assez de canaux d'entrée ou de sortie\n");
        return false;
    }

    std::srand(std::time(0));

    for (unsigned int inputChannel = 0; inputChannel < gInputChannels; ++inputChannel) {
        gCurrentThreshold[inputChannel] = gThreshold;
    }

    return true;
}

// Modifier la fonction processInput() pour appliquer le seuil de gain minimum

void processInput(float in, float &out, unsigned int output, unsigned int inputChannel) {
    float distance = fmin(std::abs(static_cast<int>(output) - static_cast<int>(gCurrentSpeaker[inputChannel])), gOutputChannels - std::abs(static_cast<int>(output) - static_cast<int>(gCurrentSpeaker[inputChannel])));
    //float gain = fadeFunction(distance);

    switch (gFunctions[inputChannel]) {
        case ROTATE:
        case ROTATE_REVERSE:
        case PENDULUM:
            if (distance == 0) {
                out += in;
            }
            break;
        case RANDOM:
            if (output == gRandomSpeaker[inputChannel]) {
                out += in;
            }
            break;
        case ALL:
            out += in;
            break;
        case LEFT_RIGHT:
            if (output == gCurrentSpeaker[inputChannel] || output == (gOutputChannels - 1 - gCurrentSpeaker[inputChannel])) {
                out += in;
            }
            break;
    }
}

void render(BelaContext *context, void *userData) {
    // Boucle sur tous les échantillons audio dans le buffer
    for (unsigned int n = 0; n < context->audioFrames; ++n) {

        // Lire les valeurs d'entrée pour chaque canal d'entrée
        float input[gInputChannels];
        for (unsigned int inputChannel = 0; inputChannel < gInputChannels; ++inputChannel) {
            input[inputChannel] = audioRead(context, n, inputChannel);
        }

        // Boucle sur tous les canaux de sortie (enceintes)
        for (unsigned int output = 0; output < gOutputChannels; ++output) {
            float out = 0.0f;

            // Boucle sur tous les canaux d'entrée et appliquer les effets
            for (unsigned int inputChannel = 0; inputChannel < gInputChannels; ++inputChannel) {
                processInput(input[inputChannel], out, output, inputChannel);
            }

            // Écrire la sortie résultante pour le canal de sortie (enceinte) actuel
            audioWrite(context, n, output, out);
        }

        // Boucle sur tous les effets
        for (Function effect = ROTATE; effect <= LEFT_RIGHT; effect = static_cast<Function>(static_cast<int>(effect) + 1)) {
            // Boucle sur tous les canaux d'entrée
            for (unsigned int inputChannel = 0; inputChannel < gInputChannels; ++inputChannel) {
                // Si l'effet actuel ne correspond pas à la fonction assignée au canal d'entrée, passez au canal d'entrée suivant
                if (gFunctions[inputChannel] != effect) continue;

                // Incrémenter le compteur pour le canal d'entrée actuel
                gCounter[inputChannel]++;
                // Si le compteur atteint le seuil pour le canal d'entrée actuel
                if (gCounter[inputChannel] >= gCurrentThreshold[inputChannel]) {
                    // Réinitialiser le compteur
                    gCounter[inputChannel] = 0;

                    // Mettre à jour les positions des haut-parleurs en fonction de l'effet actuel
                    switch (effect) {
                        case ROTATE:
                            gCurrentSpeaker[inputChannel]++;
                            gCurrentSpeaker[inputChannel] %= gOutputChannels;
                            break;
                        case ROTATE_REVERSE:
                            gCurrentSpeaker[inputChannel]--;
                            gCurrentSpeaker[inputChannel] = (gCurrentSpeaker[inputChannel] + gOutputChannels) % gOutputChannels;
                            break;
                        case RANDOM:
                            gRandomSpeaker[inputChannel] = std::rand() % gOutputChannels;
                            break;
                        case PENDULUM:
                            if (gPendulumDirection[inputChannel]) {
                                gCurrentSpeaker[inputChannel]++;
                            } else {
                                gCurrentSpeaker[inputChannel]--;
                            }
                            gCurrentSpeaker[inputChannel] = (gCurrentSpeaker[inputChannel] + gOutputChannels) % gOutputChannels;

                            if (gCurrentSpeaker[inputChannel] == 0 || gCurrentSpeaker[inputChannel] == gOutputChannels - 1) {
                                gPendulumDirection[inputChannel] = !gPendulumDirection[inputChannel];
                            }
                            break;
                        case LEFT_RIGHT:
                            gCurrentSpeaker[inputChannel]++;
                            gCurrentSpeaker[inputChannel] %= gOutputChannels / 2;
                            break;
                        default:
                            break;
                    }
                }
            }
        }
    }
}


void cleanup(BelaContext *context, void *userData) {
    // Nettoyage du code ici
}

Thanks for your help.

alt text

You are sleeping in the audio callback. Besides causing dropouts, that will also not do what you want. The analog inputs and digital output buffers are read and written, respectively, to memory before and after, respectively, the execution of the render() function. This means that no matter how long you wait within the audio callback, the digital output will not be written to the output pin before render() terminates and, most importantly, the voltage at the analog input pin has already been sampled before render() was even called. As it's common in real-time systems with block-based processing, the roundtrip latency, i.e.: the latency for an output to show up at the input, is at least two blocks. That is, what you should do is write your digital in one block and then read the analog inputs two blocks and two frames later. The extra two frames are because you need to wait for the digital out to propagate to the multiplexer before you can read it back

    Thanks for your answer. I manage to make the control pannel work with this code. It only give me a warning message in the console that is printing to the console to quickly, but I probably don't print anything later. Don't if it's ok like that... Also I think it don't work before cause I plug the E pin of the mux on GND and know, I plug it on digital in 3. Don't know if i can write solved for this post. I also probably need some help with the second part of my code, the effect. I find some code exemple with panning for the bela mini expander. It use an LFO to move the sound. I continue to work on it and propably post some things.

    #include <Bela.h>
    #include <math.h>
    
    const int s0Pin = 0;
    const int s1Pin = 1;
    const int s2Pin = 2;
    const int enablePin = 3;
    
    const int analogInPins[] = {0, 1, 2};
    
    const int lorlinThresholds[] = {122, 367, 609, 854, 1000}; // Updated thresholds for Lorlin positions
    
    void selectChannel(BelaContext *context, int channel) {
      digitalWrite(context, 0, enablePin, LOW);
      digitalWrite(context, 0, s0Pin, (channel & 0b001) ? HIGH : LOW);
      digitalWrite(context, 0, s1Pin, (channel & 0b010) ? HIGH : LOW);
      digitalWrite(context, 0, s2Pin, (channel & 0b100) ? HIGH : LOW);
    }
    
    int getLorlinPosition(int analogValue) {
      for (int i = 0; i < sizeof(lorlinThresholds) / sizeof(lorlinThresholds[0]); ++i) {
        if (analogValue < lorlinThresholds[i]) {
          return i;
        }
      }
      return sizeof(lorlinThresholds) / sizeof(lorlinThresholds[0]);
    }
    
    bool setup(BelaContext *context, void *userData)
    {
      pinMode(context, 0, s0Pin, OUTPUT);
      pinMode(context, 0, s1Pin, OUTPUT);
      pinMode(context, 0, s2Pin, OUTPUT);
      pinMode(context, 0, enablePin, OUTPUT);
    
      digitalWrite(context, 0, enablePin, LOW);
    
      return true;
    }
    
    void render(BelaContext *context, void *userData)
    {
      static int channel = 0;
    
      if(context->multiplexerStartingChannel == 0) {
        selectChannel(context, channel);
    
        rt_printf("Channel %d : ", channel);
    
        for (int mux = 0; mux < 3; ++mux) {
          int analogValue = analogRead(context, 0, analogInPins[mux]) * 1023;
          int potValue = static_cast<int>(analogValue * 100.0 / 1023.0);
          if (mux == 0) {
            int lorlinPosition = getLorlinPosition(analogValue);
            rt_printf("Lorlin position: %d | ", lorlinPosition);
          } else {
            rt_printf("Pot %d: %d%% | ", mux, potValue);
          }
        }
    
        rt_printf("\n");
        channel = (channel + 1) % 5;
      }
    }
    
    void cleanup(BelaContext *context, void *userData)
    {
    
    }

    giuliomoro
    I manage to read all the Rotary switch and pot with 3 mux (4051). But I'm getting more and more lost in my code. I try to make a 5 independant input with 8 output to spatialise the sound of each input independently. I try to use the circulare-panning example, but know I'm lost. With the rotary switch, I choose a function for the input (clockwise, counterClockwise, random, all output, +++). With the first potentiometer, I try to control the speed of panning.
    I have a lot of "crack" and "pop", probably to much volume when everything come together on the output. Don't know where to start... to clean, organise. Thanks for your help.

    #include <Bela.h>
    #include <cmath>
    #include <random>
    #include <libraries/Oscillator/Oscillator.h>
    
    Oscillator lfos[5]; // Un LFO pour chaque entrée
    
    float gLfoFreqRange[2] = {0.01, 1}; // LFO frequency range
    
    unsigned int gCurrentChannel[5] = {0, 0, 0, 0, 0}; // Channel actif pour chaque entrée
    float gPrevPanning[5] = {0.0, 0.0, 0.0, 0.0, 0.0}; // Valeur de panning précédente pour chaque entrée
    int gChannelFunction[5] = {0, 0, 0, 0, 0}; // Choix de la fonction pour chaque entrée (0 = horaire, 1 = antihoraire, 2 = aléatoire, 3 = all)
    
    unsigned int gChannelUpdateCounter[5] = {0, 0, 0, 0, 0};
    
    int gNextChannelUpdateCounter[5] = {0, 0, 0, 0, 0};
    
    int gRandomUpdateInterval = 1000; //stockage de la valeur vitesse random
    
    const int s0Pin = 0;
    const int s1Pin = 1;
    const int s2Pin = 2;
    const int enablePin = 3;
    
    const int analogInPins[] = {0, 1, 2};
    
    const int lorlinThresholds[] = {122, 367, 609, 854, 1000}; // Updated thresholds for Lorlin positions
    
    
    
    
    
    static unsigned int frameCounter = 0;
    
    // Paramètres du compresseur
    float threshold = 0.5f; // Seuil de compression (entre 0 et 1)
    float ratio = 1.0f; // Ratio de compression (supérieur à 1)
    
    std::random_device rd; // générateur de nombres aléatoires
    std::mt19937 rng(rd()); // initialisation de l'aléatoire
    
    void selectChannel(BelaContext *context, int channel)
    {
      digitalWrite(context, 0, enablePin, LOW);
      digitalWrite(context, 0, s0Pin, (channel & 0b001) ? HIGH : LOW);
      digitalWrite(context, 0, s1Pin, (channel & 0b010) ? HIGH : LOW);
      digitalWrite(context, 0, s2Pin, (channel & 0b100) ? HIGH : LOW);
    }
    
    int getLorlinPosition(int analogValue)
    {
      for (int i = 0; i < sizeof(lorlinThresholds) / sizeof(lorlinThresholds[0]); ++i)
      {
        if (analogValue < lorlinThresholds[i])
        {
          return i;
        }
      }
      return sizeof(lorlinThresholds) / sizeof(lorlinThresholds[0]);
    }
    
    int getNextChannelClockwise(int channel, int numChannels)
    {
      int nextChannel = channel;
      // Increment
      nextChannel++;
      // Wrap
      if (nextChannel >= numChannels)
      nextChannel = 0;
      return nextChannel;
    }
    
    int getNextChannelCounterwise(int channel, int numChannels)
    {
      int nextChannel = channel;
      // Decrement
      nextChannel--;
      // Wrap
      if (nextChannel < 0)
      nextChannel = numChannels - 1;
      return nextChannel;
    }
    
    int getNextChannelRandom(int channel, int numChannels)
    {
      int nextChannel = channel;
      std::uniform_int_distribution<int> dist(0, numChannels - 1);
      
      while (nextChannel == channel) // S'assurer que le canal suivant est différent du canal actuel
      {
        nextChannel = dist(rng); // Choisissez un canal aléatoire entre 0 et numChannels-1
      }
      return nextChannel;
    }
    
    int getNextChannelAllOutputs(int channel, int numChannels)
    {
      return channel; // retourne simplement le canal d'entrée, car le son doit être diffusé sur toutes les sorties
    }
    
    
    float crossfade(float value, float fadeIn, float fadeOut)
    {
      return value * fadeIn * fadeOut;
    }
    
    bool setup(BelaContext *context, void *userData)
    {
      
      
      
      
      for (int i = 0; i < 5; ++i)
      {
        lfos[i].setup(context->audioSampleRate);
        lfos[i].setFrequency((i + 1) * 0.25);
      }
      pinMode(context, 0, enablePin, OUTPUT);
      digitalWrite(context, 0, enablePin, HIGH);
      pinMode(context, 0, s0Pin, OUTPUT);
      pinMode(context, 0, s1Pin, OUTPUT);
      pinMode(context, 0, s2Pin, OUTPUT);
      return true;
    }
    
    void render(BelaContext *context, void *userData)
    {
      for (unsigned int i = 0; i < 5; i++)
      {
        gNextChannelUpdateCounter[i]++;
        // float lfoPeriod = context->audioSampleRate / lfos[i].getFrequency();
        if (gChannelFunction[i] == 0) // horaire
        {
          if (gChannelUpdateCounter[i] % gRandomUpdateInterval == 0) {
            gCurrentChannel[i] = getNextChannelClockwise(gCurrentChannel[i], context->audioOutChannels);
            gChannelUpdateCounter[i] = 0;
          }
        }
        else if (gChannelFunction[i] == 1) // antihoraire
        {
          if (gChannelUpdateCounter[i] % gRandomUpdateInterval == 0) {
            gCurrentChannel[i] = getNextChannelCounterwise(gCurrentChannel[i], context->audioOutChannels);
            gChannelUpdateCounter[i] = 0;
          }
        }
      }
      
      static int channel = 0;
      if (context->multiplexerStartingChannel == 0)
      {
        selectChannel(context, channel);
        for (int mux = 0; mux < 3; ++mux)
        {
          int analogValue = analogRead(context, 0, analogInPins[mux]) * 1023;
          if (mux == 0)
          {
            int lorlinPosition = getLorlinPosition(analogValue);
            gChannelFunction[channel] = lorlinPosition;
          }
          else if (mux == 1)
          {
            float normalizedAnalogValue = (float)analogValue / 1023.0;
            float lfoFreq = normalizedAnalogValue * (gLfoFreqRange[1] - gLfoFreqRange[0]) + gLfoFreqRange[0];
            lfos[channel].setFrequency(lfoFreq);
            if (frameCounter % 1000 == 0) {
              rt_printf("Analog value: %d, Normalized value: %f, LFO frequency: %f\n", analogValue, normalizedAnalogValue, lfoFreq);
            }
            if (gChannelFunction[channel] == 2) // Lorsque la fonction aléatoire est sélectionnée
            {
              gRandomUpdateInterval = map(analogValue, 0, 1023, 500, 5000); // Ajustez les valeurs minimale et maximale selon vos besoins
            }
            
          }
        }
        rt_printf("\n");
        channel = (channel + 1) % 5;
      }
      
      // Paramètres du compresseur
      float threshold = 0.9f; // Seuil de compression (entre 0 et 1)
      float ratio = 1.0f; // Ratio de compression (supérieur à 1)
      float lfoSmoothFactor = 0.995f; // Ajustez cette valeur pour un lissage plus ou moins important (entre 0 et 1, proche de 1 pour un lissage plus fort)
      for (unsigned int n = 0; n < context->audioFrames; n++)
      {
        float in[5];
        for (unsigned int i = 0; i < 5; i++)
        {
          in[i] = audioRead(context, n, i);
        }
        for (unsigned int ch = 0; ch < context->audioOutChannels; ch++)
        {
          float out[8] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
          for (unsigned int i = 0; i < 5; i++)
          {
            
            float lfoValue = lfos[i].process();
            gPrevPanning[i] = gPrevPanning[i] * lfoSmoothFactor + lfoValue * (1.0f - lfoSmoothFactor);
            float panning = gPrevPanning[i] * 0.5f + 0.5f; // Valeur entre 0 et 1
            
            int nextChannel = 0;
            
            switch (gChannelFunction[i])
            
            {
              case 0: // horaire
              nextChannel = getNextChannelClockwise(gCurrentChannel[i], context->audioOutChannels);
              
              if (frameCounter % 1000 == 0) {
                //rt_printf("Next Channel : %d | ", nextChannel); // Affiche le canal suivant aléatoire
                //	rt_printf("\n");
              }
              break;
              case 1: // antihoraire
              nextChannel = getNextChannelCounterwise(gCurrentChannel[i], context->audioOutChannels);
              
              break;
              case 2: // aléatoire
              if (frameCounter % gRandomUpdateInterval == 0) {
                nextChannel = getNextChannelRandom(gCurrentChannel[i], context->audioOutChannels);
                
              } else {
                nextChannel = gCurrentChannel[i];
              }
              
              break;
              case 3: // all
              nextChannel = getNextChannelAllOutputs(gCurrentChannel[i], context->audioOutChannels);
              break;
            }
            
            // FADE IN FADE OUT
            float fadeIn = (gChannelFunction[i] == 3) ? 1.0f : panning;
            float fadeOut = (gChannelFunction[i] == 3) ? 1.0f : 1.0f - panning;
            
            if (ch == gCurrentChannel[i])
            {
              out[ch] += crossfade(in[i], fadeIn, fadeOut);
            }
            else if (ch == nextChannel)
            {
              out[ch] += crossfade(in[i], fadeIn, fadeOut);
            }
            
            
            // sorties
            float chPanning = 0.0;
            gCurrentChannel[i] = nextChannel;
            
            if (gChannelFunction[i] == 3) // all
            {
              chPanning = 1.f - panning;
              for (int j = 0; j < context->audioOutChannels; ++j)
              {
                out[j] += in[i] * chPanning;
              }
              out[ch] *= 0.5;
            }
            else
            {
              if (ch == gCurrentChannel[i])
              {
                chPanning = 1.f - panning;
              }
              else if (ch == nextChannel)
              {
                chPanning = panning;
              }
              else
              {
                chPanning = 0.0;
              }
              
              float signal = in[i] * chPanning;
              
              // Appliquer la compression si le signal dépasse le seuil
              if (fabs(signal) > threshold)
              {
                float gainReduction = (fabs(signal) - threshold) / ratio;
                signal = (signal > 0 ? 1 : -1) * (fabs(signal) - gainReduction);
              }
              out[ch] += signal;
            }
          }
          
          out[ch] *= 0.1; // Réduire le niveau du signal de 20%
          audioWrite(context, n, ch, out[ch]);
        }
      }
      frameCounter++;
    }
    
    void cleanup(BelaContext *context, void *userData)
    {
    }