/*----------------------------------------------------------------*/
/*                                                                */
/*                        AUDIO METHODS                           */
/*                                                                */
/*----------------------------------------------------------------*/

/**
 * CALL THIS METHOD IN SETUP()
 * Initializes Minim audio library and audio variables.
 */
public void initAudio() {
  minim = new Minim(this);
  // Use the getLineOut method of the Minim object to get an AudioOutput object.
  // PixelAudio instruments require a STEREO output. 1024 is a standard number
  // of samples for the output buffer to process at one time. You should usually
  // set the output sampleRate to either 41500 or 48000, standards for digital
  // audio recordings.
  this.audioOut = minim.getLineOut(Minim.STEREO, 1024, sampleRate);
  // set the output level in dB
  audioOut.setGain(audioGain);
  // create a Minim MultiChannelBuffer with one channel, buffer size equal to mapSize
  // playBuffer will not contain audio data until we load a file
  this.playBuffer = new MultiChannelBuffer(mapSize, 1);
  this.audioSignal = playBuffer.getChannel(0);
  this.audioLength = audioSignal.length;
  // ADSR envelope with maximum amplitude, attack Time, decay time, sustain level, and release time
  defaultEnv = new ADSRParams(maxAmplitude, attackTime, decayTime, sustainLevel, releaseTime);
  initADSRList();
  // create a PASamplerInstrument
  ensureSamplerReady();
  // initialize mouse event tracking array
  timeLocsArray = new ArrayList<TimedLocation>();
}

/**
 * Prepares Sampler instruments and assets
 */
void ensureSamplerReady() {
  if (synth == null) {
    synth = new PASamplerInstrument(playBuffer, audioOut.sampleRate(), 8, audioOut, defaultEnv);
    println("-- initilialized audio sampler synth");
    // set the synth gain with a linear value derived from a dB value
    synth.setGain(AudioUtility.dbToLinear(samplerGain));
  }
}

// a list of ADSR envelopes. Feel free to edit and add more.
public void initADSRList() {
  adsrList = new ArrayList<ADSRParams>();
  ADSRParams env0 = new ADSRParams(
    0.7f, // maxAmp
    0.05f, // fast attack (s)
    0.1f, // fast decay (s)
    0.35f, // sustain level
    0.5f    // slow release (s)
    );
  ADSRParams env1 = new ADSRParams(
    0.7f, // maxAmp
    0.4f, // slow attack (s)
    0.0f, // no decay (s)
    0.7f, // sustain level
    0.4f    // slow release (s)
    );
  ADSRParams env2 = new ADSRParams(
    0.7f, // maxAmp
    0.75f, // slow attack (s)
    0.1f, // decay (s)
    0.6f, // sustain level
    0.05f    // release (s)
    );
  adsrList.add(env0);
  adsrList.add(env1);
  adsrList.add(env2);
}

/**
 * Bottleneck "commit" method for audio state.
 *
 * Takes an arbitrary input signal and installs it as the canonical audio signal
 * used by the system. This method:
 *
 *  - Resizes/pads/truncates the input to mapper.getSize()
 *  - Copies the data to ensure no external aliasing
 *  - Updates audioSignal (canonical signal handled by application code)
 *  - Updates playBuffer (audio buffer used by Minim audio library methods)
 *  - Propagates the buffer to active instruments: edit this part for your own code
 *
 * === This is the ONLY method that should mutate the global audio signal state. ===
 *
 * In PixelAudio examples, the signal is typically loaded from a file, but
 * it could also be signal cached in memory, a signal generated by code, audio
 * captured live, etc.
 *
 * @param sig    an audio signal
 */
void updateAudioChain(float[] sig) {
  // Decide target length (make this a single source of truth)
  int targetSize = mapper.getSize();
  if (targetSize <= 0) return;
  // Ensure playBuffer matches target
  float[] canonical = new float[targetSize];
  if (sig != null) {
    System.arraycopy(sig, 0, canonical, 0, Math.min(sig.length, targetSize));
  }
  audioSignal = canonical;
  audioLength = targetSize;
  if (playBuffer == null || playBuffer.getBufferSize() != targetSize) {
    playBuffer = new MultiChannelBuffer(targetSize, 1);
  }
  playBuffer.setChannel(0, canonical);
  // Propagate into audio instruments (adjust to your actual API)
  if (synth != null) synth.setBuffer(playBuffer);
}


/**
 * Typically called from mouseClicked with mouseX and mouseY, generates audio events.
 *
 * @param x    x-coordinate within a PixelAudioMapper's width
 * @param y    y-coordinate within a PixelAudioMapper's height
 */
