docs: private void buildSpectrogram() {
  
          short[] amplitudes = wave.getSampleAmplitudes();
          int numSamples = amplitudes.length;
  
          int pointer = 0;
          // overlapping
          if (overlapFactor > 1) {
              int numOverlappedSamples = numSamples * overlapFactor;
              int backSamples = fftSampleSize * (overlapFactor - 1) / overlapFactor;
              short[] overlapAmp = new short[numOverlappedSamples];
              pointer = 0;
              for (int i = 0; i < amplitudes.length; i++) {
                  overlapAmp[pointer++] = amplitudes[i];
                  if (pointer % fftSampleSize == 0) {
                      // overlap
                      i -= backSamples;
                  }
              }
              numSamples = numOverlappedSamples;
              amplitudes = overlapAmp;
          }
          // end overlapping
  
          numFrames = numSamples / fftSampleSize;
          framesPerSecond = (int) (numFrames / wave.length());
  
          // set signals for fft
          WindowFunction window = new WindowFunction();
          window.setWindowType("Hamming");
          double[] win = window.generate(fftSampleSize);
  
          double[][] signals = new double[numFrames][];
          for (int f = 0; f < numFrames; f++) {
              signals[f] = new double[fftSampleSize];
              int startSample = f * fftSampleSize;
              for (int n = 0; n < fftSampleSize; n++) {
                  signals[f][n] = amplitudes[startSample + n] * win[n];
              }
          }
          // end set signals for fft
  
          absoluteSpectrogram = new double[numFrames][];
          // for each frame in signals, do fft on it
          FastFourierTransform fft = new FastFourierTransform();
          for (int i = 0; i < numFrames; i++) {
              absoluteSpectrogram[i] = fft.getMagnitudes(signals[i], false);
          }
  
          if (absoluteSpectrogram.length > 0) {
  
              numFrequencyUnit = absoluteSpectrogram[0].length;
              unitFrequency = (double) wave.getWaveHeader().getSampleRate() / 2 / numFrequencyUnit; // frequency could be caught within the half of nSamples according to Nyquist theory
  
              // normalization of absoultSpectrogram
              spectrogram = new double[numFrames][numFrequencyUnit];
  
              // set max and min amplitudes
              double maxAmp = Double.MIN_VALUE;
              double minAmp = Double.MAX_VALUE;
              for (int i = 0; i < numFrames; i++) {
                  for (int j = 0; j < numFrequencyUnit; j++) {
                      if (absoluteSpectrogram[i][j] > maxAmp) {
                          maxAmp = absoluteSpectrogram[i][j];
                      } else if (absoluteSpectrogram[i][j] < minAmp) {
                          minAmp = absoluteSpectrogram[i][j];
                      }
                  }
              }
              // end set max and min amplitudes
  
              // normalization
              // avoiding divided by zero 
              double minValidAmp = 0.00000000001F;
              if (minAmp == 0) {
                  minAmp = minValidAmp;
              }
  
              double diff = Math.log10(maxAmp / minAmp); // perceptual difference
              for (int i = 0; i < numFrames; i++) {
                  for (int j = 0; j < numFrequencyUnit; j++) {
                      if (absoluteSpectrogram[i][j] < minValidAmp) {
                          spectrogram[i][j] = 0;
                      } else {
                          spectrogram[i][j] = (Math.log10(absoluteSpectrogram[i][j] / minAmp)) / diff;
                      }
                  }
              }
              // end normalization
          }
      }