Sign in

username:

password:



Not a member?

Search Online Books



Search tips

Free Online Books



Chapters

Chapter Contents:

Search Spectral Audio Signal Processing

  

Book Index | Global Index


Would you like to be notified by email when Julius Orion Smith III publishes a new entry into his blog?

  

Software Listing

The software listing below is in the SAIL programming language (generally a superset of ALGOL). Since procedures are defined before they are used, the listing starts out defining basic utilities, progressing to higher levels until finally the top level is reached. As a result, it is usually easier start read the top level procedure at the end first, and then work backwards to fill in the details.

The SAIL keyboard had various non-standard characters which are spelled out here using TeX equivalents such as &alpha#alpha; and &beta#beta;.

COMMENT Track instantaneous amplitudes and frequencies of multiple sinusoids;
COMMENT Side-result: FFT-based filtering and/or inharmonic additive synthesis;

COMMENT Completed in the summer of 1985 by Julius O. Smith III

BEGIN "PARSHL"

REQUIRE "{}<>" DELIMITERS;
DEFINE #="comment",CrLf="'15&'12",tab={""&'11},Sam16="4",Cr="'15",
  ALT={'175&""},Thru={step 1 until};

REQUIRE "JOSLIB.REQ[LIB,JOS]" SOURCE!FILE;
REQUIRE "REQUIRING UDP2:SIGLIB.REL[SIG,MUS] LIBRARY" MESSAGE;
REQUIRE "UDP2:SIGLIB.REL[SIG,MUS]" LIBRARY;
REQUIRE "JAMLIB.REL[SUB,SYS]" LIBRARY;
REQUIRE "TRMTYP.SAI[LIB,JOS]" SOURCE!FILE;
REQUIRE "DISPLA.REQ[LIB,JOS]" SOURCE!FILE;

EXTERNAL PROCEDURE TRPINI(INTEGER CODE); # Enable floating point exception code;
EXTERNAL PROCEDURE SUPCT;		 # Disable \alpha-I,\alpha-R,\alpha-T,\alpha-L etc;

REQUIRE "RECORD.REQ[LIB,JOS]" SOURCE!FILE;
# REQUIRE "MYIO.REQ[LIB,JOS]" SOURCE!FILE; COMMENT Declare File IO stuff;
REQUIRE "FLTIO.REQ[LIB,JOS]" SOURCE!FILE; COMMENT Declare GetFlt;
EXTERNAL BOOLEAN PROCEDURE AinInt(REFERENCE INTEGER Val; STRING Name);
EXTERNAL BOOLEAN PROCEDURE AinReal(REFERENCE REAL Val; STRING Name);
EXTERNAL STRING PROCEDURE Cvfs(REAL Val);
EXTERNAL SIMPLE BOOLEAN PROCEDURE FNparse(
  STRING Arg;
  REFERENCE STRING Device;
  REFERENCE STRING Filename);

EXTERNAL STRING PROCEDURE DEV(STRING Name); COMMENT Return DEVICE part of filename;
EXTERNAL STRING PROCEDURE NAME(STRING Name); COMMENT Return NAME part of filename;
EXTERNAL STRING PROCEDURE EXT(STRING Name); COMMENT Return extension part of filename;
EXTERNAL STRING PROCEDURE PPN(STRING Name); COMMENT Return PPN part of filename;

COMMENT SOUND IO DECLARATIONS;

REQUIRE "SYS:DEFAUL.HDR" SOURCE!FILE;
# REQUIRE "RHEAD.REL[SYS,MUS]" LOAD!MODULE;
# REQUIRE "WHEAD.REL[SYS,MUS]" LOAD!MODULE;

EXTERNAL BOOLEAN PROCEDURE SANDI(
	INTEGER Chan, StSamp, Nsamps;
	REFERENCE REAL ARRAY X;
	INTEGER FilSamps, Headed, FilPak, Xpack(-1));

EXTERNAL BOOLEAN PROCEDURE SANDO(
	INTEGER Chan, StSamp, Nsamps;
	REFERENCE REAL ARRAY X;
	REFERENCE INTEGER ARRAY Hist;
	REFERENCE INTEGER FilSamps;
	INTEGER Headed, FilPak, Xpack(-1));

# ReadH (rp, hdbuf, Fail, true if don't want printout);
EXTERNAL PROCEDURE ReadH (
    RECORD!POINTER (File) Rp;
    REFERENCE REAL ARRAY Hdbuf;
    REFERENCE INTEGER FAIL;
    BOOLEAN Silence (FALSE));

# WriteH (head, clock, pack, #chans, #ticks, maxamp, cstr);
EXTERNAL PROCEDURE WriteH (
    REFERENCE REAL ARRAY Head;
    REAL Clock;
    INTEGER Pack, #chans, #ticks;
    REAL Maxamp;
    STRING Cstr );

INTEGER ARRAY OutH,AmpH,FrqH[0:128];
REAL ARRAY Head[1:128];
INTEGER Nread,Brk;
REAL Maxamp;
BOOLEAN FAIL,Eof;
STRING Ifile,Idev,Ofile,Odev;
STRING AmpFile,AmpDev,FrqFile,FrqDev,Tstr;

RECORD!POINTER (File) InFptr, OutFptr;
RECORD!POINTER (File) AmpFptr, FrqFptr;
DEFINE InF(x) = {File:x[InFptr]}, OutF(x) = {File:x[OutFptr]};
DEFINE AmpF(x) = {File:x[AmpFptr]}, FrqF(x) = {File:x[FrqFptr]};

COMMENT Filter input;

# REQUIRE "JOSLIB.REQ[LIB,JOS]" SOURCE!FILE;
# REQUIRE "RECORD.REQ[LIB,JOS]" SOURCE!FILE;
# REQUIRE "FLTIO.REQ[LIB,JOS]" SOURCE!FILE; COMMENT Declare GetFlt;

DEFINE MaxCoeffs = "8192"; COMMENT Maximum number of filter coefficients;

STRING Ffile;
INTEGER Ni,No;
REAL ARRAY Ic,Oc[1:MaxCoeffs];

INTEGER Nfft,Nspec,Nh,Nx,Nhop,Ndec,i,Nhops;
INTERNAL INTEGER Trace;
DEFINE ShowSpectra="(Trace LAND 1)"; DEFINE TDpyFFA = " IF ShowSpectra THEN DpyFFA ";
DEFINE ShowTime="(Trace LAND 2)"; DEFINE TDpyEd = " IF ShowTime THEN DpyEd ";
DEFINE ShowPeakFinder="(Trace LAND 4)";
DEFINE Debug1="(Trace LAND 8)",
       Debug2="(Trace LAND 16)",
       Debug3="(Trace LAND 32)";
BOOLEAN HaveOfile,HaveIfile,HaveFfile,HavePack;
STRING TmpStr;

EXTERNAL INTEGER !SKIP!;

IFC FALSE THENC
# SAIL Esc-I interrupt facility;
SIMPLE PROCEDURE EscI; # This is called upon <esc>I interrupt;
START!CODE "EscI"
    TLZE '10,'400000;			# If sign bit is on;
    MOVN '10,'10;			# Convert sign-magnitude to 2' comp;
    MOVEM  '10, Trace;			# Save Ac 10;
    CALLI   0, '400024;			# DISMIS (return to user level);
END "EscI";
PROCEDURE Int!init;
BEGIN
    EXTERNAL INTEGER JOBAPR; # JOB DATA AREA user interrupt routine pointer;
    JOBAPR <- LOCATION(EscI);
    ENABLE(15); # Enable user interrupt handling;
    Trace <- 0;
END;
COMMENT REQUIRE Int!Init INITIALIZATION;
COMMENT rmoved by BIL because he thinks this is extremely dangerous -- you
	are depending on getting only esc-i interrupts and nothing
	else.  The SAIL runtime world that tries to handle arithmetic
	exceptions (for example) depends on JOBAPR pointing to some
	procedure that can handle such interrupts (UDP2:TRIGS[NEW,SAI]/4P
	and UDP2:NWORLD[NEW,SAI]/31,32P);
ELSEC
	
IFC NOT DECLARATION(GOGTAB) THENC EXTERNAL INTEGER ARRAY GOGTAB[0:'300]; ENDC

SIMPLE PROCEDURE Esci; 
Trace <- GOGTAB['200];

PROCEDURE Int!Init;
BEGIN
ENABLE(15);
INTMAP(15,Esci,0);
Trace <- 0;
END;
REQUIRE Int!Init INITIALIZATION;
ENDC

# Global declarations;

BOOLEAN DoFlt,DoSynth,DoOut,SwapOut; # Flags for type of function wanted;

REAL MinSep,Thresh,Hyst,DFmax1,DFmax2,DAmax,SigScl,Fc1,Fc2,Fs,DBscl;
INTEGER MaxLins,MaxOscs,Nframes,Npartials,Frame,MinWid;
BOOLEAN Quiet,InstantRise;

COMMENT DerivedName - Generate name from root name and code string;
STRING PROCEDURE DerivedName(STRING GivenName, CodeString);
BEGIN "DerivedName"
  INTEGER i,j,ln,lc;
  STRING Gname,DevStr;
  STRING RootName;
# REQUIRE "JOSLIB.REQ[LIB,JOS]" SOURCE!FILE;
  EXTERNAL STRING PROCEDURE DEV(STRING Name); COMMENT Return DEVICE part of filename;
  EXTERNAL STRING PROCEDURE NAME(STRING Name); COMMENT Return NAME part of filename;
  EXTERNAL STRING PROCEDURE EXT(STRING Name); COMMENT Return extension part of filename;
  EXTERNAL STRING PROCEDURE PPN(STRING Name); COMMENT Return PPN part of filename;
  Gname  <-  NAME(GivenName);
  ln  <-  LENGTH(Gname);
  lc  <-  LENGTH(CodeString);
  IF lc+ln LEQ 6 THEN RootName  <-  Gname&CodeString
  ELSE
  RootName  <-  Gname[1 to 6-lc]&CodeString;
  IF EQU(RootName,Gname) THEN 
  BEGIN # Gak! Must generate a new name!;
    PRINT(" DerivedName got a name collision",CrLf);
    IF ln>1 THEN RootName  <-  Gname[1 to 6-lc-1]&CodeString&"2"
            ELSE RootName  <-  Gname[1 to 6-lc]&CodeString[1 to lc-1]&"2";
    IF EQU(RootName,Gname) THEN RootName <- RootName[1 to 5]&"3";
  END;
  DevStr  <-  Dev(GivenName);
  IF DevStr NEQ NULL THEN DevStr <- DevStr&":";
  RETURN(DevStr&RootName&Ext(GivenName)&Ppn(GivenName));
END "DerivedName";

COMMENT Fprint - Print documentation from DSK;

# Put this in JOSLIB;

PROCEDURE Fprint(STRING Fname);
COMMENT Print contents of file in fname;
BEGIN
    STRING Help,Ttystr;
    INTEGER Fchan,Brk,Eof,Bt;
    BOOLEAN FAIL;
    DEFINE FF={('14&"")};

    OPEN(Fchan <- GETCHAN,"DSK",0,2,0,2000,Brk,Eof);   COMMENT Ascii input;
    LOOKUP(Fchan,Fname,FAIL);
    IF FAIL THEN USERERR(0,1," Can't find HELP file "&Fname);
    IF FAIL THEN RETURN;
    SETBREAK(Bt <- GETBREAK,FF,NULL,"sin");
    DO 
	BEGIN "pages"
	    Help  <-  INPUT(Fchan,Bt);
 	    IF NOT EQU(Help[1 FOR 7],"COMMENT") THEN 
	    BEGIN
		MyOutDpy(Help,-3,-1);
 		MyOutDpy ("Type RETURN to continue - ALT to return to command level",31,-1);
		MyOutDpy (Fname,33,-1);
		Ttystr <- INCHWL;
		Break!N;
		IF Ttystr="H" OR Ttystr = "?" THEN MyOutDpy(" This IS help!!",32,-1);
		IF !skip!="H" OR !skip! = "?" THEN MyOutDpy(" This IS help!!",32,-1);
		IF !skip! = ALT THEN BEGIN PRINT(CrLf); DONE "pages"; END;
	    END;
	END "pages"
    UNTIL Eof;
    Relbreak(Bt);
    RELEASE(Fchan);
END;

COMMENT Help - Command Summaries;

RECURSIVE PROCEDURE HELP(STRING Topic(NULL));
BEGIN "help"

Break!N;

IF EQU(Topic,"?") THEN 
BEGIN
    Fprint("PARSHL.JOS[MUS,DOC]");
    PRINT(CrLf,CrLf," Type \alpha-? for command summary",CrLf);
    # PRINT(" Type <command!letter>\alpha-? for individual command description",CrLf);
    PRINT(" Type ?\alpha-? or ?,?<RETURN> for full documentation",CrLf,CrLf);
    RETURN;
END;

IF EQU(Topic,"TopLevel") THEN MyOutDpy("
  
                        Outer Level Command Summary
  
[s> => string, [n> => integer, [d> => real number, \alpha- => CONTROL, and \beta- => META
  
    ALT - Exit PARSHL.
    ?\alpha-? - Print complete documentation. (Also ?,?[CR> does this.)
  [n>\alpha-W - Analysis Window (1=rect,2=triang,3:5=Hamming,6=Kaiser,7=Cheb)
  [n>\alpha-T - FFT length (must be a power of 2).
  [n>\alpha-D - Number of samples of input signal per FFT frame.
  [n>\alpha-H - Number of samples of input signal to advance between FFT's.
  [n>\alpha-L - Length of filter impulse response (if known).
  [n>\alpha-C - Decimation factor (1 => no decimation, 2 => every other sample, etc.)
  [s>\alpha-I - Input sound file.
  [s>\alpha-O - Output sound file.
  [n>\alpha-P - Output sound file packing mode. (\alpha-P with no arg explains codes.)
  [s>\alpha-F - Input filter file.
      ; - Comment.

[ESC>[n>I codes: (n can be any sum of the following):

    [n>     display(s) enabled
    ---     ------------------
      0     none (type [ESC>I to turn off all running displays)
      1     time buffers ([ESC>1I)
      2     spectral buffers ([ESC>2I)
      4     peak-finder 
      8     debug level 1
     16     debug level 2
     32     debug level 3
------------------------------------------------------------------------
",-3,-1);

IF EQU(Topic,"SubLevel") THEN MyOutDpy("
  
                       Inner Level Command Summary
  
[s] => string, [n] => integer, [d] => real number, \alpha- => CONTROL, and \beta- => META
  
    ALT - Exit to Outer level.
    ?\alpha-? - Print complete documentation. (Also ?,?[CR] does this.)
  [d]\alpha-M - Minimum spacing between partials in Hz
  [n]\alpha-N - Maximum number of partials to track
  [n]\alpha-O - Maximum number of oscillators to use
  [d]\alpha-F - First (lowest) partial frequency allowed (Hz) 
  [d]\alpha-L - Last (highest) partial frequency allowed (Hz) 
  [d]\alpha-T - Peak detection threshold in dB
  [d]\alpha-H - Spectral wiggle tolerance in dB
  [d]\alpha-D - Maximum allowed frequency deviation per frame in Hz at LOW frequency
  [d]\alpha-U - Maximum allowed frequency deviation per frame in Hz at HIGH frequency
  [n]\alpha-B - Buffer display code. If negative, all time buffers are displayed.
     \alpha-S - Show spectra of filter input and output on each FFT hop. (\beta-S disables.)
      ; - Comment.
------------------------------------------------------------------------
",-3,-1);

END "help";

COMMENT DpyFFA - Display interleaved dB spectra as computed by FFA in SIGLIB;
PROCEDURE DpyFFA(REAL ARRAY S; INTEGER Nfft; STRING T; REAL Fs(1));
COMMENT For displaying interleaved dB spectra as computed by FFA in SIGLIB;
BEGIN "DpyFFA"
  INTEGER i,Nspec;
  REAL ARRAY Spec[1:(Nfft DIV 2)+1];
  Nspec  <-  (Nfft DIV 2) + 1;
  FOR i <- 1 STEP 1 UNTIL Nspec DO Spec[i]  <-  (10.0/LOG(10.0))*LOG(
    (S[2*i-1]^2+S[2*i]^2) MAX 1.0@-35);
  DpyEd(Spec,Nspec,T,"MAGNITUDE (DB)",0,Fs/2);
END "DpyFFA";

COMMENT Qinterp - Quadratic interpolation;

    INTERNAL SIMPLE REAL PROCEDURE Qinterp(REAL Ym1,Y0,Yp1; BOOLEAN InteriorX(TRUE))
    ;
    COMMENT Fit a parabola Y[X] = A*X^2+B, through the three equally 
	    spaced Y-values Ym1 = Y[-1], Y0 = Y[0], and Yp1=Y[1],
	    and return the X-value where the extremum is attained.
	    For example, if -1 is returned, then Y[-1] is the point
	    of zero slope in the parabola through the three points.
	    If InteriorX is TRUE then if the extremum lies outside the
	    interval [-1,1], a warning is printed and the returned value
	    is clipped to lie at either 1 or -1 as appropriate.
    ;
    BEGIN "Qinterp"
    REAL X;
      X  <-  (Yp1 - Ym1)/(2*(2*Y0 - Yp1 - Ym1));
      IF InteriorX AND (ABS(X)>1) 
	THEN PRINT(" Qinterp: Clipping analytic extremum to ",
	  X  <-  (IF X>0 THEN 1 ELSE -1),CrLf);
	  IF Debug3 THEN PRINT("Qinterp: Given Y's ",Ym1," ",Y0," ",Yp1,
		", extremum is at X = ",X,CRLF);
      RETURN(X);
    END "Qinterp";

COMMENT FindPeaks - Generic peak finder;

IFC NOT DECLARATION(IntNoSet) THENC DEFINE IntNoSet={(1 LSH 35)};
IFC NOT DECLARATION(RealNoSet) THENC DEFINE RealNoSet={(-1.0@35)};

INTEGER PROCEDURE FindPeaks(REAL ARRAY X,Peaks,PeakLocs; 
  REAL Thresh(RealNoSet),Hyst(RealNoSet);
  INTEGER MaxPeaks(IntNoSet),MinWidth(IntNoSet),BlankWidth(IntNoSet),
          I1(IntNoSet),I2(IntNoSet));
COMMENT
        Find amplitudes and indices of all peaks in
        X = array of values to be searched for peaks. (It is overwritten.)
        Peaks[1:MaxPeaks] = peak amplitudes.
        PeakLocs[1:MaxPeaks] = peak indices.
        Thresh = Threshold below which no peaks are to be sought
        Hyst = Wiggles less than Hyst in magnitude are ignored.
        MaxPeaks = Maximum number of peaks to be found (starting w largest)
        MinWidth = Minimum peak width in indices, after blanking.
                   Narrower peaks are removed and ignored.
        BlankWidth = Minimum blanking interval in indeces.
                    If = IntNoSet, then blank to nearest local minimum
                    to within Hyst interval. 
		    Blanking is done before width is measured
		      for efficiency reasons (would rather measure width first).
        i1,i2 = index boundaries for the search within X.
        The return value is the number of peaks actually found.
;
BEGIN "FindPeaks"
  INTEGER i,j,Npeaks,NdxReach,Owid,Odig,Peak,Poff,PLoff,M1,M2,Nfound,Lb,Ub;
  REAL Xmax,Xmin;
  GETFORMAT(Owid,Odig);  SETFORMAT(0,5);
  NdxReach  <-  (IF (BlankWidth NEQ IntNoSet) THEN (BlankWidth-1)/2 MAX 1 ELSE 0);
  Poff  <-  ARRINFO(Peaks,1)-1;
  PLoff  <-  ARRINFO(PeakLocs,1)-1;
  Npeaks  <-  (IF MaxPeaks NEQ IntNoSet THEN MaxPeaks ELSE ARRINFO(Peaks,2)-Poff);
  Npeaks  <-  Npeaks MIN (ARRINFO(Peaks,2)-Poff) MIN (ARRINFO(PeakLocs,2)-PLoff);
  Lb  <-  ARRINFO(X,1);
  Ub  <-  ARRINFO(X,2);
  IF I2=IntNoSet THEN I2  <-  ARRINFO(X,2);
  IF I1=IntNoSet THEN I1  <-  ARRINFO(X,1);
  I1  <-  (Lb MAX I1 MIN Ub);
  I2  <-  (I1 MAX I2 MIN Ub);
  ArrMin(Xmin,X,I1,I2);
  ArrMax(Xmax,X,I1,I2);
  IF Thresh=RealNoSet THEN Thresh  <-  Xmin;
  IF Hyst=RealNoSet THEN Hyst  <-  (Xmax-Xmin)/100;
  M1 <- M2 <- 0; COMMENT [m1,m2] = index interval of last peak;
  Nfound <- 0;
  FOR Peak <- 1 STEP 1 UNTIL Npeaks DO
  BEGIN "fp"
    INTEGER MaxLoc,TmpI;
    REAL MaxVal,ClobVal,TmpR;
    IF M1=I1 AND M2=I2 THEN DONE "fp";
    MaxLoc  <-  ArrMax(MaxVal,X,I1,I2);
    IF MaxVal<Thresh THEN DONE "fp";
    Nfound  <-  Nfound + 1;
    PeakLocs[Nfound+PLoff]  <-  MaxLoc
      + Qinterp(X[(MaxLoc-1) MAX I1],MaxVal,X[(MaxLoc+1) MIN I2]);
    IF MaxLoc=I1 AND Debug1 THEN
      PRINT("*** Peak is at right of search interval for peak ",Nfound," ***",CrLf);
    IF MaxLoc=I2 AND Debug1 THEN
      PRINT("*** Peak is at left of search interval for peak ",Nfound," ***",CrLf);
    Peaks[Nfound+Poff] <- MaxVal;
    COMMENT Now slice off peak so we don't find it again;
    M1  <-  (MaxLoc-NdxReach) MAX I1;
    M2  <-  (MaxLoc+NdxReach) MIN I2;
    ArrMin(ClobVal,X,M1,M2);
    TmpR  <-  X[M1];
    WHILE M1>I1 AND TmpR+Hyst GEQ X[M1-1] DO BEGIN TmpR <- TmpR MIN X[M1-1]; M1 <- M1-1 END;
    ClobVal  <-  ClobVal MIN TmpR;
    TmpR  <-  X[M2];
    WHILE M2<I2 AND TmpR+Hyst GEQ X[M2+1] DO BEGIN TmpR <- TmpR MIN X[M2+1]; M2 <- M2+1 END;
    ClobVal  <-  ClobVal MIN TmpR;
    FOR i <- M1 STEP 1 UNTIL M2 DO X[i] <- ClobVal;
    IF (M2-M1+1 < MinWidth)
	OR MaxLoc=I1 OR MaxLoc=I2	# JOS/10/25/85;
    THEN
    BEGIN "Throw it back"
      Nfound  <-  Nfound-1;
      CONTINUE "fp";
    END "Throw it back";
    IF ShowPeakFinder THEN DpyEd(X,I2-ARRINFO(X,1)+1,"X remains after peak "&
	CVS(Nfound)&" at i="&CVS(MaxLoc));
  END "fp";
  SETFORMAT(Owid,Odig);
  RETURN(Nfound);
END "FindPeaks";

COMMENT FindPartials - Find all significant spectral lines for current frame;

PROCEDURE FindPartials(REFERENCE INTEGER Npartials; 
  REAL ARRAY XmagDB,LinAmp,LinFrq; REAL Fs,MinSep,Thresh,Hyst;
  INTEGER Nfft,Frame,MinWid; REAL Fc1(RealNoSet),Fc2(RealNoSet));
COMMENT Find amplitudes (dB) and frequencies (Hz) of all partials in XmagDB.
	Npartials = the maximum number of partials to find.
	  It is set to the actual number of partials found on return.
	XmagDB[1:Nfft/2+1] = DB magnitude spectrum
	LinAmp[1:Nharms,1:Nframes] = Partial amplitudes
	LinFrq[1:Nharms,1:Nframes] = Partial frequencies
	Fs = sampling rate (Hz)
	MinSep = minimum line spacing (Hz)
	Thresh,Hyst are args to FindPeaks
	Nfft = Size of FFT used in computing XmagDB
	Frame = current time frame number
	MinWid = minimum acceptable peak width in FFT bins.
        Fc1,Fc2 = lower and upper frequency bounds in Hz. 
		  No partials will be allowed outside this interval.
;
BEGIN "FindPartials"
  REQUIRE "HACKS.REL[SUB,SYS]" LIBRARY;
  EXTERNAL SIMPLE PROCEDURE QISort(REAL ARRAY A,Data; INTEGER LB,UB);
	
  INTEGER i,j,Nspec,BinInt,BinReach,Owid,Odig,Partial,MinWid;
  REAL ARRAY Xsave[1:Nspec <- Nfft/2+1];

  GETFORMAT(Owid,Odig);
  SETFORMAT(0,7);
  BinInt  <-  (MinSep/Fs)*Nfft+.5; # Blanking interval for spectral peak (in bins);
  IF Frame=1 THEN PRINT(CrLf," Blanking interval is ",BinInt," bins",CrLf);
  ARRTRAN(Xsave,XmagDB); # Save input array;

  Npartials <- FindPeaks(XmagDB,LinAmp,LinFrq,Thresh,Hyst,
    Npartials,MinWid,BinInt,((1+Nfft*Fc1/Fs) MAX 1),((1+Nfft*Fc2/Fs) MIN Nspec));

  ARRTRAN(XmagDB,Xsave);
# Sort for ascending frequency (for convenience only);
  IF Npartials>0 THEN QIsort(LinFrq,LinAmp,1,Npartials); 

  IF Debug2 THEN PRINT(CrLf);
  FOR Partial <- 1 STEP 1 UNTIL Npartials DO
  BEGIN "fp"
    LinFrq[Partial] <- Fs*(LinFrq[Partial]-1)/Nfft;
    IF Debug2 GEQ Partial THEN
      PRINT("Frame=",Frame," Amp(dB)=",LinAmp[Partial],
      " Freq(Hz)=",LinFrq[Partial],"=",Partial,"*",MinSep,"*",
	LinFrq[Partial]/(Partial*MinSep),CrLf);
  END "fp";

  IF ShowPeakFinder THEN 
  BEGIN "ShowHarms"
    REAL ARRAY CFrqs[1:Nspec];
    REAL Val;
    Val  <-  LinAmp[1];
    j <- 1;
    FOR Partial <- 1 STEP 1 UNTIL Npartials DO
    BEGIN
      FOR i <- j STEP 1 UNTIL Nfft*(LinFrq[Partial]/Fs) DO
	CFrqs[i]  <-  Val;
      j <- i;
      Val <- LinAmp[Partial];
    END;
    Dpy2(XmagDB,CFrqs,i,
      "Centers of pitch-wide search regions for frame "&CVS(Frame),
      "MAGNITUDE (DB)",0,Fs/2,-30,30);
  END "ShowHarms";
  SETFORMAT(Owid,Odig);
END "FindPartials";

COMMENT OutPartials - Write partial amplitudes and frequencies to disk;

PROCEDURE OutPartials(INTEGER Npartials,Nframes;
		      REAL ARRAY Amps,Frqs; REAL Fs,Thresh);
BEGIN "OutPartials"
  INTEGER N,AmpP,FrqP;
  N <- Npartials*Nframes;
  Sando(AmpF(Channel),AmpP,N,Amps,AmpH,AmpP,TRUE,AmpF(Pack));
  Sando(FrqF(Channel),FrqP,N,Frqs,FrqH,FrqP,TRUE,FrqF(Pack));
  Sando(AmpF(Channel),0,0,Amps,AmpH,AmpP,TRUE,AmpF(Pack));
  Sando(FrqF(Channel),0,0,Frqs,FrqH,FrqP,TRUE,FrqF(Pack));
END "OutPartials";

COMMENT Decimate - Downsample by integer factors;

INTEGER PROCEDURE Decimate(REAL ARRAY A; INTEGER N,M,I(0));
COMMENT Downsample array A[1:N] by M. I nonzero means initialize;
COMMENT Return the number of samples produced;
BEGIN "Decimate"
  OWN INTEGER P;
  INTEGER j;
  IF M LEQ 1 THEN RETURN(N);
  IF I NEQ 0 OR P LEQ 0 THEN P <- 1;
  j <- 0;
  FOR i <- P STEP M UNTIL N DO A[j <- j+1]  <-  A[i];
  P <- i-N;
  RETURN(j);
END "Decimate";

COMMENT GetWin - Compute overlap-add analysis window;

INTEGER WinType;
DEFINE 
  Rectangular="1",
  Triangular="2",
  Hamming="3",
  GenHamming="4",
  Hanning="5",
  Kaiser="6",
  Chebyshev = "7",
  Nwins = "7";
PRELOAD!WITH
  "Rectangular", "Triangular", "Hamming", "GenHamming", 
  "Hanning", "Kaiser", "Chebyshev";
  STRING ARRAY WinNames[1:Nwins];
DEFINE WinStr = "WinNames[WinType]";

PROCEDURE GetWin(REAL ARRAY W; INTEGER Wtype,Nw; REAL P3(-1.),P4(-1.));
COMMENT
	Generate analysis window taking special care to ensure that it will
	overlap-add to unity in the case of a Hamming window with hopsize = Nw/(2K)

  Wtype   Window
  -----   ------
  1       Rectangular
  2       Triangular
  3       Hamming
  4       Generalized Hamming
  5       Hanning
  6       Kaiser
  7       Chebyshev

;
BEGIN "GetWin"
# REQUIRE "SIGLIB.REL[SUB,SYS]" LIBRARY;
  EXTERNAL  PROCEDURE !WINFLT(REAL ARRAY H; REFERENCE INTEGER NH;
    REFERENCE INTEGER WINTYP; REFERENCE INTEGER BNDTYP; REAL ARRAY P);
  REAL ARRAY WinPars[1:4];
  WinPars[1] <- 0;		# Lower cutoff frequency;
  WinPars[2] <- 0;		# Upper cutoff frequency;

  IF WinType=Kaiser AND P3<0 THEN
  DO BEGIN
    IF NOT AinReal(P3 <- 60,"Kaiser stopband rejection in DB") THEN RETURN;
    IF P3 < 0 THEN PRINT("Stopband rejection must be POSITIVE dB...like 60",CRLF);
  END UNTIL P3 GEQ 0;

  IF WinType=GenHamming AND P3<0 THEN 
  DO IF NOT AinReal(P3 <- 0.54,"Alpha (0:4)") THEN RETURN UNTIL (0 LEQ P3 LEQ 4);

  IF WinType=Chebyshev AND P4 LEQ 0 THEN DO
  BEGIN "GetCheb"
    IF NOT AinReal(P3,
    "Chebyshev stopband rejection in DB (or minus transition width in Hz/Srate)") THEN RETURN;
    IF P3<0 THEN BEGIN P4 <- P3; P3 <- 0; END ELSE P4 <- 0;
  END "GetCheb" UNTIL (0 LEQ P4<1/2 AND 0 LEQ P3);

  WinPars[3] <- P3;
  WinPars[4] <- P4;

  IF (Nw MOD 2) = 1 THEN
  BEGIN
    REAL ARRAY TmpBuf[1:2*Nw+1];
    !WinFlt(TmpBuf,Nw,Wtype,1,WinPars); # Analysis window = lowpass, Fc=0;
    ARRTRAN(W,TmpBuf);
    IF Wtype>Rectangular THEN W[Nw] <- 0; # For odd lengths, last sample zeroed;
  END ELSE
  BEGIN
    REAL ARRAY TmpBuf[1:2*Nw+1];
    INTEGER i;
    !WinFlt(TmpBuf,i <- 2*Nw+1,Wtype,1,WinPars);
    FOR i <- 1 STEP 1 UNTIL Nw DO W[i]  <-  TmpBuf[2*i];
  END;
  BEGIN "nrmlz"
  # REQUIRE "JOSLIB.REQ[LIB,JOS]" SOURCE!FILE;
    EXTERNAL REAL PROCEDURE MaxArr(INTEGER n; REAL ARRAY y);
    INTEGER i;
    REAL Wmax,Wscl;