public void audioMouseClick(int x, int y) {
  ensureSamplerReady();
  int samplePos = getSamplePos(x, y);
  if (isRandomADSR) {
    ADSRParams env = adsrList.get((int)random(adsrList.size()));
    int len = calcSampleLen();
    print("-- envelope: "+ env.toString());
    println("; pos = "+ samplePos +", length = "+ len);
    playSample(samplePos, len, 0.8f, env);
  } else {
    playSample(samplePos, calcSampleLen(), 0.6f);
  }
}

/**
 * @param x    a value to constrain to the current window width
 * @return the constrained value
 */
public int clipToWidth(int x) {
  return min(max(0, x), width - 1);
}
/**
 * @param y    a value to constrain to the current window height
 * @return the constrained value
 */
public int clipToHeight(int y) {
  return min(max(0, y), height - 1);
}


/**
 * Calculate position of the image pixel within the signal path.
 *
 * @param x     x-coordinate, must be clipped to window width
 * @param y     y-coordinate, must be clipped to window height
 * @return      index position in audio signal array corresponding to (x, y)
 */
public int getSamplePos(int x, int y) {
  return mapper.lookupSignalPos(x, y);
}



/**
 * Plays an audio sample with PASamplerInstrument and custom ADSR.
 *
 * @param samplePos    position of the sample in the audio buffer
 * @param samplelen    length of the sample (will be adjusted)
 * @param amplitude    linear amplitude of the sample on playback
 * @param env         an ADSR envelope for the sample
 * @return the calculated sample length in samples
 */
public int playSample(int samplePos, int samplelen, float amplitude, ADSRParams env) {
  // play the sample
  samplelen = synth.playSample(samplePos, samplelen, amplitude, env);
  // get the image coordinates that correspond to samplePos
  int[] coords = mapper.lookupImageCoord(samplePos);
  // samplelen is in samples, translate it to milliseconds
  int durationMS = (int)(samplelen/sampleRate * 1000);
  // prepare a little animation at the image coordinates
  timeLocsArray.add(new TimedLocation(coords[0], coords[1], durationMS + millis()));
  // return the length of the audio event in samples
  return samplelen;
}

/**
 * Plays an audio sample with PASamplerInstrument and default ADSR.
 *
 * @param samplePos    position of the sample in the audio buffer
 * @param samplelen    length of the sample (will be adjusted)
 * @param amplitude    linear amplitude of the sample on playback
 * @return the calculated sample length in samples
 */
public int playSample(int samplePos, int samplelen, float amplitude) {
  // play the sample
  samplelen = synth.playSample(samplePos, samplelen, amplitude);
  // get the image coordinates that correspond to samplePos
  int[] coords = mapper.lookupImageCoord(samplePos);
  // samplelen is in samples, translate it to milliseconds
  int durationMS = (int)(samplelen/sampleRate * 1000);
  // prepare a little animation at the image coordinates
  timeLocsArray.add(new TimedLocation(coords[0], coords[1], durationMS + millis()));
  // return the length of the audio event in samples
  return samplelen;
}

/**
 * Calculates a numerical value using a Gaussian variable, with average value over time equal to noteDuration.
 *
 * @return a duration value in milliseconds that varies randomly from the global value noteDuration
 */
public int calcSampleLen() {
  float vary = 0;
  // skip the fairly rare negative numbers
  while (vary <= 0) {
    vary = (float) PixelAudio.gauss(1.0, 0.0625);
  }
  int samplelen = (int)(abs((vary * this.noteDuration) * sampleRate / 1000.0f));
  // println("---- calcSampleLen samplelen = "+ samplelen +" samples at "+ sampleRate +"Hz sample rate");
  return samplelen;
}

/**
 * Tracks and runs TimedLocation events in the timeLocsArray list, which is
 * associated with mouse clicks that trigger audio a the click point.
 */
public synchronized void runTimeArray() {
  int currentTime = millis();
  for (Iterator<TimedLocation> iter = timeLocsArray.iterator(); iter.hasNext(); ) {
    TimedLocation tl = iter.next();
    tl.setStale(tl.eventTime() < currentTime);
    if (!tl.isStale()) {
      drawCircle(tl.getX(), tl.getY());
    }
  }
  timeLocsArray.removeIf(TimedLocation::isStale);
}


/**
 * Draws a circle at the location of an audio trigger (mouseDown event).
 * @param x    x coordinate of circle
 * @param y    y coordinate of circle
 */
public void drawCircle(int x, int y) {
  //float size = isRaining? random(10, 30) : 60;
  fill(color(233, 220, 199));
  noStroke();
  circle(x, y, 60);
}

/*        END AUDIO METHODS                        */
