Reading pulse train with DC offset
The proper method to use depends in part on what the data is for, and on what other processing your loop needs to do.  The following example assumes there is nothing to do except compute the difference between the highest and lowest readings from among the most recent 8.

enum { nReads = 8 }; // # of readings = 2**3
enum { rMask = 7 };  // index-mask of 3 bits
enum { aIn = A3 };   // analog input pin #
int reads[nReads];
byte rindex=0;

void loop() {
  int vhi, vlo, vdiff, r;
  r = reads[rindex] = analogRead(aIn); // Analog read takes about .1 ms
  vhi=0; vlo=1024;
  for (byte i=0; i&lt;nReads; ++i) {
    if (r &gt; vhi) vhi = r;
    if (r &lt; vlo) vlo = r;
  }
  rindex = (rindex+1) &amp; rMask;
  vdiff = vhi - vlo;
  //
  // In this section, scale or use or report vdiff
  //   ...
}


An analog read takes about 104 μs.  That, plus any time taken to scale, use, or report vdiff, will be the major determinant of how rapidly loop() runs, because the for loop that calculates vhi and vlo should take less than 10 or 15 μs.  

For example, if loop() takes 125 μs per pass, the code will get about one reading per half cycle of input.  (3.3 KHz is 303 μs per full cycle.)  This will produce a steady precession of where the analog readings are taken within the input waveform's cycle.  If that phenomenon is undesirable, you may need to use interrupt-triggered ADC reads.

If you don't need new differences as frequently as the above code produces them, you could instead on demand take several readings in a row and produce a result.  For example:

int getDiff() {
  enum { nReads = 6 };
  enum { aIn = A3 };   // analog input pin #
  int vhi=0, vlo=1024, r;
  for (byte i=0; i&lt;nReads; ++i) {
    r = analogRead(aIn); // Analog read takes about .1 ms
    if (r &gt; vhi) vhi = r;
    if (r &lt; vlo) vlo = r;
  }
  return vhi - vlo;
}

void loop() {
  int vdiff;
  if (readingIsNeededNow) {
    vdiff = getDiff();
    // do stuff with voltage difference
  }
  // etc 
}