数学があまり得意ではない人のためのFFT

 離散フーリエ変換の高速化(高速フーリエ変換)をソースの変形だけで導いてみます。 「あまり得意ではない」といっても、DFTの性質や、複素数の極形式の演算とオイラーの公式くらいはわかっている必要があります。 最終的には実離散高速フーリエ変換を目指しますが、書いていたら思いのほか大きくなってしまったので、今回は \(N^2\) 回の計算をする複素DFT関数から、ループを用いた複素FFT関数を導くところまでです。

 以下のサイトが色々と参考になります。

つか、まぁ、「FFTの概略と設計法」に書いてある「機械的な変形」をていねいに説明しただけなんだが・・・

とりあえずテストデータを作る
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
src.map(z => LOG(z));
STATUS("finished");

 DFTは複素数の方が楽なので実数化は後で考えることにして、とりあえずComplexクラスを作ってテストデータを入れる。 LOGとかSTATUSというのはこのスクリプトを実行している外側のHTMLファイルで定義されていて、なんかログ文字列を出力する関数だと思えばいい。 興味がある人はjs/fft.htmを見ればすぐに分かる。

 ログ出力の時にconsole.logと同じように文字列化されるので、Complex.toStringも定義しておく。

とりあえずDFTする
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(-i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(dft => {
    dft.forEach(z => LOG(z));
    STATUS("finished");
});
GhR"4gJ6c_¤:O"KN"?LXU#*Y$i?;`3Zs8:¤AL4qI!?nZeB4Ef=.bbQ3.0%$AgbpU@;S/>Q5%j#¤k¤Zn2bmmMl_"IRR bi^*(DCtc9nl7$gn2Z'n1")=V_qY0';OEudq_FL-$0"ioR?=k.SVYF¤obQ+0eKhqqa"c7D,S"#;Q2lLB2RnNP5iiHJ o)CsZMT;2o_jm(HQrt-W3m«>W'+)FP7NOdmQ!rB>*c*9KnNP]o29Ht\n#?oUUt!8\6GjI>G@e4^>/#3HPFgos]9J/h P%.Ii5CVMJ^GS.oVc]e1Usp8=XED)PciSOdMlR#d7W]A%->@9*K.N5_Y=lb6'¤tWskEdNM2[gsq%[Is%Kt*\)`[qIN `q+DW*'>k!(OmLJP(m1"@S?1NYie;EYFld;eL2hFk]?ZS,g=T_':C=dGmW@n;s%]mqfl-pFMN_rYZ5C9JqrQ/A6SjB `t>F#?%#28p:2pUK,gFYQDc>Z1F2-6nd«dj;«bLSL?l@j$,mLFYod:ulJ;(;2ZCMZ__mIO¤%AG¤,t]\Wb';7adp39$ WRN1gS3PKTM'!-X\`=sjU@RMC[L/@NMM_iB~
GhR"5?#/1K'Re«2\«K.tFe^78ou5+ID!Mr)$^/qn;H@5L[>6C>8Z$B_s825L5AIeB`/=#s3p5:nCIfrr4¤p#d92c,j ].0UR0rkD$=TC'!?"GkBgm-KkQ3k/3pd$00-]DR$FV$-t=[8JbCZ.oh*-,2+-f\*/B«'ULL)C::=C$G,B^`3?\P-/o dmcgUW%V1?)f>N%Q!7mcIUlrE:?'1'QY?sNFO7hFN%0GATa)+u.?]?)#rnGh$TE%c,VP\0o*J;3:8Le$%CX>HPu!OU 2rNHY8>8¤Ej9V9¤AloP-k;1+ZVn-W+25qX,T?p(@LEX0E00Xb8eaf]S=#Rkl¤h`3aC"W`%n1T9@i4h=_R$LjNhj!mC *1ILbKmhN,%.%?)GKcMc=4!D3Yr)s\>j=hG7AEP.^CAuS[kYO(@(CB`V\+i+qZIhH0H;#bSqM-gh-KDc[VR$RZQ$.a 3B?MQ«¤-/+Q9hne$5nR:=pVuZQ>YAR@EX"B%m¤W*"5(Yb\dq8tfE4?;2Igt[(lq@'5N]XZ]fm)SpQ[JmRB?!P9N@[j 'nV>GqZiV:'C"@r,BQg!pq84e`BP".P2)i87lp/2cZ^@'$?rA;V+N8-alJEQoN/n8-8eFo7qZ!enV*_ahaF!ZTI>." Xhi¤-p_Pts~

 複素数の演算が増えている。 Wというのはいわゆる回転因子(twiddle factor)というやつで、オイラーの公式の引数の \(2\pi\) を省略した感じになっている。 つまり、

\begin{align*} W(x)=e^{j2\pi x}=\cos(2\pi x)+j\sin(2\pi x) \end{align*}
である。 \(j2\pi\) の前にマイナスをつける流儀もあるが、ここではWをシンプルに定義して、使う側で符号を付けることにする。

 回転因子は(複素)指数関数の一種なので、指数法則が使えることに注意。 DFT関数は定義通りに書いただけで、引数srcComplexの配列を与えると、複素DFTして結果をComplexの配列として返す。

 ここでのポイントは、実DFTなので、

  • 最初の結果は実数である
  • 他の結果は前後対称で複素共役になる

ことを確認することである。 以後、同じように石橋を叩きがならコードを確認していく。

 一番よく使われる基数2のCooley-Tukey型の変形をするつもりなので、要素数は \(N=2^n\) 個にしておく必要がある。 16点くらいがご機嫌である。 多ければ結果の確認が面倒になる。 少ないと基数を大きくしたり、FFTを流用して高速DCTなんかを計算するときに、書き換えごとにどんどん要素数が半分になっていって、最後 \(N=1\) になってしまいました、ということが起きる。

 DFT関数がasyncになっているが、これはDFTの演算が長いとJavaScriptのUIスレッドがブロックしてしまい途中経過が表示されなくなるからで、STATUSsetTimeoutを実行して処理をJSエンジン側に譲り渡すようにしてある。 トップレベルではawaitは使えないからasync関数も使えないと勘違いしている人がいるが、実際にはasync/awaitはプロミスのシンタックスシュガーで、async関数はプロミスを返しているだけなので、awaitは使えなくても普通にPromise.thenが使える。 逆に、awaitにプロミスを指定してもちゃんとプロミスを待って普通に動く。

 念の為に言っておくと、本当に普通のプロミスなので、thenに指定する関数をasyncにする必要はない。 というか、アロー関数にasync付けるのが面倒だったので、最後のSTATUSawaitを付けてなかったりする。 まぁfinishedの文字通り、一番最後の呼び出しになるので。

とりあえず逆DFTする
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(`${src[i]} -> ${z}`));
    STATUS("finished");
});
GhR:=gMWKG¤:O:SN)1R]!#q-3«a0=*W4/*oI>Fpd6rC;M;HA@fg4_T(o"TC'Rn_BTfZT\NZY*p*0EgjmG8K2qp^Y.P U0JN;T?:W4-Vpt=63$/EWjRF.P9stZ_p#sE6m>Eo[aERg4irED">3`JK./:.([-P¤Bj'[pCDBjT«,g$PLcQfLDdEAU S?"0/¤:!G,FETq.ShSJ?3]_Zf\m\¤uXiCC1>`e5(XMFE4VPmIk!ehJ[H(Ls!«>M*,@f08GGMchCWIa/¤"cir7".VQ* _T?eRcg*57G!]5KZanrj\m^Zn0aeWs.9XP.5(\J]$f6N$mUF?CMAWA@M(j('kG_uNe;Km-N1+:T8TpOJC)Ri7K-X^? 0tV"gKQEB,!QsOnSPs"cjLoscFQuXocf*EI+TPH[\$a«p7bU5u:dl27]*)cOll,?cU/=GQ(Y01)k#uQjbl"«^;jlfb NX(Ioq[KV!,Sr[5Q#SBT>SbNf¤*Ed!amoXJ?=et¤EEZ4I=!cX@bhEpGAZR'Mqp02^BeqG2c(%?F_74U'[muW%3@SmR CB_/"%Ia:`@olMI\rGmY#.r8(%Y$ImG8WPo[t_5;F*JOBg\6Vk8gR2t`)bXk3;`k/CC$a~
GhR:>a_oie¤A@rk#!rirAMJ@Zp/'?nK!m*8F9CkRLENN%/J2EJ1+9/G'_pQIU_\lF.bG@obF:Gc-UL_Q:l@Y,;Q-,O +¤s@4¤X!f]V[;:X@U1Dh-`2e@EJc"3QEgUR2OZ-j((7^D^j,ui`leuY(AT,3:Mj]GaAT¤ge:f(rL)%pF+EI)?Yg'"C ;)7H«n^=i!@$*D!Zb>[6UQpoOA-gR\gqA#SHYB,;)+Hb5VrukIS4=0ja11pqW7`"Nlf]/qW#5OG3Nk1Sg!0e«gMN)7 kX8fn.mq[MS3EuuR8b,BBK=I-lk+\1hTM!n0i«\!s,=o3!\[h0ijr.qeSi%=?SAP9(NP]/Yu;%paaN>5ARSH!f>u]a IXT2cM9an«;TKXt2PLdn^ro4c?E/Ldfof6e[^gAol;IW##mLbue1O;!TpoVuI*h+ui/-s«K"Lp"L>i8t`>\G>h+N.. #DiuK2g@X3SKGDhYh«?$IrEE"?H2.f%$NQmi«+>7o\^a7aYE?u?nu!_Pa-O]_bgn1H;:3n,T9DZ3VA?TKcnSFs-«ch -b5!_OH^X!@cqspSdpRfnV69BrgVC`i2G;%.T:qeFSJ_?;F#_YL,;W(¤.)\o`?32'c^I74)4;^/5EJ+6>!)[d"Xl!u ,0(??J2qsd8dF0¤B0)"uM%XY«Rl>m;L9p^"_j¤Zjr9`Y`r¤.99?.Qr«nr+HQ1K`#\-$i6@)mcjR/RZhlcfq/DPD«Rp >uIHWBNH2"X_)«\#hF6"e`U3s^K)"S,,?X93:a/fnb*LjPE9e~

 結果が合っているかどうか確認するため、逆DFTする。 ここでのポイントはもちろん値がちゃんと元に戻っているか確認することである。

 DFTの行きと帰りの違いは原理的には回転因子Wの回転方向だけなので、invという引数を設けて、正変換では-1、逆変換では+1を指定する。 実際には結果は \(N\) 倍になっているので、これを元に戻すためにComplex.scaleという関数を作ってある。

 プログラム上のポイントはasync関数DFTの結果をさらにDFTに渡すので、thenを数珠繋ぎにしてるのと、ComplextoStringが定義してあるので、テンプレート文字列の中にComplexがそのまま書けることくらい。

誤差をスクリプトに計算してもらう
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(z.sub(src[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS^N;,;fu¤:i[6'piJBD1KX*kfgnq;«NkWLX#U%9ZC2>9qW¤$¤qoc3GMSXt#@Ab;8FO:)q\l17r(5gPJrB.'U'Qo5 ef*t6]ZbU¤SHHc.EDopGN0¤@o:\Kg,;:cc8;WQVaZN.$cp`[$>SXgO,ION;8Vf4EEh+tKD]Zq"NNs81LZp>VIj-#g_ _Z'@rotQl=fDk?$2k'Zq+0aq\#U[:._N82mGJ!qSQGb_a«FTL4i/KR:N:dM¤]E$_pM@kpDM\XW>660H'E(/hG2%\%« 1OESV>a!\#>,IKCj-F«q=._rYlM-1?[¤>¤*6kIdm+K8qE?6747g`cMH5C2h^0^dYD9aA?r3cG3#YX9(8qo^co/7cZ- A4g^F[9B_N=0G7P8u7\SnEn«ZHOMu?gQrW~
GhR:>D/7l[¤H:NnEJ/b%i`JnZ)e3#3b-m>HhB3j;:s,Nd:+^5(6'@S$ir?`V/A$)bRRFH(VrtU;mNbo$1,>ScYV¤o# c-J.@P_dD@_UBC\UCpKT#s2aV/$V`Vns6QjO-bSO6Y8,85D/50U5WjsT8J#[,s/jt.\nu¤N(fq$8mh\K$¤IB^%CU[Y .Pu3YQ-B-+c«96$C?7o`/«(;5JTN¤_LoXTQr>[pVX^KL"Ea#TVi¤m%A@48Ql!=(LsShi28G!`J-#!'IrQhN@XE.f2h aMAI/fH:Dda?^bd?=s/'Sa,,9od',pX^UR`+`¤UJ8%Cb6^jFghHer¤pJ^!M[,G$2L%B4Z6^o)#IeJIl=pR63coPQn' 8QiX^'W.l.Uqrn0B_Z@iroU$¤A[61,R']^Mgo/T\25]b)IXf8E*M)h.[#cXfcGQ7eY4j\tB)d7$9sffI«60QfE]BiM P62HLd*/d`5+$HKZ,HEE_!PF@L91n19+3r:ZSfef"%\r6TW:-2Ea`@$I«kd(8M¤g(9]74Tn9"m¤@C4n2UBs'2q,¤O( dWRlWQB"U2/S]#t;[]C=I9[SQ3O>XL`\EP%DiA+rK;?^L0P5S\mUuhB1LutQVZ_4lDEmqPU"s'«Fc\)i!%sPrHSWJH C+Z`M9G*e5aBr%So\t)F;-_+0N_L5)PeD(d^)RtM1MJ%)]Jar6?,kJ¤b\1c)hH%g8^Dte?XrUX#o?X8NI3H0V«9jpQ ;F1jqEb^$;s8#@Wg8abJ+M>LZl;4U]n(¤:u+9UIJ?8Ufi6@1f/UQ(5He>'F56DmC;E_VA-J$1Z#3W~

 せっかくスクリプトを使っているのだから、結果の誤差もスクリプトに計算してもらう。 入力と逆DFTの差をとって、複素数だから絶対値を表示すればいいだろう。 絶対値の計算は真面目に実部虚部を2乗して足して平方根を計算してもいいが、実はMath.hypotという便利な関数がある。 マイナーだけど。

 プログラム上のポイントは、差の計算をする時にsrc[i].sub(z)ではなくz.sub(src[i])と書いているところで、sub関数はthisオブジェクトの値を更新してしまうから、こう書いておかないと後でsrcを使い回す時に困ってしまう。

入力を偶奇に分ける
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < m; i++) {
            sum.add(W(inv * (i * 2) * k / n).mul(src[i * 2]));
            sum.add(W(inv * (i * 2 + 1) * k / n).mul(src[i * 2 + 1]));
        }
        dst.push(sum);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(z.sub(src[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhRRCbAMqd¤A70V7\¤f^#UDQuVPS%X*sbjV;%ACP@g,O?TVSUYmp4t«2H¤d3eloDd1[B0#_RWh%^'C@\!b$nt«CnDc Kf(r3Hqk7)+"I);T3idC*toSk]jhAhmn"3bV«S8B0Ij\'VV[bI0G-[KUl*"toACEJV6e*PCTtH4Je_F@4b6p)kmiT6 ¤Dnfqi\$N+«mbFL\mfuCH['dc799l+YjsESD!ZF$F;V3mm755Y4B:L:aA.[Bc@n#U94KQ5[3Y'hUOtti3)/);GFfFc 8n6/>\6HCqhTSH,3B%Hj'/#7u]h/,Hc*[27s'_Sj/!KLH#l%mB\ooB@*]?LfiZ?g"a%5m/aReI#_[;=M~
GhS-WgMYb"%"6H'¤A]E`>a,W#@@3Ak`O66YFp!o$%n3b'[SmRAjZnuu99F]G,qG#335^]n(Qph7U^h6D(,@]jQa"\6 `m'PgMj4RiL;J!:n5-A=P_9K%q3BUN¤U?asMFVa*Pp)=t%6C`^5aZSoiFAI]dWjeRL:Pf''qr_a;)-:¤491MslN,`] -4u\,[u,g8eHLl4[-lc'jE%)LHm!P;TAn\-k@P/ERdQm",;QjFX+U!l,.k;=qD6d/[e-*MOC9^Ul`VQ!ik?FYGRf#o T?(/t2N_:s_)9`+"Kf@lSn!L`7G3%(Gh`BCD$t5Lf«NY%*qD-7Wibt,!hZbUX_1K+]l$caGA\%ZCfe/-m/>jm9«71L X#,«SU2q3U%l,IH]Yb2"9r;%$Hhd7Kml#:7%+.D5KX_:P-!YA"G\$hr[3/Y,3#1uj]0)V1qW*/p)nK/jb6O46SUT\* HQll$e"@HtiI;%j\"sQ,%O@#C4PG>onF@VG?MaVHhtWWJ"7sLniqJtq/1t«=+/"P$?$HV1jZ;Z:i/n3sjJ«6N6EO«+ 1@u_)[4¤"C8M-%8MCF«)@s6q9*HMbOHC3h#,\:$,=Se#eV6hfO*,np[@nloO5SK¤=V(Di$hB4hn*5t-¤#@W#iR!mL> g_[CKF@$P(ej\p?LOT75/R,8BXR8t8^*OG?f.KL\OtkDT(¤7s«Mal«hckMgDT¤Dd?@E`'4,S%s`nM-6GP!hrQQWc3% %^:u9-+pDqKmdu@4)«TUEdWotUnsY#DT-60-A:_^^!t¤NqtYqR4ML%W`\BEJeS!noV7q7/\*d-bTp%J$]'7#Z?i(\T KE~

 見ての通り分けただけ。 Cooley-Tukey型FFTの分解には時間間引きと周波数間引きがあるが、最終的には実FFTを計算したいので、時間間引きで計算する。

回転因子の展開
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < m; i++) {
            sum.add(W(inv * i * 2 * k / n).mul(src[i * 2]));
            sum.add(W(inv * i * 2 * k / n).mul(W(inv * k / n)).mul(src[i * 2 + 1]));
        }
        dst.push(sum);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(z.sub(src[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSEX>ml6L¤:EqJbi«!,Q4ZDb+;Fd+TSM_sr#j*C`R@#,rd1]C4__86ZcgY\bdoFNq9iA1eEFL5oL¤G19'$k/Wj(=H REg3_Z+=BRe)kn8n0pS%\puCZ0e0hEPKGE_mu0?!,YE-^,V0LjB'N2s5uJAj1,AAOZE_%b?jB_$NPT(U)(2q%!lZ`o -IjWeH@Oogo(sC.G+V4BeM^mPVGRj$nE4da264HP>/OE#"hLqD;P+h«OPgR\c>1K~
GhSE_gQ'uA¤:N^l7]6hU(G?o=Wg^6DWpNiPHBY#ULDB*#PH_;_*+V.mZJsb'FtP%5fX;CEAnHG9G^LjR16]EaWJ"i_ ¤oPEp8Z7;«[M0cEJ«(%3VBfuG8[$)k%*0sAei]«;Xpc2/WRl\g#Q0s)%I_bV*J`BYVk6Ru«9BZL¤J`.s7KX3YSP*er Qq1oQga_rb«Ym«Mgj3q78S:L!q*=3V7>c:B*h-djT]lJ\4X-ST]/hn^!jZafXZqM¤CJ2u6ap`'b=FV>oBD:f7_fNo: Imd¤;\;BRP%^)H.M:YpHkW*5l«g:9FN5n".U@!LNd«4es-hhm+'%M1!$NrY)CKf¤WnpB2iJ?R`r/D@4);r7JG`22"- r)B-"3RPAG2l2I)"n-F/«0IV0g\pp;)-:$'BSL1D:I8;Sd3W¤dUjlqTE'5RPq:HGQ2cb2`VY/48M)KI^30Xq#nnK4m -PA%>;E"gTS\jb«\"VZ0`1_aID)`'([if\UpZlt^)_MQCk#_/H1Ld]#KFtkL/hXd*#.huPclKoZH;EUu1RCG1«je$H >3+07W=teBl6.,hKOM=RDB*j2LqmFcEEM2a,\:$4=Fsai?Vi$Dqf^KQp`+t[3LKe%0R;_83W/(¤dZp«IAQ/^PWYJhg >A#u@a?f.W0;OL)ioQZeM=Rq.=%`!')«4A9.H6_ROH-Nd0:6'(Y0/7nE>bPY2UW.+m%m@LO«gilR4+:F=;ng%RW1@' b#U,lpi87F?]Jl>D0qTCrDC9cDZkgSprNDiO?LaXg=S(957rjp2l%tHJhks6ec3m0>*c)5Bm2#%cqtX%?!H:W#MDeq :]~

 Wは指数関数の一種なので指数法則が使える。

\begin{align*} W(x + y)&=e^{j2\pi(x+y)} \eol &=e^{j2\pi x}\cdot e^{j2\pi y} \eol &=W(x)\cdot W(y) \end{align*}

これを利用して展開する。 この段階ではとりあえず展開しただけである。

回転因子の中身を約分する
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < m; i++) {
            sum.add(W(inv * i * k / m).mul(src[i * 2]));
            sum.add(W(inv * i * k / m).mul(W(inv * k / n)).mul(src[i * 2 + 1]));
        }
        dst.push(sum);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(z.sub(src[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSFCh$Sa!¤;BS"MK?>oWb"R0('m?)¤9JLiLd*/qX16jodCV>@)FRC3m*7;+c8Xd/YU2/I$ISdC#@Ch«J@JKCR:HZ* onM]T0Ti:I(9ed=gAnD_18RY-9%$*rB.Pj,VpU3«\!+a5!L?2$'%2_D8omNq6I[+cmi[SVLEYEXHBgcg"(QXI%¤(0_ X0b'$f«k:B«qJ5XUM¤lp#Q^iSMBC4!/\bT?_t`+8/aV=oTe>`FdK`6h¤j-/R0lFo~
GhSE_D/7l[¤H88.EJ/b%ntIIbNA3(,AR!2_Difp,-t-+mV\ZQ!+N_f"aa`2>IYjUTX>Ph\hjpA%FF)5e74*F-;@/7\ -/AF>P#)IWCa_Dg!¤#(U9M*kjPMV*i(m$\@X`uNW>T#:«;m-«#%eqd1)Et%?.LnDV8jATpW6@3"+tA6PN!:C;4-nOm 16Kr-\Q.iNXY*^$\TRB/-Cl¤$j«LWk'IhnRH>;lAJ>R[,on\:hn*Amo#fs/#\,HMRX=ZTu-o7s$@f:F[SNX>)¤'lC0 T8B(KE>.#**F(l«'!J^ocu[DbX=/EcN/'2;U@![Sd«4es484"?'%OGb$NrY)CKf¤Wnd"!JJFD8]/D@7);kEr\`22") r)K3#4jgeKf;4Et"n-F/«0Ku1[X7fB'k\IHBSL1D:I8;Sd3W%9V12nQF>G.Fq:HGQ033?XVY/48M)KI^30]INnnK4m -5%rhZ>L*?5CpFlDF;)>M*hA«gMe2Dmm5s[n(6mFf3B/[cE:fpVo31e"XEdB?(?)s*HW+`TK@ZMooA+01RCG1«je$H >3+07W=p7ll6.-"KOM=RDB3p3,C«ha\¤>sOb,i%V!-c^NVb$?p`MG(D*«Dn#`t#KV+¤KR1C!5WB$_NbW$+@$LGc5GD l%gHGX`m#R=7d:^i;?iEO#=[QMJpTR?doGqa(0jFHq"%MjJm31:8Nik+AK).L+m)?>nViB;7Xn=Td7IkfVR/XdfGDA jV!8#=-S,R«'+mMZ+TfA¤uKq10GMT@mRMrs;#8\]If¤qp?p).L>uBaK\IPgW3H¤V7SjKL6KoeVk@F0/!KsuPl0@3A~

 m=n/2、つまりn=m*2を使って書き換える。 2*k/n2*k/(m*2)なので、2が約分できてk/mになる。

回転因子をループの外に出す
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const even = new Complex(), odd = new Complex();
        for (let i = 0; i < m; i++) {
            even.add(W(inv * i * k / m).mul(src[i * 2]));
            odd.add(W(inv * i * k / m).mul(src[i * 2 + 1]));
        }
        dst.push(even.add(odd.mul(W(inv * k / n))));
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(z.sub(src[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS-Rb>*^U¤A7TL'i)XV+;bb*nU]*_",l\W4:h]@=Vs%*//kkpD@\]0«tFqS-C(UrWek51(RK"VJ57\UOfD3FU$-(= Q¤W`B;o2M'\L?P?2t0-=?P[=4V?O$k%Qf8pLt4F//^=em:HG:_LM_((4E$Xr\4(p0f^rt>$?e-0gVqHp/FQ)k2,\Qn I8..*+7RY,q^VVR7,bm/q¤u*¤*#FSGIt]5oS@P.FQ^#%X2.\ZPo[¤"N'nX,XTf9pF:r>*=NaMOGIZ[\@S\I1CkUV;j erXuKbeOgNL4!F.CT``(«74^H'$b-Ln2N?>Wq5tuH=p2miBG/#3.:+bI`D%?IbVO$>H-/8oaPN-U@¤~
GhS-WD/7l[¤H:NnEJ/b%ntIIbNNk#TAR!2_Dig!.-tQCqV¤$K#+N[?HP5i7[r";#0+XXJ\H[5%ie"$o*6:Fr;$5U1t \G]E«6Hb1d.D2/N?«YuJ.p+Ts?cod4aZIY24J#c[/X];roE;TM¤>e/!pmc!?«3.Ur-s+FR,-na+>/1Qd6]d^?CM\4J 6lP4«>"m[KD_/4m](L9:c`NMuJV4RL`Oe¤^m2RTh\qNn=binaQ"Qsbu2I5)B1YAoaH«sH)%¤c.6LhqtGDWK2\aJ0uB ZQa62ql59%BGG(3=_m6ua=q?qp@fd!p7S^7$+I!'m+4"RGUeO;b5E$*¤#6P/YFtH5e("I`\P62s]Zb6+pVRo92X^?U HN*c6c]qV_D+kjmNuR28S71.Njj/-u>16p7`(5#Yi[q/ZiT=93]BETSYDCR«:5+#2.ZYo5S/b?R6T%MjHEbLJd'u59 E!g6O0ZO,$DJi)bp,e@8=Enoi2oXl'-sQt/Y\nj-V1n`q`RWj+E@fCD%%j2JO$37«]u;Jh%H.o`O':p^$;VhD'q,P7 -K7UqI@DD@EI?tPZY;DBP-[R%)(m5uRR¤r0)0=8Qm7I'"ZB#lp6:R@^#cua>':mt-JGKk«[Fg«Mh#kRWT6S.;!9hin SkA4'V)ZW¤$!TK/RuD=0fs3:«_6a:97bj)HYr_HN1`WN1!mnP?e'"lE¤amYmf)1q0nEaN'1aQYmaHts0bg'F^ko3uN ($?I[k6T)oXcH>%3Xu7uM,5I(=Qetfpc*p5D,dghPtgQ#I¤N$gD-2(Rm¤#B#o>RqV^@EZqA:r?!SaDKqh[%rr_«WUs 9:8"$Ra9X`-MshAHOHgK8OVA*du(X7)SYIE~

 W(inv*k/n)の項にはiが含まれていないから、ループ内では定数である。 つまり、ループの外に出せる。 今は一種の総和を計算しているので、乗算してから総和を取っていたものを、総和を取ってから乗算するように書き換える。 乗算が必要なのは奇数項だけなので、偶奇別々に和をとって、奇数項に回転因子を乗じてから両者を足す。

kのループを半分にする
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    for (let k = 0; k < m; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        let even = new Complex(), odd = new Complex();
        for (let i = 0; i < m; i++) {
            even.add(W(inv * i * k / m).mul(src[i * 2]));
            odd.add(W(inv * i * k / m).mul(src[i * 2 + 1]));
        }
        dst[k] = even.add(odd.mul(W(inv * k / n)));
        even = new Complex();
        odd = new Complex();
        for (let i = 0; i < m; i++) {
            even.add(W(inv * i * (k + m) / m).mul(src[i * 2]));
            odd.add(W(inv * i * (k + m) / m).mul(src[i * 2 + 1]));
        }
        dst[k + m] = even.add(odd.mul(W(inv * (k + m) / n)));
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(z.sub(src[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSE[gMVU.'R]X*-9U\,LmRs;C5ZfT+4-kq`WlQI$*Y7!>1«-$H]D'%j/"mF*-0?M0Ji#SkR«5Yrsjq?TEiMgRr>]i 7B*sE9Jp-_V]sGL';8`BKMGn6Ws't+eTh0i1Kn"¤TKe=1IBZ:VUX@¤rDKY6«:X9«QGr8g%)+5GYLhPGIWBELF;i6(! :/Kt%F14J/Hac"j9kl5.bMB[:J]W!rI@CLcn;¤-W@*kg¤HPh[#DjZ>kV5FnIX93XCK+4F$A/,?A6CLGns)_A"iF4D= JF+*¤5/PH4=SW5`I;gpOjnc]9h`5E"d5K\G,*>o(`dCE8*Y%9¤J=h`¤r!«X_rXnq;NeOXO>*\n]e1-iMd«eb«e5FGA 8-h¤1k2bhTW55!^Yh«?rJa615>>^96r$V¤5T^#">Qf3(gWfHa#+i[e^WM-K#W]9dE"\"hnVQj>i%n6>M~
GhSE_D/\,^¤H9tY32(c)$+NfGdkjOlC2]QJ2i1«.:ncE69Oj2Ea>#9CikOsK,YldB[^.)0p[[+qA)@HU)\j9Z.9L1C jlY_Q'j??¤9B!Si\?mq/X^K=Gp-ijf7,CbKk0_t'D9-6SV`i],+A@$O_oF«6-Xp;+L7-2DhLm6)6joBuGR-(4pBoY- '?cn+[2.6"e/?H:ORCrTA94NB\gC.-1«%F«p[¤kkBY¤soND/U9Z3egOM!V]n$YHVQ?0O@«Q!kmPX)B3FS#^Gfd37Su $^¤n4dRJD]K-R`31@=-@"thZiI)s-VM"MgRf!aM¤FVFXKjWRD¤d9X.%+n4N*D«V(#I[,,JJ03Rb/A>>$VV:oUKhK*@ rqr0KIrru$\?1cGnaI#Ib]7`_A\4;dhsb>QLM`5[W^eD]U?iMahl3i?roqTDa`WDFTl,tRd=aD>#8G«Wbn?_Q9g!9+ m%,UbK¤^Q`jFn3FB5k+(A9cs^,f7@>2V]!bmm5na#],U\J_-mPr/N1!1H=«^HJb`3I`nR=q:Y#cT/j.kSA5t*5T_*F n.,««gSd>i'eT!UP8?0p«n8Ob%O$b)LVb5Q]50,"iDsORl]=OUZD^.!l`eQa1Y_olY*¤gS%^>:E81CkcL>-ZU7E5ei ^pa[bG`a4ocD9R7pU¤ioYk9?(M9j%j>42gJ!\-fVK+`:;==pb[B:We0/dcC]P«87jQ,.N@OqlQ?dR/ZlcObETh:!9e >$DD«^F*0aTaS$g),h"JKIK-*mdst!h_,Z@'J«s[O>1@dfAnU/:0Ie33M«p?Z4t@0#fc5=MbuArc3@liTh)OIf%#ZJ E«m6C«O1*hkLI;W:cg2^Z!t;q6\6*6L>d«(k$^s)%YkG"[blR_-"r@s+iiDj1,hT!=8f8)^?k(_;C\*T,3lf!KpR;F PqLt~

 最終的には長さが半分のDFTふたつに分けたいので、kのループ回数も半分にする必要がある。 ループ1回ごとにiのループを2回実行するようにして、kのループ回数を半分にする。 今は時間間引きなので、周波数側であるkのループはkk+n/2、つまり前後に分けて計算する。

回転因子の最適化

 先ほどと同様に回転因子を展開して、

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    for (let k = 0; k < m; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        let even = new Complex(), odd = new Complex();
        for (let i = 0; i < m; i++) {
            even.add(W(inv * i * k / m).mul(src[i * 2]));
            odd.add(W(inv * i * k / m).mul(src[i * 2 + 1]));
        }
        dst[k] = even.add(odd.mul(W(inv * k / n)));
        even = new Complex();
        odd = new Complex();
        for (let i = 0; i < m; i++) {
            even.add(W(inv * i * k / m).mul(W(inv * i * m / m)).mul(src[i * 2]));
            odd.add(W(inv * i * k / m).mul(W(inv * i * m / m)).mul(src[i * 2 + 1]));
        }
        dst[k + m] = even.add(odd.mul(W(inv * k / n).mul(W(inv * m / n))));
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(z.sub(src[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSFEb6l*?¤4Q>B`Eah1q>s*a6:(,L";g¤aRL#`a.Xh(Wr-'N^[M8p¤M]'$mHflY?!,jX[bog7#!Z^d#$#qLj=@B.) C;"¤$(A8«-1D]IL*;9rCP=eE8;¤:c^I7i"SEPK4MVAWL1$:H^,oVp*/W«luX4>]R!Q0BBdA6WZTFD8)i(.1?.%¤if@ R95?;]bp2uD/¤kRit"69,8VN;+iKAP/Sc_`JeDF`rX#Y\?\+:7bq^qqmWj0Ufp^\KCaV/+@!EXY2g=V9Dj%8t.]/!% R4?A'fMK?S"«#4D¤H~
GhSup>Ar4L'RoMS3'qtrJYZ+VdkjOlC0RT-]1I,06\SQ"0o\Wg+NG0g`IHdM',%3r[FHHlbdi-#cG_0HCoJU'1'P!X 94iW(-.a=bCC1SBJ=#D1W>2L%PF[UhO3¤=Z/m(4j9H,`,da8YCO!%%tT#8TB/lKD94ek^47P?!MBqdVH@F,6¤m;n¤" 09_2SFDB¤j9rToaZB/t[h..1oKt8Qo6qQqFeO)-I¤t3^3«?idF?MiY3Nle/X>Ajq>Y`55Qlkk%JXPQImQT+«Q_'.mt RE0E:2N_:r_'QJDACR¤`kdm«.W4)%Vg9IeHBl*ri=Spa+,*,jKM/J7M(kMF*CJ#,1ZCAM/"26RTKZX0:1V;B8GIY## a%t7WQ[=.`phJF#Uk3g#;3mCZ%,:aUobCPDg2EG@6LC1#Mt3+:r*Dhbm[qcX8?O1"($Z:8*"8frE^6«,eR?VNX[u!R `n!Po0qA$U(r'Q?e!f)+«JHE;%j^6@k4[GenF7PF5'f@VHYgZSJ]Yma`U=,«>Y\]-+/"80%_RG>C__0j0W0%r/0r!j QD'p)g4q5>*t)knMN0+KLk:Y#d]o,B¤++YTFG.5=g-t_U+DGLn*qOD`742%!iS+_H4Ne=@H.[)m«\)DTeU35g.i¤oM HLF*NTVB/E>;g6`Iq0jM.P#%iTh'JX2o-0«3UC'`W:tn\pgWL+D5;L(0A)J=/VOW26JCr.=j/o/77hhZnh++>+d+,c og`%C?P'SlA%G$c;,i«oLi:k485h9/349rZ@!p\l))kSQ;fO.(q?8PY?b8JD3J+"Q1^R5/Sf-j/`OP9POB*i.lAF¤¤ J%;[>«ji«m@9q2!>"l+247k=e¤jeK9X/NH)p@AYXH[Ms+3_X8O7WS/LiGB«X3EA0sTgYLA\Dq0EhYmTPJj14~

約分する。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    for (let k = 0; k < m; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        let even = new Complex(), odd = new Complex();
        for (let i = 0; i < m; i++) {
            even.add(W(inv * i * k / m).mul(src[i * 2]));
            odd.add(W(inv * i * k / m).mul(src[i * 2 + 1]));
        }
        dst[k] = even.add(odd.mul(W(inv * k / n)));
        even = new Complex();
        odd = new Complex();
        for (let i = 0; i < m; i++) {
            even.add(W(inv * i * k / m).mul(W(inv * i)).mul(src[i * 2]));
            odd.add(W(inv * i * k / m).mul(W(inv * i)).mul(src[i * 2 + 1]));
        }
        dst[k + m] = even.add(odd.mul(W(inv * k / n).mul(W(inv / 2))));
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(z.sub(src[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhT!U9+¤Ni'Lh2fiXV0lq?'0b6:#SpJcg!89nZu>8q/>MhL0+G«mXEdK*6)/\L6oq\Dp/)$;mt?O*u=3!=lXHZ7n1= Y1>-uJhBAg?k.D%EU2OCZ':IjFiGWVVat)1pZfX`S:F$X6V)D**c3]Hg4Z[9([k4T1;1AcCr:(I6WY9Jd>-jISOXd, 0¤nE/YH?¤=4¤3I`,)Q7%KYXgFMDgo1ogB2$T3«np]md@_ZgEn#oo=L52RPtJU+J*l-+]%[;m@XLO¤«lqbj:ds/5M3. HCY;U:7`[~
GhSup>>O!-'Ro4H**%?igW$TkS#1=>6JIkSm3\*T#D@h'>[e:,Q/b1[l@7]rS%9(4dBTWEgiN;hq:nl8c?3l5WF8sK `U/_s¤k8a/6/d8'n1ql+NJ7VCjfr'HN4bj#`j:N``ff;BNqQkg#AM#T$9jbj«es33R?kFiAtI`)¤0kQkK0DifSf;Ki fj%S%X,RrRV;>=S[«1O![[^GEobE«,C5Nh2m[IjfU$2nf2,_iP71@JJN%E_nou=4HD=kJM__@hEoJ(AD^MHD8SjH1q 6rroRM«puI5+qQqUXiO7fC',>fWtB^a))«$CNn]kFc?:Ka\uLO@bT,AO2-g#CJ(3ajd/tO"aoJkBDLAb[%#kF2n1]S j!W@Sl(^;kic\8?,f"c^[¤h=L*o$_iobCO9g2EEZ;Elr,(3Pu5l;"83h*2pPONOBO(¤A-0S.',e3M96(C/tUgAb/cp EB2FN%gT?n_s\G¤N8\tKf>kk1#>tg8f«4L1pim;V+2(u#4atSNKuqHaamTOU>Rk0B+/"80%_RG>oJZ%-iL4Z5iZQ4" E"(cWd6«dtp/¤8A$ZtHuNa1%q9fh]_i6:lSW@-1adk7r`#=Y%2H¤Tq7'-3Ul#iAoY?meGqj^H¤P'il0nZg2\.(g¤ao kIgn=J==L7D:.!Ns!'#85=?e5.kbdu«jI(ZScoO"m(J'a/%sdIkgARWS¤EE;^"OXG%?[Z3*`7;.(,dcS`NtH"%g(>N ;r?#JT$YnQI.'gA;31fFnJV-)j24>Tq4V«.c:Wes0nB@PmJ!QW$k=InF.l/`[Ed«ZES6*j%4YNHf"L9rq5orX[Ue]$ ^7@f:4kgh)mq86SKIh,+;.odnk8/aDpsSaO3N((3m-khND)cr,F?3@"AX/1Tj!Q/0rW4=J8M0~

W(inv*i)という項が出てくるが、回転因子のパラメータが整数ならば、その回転因子の値は1なので、乗算が丸ごと省略できる。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    for (let k = 0; k < m; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        let even = new Complex(), odd = new Complex();
        for (let i = 0; i < m; i++) {
            even.add(W(inv * i * k / m).mul(src[i * 2]));
            odd.add(W(inv * i * k / m).mul(src[i * 2 + 1]));
        }
        dst[k] = even.add(odd.mul(W(inv * k / n)));
        even = new Complex();
        odd = new Complex();
        for (let i = 0; i < m; i++) {
            even.add(W(inv * i * k / m).mul(src[i * 2]));
            odd.add(W(inv * i * k / m).mul(src[i * 2 + 1]));
        }
        dst[k + m] = even.add(odd.mul(W(inv * k / n).mul(W(inv / 2))));
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(z.sub(src[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS^Mb79+X¤4Q>B`EfoO5YYda-6sME^r1qH6"igqZl$V\N;r;['ejOT=`iACS@R*¤KKB\?!CV?j+enO:@O)0l=(u*I =KYc:O8DJm¤MppaNb]/$/#8u.;('Qq^!"ronO\p«MEP(E.\p$"R#me(P7teW>dUAgb'9¤5CsD;\LIrRtn=dCj%s],V O#Q=VfTOXJe%@;i"T8LU^p¤QM+(W(.¤Dt\+*;aK`2)eZ=RuHoa9>B(,Z/G$o(2CFmq¤"*hM0H04G9H~
GhTQ*>Ar4L'Ro4H*2,OJ5[:7pBc«aF[B7oRh'>¤STrM-P0o\W]+NK^>`IHdM$-$\3`¤>kl`¤[#UoB10N9]df_Z;«5( FO¤.?GqQI_acaMN$ZKcfk$#TaRGpOc77(«X2WMp3E,E[(5_grDZlmY==i1%[T4MJ7Xc*Wf)Ej-UTnsO=Jpn5@DVmNl 4%q+sk1!QClV_@Ho«!,pSu*>D/_)uj+Y¤AY0-9FlW1`AZ)Ds:JS(eLE-OEp7EU/Wmc'miV`K"@l`TfhOEdlW!*$%A+ N^V1U=WQ1@*LoKt9nnYAL5t"+,/7^Pac?68«7^cY2«thNd)fdh$)3p7IY,A]GH4@IT^o_#.0I;)PHo9f/'KX>]cA`u bOOb:rp!XX/$[a'1%F`C2_I^CMT9l^5«e+D8UGTD;Q=@VcD%«O?ER8?hu1I3,q3tq«M:J\bL(ipPlhA)W+MGopEgmb )B0=n-ZBt2'E!pL>\h%tW]DbE4ci>SR+ehbip/2Wq!*#in*pRJ#0*"mQTrO/E]Jb9]Rr+;g[GF$P=«pf_3FB*@Ni^C YREmSd2iunnl3,+k39r=*P¤!klZDKti69d4Pp=dEdgiZJJXeKbo¤uEU.ltA;!PTRFi2D?sb(JF-NLumlYGX«MdBg'M G4c?p!\,+kDXteRq[["irkL$j9Ui[/*UfhW5i-;4^CF«=56MAeq%3W86s)S$#B1?"!!KCk1fm`!_3`[Pn%OY¤1=AZ@ 4sRgMFB4=".t#TA;WQK¤,3ZA"DNCgGYo,6Z$:U,7YtM29V`bknYEG/g>7^GND7-gX2;>RgI93)rrk^@E54UZZA«#Du >2(^f57t9CMoU,5L,.@;rAFT.T=ufQp-^B5VBT`dDM=//7Z8`#~

さらに、W(inv/2)という項は正変換か逆変換かに応じてW(-0.5)W(0.5)になるが、 \(\cos(\mp \pi)+j\sin(\mp \pi)\) のことなので、どっちにしろ \(-1\) である。 したがって、乗算して加算していたものが、単なる減算になる。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    for (let k = 0; k < m; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        let even = new Complex(), odd = new Complex();
        for (let i = 0; i < m; i++) {
            even.add(W(inv * i * k / m).mul(src[i * 2]));
            odd.add(W(inv * i * k / m).mul(src[i * 2 + 1]));
        }
        dst[k] = even.add(odd.mul(W(inv * k / n)));
        even = new Complex();
        odd = new Complex();
        for (let i = 0; i < m; i++) {
            even.add(W(inv * i * k / m).mul(src[i * 2]));
            odd.add(W(inv * i * k / m).mul(src[i * 2 + 1]));
        }
        dst[k + m] = even.sub(odd.mul(W(inv * k / n)));
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(z.sub(src[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhR;cbmK(W$jZ:6:[pZ«q1XEU0a9e"4i(M2n_gJNECR05(rLQiOZC'V8-ir$CQq/I*rQf0P*J@BhH]¤S\K'mpEOjq2 e!E75*cXX\'0ZmaVpd[X[!(o@]JuRUUHsR!_UcG32Nq/C%rk?dq@?@sn>io6Z=^ghoD+/VXEXaL(1Dn#A_OL8,X`i¤ ]8j.W*G=2=>¤msZ[V#V9c5-K[7q)~
GhU\JgQ'uA¤:N^l7]6hU(NY^B«MaN>C:dKBhB]¤n@>2;L`,$t;#«?CIXSDOM9aK#W.UF¤[ilpN.RIQ4«o9BZA9GPcM ST+11PmArDZD$dC!5,Zm/$33s9R7pB+s_(T>Ol`B\2IkOTVD_X)A'hLbMZm!/7Yq64G-UE7]ulWC8(ljU#,m=f]N:i ?7(gQF)!9r«N.d?a^2Q"GEYd\QKjV-¤-^¤M=3oD:$>@#'>)c5>mbS33@7]rMDXB1>XH¤lN*#O!$XPQIm«Y+lEh/gZn $^$WQ>X`g9#?T7raJcPucuP8B>D?t-Lj7^-_V->AZ1bHKAKL>C¤7(WL%tRNO«Ma>)q*S37!T1GE=L;A2k-4TN""l1] nQ5G^m7Ep#Ys,NL.H):bOCi$ZBtK^=?ZEKISZ57.eVT-h.¤MZ1>88-QJ%D¤29F!Nll3XNH*Dh6+5bSV://INu6mGK; pI%:XL;#@[rtqoHk$+R;FKt._`59=,Nnjs_mifM;]Br[\YIQJDR9i[=:Pe+Q"b$.G9jBU8*AlsFYh=R24`613'MlS$ oF",MOG;^eiR+d',PQ«@SrTo#Z'PW,FrkQBTba**OF7WPb¤#72?1+e-327P/]o^#uNnDL,MY!uU@3t@X1Se-YYb9SE !,;NLotB[kERb!6«U'?[6%*c>ANUQJ.«UnjT>e(5j1d+Wo1'JqMN«e`HfSN,r;]¤WEuE#oNthAPr.4B«V«i(T2-44T huV8[5'u[+'VgK5b"O¤p$0T=>=tqWk0VG77(:sIqfJaTr\$DOkcb¤5ides3^E0ENX+«i)OEMqC]gWqMuGIb?NcR«k4 >5=_;>@q59AofSibT[WWh/S%U^B)1_rV~
ループの共通化
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    for (let k = 0; k < m; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        let even = new Complex(), odd = new Complex();
        for (let i = 0; i < m; i++) {
            even.add(W(inv * i * k / m).mul(src[i * 2]));
            odd.add(W(inv * i * k / m).mul(src[i * 2 + 1]));
        }
        odd.mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(z.sub(src[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS]d;+ne\¤:X)O\AuOrduB>1«@B7ZqBYhip/'3P_jWuVFA?BHh7IN04'*.Z-s(HHpYFYlj>j$bA.%i`\V«5%W\G\G CS7t.(ZDXHdOE!j*'PSYFl>L==oDCE%38g^COC)\#hC^Xi>/R/:ms.:@b?I9J>rs]Kb;a6X\'['¤RL_Em«"l:S=LI? i`K1V[-2%6SE\>-d?%rUSVrUCY+Eoca`mmWCnAkmMSai[d2@!)nAMsQ\=HbI.38#4")Z\r*#3¤,Sf¤\(5iK2(9@/'^ S!php3l2@\??4msZAm;M?g5I;[7#/JQ=_L6U;Uq«8^MEc[iBuD_oVT';q@uns5tT_0sFgc*#7om_fPu!;]O8(BlCK@ 4F\rlm#Uin=41MYYca¤4pu^ZSBB!7`!CR/Pg>2LY)Xu>Tn)0lg%QCqJDu3U,i%4^+8uUU/~
GhSupD/\,^¤H9tY32(c)JYZ+VdkjOlC0RT-H^![C%op$KM,UK¤%WY69WVGqo1O3t4R¤8+0P5+I:Hi3Za!Kr_.XNECG \]KO'[`YCC¤R0=$l2_A>mE0*ELGh@qCDjH[4C!bgDM>?31?$\'CjQ_iU+"rJSh3mO/gI,r(Y5Z3Ri>rbb[«O7Q-kl$ LTh^I!O=3I'fqM';PEk\2!dODTu/StYL@op"%T-/g;_q]R^m:=$Zk%V#:$#k_WGoTPeL^o2P*+a0:pl!QI(!eSJIL\ mBmHWig=S1i/69.#+MiTShj=S4Xt":Oc.aa2f`jC3VZ8.CmDGkh3N[,ZqN7Be/AND1B_jC_6]+"L6b:G[_.%T6S"3c .[fO)q[inuB5>g75SZ4H«s«sdFqH>JQas6lIT5:s3ZLA,_d2UdR?e@`os5RniC8+/NcEb?iQ#TD>Ot_HV,2Ka`Hd\" .Z-_:2;(3aS[61$fTQ(P«=UA2Dd1RJLZU:UqUR9rSqZl+_\!mcP;.N!E,(>VA@c"u«^!Zd>.C?!TAnraA9\t%IJ(O2 :_r3VVrp;-N4>Cn(u@P#Du9#.qT2+[+2Jt9[Y5jc]d/?H@!i*¤*'^'3_J2,H7n;t1W¤>Y7"«LH*?8t7RF«)M$,KP+# -',WBda[e%p[Dl>J\MQ1;P(7I\%SHI6R=DSY)m,;Jj:cr![U+V.6?r(mk6e:80'Vf=d@p;«=/XpQ(V=rZtHd%FK-?^ B'?)Zn7WeV\UAlS!O¤k[mbGd5qr^?"bii«G)dS6W.dIS4i¤`bmC!+GoJ69*a0KWt\E'8#jNq3[?RK1@d933]Vdpc[> Fs/`$'2,#UNOL."nCj[1gTIpJaed_«ga:gIN(nrrh,+bn3IDOgK!>b+%%M^[I/h,CDOk[;Y##oqs6@?$YBa-gAInMK V,ad^EShd!q"#H*Ain9~

 ここでよく見るとふたつのiループは全く同じになっているので、片方を丸ごと削除できる。 また、oddに乗じられている回転因子も全く同じなので、この乗算も共通にできる。

 JavaScriptの仕様から、代入はオブジェクトの複製ではなく共有なので、evenoddのどちらかは複製する必要がある点に注意。

kのループの中身を分ける
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    const tmp = [];
    for (let k = 0; k < m; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        let even = new Complex(), odd = new Complex();
        for (let i = 0; i < m; i++) {
            even.add(W(inv * i * k / m).mul(src[i * 2]));
            odd.add(W(inv * i * k / m).mul(src[i * 2 + 1]));
        }
        tmp.push(even, odd);
    }
    for (let k = 0; k < m; k++) {
        const even = tmp[k * 2];
        const odd = tmp[k * 2 + 1].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(z.sub(src[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSE[gJ3Ad¤:N^l:3^70,¤M//@$O4A¤%%«,KEhVu$/L;S.5n('mL1ZV)IqbP]«dncF6:]Jn¤(a-eeLliBCL=;oj5Vi Zmot!6K.`OA^j^r+]CJH@8@"i23>e2WZ+Kd.9p?INIhJ-3>PU0m0t@(brfVW=]-RPO6B;8_:YUoEXKB!\U-kfVE$JA Mn1«PYZrb\l_VEI*oQ#i3^%W@6/Qd@DBYoA4qD^/A!jSAlh3Q\$s"X1'.¤M*g,HT.7OP,(Ui9kVjk>5P;j`r*6?q\Q K4^/m(ctQ5\ItelYgErNZE«#DFI+J8nYbHA^].W_5]U*W1D#E\K:HM,h@BUKO7U'Ohpi0gd,,"PaM40eGUd779g«s# U0l,Am5H_]8hOM5;n$ZmbSR`MS+6~
GhS-WCMVZY'`FV1E=)Lbjl%P?NA3(,AM4775s"icKN5GA>/Z_Z/@T>Ns8/t[8*%T/2+d/uhL"\«OcBiI+Tj;O#uRQt VLK@1=!AbK$;Uc>96"5c#s1pq?c7t[j:1dj/6lqe1mu[h5CdbS6,N145%a%;iOcZVaCMGR¤a2n#`F/eR$¤I*d)f0SP #Y\3*/s?=^U'AS217+"imTCOKna«"R/N«r'g'q(]3/j,T5"pHF$J$>"'foQ6-0-(dWR^k#]R¤iM=@:OMH6XW1jc'Yn _N=+ii.g!2!IM#2Sdi2NXf7\tMjtTmrQ*INOJaa":5bp+CmQ-CTQLp_=)/Z+c7j*>NfH03$X*dDgrX`ET::,A¤l)E` gs6Q,Zs.bH),tdKp[*JQ9ZLK;\BT-%p6,]I[cR9u%«m501«CW,h+q4!M]61e3QGjg5JOtlduI27.CkNf.5ms0.*E]D E9R;cb8¤-!J«lGoW:G%*?/'tL0arC8QZkLaSVUD^$"n5F%Yjaa^njrqO[s:JDBh3u@UumLZI¤tD'!>YQc2f#*mif[) GjHp¤*gk«Y]XkcPZQt:_"jGm,%.S«CmpHtqKYn+4k[CG!,erMs[4«Gl5Sf*pK86auNi;/tMN6oK0p_q8N(:''Tb4/^ OSp¤me2'!JX]B¤tHqe>LoK`O3.`C^;mhD+@W«6RL)[K@pBe?[I^Zj>((3rR^9fqN!=PD[4X$5s=[oO(Q«uX4Cs!XQR [#J;DrTou0i9"-YejlcKqG8\9`O:thTW'BEWgiVGOAi`F_;tIkA#Z/fm4OphHR]Y"6>!/PgQK=U\EFL.5#G".=c.Bo _T[@D8_:sIms$nUP(\rUV.LPd-SVH88CXnZX-eCFe_Z8P^$>5MR#"ddoPcZOZQLT*O632ap\V.tA8gXFGJb>A0kX2W b'co%3GVD!,9]Jso5JBfNBdFCmGSdM~

 DFTっぽい部分とそうでない部分に分けておく。 DFTっぽくない部分を便宜上「後処理」と呼ぶことにする。 evenoddは一度配列tmpにとっておき、後処理部分でそれを読み出して処理する。 その名の通り、eventmpの添字が偶数、oddは奇数になる。

iのループを分解する
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    const tmp = [];
    for (let k = 0; k < m; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        let even = new Complex(), odd = new Complex();
        for (let i = 0; i < m; i++)
            even.add(W(inv * i * k / m).mul(src[i * 2]));
        for (let i = 0; i < m; i++)
            odd.add(W(inv * i * k / m).mul(src[i * 2 + 1]));
        tmp.push(even, odd);
    }
    for (let k = 0; k < m; k++) {
        const even = tmp[k * 2];
        const odd = tmp[k * 2 + 1].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(z.sub(src[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSEZbAMqd¤A7TL,>NCjKN*dBPrsk+o`mh56^7bj7R^^;GdaXCn$NbhcqBikGcfN?m*(2-gs+R+JrO[c,¤TSEK!\HJ S!=?,0+hp``'L8Hm,>)q^+;qm%rp1[*1`hIR2E^?5)ul?f>]'%FPsT[S-CdE¤1p:'Pa53V^f)\_6@9@0T]H%F0X0>h QBjCaiFX"g#J0:FF^rYJ@=P%a"/_"X0BSe;Ni!U2\3Q'#,kY\LR(R?LoPgY\_nG=raM5[0YSn>UKY5PbXfT18CYjnf 3hBqEiQ_3'Pl>HZ~
GhSE_D/\,^¤H9tY32(c)JYZ+Ve$P'/e[A5:p*S1d*tVW3`+^b8#«>PQ«;cK]SJN9==r.2GGN\.\Hd:e>>fom$>XFs] N9MEq`g$hOL;Hk"n5«MX9#AU"q3BV%LqKG\MFB>GPU;S#%=58E6("sPJ«+ua9)5>j)«S6OWXO$QLuL+BnH_NpmL?V8 6k%'c0UBhBkAc.6#+8+3U@9apickuoSJE39,3q:j')¤b>mhBB\3sKJ^WYIReBdMh'EN.LbgqJ+"$«T[E*LubXN:(Rh E#(lQlN0$#L/Zb$Vs%9=>AHsPpg)sT]K;,>4brC;¤328i0!]=S=@)6u_+Cb;?o*GeVApRdF;nCF@G$E[Z(J'ZA18nk «:+«(I%7«SI0Ptm'rQ5jGGZ$«H!KIBl2@QsV«r=$ZibMn1h;cQS)$kDG`*O'"ZaW/m@R`mnS(?\h2jtEUC\AAO!lM" A[p(cl0=]O.`@I6¤#ccmU$gQb6lW`9PfnEE"HLOp7)Bh3]>b,k;n,ea4VC=lO3UeiR4g$/:'-NACKR$H+Mc`YaRD)- \r*dN',kK7Qe/ub`:1Bh0mnL855*qUo]()D+2O-.>@erQ"#m;e¤a9Sl-os0h4g/2CdLd37;:,sjS`.>Y3p2GFi0ua@ Z(qUA35>mI+JZXg("@;B9]le@*«$`p$[E=DNlf3dn;`RACuqkC+sD]?Q%ljhqdn$Og00DoM"@Gp.6%U3FDq,DPpP/R 2r#^%HAHo]^0DX7bN,A*mQK;_(dTsa^^>"?\\j«i\\d;$2.`b\«:+:gi9Mp0BhHAn,?p9p0YQZ]YiDOHN:ICThLi*5 g6/W%#CGOtKM)rsg+5*nHlTtU[OtGS.LjWeRqc_T>JE4pp9R5h[en^:cR_`qg#@rg'Dq1[(UCGkg1E!7(NU:uDs"O9 Og?+"IOq9C¤g$`5)1Xr$!P\(E5Q~

なんとなく見えてきただろうか。

kのループを分解する
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    const tmp = [];
    for (let k = 0; k < m; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        let even = new Complex();
        for (let i = 0; i < m; i++)
            even.add(W(inv * i * k / m).mul(src[i * 2]));
        tmp.push(even);
    }
    for (let k = 0; k < m; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        let odd = new Complex();
        for (let i = 0; i < m; i++)
            odd.add(W(inv * i * k / m).mul(src[i * 2 + 1]));
        tmp.push(odd);
    }
    for (let k = 0; k < m; k++) {
        const even = tmp[k];
        const odd = tmp[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src)
.then(dft => DFT(dft, 1))
.then(idft => {
    idft.map(z => z.scale(src.length))
    .forEach((z, i) => LOG(z.sub(src[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSE\gJ3Ad¤:O"KSF?Ye^t-\]iWZ+I#MR1QV@U`oP'.YiKiV^]GA!8#"OYqK?oG>ILNCbBn¤)[qZ_uto.UBpc¤]f/. +t5r_Um]$Ye(g2BcpfL.\:HHDn1au5P¤4WFiW«mF:HV8$i05'o3`H7Z+>#::hp3)p;aK2JJiafk1iNUCfggdV[H3C« H9sp;]3#WEM"c/8GeT$l9Zom@7K¤r>@L'ocQ:ae0XU(g>iKOVW4Xq/')aohaX9jreED5Xm^b(n>)s,(;PtkC]kPk_3 h7SuUVAcZ(TDJGo;=%tk\r$_E3E@hbr(A[[VHU;^Ufm/!aH3D1mDn1@dpRs!OR5LDCUr#^HbG0VJG*;Je>sTD8r2[` l6l)$J(Y!:i4f7Mp-l80N8>KF/'tGJbAWk--k5C,-Hhaek6lJ0lVm~
GhSE`D/\,^¤H9tY32(c)JYZ+Ve$P'/e[A5:\eg2urD93«9q94«iZQ:KP`#LTZh¤bNTkbkqiU6X!h/b4LnPY*Fj:^HW APE,/AmC1SX=-4(!¤)=a;Ft!d3^49.*g(Zr?a+4@RF5V$Tg92I4U=te'_"-/(+ETj-c91)=tN+4/##!s@6=URR«sp1 /eN``$FFA"jEY"*\45W/M(!b0UH%+_IAI*;V9%B5152iHe*FjZHM_)-;L(Ul1Tc>C-#RB25M'hFg¤tI'F9A'POjVF] $dPK¤XBjaF[c%qOITLbpiT=«t@e;9nZ+T«jp'r6m01=*OBcG`k*%[eKPG"+T(g,,B2(f+q*o2EK8f5"«?0jqHLFhOT Fle(*_#*!BdK?H7VWd;q+¤"¤2o!lb`?2IKY9:Ou:C'O*HDoa2RW4SZYn.mt,$Z_2?gFp=Vj3rmGg*VN8HR@=«)GsNt b¤H\FbLm%J«/;bJmu_1"6J\mMM)¤'-.@7M)#onr(M1d*5G(gOBVf8XMHR+cc)P¤HVF#>U^3!hiJY2R[mJH0U1+iE¤R kVC:%R*[P!35,)C,::,«(rI`bj's/p5X0^RqBNL;5@/KL5k98>U39,(,KQWn«uCRas#IOTJL4c%"t[oI"U«c90a>TU 1ONm=*h_HlZTEbtqIWE_b:=euSl«ZoP2e3]77,5g9"qR¤cP¤0`2,S82SWpcF@0CjQ^,WM6S$U_3Z`R">+f0EFFO0R; [J!Br;8g¤WZ]k\V"sj;0hM,^=Y*1Ss8@MFm_"1j.$T`t-!+8MC%Lch5PehNoATaJk@r+I$JF,2"ds!7@Q3\C1^WAAb /4;:^'E%6X7@KAo(5XWa?^RFl!jK:(0cB'jXuHs3r`^=`[4JNaI%>4JU0*n[7Z«BQR*\;K.U>'Zk*pM$2R9_tDWL1o Tk.0u=Pg549F+e]6DDKnr7«m5[X$S-RNjItml#Y_hU@q=Wr;h"OJ59je@,er5Q1Zohd/#~

 3つあるkのループのうち、前のふたつをよく見ると、長さがn/2のDFTを計算になっている。 片方は入力の偶数項、もう一方は奇数項のみの計算をしている。 先ほどまでは偶数項・奇数項をひとつずつ交互にtmppushしていたが、今度は偶数項・奇数項をそれぞれまとめてtmpにずるずるとつなげているので、tmpの前半が偶数項のDFT、後半が奇数項のDFTである。 それを後処理することで最終的な結果が求まる。

 それをはっきりさせるためにこの部分をDFT関数の呼び出しに書き換えたいのだが、そうすると結果の検証部分にも影響が出る。 そこで、DFT関数は素朴な \(N^2\) ループのDFTに戻し、今いぢっている関数をFFTと命名して、正逆DFTを行って元に戻るのを比較するのではなく、DFT関数とFFT関数の結果を比較するように変更しておく。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    const tmp = [];
    for (let k = 0; k < m; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        let even = new Complex();
        for (let i = 0; i < m; i++)
            even.add(W(inv * i * k / m).mul(src[i * 2]));
        tmp.push(even);
    }
    for (let k = 0; k < m; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        let odd = new Complex();
        for (let i = 0; i < m; i++)
            odd.add(W(inv * i * k / m).mul(src[i * 2 + 1]));
        tmp.push(odd);
    }
    for (let k = 0; k < m; k++) {
        const even = tmp[k];
        const odd = tmp[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS-Thf$st¤:Vr4EQZaU8PG%74"),?JF^RaLDB2bRZNjq`*2G*M*nmMrSP.Yd^1-OYS>jfm`0NqTBC9O7[%co!U[cZ bib)=2Kkt%E/Rp$cQ;F9:t:bhO-WS"-!"RsK!@OK#18P2!W\#a>«/p@O]gi#rc$Q_`0/J+K=h!r98aE:f+¤i.*),UA ¤!i4u399+r2Te"h=^E_qh¤mA;^]6QK:KJXpJY-fm5D2N]m3>f;Wa8P+X#KOQ=KQYX`=nT0g9fA,V)C`tqs^=*;C)Y+ 2U#gpG=H/X!\«?3c?T\>k$FQ'V@M_!Xtlfo%c0-kWEie5[M)9[+!Y(hD5?[\8«9(68ZM"Ie0FLXaXDNE_>-'\Ya5«a )sc6QXGdJ!/5,Yh_m,«¤a`AFuVeV4K@«V1q0X$_aA>sBCXAD.b"8R]7¤9Su0MknP'`?]RGatR*g(lI0\3nptW;*4^- ;!f+SaXU?%`adI;7rFKcqlJccECYE_pAK=9"j+N.39VO96\Rk«@UZ]+_/n!PFJu8eDb¤8XE."E06f"g/«9]¤]b«OZ/ fo,2J=SIRKe6#X\3q4*Fe+O0ALARY~
GhTQ+>AMtI'RnB33.sFc8aIdXNA3(,AR!2_2b6F\8m#)#C/J1FZJ;«+q«pD.AOf=^dV[2R^7«Ajm`1'X¤u"«=J>A8e ;r=T;Q96Ec$BI?c6cQ-h!g^PMcM0/Ta=HO/=>WrM.PdF=TEjd(KGc:5G[KNi/7)N)YRmXo«N$6rA.2BRO+?Eig0j6! -_S!iAXNha,¤'kY+ap;RQbL:u0j]K\V?'16caT«b#TML/f7b+7p;%ZC>!j#\-+UN/Y%op[="bW('54*()EKl@Vs>iL S4+#m\\O2C_^V\b0)#dlNRbV7q(?\j?$@N?LQK2^,3Mt7NGM2s-DB!]d$\Yd(sCjF-3lUJh1AfW%(E$«lpHnj/;oAO MZ!g3pT8:2¤"p5oZE")A16q_#_kKt%m7.U«D:S1.%c#%[OGWbHl$h't)r.YGaYANe*e¤LV]-SJ7Mj.K5M¤6O\U9¤_R iPt_LZZB$e¤4]XR@@"S5FiP7#dZEj/Z+*YFcl9E`#ldi5SC]T6D'2u_i)UA3J=!_fn1ktu]V0J0h(u3AG(+c:F%3OQ ',\VKki6M%hEX«9O8:*2V*V.ZgF=`0!H)'\Ok)@m:%R'q8(4S6h>b$hVsD3%*h_9gh)ka)IGM[UjX$Nt#8e]X(KHk9 e?n;E3SQ;4+/qfb8a0AZ8r!XM\d?ggM¤m\L`¤c5B;R-NOW68'6_H$XH«$6*0!.S7)7RKP_GHodR!'F,/g_R]]^D`rT !kD51,TW@(Znn+'l7-Iil42bPN4=\«Ih*A^SeWnrZq5JcM!u3(fBm")L00eOo3d>Va(KX14eE[K)F*PeE;[Nn7#[[i R!P.p2I«i/"r%eGc1RQ=2\L=hba1[[kV?tDI=bfC$-t@^`cSHhq$#62/I0LSP>"A5Nf2PBZ2[]`mFdaoH_.Fh=HFq. >#gdVFp¤K4ZFK!Zp,r9_JksD99%-girSH2NWn'Nl+04mnKde/qS8kQ?^O6¤L«[LSR7XXfNBKqeh;mTREcYDM5g1WY% CktKCCua5\@eUCRT!XMc#Hd«ngG.n>>jsM.UV;"¤i9«1T@BN$~

 その上でふたつ出てきたちびっ子DFTをDFT関数の呼び出しに置き換える。 入力はあらかじめ偶数項・奇数項に振り分けておく必要がある。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    const even = [], odd = []
    for (let k = 0; k < m; k++) {
        even.push(src[k * 2]);
        odd.push(src[k * 2 + 1]);
    }
    let tmp = await DFT(even, inv);
    tmp = tmp.concat(await DFT(odd, inv));
    for (let k = 0; k < m; k++) {
        const even = tmp[k];
        const odd = tmp[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhTQ'gM2@+¤:Ml+*.1qb\`cDS[:p(%q12ZE+F+MO«/bThP;JO%HY7N(0i1K9F?jJ!1OIEkm.`ak2m2Q'_ID]DpCT81 )'dnZ\BQO\eLD,)*S\Zs!=MR@'-AH\`Kb7CP_;WPi-GJZTl5Kn>t67c9lDlQ,Ajud8@\1Z«fa)4«%?6aPpGXj\?`,h fVU5-_WDoMH^*SBMb@GQ[ceRi/_7`nGZbEWnH!\n_P!ZT3uHX*0l.P0'l6RsC!-((VlJNhds!Ofk`p4kS875VPkuFY Asl+[,$pQ*QkIG=jp\(H/b`J@>KDo«0aI¤%KB[:2]Eu=/iFhi`c]7[3s4B#fr2O++E5(e/(ejo'r;[SX«sq0k9:4hD URG«Rg0*NWeU":g>b5Y:R8tnUa/.g$")V9_[Gm?A¤j!?DrV6c6k¤@`«`#BK!d@X^\g.h~
GhTQ+gM5J.¤:O:S%¤OQIZ)-*(Q;Z6^Tl!Dq2fMA1Jn[-S7#Rn,Ok,BLi.(4^7InTe6eYGs=N\qUR@/,.«eb-66HKT9 8Q"VDITtrSa[«A.nCRiUb#g8h!@qLmkBn"7j;%>(/=K138hqI:TEgr-KGgg?G[KNi/7.##@3I+'/jid+Ul8^`UET,3 h.phLnH>)AUG!9h>U21al_H/$o#fQDAW6@khl+B=aRcjfPd"HJoXZ`/hEG¤$A9Q%'.SDC3M49sqkPE/-Rm7?U2pc9t 3SdrPi%0c,g'+X#)baZ,GGV2HDQPf3ATs9;j\7\>G6llK-??iEg;mH«d"L*q5pt9'Ua(NDKKRtFkR)'a0PGaDfS:pe =Vd4,X7g$+hN-5o4b1[mWicj,b%0e8%uWd*g(.t(g2+f*1N8\-6RLgi]),¤$YAqIS+t$L$ro_=7Hs4p+gSNn=cUE35 ¤S9LF#e/2GS6qAX$umQ3*O>$kWB7O2E"1'@(5lCb#*("H^Bn/ibSmoJO=Z\O=+_OOm4AUV%0eFRm%/2'du1PjT2VHc T#(ci5p%3Gn-8a2IFK@US-D'G«%V)T\?n4J9k\E2Etb!PkgoMRLp*]A;F`o;^EZGD(^0M3"(RTtHq#,aXO(«*X-DO; l7-^`6@1)VkC6.(/eTs$g#r14/D1+;R"j3pAA58T%%=U!/f¤bQ#XoHlVh=j,NBZ2@lVm7KO>c6]4PgbBjAm>JO[l*e lB==+i6mJ5W8«@N(ld$gC7¤\]N-MjNDY@qlaN]_[:R1IO;0cPB1DCd3e>+KpXfKDrG*N3Y+#6ErXr,$#dbqkj@¤3cl !>Hp-iSQ?>V#_dmjc07g]¤`R@gEo7gX8-RUr_7gpL=,U\:8lIeb]%oH¤S$S-h¤j3lP@_p?KAm*k2«m?8p$¤¤mHRtFj 1R_hO4d>Fe,9s"B/]]T4S@0'WkAeU3«93i.n6k=le4JBsR//Rt"YiM=PXr4«4Z*k[AD>!C0QgkOdRU0!Z7mS8NBKe^ hCmbNOgV=q_:6«`lh=ENoBeE;.C7:M:Uo=_)Fo5SU;3tibp'Ha~

これで偶数項・奇数項に分けてDFTしていることがはっきり分かるだろう。

DFT呼び出しを展開
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    const even = [], odd = []
    for (let k = 0; k < m; k++) {
        even.push(src[k * 2]);
        odd.push(src[k * 2 + 1]);
    }
    let tmp = await (async (src, inv = -1) => {
        const n = src.length;
        const m = n / 2;
        const dst = Array(n);
        const even = [], odd = []
        for (let k = 0; k < m; k++) {
            even.push(src[k * 2]);
            odd.push(src[k * 2 + 1]);
        }
        let tmp = await DFT(even, inv);
        tmp = tmp.concat(await DFT(odd, inv));
        for (let k = 0; k < m; k++) {
            const even = tmp[k];
            const odd = tmp[k + m].mul(W(inv * k / n));
            dst[k] = new Complex(even).add(odd);
            dst[k + m] = even.sub(odd);
        }
        return dst;
    })(even, inv);
    tmp = tmp.concat(await (async (src, inv = -1) => {
        const n = src.length;
        const m = n / 2;
        const dst = Array(n);
        const even = [], odd = []
        for (let k = 0; k < m; k++) {
            even.push(src[k * 2]);
            odd.push(src[k * 2 + 1]);
        }
        let tmp = await DFT(even, inv);
        tmp = tmp.concat(await DFT(odd, inv));
        for (let k = 0; k < m; k++) {
            const even = tmp[k];
            const odd = tmp[k + m].mul(W(inv * k / n));
            dst[k] = new Complex(even).add(odd);
            dst[k + m] = even.sub(odd);
        }
        return dst;
    })(odd, inv));
    for (let k = 0; k < m; k++) {
        const even = tmp[k];
        const odd = tmp[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhVO_95_U#'\mbe=kX!F5o!\$WD^.`"I6P`=epm'Lr>,_#Dt#!hIhVI90k,?_bt;$1N,Boll\Q;ps9nqU%s.f+@¤o. A;j)3@l;FU7\_nCQ#R$b\U]`YTB!P;c,c*e/a@)N)«3a2(r/F%jITe]>@![rTqJ]CC85>s;g9TSYn5>3¤lWiiS7X_* AK3/-VpZ.dPGlb53S*u54o.rbe(NA!d7Q\;6nC83nl¤7G$dAsBk[@j$$%5"/b;*6Ua_b"$6LZu$kV14ua-bp6R.`V> s*#M9O2u!S*.D9j¤$9:E)'0c?53a_8^"ZhMI*9,Gp?VQ8b(fQ/o?]QbBG**\23W`3«i>;Tl!%tfH4Q0C.F;XfQG+iR `(Q[:WVA6NEApK¤mJ9e5¤Z6fdF¤d]l]R=aq:j"40;1l%IYMB,um.Lj+cgX/kiF*:a6T?%#¤%c_>#6~
GhVOd>AMtI'RnB33.sFcET/=!NA3(,AR!2_2b6F\8m#)#C(XY[[aM$%q«pD.09L1S/aTU:YoTC1n*#/^I$FcL7_"U/ !eM\WRJa=HrKOp)9As:ZGbr.0U]S!59[«FapW+:gYF2k]/«sa(UWrl5")oKNAahF2f9*g"B"Ei?A@cblP*4=M3D@K/ A+_CXe\qVe«fXmf\:,'T.¤N3>AB6Q2j.nk¤S[MCJUQNT>A"LPGCd7.#F9ZTd¤h0FrPD)]@\0:"_$@]-a4k'chT]mLY /Nfp-.heFu*ncssjk?drm-Ln:BnMghYnUb.OgE;T2tEaTH1FMeGSAViFico_=p/h+\V?6,%$N1%W"IE.BOM%Ek¤"T% AaZkF-YZZM7gRn1«Zg]pA_1'd#CL/>[);c`,0S]$93bXpa1k5c(G1aUs/t^;;Sd-R2p',qVQIW2qMK2q3j-)*>dRbC 7j1`qh$`rgrR9F[a\ip$.;k:0ac)[o"bC/s[Nf/7aNuXm*e*6j\]i*u[=L`[KU!1YK9,aLn6oHH7I6%S$¤eE(oA9qM !hR1A5/7_:L[Ye._4«M)_hpL7:«GJp"¤)l,QRXi«6h«Dsa3Z^>,D`JOFeL%@cu4T\49@fce\IR6-!sOP(c/PNRk,gE PF@aib(TI`9$a)%WD4(-FPQ$S1elXd¤_+2_\%^38(dm;.qQ74BEN9jiG**hX^-XT8QU^P%fW_+«B$6'e$/SYj)6Ybe ?«G`N$«Cf"T#rFiiuNJJ\4i'«.u),.dFIN4,«;r%0'=ULKiRH"-Dbjc6#5«'Upb1.U=X/R>URLVd«.T/RkFY=>LcJK ^AWu\ddILL5:rKG2i)so=umZ63Fgd'ai$h(Yu5kXl5D3]0bCB/EtsIFeKSDQ":^BT1sKcjX8«on1Gu5Qp9_eC%qdP4 psOosZOKVQ*rk]@PCVgAs.mRTPB?gg=`!6">IF>:ImjK¤MT?Jj@C@M4PcW(4kYk0tKU9W%fUBV*I]XDl**]?n6XLN7 o`CP^nj#L6L5I$([cN\65ha«OBBFr%m'$IndGKD%+Zl5¤JuRd\X\X5mO.NhfUoZPo$j/,,(Acla+0Y2-(JmMXh#~

 このままFFT関数を再帰呼び出ししても結果を求めることができるが、ここではループに展開することを目標にする。 そこで、もう一段分解したらどうなるかを調べるため、DFT関数の呼び出しをFFT関数の中身でそっくり置き換える。 アロー関数って便利だね。 JavaScriptはこういう検証にも案外向いているのかもしれない。

 inv引数は別にアロー関数の引数として渡す必要がないので削除してしまおう。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    const even = [], odd = []
    for (let k = 0; k < m; k++) {
        even.push(src[k * 2]);
        odd.push(src[k * 2 + 1]);
    }
    let tmp = await (async (src) => {
        const n = src.length;
        const m = n / 2;
        const dst = Array(n);
        const even = [], odd = []
        for (let k = 0; k < m; k++) {
            even.push(src[k * 2]);
            odd.push(src[k * 2 + 1]);
        }
        let tmp = await DFT(even, inv);
        tmp = tmp.concat(await DFT(odd, inv));
        for (let k = 0; k < m; k++) {
            const even = tmp[k];
            const odd = tmp[k + m].mul(W(inv * k / n));
            dst[k] = new Complex(even).add(odd);
            dst[k + m] = even.sub(odd);
        }
        return dst;
    })(even);
    tmp = tmp.concat(await (async (src) => {
        const n = src.length;
        const m = n / 2;
        const dst = Array(n);
        const even = [], odd = []
        for (let k = 0; k < m; k++) {
            even.push(src[k * 2]);
            odd.push(src[k * 2 + 1]);
        }
        let tmp = await DFT(even, inv);
        tmp = tmp.concat(await DFT(odd, inv));
        for (let k = 0; k < m; k++) {
            const even = tmp[k];
            const odd = tmp[k + m].mul(W(inv * k / n));
            dst[k] = new Complex(even).add(odd);
            dst[k + m] = even.sub(odd);
        }
        return dst;
    })(odd));
    for (let k = 0; k < m; k++) {
        const even = tmp[k];
        const odd = tmp[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhT8t5u,«O¤A70@F0oqM2AIf7DGa/W!Oo(aaDkpY`i3FF#bh0eN?'tm#o/"%]@QCp]JF`¤,?[1J9*cX2*//es$A[Id =1¤7em5))>MG]cL2FoT`pB]5_b;)6Tp,F7>2=@]!5dok(,DIV?Gg[))3Rc[@[AY'I\DWfto0g7NBDdGZ]Nqqb3Z(1] 00O%@if«T0gJHL0L4tRW>Oo:_«tIRp;X3bZla5t>K@2gF"V.'eM`1PO¤#CsE`K[5rKk%S`T9tF¤gdL^W5I1+`q">m0 V^(giJg$>F:u]sM34'^Yps:n'c6];E)G3?cDgFlCJe,J>0;B¤AF17G~
GhVOdD/\,^¤H9tY32(c)$`$U5e$P'/e[A5:p*S1d+"FY/H"egcK+b)Y«;cK]*@Efi'NXU2,FYFBo5hY/m_K+E>^2DZ !l?4BRJ`2f6-;?rV"!";][B(-WIUfiPB4GHI.E"Q$g(519-kI8W7ulG#J«(?1AI6TMg;N`1VH1e1«htMUa-CW%)Y9u =?T;nFn$$-Q(ouG?BBgdMX@2l)=bqu3,*n=GMB(4dE(@ZYt/:Z[dLM2\t*]B¤uh6APDMuD\0^js$pW@I*pWqIBY¤q! $]jQKg'23(:KBAm\[L?3«_hE?[)(¤«/4J6/j=J¤[7iq"`*ZTtUkOUa?/idT39b=e!CH0b)@8MY?C.,E:C5gN?H/$u` 1ABE`+DG'J`sB«uXZj;uQ/,tf*UoWAaGCrRM(EH"06(\`*C#hI0"44qK);kN8Hl;;]d+gMR(!"Wf2m?(dXL!uj'p2# HPhUFlp5lckb$-R8="QM8TBM:%n>'7/I3=`ZSN-1Xc-N.ioAo5Y,9/T[Pj4m+CV.#mGd.h#^l8j2FZu(E=%heZ]!]S U¤pq'¤3`>q?b>e;]¤C?[mDFmoND@W:eH%Q[Q:hs](SQ*dRUbQMr"6kElFPK-O3m>$Q['!k3i[`cb:>cQ(,0@a/Fk>9 30W']2e>=¤/#DFTV,Sh!.luAGq(,7n!tj%k"F#FD-0R_Uqtb/B/`Nm9W7`L\b^>.US4p'G¤[>b5h2S9H%g0JF(c#E* PbPCX+JP;%p_:X>T(Y*Los2%][?I_j);EKkiQN=XhL5[p)c2_9V+MIhdqHuSI.30R"N"Z3CtpaMiS>i?^%p0f62[e: e891R(t3Y%-D"u6P_(C'e6i«8JQNBSW]3t9¤HUW5BQ0K;BiZ0Sh/D-Ida_5p/\.`9h?LSPgXiM2#sikBJ\6*o9*bKp %^q5X«C>lZ%\r)V3hA^X5LH]rK(aWBD"ai02?*r3AV>Fp-UZ[bfn0-!,-r)Y:H9j;IM1[LC]Hic[9g4#ekek+JM8SH 6b1TX¤M$gJVIRlZ?*YGB#FYS8]SMXaQ:(!8gWqN`V/npVF^/nX.BrYGoTkf-R6s_.jFV#Ai-4ka$ZCLq)(LeT~

src引数は外側ではevenoddだが、これも内側の関数で展開しよう。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    let tmp = await (async () => {
        const n = src.length / 2;
        const m = n / 2;
        const dst = Array(n);
        const even = [], odd = []
        for (let k = 0; k < m; k++) {
            even.push(src[(k * 2) * 2]);
            odd.push(src[(k * 2 + 1) * 2]);
        }
        let tmp = await DFT(even, inv);
        tmp = tmp.concat(await DFT(odd, inv));
        for (let k = 0; k < m; k++) {
            const even = tmp[k];
            const odd = tmp[k + m].mul(W(inv * k / n));
            dst[k] = new Complex(even).add(odd);
            dst[k + m] = even.sub(odd);
        }
        return dst;
    })();
    tmp = tmp.concat(await (async () => {
        const n = src.length / 2;
        const m = n / 2;
        const dst = Array(n);
        const even = [], odd = []
        for (let k = 0; k < m; k++) {
            even.push(src[(k * 2) * 2 + 1]);
            odd.push(src[(k * 2 + 1) * 2 + 1]);
        }
        let tmp = await DFT(even, inv);
        tmp = tmp.concat(await DFT(odd, inv));
        for (let k = 0; k < m; k++) {
            const even = tmp[k];
            const odd = tmp[k + m].mul(W(inv * k / n));
            dst[k] = new Complex(even).add(odd);
            dst[k + m] = even.sub(odd);
        }
        return dst;
    })());
    for (let k = 0; k < m; k++) {
        const even = tmp[k];
        const odd = tmp[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhVO_?#P«K'Rf.GSB#dV#Ae8r`kmnX?a)KL"$rGH/I¤55=o@a#7DOra3jZjaBlDIIHLC1b#m+4)Y3A¤9khmWj¤8#,a @Bu2]RO¤T!_$A03aM^bii59Rp^(¤GF¤t(1"ktL(XY'4tIVWm+m9uAQHgbZ#]'5q>OWStod1*5Vj4?H,4YXPWIV_d)¤ n:Dqi+`o`(cpQ^YXOc'(G;PiM%dp9gf7OpdZ4LtI`%GD>%CKG«Q1-;o.Brblob@u:or-NW@GNNs3f@*hWi9We(63u3 Zk+4AC6Yr*`m\apPU5«M3NnOO>uo?D`,ZhNksQ1*X8"/\M;)%BF1pJQP.Fod8\.3b[«c--H%>,33rHk6Vj5\4X])3Q 00I/D6A6QZ*barf%n*+#[p+ZE$X#*Drd1Zj;g%"V?^:E@]]Ie,Af%3!VtD^F73-Gun3bT1]?[ha71U6\8"T5Q?F>+2 ~
GhVOdD/\,^¤H9tY32(c)GtF^XVJ1uae[A5:p*S1d+"FY/GpsNI6¤B;)(¤r\p`sE5qBNJp*)RW/XSY,L:G>C1@=p\R, 6pR!40p=h¤EL:d=X=69=!08t$X¤QmgPK=\LLB2Zf]d4%5P,4«MLNM$tD%$7Fb;/j[`61TZ2Cn_oA27Hd1.b%U_AEJ? i3Bj/f]qB,-n@ep-N#QS$5D5d*HK.U:ZJ5o?:T.5#"Ko!X2[8Uh6UtAKfNnu_$ZucKhNgKjl2$I>;Q1^]m?UiBV=T* S/*0pm?J.+Jq.ZRi*pW\0l/u(p9_Z]eSI\-D«,MUm7-M¤+nqUb*3U,XA6j6¤KUO#.-eH_mOeH-:m-I0J$YkXIXa¤UQ 9ptlp¤X(Y/q[in]UK*pETX*n-eH+QqS9_I«AKm¤dpBk//0#t_)E#1BoM=@\3IT"$u>X-?O0rD';r;):pCYGi#A6dVh ["jblbO1)!MuMu-S7JU[OqbCHEdpVsfn."PB_E48\«4+[AI7jU%%#H@)7DGb74;TIKK[2qHOtGdG¤«,h*TIbZkGRLX -U\,#JH:.K2Vtn$*OMTRT\Y5\*URuI[Se,RdpfkPTm7]JkFjnjY/h\N4Jf1g*P$fO*Sfch#jSAn`[ie=PZkkiTsn9« 1P.-V?$T?qZo]^"HS2-U8_h^5S`eHYAbSUN2Ys6r]FBQI5nB,$d!+4/W^U1DU2R>9X%do-q¤IL9Sf2ir+QAt-FZ1j6 [ed(7aEWP=o"pQ[#do?¤pr!O%cFeK(_Z4ch!:6(fUp-9,WmsOLXhP@oq#j;#e(SXA,2/(nE8;j#;W«tUorkrT^6S[t 38L^fMVJH_ps'I8P@Wj:6$$t2#b«6o19;%Ipm:U:aPi5I6/+,hGcEW)82lkeXffPK,M(^u6H,X%p'BkcTYYW1VHeO« «rKK(YBMRE/HM;mkFW[VHB'0\fMqYYckhPK,iWi.,a6+F74S=9.a"%EqO9B=%«X-1ddS4`r[Ifd;Kn0)Ao:3=;GfgZ _%cFYJdcVT?q^¤\WRp!W*nX@;Jg«g"pL3!neseQ2)YN/B'Lqsc,]iME.#?@te#?p\.9MV+LJ320reupJp7m(VI/E_n Cih98VC!B\¤%(He]=5/?bfjkiL^Tg!_P""Y8NcqTZKo9Y~

すると外側のevenoddが不要になる。

添え字変換関数の導入

 ここからが佳境である。 ループの中のevenoddsrcを偶奇に振り分けているだけである。 そんな無駄なコピーはしないで、実際の計算の時に初めて読み込めばいいではないか。 そのためにDFT関数にも協力してもらい、srcを読み込むときに添え字の変換関数を通すようにして、偶数項の計算をしたければ変換関数としてk=>2*kを渡すようにする。 奇数項の場合はk=>2*k+1である。 srcを飛び飛びに読んでしまうと、DFT点数をsrcの要素数から判断できなくなるため、nも別途渡す必要がある。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k) {
    n ??= src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    let tmp = await (async () => {
        const n = src.length / 2;
        const m = n / 2;
        const dst = Array(n);
        let tmp = await DFT(src, inv, m, k => (k * 2) * 2);
        tmp = tmp.concat(await DFT(src, inv, m, k => (k * 2 + 1) * 2));
        for (let k = 0; k < m; k++) {
            const even = tmp[k];
            const odd = tmp[k + m].mul(W(inv * k / n));
            dst[k] = new Complex(even).add(odd);
            dst[k + m] = even.sub(odd);
        }
        return dst;
    })();
    tmp = tmp.concat(await (async () => {
        const n = src.length / 2;
        const m = n / 2;
        const dst = Array(n);
        let tmp = await DFT(src, inv, m, k => (k * 2) * 2 + 1);
        tmp = tmp.concat(await DFT(src, inv, m, k => (k * 2 + 1) * 2 + 1));
        for (let k = 0; k < m; k++) {
            const even = tmp[k];
            const odd = tmp[k + m].mul(W(inv * k / n));
            dst[k] = new Complex(even).add(odd);
            dst[k + m] = even.sub(odd);
        }
        return dst;
    })());
    for (let k = 0; k < m; k++) {
        const even = tmp[k];
        const odd = tmp[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhUtP?#P«K'Rf^WgrElTM4;G:3E`fc$^UQqHm!sG!K,i'0oQ'[[fm?]RksSC=VFDAcb67Ngk0Hrp0amI:c[8gXE*`@ /ESW6K`W>Z^nFjn3:o$Th/i;nGb)O11iZN'Hj^V7$j"rkm`%B6JE%[ll_2Y^ZaJ>YQbl`?_XfJ"4g2YD!0\c96s>I^ 3D?@6.A8*[n«q($=miEO"A%`h+or*'Hp%PO(Sr;l(9*;]eIbV«j6(=fK5-WuBc`,CqE¤qYUJ9qg7'UJt:!ZFX«mie" Qhh[,-IEj[,BG¤bqM=It«UYGgHBa3E$4F:A0JU6ra=@NpAE5HPp!G^D\gA8#D',PbVPtI«:p]G998V>5-);$l5I=T! AP8VY`7YfaSD)Yq^\,/(QjU+]a]4>aC0srLYGn29;UpJDP5r!-.UlR>#fT¤h2@_,P?a,«3-2=f[K/[am%a1Xs"J+¤: ¤Q*"Vf!-p\aiFdKUa«V"_QL79KiNrYddrGj)R2M'I@$Z\;X*abVnNVjc?k8`MZEc2@@QdXg1EJB?a^Pe?"9E`rpSN1 ?M-2lP)J)JkS¤LuW¤=~
GhVOd>>O!-'RoMS33)[[pC^Z*:"-\*co.am]1HSn!EI%u6EMS0PrMTtqVQL5-`N:V0IHKK/LjKa3TiKUcV>Y)KI_=! +Gc5;afek.-ep"`$MPRV/+I2:#Zs/D536S;,Hn¤P@bsea7$@)-:-,WHJ/bdNKm.OD$N5*bl,Fp48`n/T+c-_.(_)!c l«[l!M[,oN*-IZgrAZ3%CaF5G"]$.uPPBBpljG[1$@,ScRM9p\gJ/FEUpCS4+;kP)cXao326fNllZ;m]^08;lRVnfr _BZY*[)¤0nOc9fT`7CQt`b>cU$g_-i:mBa)::%!5jC'"un2HK)/?C¤]Q@gf'mUXaJCZoKY0J$N'C*)bsk!J95Zu,0« bZu"\,\bF"!.jmX«EEC$fi?`_!FM4!3/bCK/;r?q$=$c'j/40Xq¤[Y2Ip5T2.H*.e)lMa.e(4PHs+F0`3iK'S>jSG' %7AMd9CIPGoD\a]N22;gPS?¤Yjl'=\1+[c#E"0c]k_j5eQur1cR1HZ]aMJM?]JVl+@«r¤L"kuOL!QW5\aPFXVpc!nb \QM;.!""(Q!+7Y"gKP56lhBeHZWkEqbg¤u/Ia!TTiWr"m$se8I_Ah\C[pF9/mO,F$9J\P*G,6dUdPWtd\T,mA[B«5D +c)m!H`fO+:p5X_*e9oDiWNk=_«2M`qbK'>eq?t==]f*LS5kk-#b"\=%.flQ\_2RM%6A@k0=:Fs-dh%lYR^ChFHRp9 Nefe+«@8ZdE8C/«QjAcmf_>66;"_;MDtVlQHWZF,2nmYV1P"-LoM\ocHA)"=%Nl[W¤LC(d`$*Z«]8dajr'p)\QV$a- "E%%G"4K))ehfnY1ttBo1«Ea[UL6MJ[usc.k#p:d?/)sFnu8T"b+2;FZ?RL+FDA07onRUpO5NX6¤H?3"T/7s`%Ve"9 '[8Ki$GI@[d¤J)r#@c4#Xo)#?2$@_Nd,8T59«QR\Wnt1u[i:JZ#q`TDQ;[Y`,g`mi4@IO,cV"HGq%Q0Fp¤C68YE4@E TABCd5FD5«,lONl+%s'«Gg5+e'"=[q(k2hq4:K=;%res$58=4c!;)a__Lr^q\/u,fjLiu.kL?2c>kfN#0YB[=Fq!C] :eUV#k,3K%jZkk/kmu!D¤@0+Ye@WugD]d(jkCR+,rPR$$WoXc#f«cr/rmoR.\DZVgpU^29¤\ibqF;2+C~

 DFT関数は結果の比較対象としても使っているから、互換性を保たなければならない。 nは省略したらsrc.lengthである。 変換関数は恒等変換k=>kでよい。

出力先の変更

 アロー関数の引数がなくなったので、今度は戻り値に手をつける。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k) {
    n ??= src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    await (async () => {
        const n = src.length / 2;
        const m = n / 2;
        let tmp = await DFT(src, inv, m, k => (k * 2) * 2);
        tmp = tmp.concat(await DFT(src, inv, m, k => (k * 2 + 1) * 2));
        for (let k = 0; k < m; k++) {
            const even = tmp[k];
            const odd = tmp[k + m].mul(W(inv * k / n));
            dst[k] = new Complex(even).add(odd);
            dst[k + m] = even.sub(odd);
        }
    })();
    await (async () => {
        const n = src.length / 2;
        const m = n / 2;
        let tmp = await DFT(src, inv, m, k => (k * 2) * 2 + 1);
        tmp = tmp.concat(await DFT(src, inv, m, k => (k * 2 + 1) * 2 + 1));
        for (let k = 0; k < m; k++) {
            const even = tmp[k];
            const odd = tmp[k + m].mul(W(inv * k / n));
            dst[n + k] = new Complex(even).add(odd);
            dst[n + k + m] = even.sub(odd);
        }
    })();
    for (let k = 0; k < m; k++) {
        const even = dst[k];
        const odd = dst[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhU\GgMVU.¤:O:'k¤K!a;«UF^C.$Ii05/c=ir]I$%Bp[O[¤3BN43O?p6L,qGkpGb[\X]'JB3Z8JX_j#JiB77o"Y\a? *u9S"jY@Q)SN`6"o*nfZ2,¤O)'L$koa%o,V+[MTJ!SC/b(uR?(LFi¤*6r9f%d1_9JA?nP6\+,asLos)n9^WLT@7[9u WhY[[k$p>^*/Z]N?J"R\LjsKQS(adJ)gc«.Cd;GpU`(BgoYIEd«TiX0]M5KdH]fA(!!\aD5oc\iO0/q-#]+U:,+Lt! rnpnb#XOY7]#/Y=be/0HBb3,Tg`«?76«Rhc-#)h.P"M>g7eseSPskY3«96n%fD*@icnMm;bIU(p4f¤c`[kis):.G9k \G)?LRRn*T(SXb'f])r7V)«CN78QW"_@8W)aXRcbLoNg:0*nS;i$WCPCW-U=3ssi4FO=joC,IM«]qm2lO_-l..\KHh FRZhn"«YVtcho_^q$$]%/jN-QgSk3h::gPQ''g7*^¤[.$JB7~
GhU\LD,8n?¤H:NnEN8`KVG1`d2GNKE6JIkSm7f¤D+0'(C]MOO)W=fG.;?-O=NfuM«8c_YT(-rR\kND7S5A(3,F5]]^ #RHVNB(C9i#:]DgP/9Qkn4EeNVZN0e9[«DgnAuX7/snkc/«sa*Un\F$45`OYbMY`#0;;igApT«TA=@j#8P'i9(^>EH Z$M_glJ`s8.n«qn]H@MR'\c2bEm?IBj.l]>8)$KBN^b)ANC=/CZ#urRF4AkI/!5],17?4JMece"B)¤oaoh[$_5gqTt >'N`mQSphpIBp/0bM7DR=Li;5BnK]l8O_«Lj=J¤\7s9¤!4¤88)o;I\NQQ.iU-HN-NCH0\'_5TaB1a:W]C5gN?H/$u` .ed$ZOL2.`6Nl8+«Zg.Kb-¤$."ajg#m`t8;M(I8m?fKLK3rW(fGHo\/,.K/R:F^UKlQmE/_QrhV5JQ.2P4n2o)¤HL0 g;C9GbO1(nM>m¤3S7JUS/LbS"AS9X#JPVCu¤WZZ[I(t7¤1#/!R$91$`"¤«j25[;M.B[c¤#j6mh^ZXO@)%O;CW!eA)m +7DMRUXuhd@s«l)R%0e?s-aLa`%_\Y"M4B^Km1lQn,.¤F_iG`$b¤X«cRjuUX$I+LU]Elb:FDXZ9¤6F#0KoUP2IKu8¤ +UI?_;.GkijFD21UD>nVkq;OC:2i8%-"5@8mB'Wc27]UX(9)Rtk>bT@BcV¤SI@@¤"Sd8$-6¤R3q`X«b>BI-(OW)Hs« LJG¤4]4%X:>>JcC[l#>,AnWMk:2;1gbH;@q^CV9n62lqD^0+a@kM[HC00CT'IL+^RL':8?-nIO$>C,)S'¤3\c#X#eg NER;7h_4@60V3"'PYruA7-jH+*lMgW«lLb9L'ZEi6%e2Cjli^]]^E83etAm)AYI$J@g@ehY«MRcruI*_Q[c1PkIEWn Su@iZ_bZr_?LZN;0+t!hKP3UY:%%2tH¤Q7r"oS)J!"5Nj`WC+sl/Rd%XJXk+d-V'p3V$(OC7ZStItp.Da8h8N[NN>M YjDScq4BG$-S]eL!sA;*:>3¤XFlb^sVTo'9DYh#%'riG+N98nFQS"FS0¤7o;bm@N]C[""Al]9A¤V/¤B$.A(¤9lMIOV GMt,*.5OJB2roR'o>'h[%hlVS!P^']qu~

外側のdstに直接出力してしまえばよい。 これでtmpが消滅し、awaitもスッキリ。

アロー関数をまとめる
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k) {
    n ??= src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    await (async () => {
        const n = src.length / 2;
        const m = n / 2;
        let tmp = await DFT(src, inv, m, k => (k * 2) * 2);
        tmp = tmp.concat(await DFT(src, inv, m, k => (k * 2 + 1) * 2));
        for (let k = 0; k < m; k++) {
            const even = tmp[k];
            const odd = tmp[k + m].mul(W(inv * k / n));
            dst[k] = new Complex(even).add(odd);
            dst[k + m] = even.sub(odd);
        }
        tmp = await DFT(src, inv, m, k => (k * 2) * 2 + 1);
        tmp = tmp.concat(await DFT(src, inv, m, k => (k * 2 + 1) * 2 + 1));
        for (let k = 0; k < m; k++) {
            const even = tmp[k];
            const odd = tmp[k + m].mul(W(inv * k / n));
            dst[n + k] = new Complex(even).add(odd);
            dst[n + k + m] = even.sub(odd);
        }
    })();
    for (let k = 0; k < m; k++) {
        const even = dst[k];
        const odd = dst[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS]bb6l*?¤A7lTF%f83?\h6QH4[j,Rs:lGeP`^g',;,Zh2R:k07l4.0%npG8Wj58TGJ$1)#Hi:V"R+nlQ^+F«.rq@ 0'kk/.-Q==_N)RBJ_J5I>Ehr«9-38oS0E;bM(6\Zr8ufRTgpR,c?JM5/KD^X8k=mg//l7D\g$H*XN¤!%/uO]`U4mL_ `A#N/cbV5XE+[FKVo,kW5(Er9H7MjeM6?Kd\N?«Q^%VKZ`Q'T@`:7s7`,D'!_Rld]XfDPWIghOgNk(*V\1$HgrsgX\ CAI~
GhUDC>Ar4L'Ro4H*2,OJ5Vf:FBj+!,C0RT-]1I,0"%'¤X9«h#-j>\`fikOs7\_o^imBhX;¤-nhK^2s13"%9]h'i,!$ WD¤]UQ7¤Be5$:a-li:?I«4N^IE`IG%=(+@#;g6?8M.0AFd>3)hfQ=oi=dHo$1g.iWWjMCsOXZ^q.[«DV0S\NdGbWds >@0Jo/`iqSKHI'gKjlRs9?hbE)B9XSlKnrg3!srZ'G`-6pGSR#9Wtc5«Zr1n.\/!0`@\>^/*h9TiiuY%¤t3^2«PfE\ \[K=W¤«O:/>fG89_qJr8cNrlBb>>S$7aCRoG%BbIZL^KF,`eG9VWD1m=uu1WCCNGe)4]DlDo\X/cr(/JL:3(i.!)$" +d$WP^I7".Dp@)?W9bjHA¤)0Ck3i.URB!:]K*msrk4kn\mV6[]7!N(DT@E?7f2"2CJn^\r5«e,VM@Q())¤IWL7\^r# Q9>M'li"`?--;sD,:W¤bb0FZ#!`tN)1%#2qHsXV`'Ga]-!m]*2J0o.RMH_t4[R1N*I`F.5'miJq+E=>:!";"qhLWJ= hm^eKmMT([\NIY)3?^G>;]*@hFRcUDDkS-+#MfElpNG2SjdHp-W@X=g2(r5+_DOj8N?\Hi>Wm(.`cD[`H5Pd;¤Cf7+ `[n#i"2a;]>3M?N]?ITA6$$d>U?iuE>AJYl>AIsqOf%R6FZ#Rr===!b5*qEq¤5e0Z'U'5[Oe(.7h5P4:fn(:;VCu4I Pq5d/Gd(=,J7MJqKIj?T-e-qQTe^e98+m:^.«6C07sn:O[.VqC;mJokD¤VA=-jn#D`QW*p8;!RuUVgVS>$`qW?ib,u @EB*sLN_A^2(V)02?O¤A7U"3iVf5RQdrG#NCAp8LHKZ@9Tl[/fcJS?BFM[;8)h!+*mD%S0H2.Da+Ahgc>VJW;q#E8o St9jFf7Kp4F'[k;V«"9?Hkt=pn\"gF/.ZdcfB:cls¤DNNNl_hC@(%>q1`u¤abZdds¤Yo_nL''g;PmP$4¤p;_e']nZa OkLk,Ks+$cPoMDPj/U7aEj=%7R6N'0NJo]HpZ;9"Z8T69fTa]9GQ$*rrrg*1«QWB"=?#5kBCdh@Rb'.JIKQ[PruD~

 ふたつあるアロー関数のnmの値は同じで、tmpはアロー関数の外側では使わないので、変数を使い回すことができる。

DFTと後処理をまとめる
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k) {
    n ??= src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    await (async () => {
        const n = src.length / 2;
        const m = n / 2;
        let tmp = await DFT(src, inv, m, k => (k * 2) * 2);
        tmp = tmp.concat(await DFT(src, inv, m, k => (k * 2 + 1) * 2));
        tmp = tmp.concat(await DFT(src, inv, m, k => (k * 2) * 2 + 1));
        tmp = tmp.concat(await DFT(src, inv, m, k => (k * 2 + 1) * 2 + 1));
        for (let k = 0; k < m; k++) {
            const even = tmp[k];
            const odd = tmp[k + m].mul(W(inv * k / n));
            dst[k] = new Complex(even).add(odd);
            dst[k + m] = even.sub(odd);
        }
        for (let k = 0; k < m; k++) {
            const even = tmp[n + k];
            const odd = tmp[n + k + m].mul(W(inv * k / n));
            dst[n + k] = new Complex(even).add(odd);
            dst[n + k + m] = even.sub(odd);
        }
    })();
    for (let k = 0; k < m; k++) {
        const even = dst[k];
        const odd = dst[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhTioh+iSf¤;BRuMK?=M(UX1+(p"n.oEe4(BR54p'*>d¤ASF$AGBXgU:t.D28bZ[P@HW"oF+tff1\¤Bm35l2U!+G]+ +tP5Zl*O9Sc(8(KNW`7f_%b2I`eT9:P:%8PY3:5B%9¤7a?F#(qa=r#Bb:peu>mJ-n2EH_5«2_4plnAJ\is%jjrdTsa _+s$4rNrYL[Ul]J1ZhLh/q(>!+mJ"!n")O=X40ZuM/%poZ\«>F(*)dHgjsg]_E2oik/=*fB@N2Od'KFXAbF3'3:8[" BfS8M+6/hTd%8-5kE'OF'56=t]f*cn«R+p«EG\,9$`O%Hn1.V'd(oGuGb)1=^i2(=PJes(ij"TG4F(G6VX9+X4BU'm k`;!X!PWjP!W~
GhTi3gMY_1¤:N^l7]6j0J6du\WiA^dW,8P,]h,$B"%(8PQa">5,C>sm,6#9^lO.KE84]R\`-+7!B4fG\*?0T(\«=l6 JiJUSr/jEs_HjIAnCRb(¤mo7-J«__H$bl0pns8!¤k"R*o,T2T?dt(3uglS?]I#Oh3kc]k72\rAd10;I%8UMAmDu`!u f+M:.49/sSkf\YFa$GPk3iPKlH"q,b`5eq-1hptdnu-HM>M)-oFP8F>:(s]sas1FZOU2i5`%8%KkPE_=S\P;qDP5Os ,J(ED_3sJK[0^*hefVf`DC\L%b[-3*Phe`¤Z^Z,KCEKkT9]RT0lc=Co5ndEIEE]o-T$@aFQ*Rl_d9P¤8#lU4$M7p*6 Q\@Upg3S7.^s-]gVgD7n:"lIpmnD$oq84tRjk:5;>ZujAYM*qsND1btqbmBgK%IMMS«=P%>Q=«\/)IDlT^It`d;[JS `>3%LK5e«Q5@%NW1i[M.ME>A%)@\si«FfNocb("L049/7Mm>eQB4k_i¤3KO'XdhK-9b¤6DNF=lkT:t%kKYCCc0QcgT !«ca*I?QR4ca\YMjn\$,#uO-]_@Y"cMmlu:¤)H1#lOGV^Z_OZ[;lVON10gj/4AX5ne/VE=7KS1oC«'WEbWYUa@@=;b :g8bp]::¤MK[Uerd'6;lRAFtViRF)[Z$pA)jn`GQCMJl#c$MOU\l10GUe19*P->08MDB3-8Y-aKM$pf)/*g?HTR4#E 6W#;-,oZ=_VJ6;H%«@Gi"k%T]ouYic3Y'K7?K!Y2Sqb$450@:FVuLcW>(QVk:/t)He^0^-nYdW$/[bSK0Cn2Q`QW%9 LcqrKA$0LBAQYrg=!k¤iTaVKJB.KoC«%AmggB'2k"t«¤H]c$*aRuUKYlPQXmS_3tY[.P-^edIo6*s*2pW5/98^pVK# 9:992\X_D"LR(o3BfRN4-f%tP\^YfC7Gd?2«JUH69!1r][=.«Umt\i``o@=h«WaCtA;G;t/]f(\33rV`Z:pYTiWuHZ "[i0H0"R,o[H60[`E;d7\XfdHY!>-"2G#Xa99oZo,L`9Q4""QuP1MMcgWqN`Xa«@«.A(¤;l=6Rb[XL_>b)5q¤2rK:c F5Q«K[lBuY!^LB6q>~

 tmpに全部のDFT結果を押し込んでから後処理を行えば、DFTはDFTで、後処理は後処理で固めることができる。 ふたつ目のアロー関数から来たtmpの添え字が変わることになる。

tmpの削除

 ここでまたDFT関数に協力してもらい、出力先の配列と、出力する位置を指定できるようにする。 互換性を保つため、出力先が指定されていなければ内部で確保した配列を返す。 tmpconcatをやめてまとめて確保する。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    await (async () => {
        const n = src.length / 2;
        const m = n / 2;
        const tmp = Array(src.length);
        await DFT(src, inv, m, k => (k * 2) * 2, tmp, 0);
        await DFT(src, inv, m, k => (k * 2 + 1) * 2, tmp, m);
        await DFT(src, inv, m, k => (k * 2) * 2 + 1, tmp, m * 2);
        await DFT(src, inv, m, k => (k * 2 + 1) * 2 + 1, tmp, m * 3);
        for (let k = 0; k < m; k++) {
            const even = tmp[k];
            const odd = tmp[k + m].mul(W(inv * k / n));
            dst[k] = new Complex(even).add(odd);
            dst[k + m] = even.sub(odd);
        }
        for (let k = 0; k < m; k++) {
            const even = tmp[n + k];
            const odd = tmp[n + k + m].mul(W(inv * k / n));
            dst[n + k] = new Complex(even).add(odd);
            dst[n + k + m] = even.sub(odd);
        }
    })();
    for (let k = 0; k < m; k++) {
        const even = dst[k];
        const odd = dst[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhT9!gJ5UN¤:O"K9R,96[oF.3;Rr/3IfKlfKSQE@D;$V>(Vgmfh:W9T¤PNmX+(fFIB?m%3Fu09,e!¤B+8imqW:ae75 (CI$Uc/HP?*8EI1dkI?Z?+Gs4j3rZmW@K,k9B_#]QCr1t73(?U\¤NOnr;f;)7N«?u_9;EGAe>06YI>;_2[.*-*tZSP IJ¤c«g4kH^mQt=r7F(¤(+n/1o[-pAN3q#c,U2U6n'Ng«]O[c`u\d*qQ¤6/otS«XcM0V1C#T#XN'*l%pfTnUZ*gd*Gm GqFl«g'1/_Fuuu;U9qEn7CF[qbq^Q1+gDH«AaQ6_«[=!kH#-ZI4@?k>6otXmXe^bqhAN1u+eNgN8c^AN]bT3N_f/@[ D2r6@LlLV>b`):lg1P+u5;,p2rM]-j=5JHmHi$1d:V1aff15ef3]*Bqg6_Mn8"u9CMi,/J,u«CeJss'KYCo«,ROgcO /pV0"=GfI]+h[.%lur`bUJ3G?iU@cPTD2AfigX>/8=t8Sm?ZKKR7`Gan_"oq=``=c-\pm8¤68A"Gt6rQ6+Q"Krla#A s2j-pDZoa"^7l[n,5e]n!Dot¤SH~
GhT9$gMYJ*¤:Ml+N$9«d)ok[15R::«";E#!P>]=d'Z¤d%Z\lGVluef+b.f`ZZ+Wn;8cYotSXl0ik-?f/6;iY:nm"GC (E¤F/W[ocI82_1M#(TbebGl,XnR[0p2fV6W;LcrcR:M!sdjY_1YT5urZEf/8H+$K.NO919;9GU?AR:HBL/XIa^bbYb Z.6f,:f«=b9S@DWe0qJ_]¤]$@^;_U2Hj9I7%?«cHFbM/qG¤9>)_[HbH?o=+POSMQc>[.on24L3[O7'Rc[*Wn¤]b6pa m11#'71m(QJSX.4Vf:tr\YfUKXO\+4GEL)6B0Yt^5325U,%jp07FiaiSPVrlo]@'P1c((0b+ftH\"Hb-UQ2d@I:)%Y 94Lll`I«c]qeY*=HViDAZ>TtqA9-ofDq;J#C[]f`Zn]V9VW#8V2499(leP>TG`«[),rrfGooCcpnSLY,Y"HgTSr2)Q /NRM*N«ib^2s2!Q/4+/EEJ7O::H]9a'NLU]>-u-?'qE!kQ1Aqe\¤KIW\05TYSg7kuh$G4^m-GDtb"lMe.@7kYgh\3F 3F;oA¤[¤*?9(u=_YT"d¤9!P^QUklQZVu](gJAB3=jAq(B1G3*Zn`o-"N$t$"_?A.lLp'0j%`@?g)L«67Yd9J88o6i¤ .ZR+gbmcNhe/YGLdg¤0de_K%=o+C7.Tps(FW3?%¤A"R8N#cA¤¤:¤¤,5p"*3S,\TqI#41o.KoKIq\Njbk]:7:;,h+W¤ H)GWE]7m$JYgu6bMpY.=,1,WhGL9Bg>7m'7@X,.RE#7ldLFu4-g«3ME6tKP6CD7TiG7/_YH"OsV'k[2nl«A%>m-p$` ,XNj"_[r3qWIak(*85rA-\#m4;(#cVd8Zi7XeYTFSpCac_u;d:ZLQ1CB8?(.Lt^Pumk=H[:`f@RX"u00]g:$m1qnj$ WR6+$#^R3U.¤uB"Rk8_«f0+$JRs+1@mk/bfMW1=U4s47*JHeUhV«FtT]W8O?*t\i*Gpl:eF$QN*o>#$DP(t7«;Y$;% ?pF'Sbbc#b6c[.!-N3[Y*`UC>.bR%um3,G^9l!Ppj#c468g\e-YVl6ZR_X¤2+8uEW;4cb5*Q,gFU7Ja[6[cM(KcSH9 ,2;?u\.Z2[43el^Fd«^*A#A.hE(S,¤[\9;Z+$RFa[lP9-S+?g'Y!.l7rtV-Q3bA$S*@>%\EZ%A7rDeHY?X$2u\,~

そうするとtmpの代わりにdstが使えるようになり、tmpを削除できる。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    await (async () => {
        const n = src.length / 2;
        const m = n / 2;
        await DFT(src, inv, m, k => (k * 2) * 2, dst, 0);
        await DFT(src, inv, m, k => (k * 2 + 1) * 2, dst, m);
        await DFT(src, inv, m, k => (k * 2) * 2 + 1, dst, m * 2);
        await DFT(src, inv, m, k => (k * 2 + 1) * 2 + 1, dst, m * 3);
        for (let k = 0; k < m; k++) {
            const even = dst[k];
            const odd = dst[k + m].mul(W(inv * k / n));
            dst[k] = new Complex(even).add(odd);
            dst[k + m] = even.sub(odd);
        }
        for (let k = 0; k < m; k++) {
            const even = dst[n + k];
            const odd = dst[n + k + m].mul(W(inv * k / n));
            dst[n + k] = new Complex(even).add(odd);
            dst[n + k + m] = even.sub(odd);
        }
    })();
    for (let k = 0; k < m; k++) {
        const even = dst[k];
        const odd = dst[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhT8ub>*[T']¤X$B/t¤^I2F%4emL?5!YZBI$\YLB1.)/mf#ILRhLSRdei"(B0F@J9k+#[=¤,kfEHZTX5NZ33ar+rD8 bZ?:@;5,b85-ldLCd`#t$2PkS?Ph?X6)$+D7GeF2`'V0'%h(.8)[K"@«=J`=#$NWnoT5sLQ;nPu"nO[%V"%ErKFMX* J]U(-!^2Joo«O@=NF==a3(A¤!D!u-b8p:3=PCGs]P#Q\\r*cI\r4G]%S1WLm(,mo!]DT«Wrk432?gc6¤8\nl0%1bgm N]Tcm3¤c1dG$Q\a=:;HqeE-"*jrOOK@9Cgs«mD\qMV/@ngYF+I!L-RlE?IrAb_>j7_YO+_E/h«T1;ARri%^A;=F;ma @l\uT1(l]=aMj!HJ53P!CZ)r,UH1gjkN^5F>q7H8:=XU7T\3#4fs8oGjKR¤SSm+X>~
GhT9$D/\,^¤H9tY32(c)+f*J"dkjOpC0RT-H^![C%g]:O?A_22TYtOn(¤r\p3seXfA["UZ"Q/¤,p`A>G!J69BK#J^] 3/JXF4cB5k\R\:j\HR4(^;pos93]>9D!?«-%CI"42\o.4aC%8g/QfKt%@o>0%rnr^rFI¤7`]Ou-k)),AM%:ip>JMG. 0«jQW!g$H;^(,Ac3%.Ts-[COd95TBHpimlu)\k=N=]@9D'K0Ie«YOW"[1-.Zkm.l^7Deg_0rL?'F8P75.UW`\f?e2> ¤RF:_i/Zu_PmL^>XZM-"h)%9+0QYL:,4-^tRb]hGpN@_SrXq66_CDipE_qAM,f«*RTqe!K=¤O3VN95`S7d*mc:$c/R ]2%RRUac$O"Q!akNp]/Tma@s>pu[C#4aUANU0)cC^nkU%I`dDWfkN;158MdF8pH'U`[Npe[+150?gqc$Xf>%:pm-j/ aBR.cT=DLAs*/Z\d_s+jj:A20DdWUd43tbTa1N]^cXCo*Esk*8e%u_¤60\iaM)egGMu]R«d-(.4VK8"WEr7[aS0u0p "gAShKGZq?#Xc8«#EH92f+c_[FI7UKp#YkM0p0pHLH\s.)^!i(%beaof=#'^LUF]d1KcB.Lk\Pobqq«,e-r«,Rg"Dr Z57EnEsMY)36`,pe«]3VGtq0V_W[4¤ka:dKNWlHER3DgoQpm4B0`!Z0``7FEW4@bbQ)Q:8/Lk8#hPiN6(T]+"(6rQ> XC#5dHJ_b+2P?%G]o#*DMir¤^5rW\MhCL\q,"n7>RA7d"«.7s_ahm$u,SG5AR*j=?]Fprp«eEp4f20a(V0cnT\99q5 mP4O«kictl4`_X7kMJE\rt=FpWVqe$O35c:)%EMPb(¤nl3MMh'q/k9I¤:7_l8(qXV]!H'Rh69?]>JR7IVbIX>\7k)3 ^ugrVZ"dL(jIp«96,9>t"cbmsd?\EeeSkCNUuP"O:Hml]XFpbn9/>o[W%4Vt¤!V-¤Z;8n+,qkcXdFN$]=>p1W1'a)f ^/AZ/?1RS@EQ_(%J?S>@$U`@_F3IdKhm.[T$)lI1`8P#BiT\^A^AV503cBC\Mj_o_j228Ck5«S2JK2i,MYo.Y«"=S@ 8q1m:)'LNc5h+KYouC^N;Rmq¤KM=1Fq-$etbBQYfan#1VG/-(1B>2tumkab3#@]Jge,~
後処理をループにまとめる
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    await (async () => {
        const n = src.length / 2;
        const m = n / 2;
        await DFT(src, inv, m, k => (k * 2) * 2, dst, 0);
        await DFT(src, inv, m, k => (k * 2 + 1) * 2, dst, m);
        await DFT(src, inv, m, k => (k * 2) * 2 + 1, dst, m * 2);
        await DFT(src, inv, m, k => (k * 2 + 1) * 2 + 1, dst, m * 3);
        for (let j = 0; j < 2; j++) {
            for (let k = 0; k < m; k++) {
                const even = dst[n * j + k];
                const odd = dst[n * j + k + m].mul(W(inv * k / n));
                dst[n * j + k] = new Complex(even).add(odd);
                dst[n * j + k + m] = even.sub(odd);
            }
        }
    })();
    for (let k = 0; k < m; k++) {
        const even = dst[k];
        const odd = dst[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhT9_6#2*«'SPrR'm¤/Qe^fs#MG4Xcnd>dfPT6bC0^9RN-7pPD]l.1DeQPD?#G8L^hfAub¤(^]3l.eS'iP>'C(S«Iu [ZWook7[+R$S)^«M-(h(K]R0#?(nh,P96-9DFmN4-5%k2?jR5m\@OuA0bG0IS^oIM)$FLlU>-C#C+.j7!1$*No¤7Tb nVE-Y¤F+$a.Wkp*/4r=N8«T-«Dop!>¤EI8E3N':«`m1TK]e\,h61eq`cFZ9b,*Rs\Gfm@@)LX«ILYI7p1'.q'C,hWG cn;?mT'lLu#ph0k8m,qA,h:,,Bn)HH:+Z6%G«4oIE\;%hS>¤FlDksN5UD8F>\sZuH]:DDQdSQR`h(d=,W-P*lrL\2( cCC$V)nS]42u~
GhT9#D/\,^¤H9tY32(c)+\@6!dkjOpBj7K,H^![C%i¤9s]G-^N6¤F¤g.fT=jFqU;Vk_g3V3484Ho0%Qj*!gWR$'oN9 P;II%cTD\8k)gC4LCW7+"sQi'X;m@mZ;ohH\("Qlr`$D,+N#?CG@8l#064B-4sf_S$«DkYQgd\E\L3Q)U]F.s?ic2I =CTI/KcjG7g:H¤IP-@>/NT;>H/9;M#)W3U3>u]alaQ!¤j:165nl*OlZ41elHP4fI/«2>I['6@pm5F8Ppl3%mLF9AWq />en0'AW8/Z8R9W^%EbWG1sC¤(«g$AXiPVu3QnV[OmRS«p(NSK4K9X,,6pYN¤S0\>2=sjYYcEl!7RZi5Macj]XA/¤0 R«rfcOL-V6,6Zk_«hJ3#l«\%=$M`S;\MM!--DEVLX2Lj"48r4`GJ7!d4Lcol:F^UClQmDd@G:"=hr!p?7BO-m`qYRN Z7¤>pc0i-ONW/bGN+AoC--:(fAUA(ka-#O^KQI,"h\Rt,L4odpSnY`uKOGW%Z\UW3S4ipVS1*=I@]jd;5'$X4R_¤6H (CX[p!/]'P+VD^RHiT0o^MabMhRQ=qo^(VkT.,oW4,*d+5Sme>n/f1"S_ph_6,sGjA\$«K8YTWkKk«A¤e"IY6Xu-Ri EF^YmX2mH2lesa_J5\t\*V6p/FpOj!C7o(J(CLqUVItW,¤B>JZEPcD@H5`^+e"C:d«@?7+=KeZ^EI/pO;BK#3H;,m: EDaJq[*?1a8(iL^NOHe`eRUs^C$gucOuK-+8#aQreNCkc#p#Jk0n"4M#uQ#LaVVp7TY*`.];[;JdW$RIgJF350XqQ; cUomtEJm-BqrKknNFtk,V¤H`SC#)Qp25Dnc:o.\*Ci`G2kTgTXGt2P[IP\/HS_">528VZBeMkhb1LOCpZG82s)r-B_ UYU1«87d-uR3kBd`Zm#TFFJnJqPV«pIjfsd]\!HL3R!8lPKjhdBQ`$m^Ko$26cjmk+7D¤`euEsd76IUhSl,mlRs/l. UgDS_L\OjhZ7$n!]I)D[q(%qj\H-h'V_7_-(n7]«"6YZtFO>Q9,N]"[]!EK9D0sj=69/%fXQ$B2=Up.«9Rh¤q3?`0S A#cqds7MYY«A"Q#_.K(Hr+`2pinr.HZJq>?L5`HT*r'%6\UZpE!+cKd3«~

 ふたつある後処理をループにまとめる。

アロー関数内の後処理をアロー関数の外に出す

 後処理のkのループはなんとなく元からあった一番外側の後処理と共通にできそうに見える。 そこで、変数名をかち合わないように変更してから、

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    await (async () => {
        const h = src.length / 2;
        const q = h / 2;
        await DFT(src, inv, q, k => (k * 2) * 2, dst, 0);
        await DFT(src, inv, q, k => (k * 2 + 1) * 2, dst, q);
        await DFT(src, inv, q, k => (k * 2) * 2 + 1, dst, q * 2);
        await DFT(src, inv, q, k => (k * 2 + 1) * 2 + 1, dst, q * 3);
        for (let j = 0; j < 2; j++) {
            for (let k = 0; k < q; k++) {
                const even = dst[h * j + k];
                const odd = dst[h * j + k + q].mul(W(inv * k / h));
                dst[h * j + k] = new Complex(even).add(odd);
                dst[h * j + k + q] = even.sub(odd);
            }
        }
    })();
    for (let k = 0; k < m; k++) {
        const even = dst[k];
        const odd = dst[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS^PbAMns'Sc?G`P7p_¤9!CEC.$Ii-YUpsj+7=J"bq%pCRVtac[`,"fM\7CLG8bip]YIQKA"`kcQK74[c-9Pl!h¤c DQuq^(5k\KB«HfnggQ5ZR,aqH>¤GPU!ZkWgggump@$4aUM6hEW"]uMX39I\\)5(/u9p1?2c,c6T7=ZO.«^RI_6Il[I g]q[="0mDR'sJ6t9XspRe5-?=r":@[`k"7o(\aqcDg2$HWr>K1PYsmY9Q12A?'1;EEH#fGoM9T/G;djFr8M¤M:T$^) =7eFLVT7:cX]:O87=KdsYo-]Oh%1WDK](Rr=]:'?1*gF)>?c@SmdGPr6«!K^UuDgm¤h^:n`/3Y7[«0A\1YtUfM5eh¤ :QL6lS@:«>*U4U6pUF#Y\¤a¤3C(>,BT#[e*b?U?2J23Pgp>tKM*A#V\(8?^UTWo%K/-9[i[*9fQ]U_pgk[«qY\L_U( d.O$Ee(kfE3C#n~
GhS]iD,8n?¤H:NnEN8`KVJ2]r:")-gdl+'pH]qjs"0+i3$Sb?qR^c0ZrU2i¤8STVe$WKq-qsM#,a7g2hRWDi?N'iik W@*/>dkGDu=\sA.!IjT9b]=7%=(E[p¤I.;:F`4tO«-YqrUCeO@@+bWHnVRB3BBa7G/kSi¤j+YS;^;t>XLN!NG%^8p8 't4%E#imF%=c`Gg(0)c[OSjp%'s>Eq6X7\!6NrL*/@?"2#ckXMY7#F,CP,VX>!lhu,%phBR')_P>eYKh#qVe7¤irfj 8sbkk\/,;/d0-V")]Z_igZ>"f0Ol\?Oln'30u*!C*j.jO4qfrI-T89AE_qFT7`M!(_iD#2_qO0H_kMFMjWP$`BbPi! jM$t6,k$V=(P1>lm+jOthGqsg!C¤^Z]CEdU-E=\%.U6/Xj%is"kH,rgiI.!iUkVs\ITDPY$sW]#k;tjZW+1`"N423> 6s66jn\G*goe.\*Ur[s88UcFG%n@+K>kpKRE%b>Oo+V[q@-U"-G?UPJ;,i4c/7K+0a+o1ADA)MU¤aNPqNnjZ!kM?=" ,m.6T5SDS=8q\oO++`H-(mou¤pDsueHM6UimjXCe*Pid:6Q[EIn,`C5IF*1G9#lq(/*8To9G.esg,.9UR5W=:[+-]= `0I`TXt9%7g^^p+$_4d`h(d1_A;;j"Ta!Q4HSNJBeauP^"QGe?;r04NFl(,0o/«Q#7+KO=):g/b@WZPGgsd-P;m/b= g+MM@?L:[0B^o_>d@dq$@;T[`QC1GWUlN_@1.Cd-WVfXsjC$QO6k]kIRG'*«Z!@Z5BrfK[fl*N'+NhJD-LRPPQC+@6 j.OSHfgb7uYM#^h7UKu`#^TNLpBpd,P_6-%][8$NK-e'hMs^d:UF«bN7U#Nq;fY«OGSeMUQBfS>n-qE6SW)/+B0jn/ `SYd3C;+Ed\dd"EL2E^5K;9Sg[1kI'9j¤?¤!3,¤%_6%ha>I^'N>R\¤\¤K=s`[VuftDU$m?`0@\bC`?TgfVkm1[^e*R Q]64#,paKW:"l$m/TrBile'jS¤_7rS(3O\3Uhc^kSfHjnbPf#kO78Ds@S¤o[E(5,^jI[tjdfK,6OF'+2;XF«KfQ5q" Voo.=Y)#GONeSYhH8_iQLu_T>oAuXF=H8¤m9Rla5R#jgS"DY+;q#-17W`Ti!Jk[qBq8u\:#P"BdPa+¤=+#2¤u3"5ZK >m8"B"/m2(cN~

アロー関数の外に出してみる。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    const q = m / 2;
    await (async () => {
        await DFT(src, inv, q, k => (k * 2) * 2, dst, 0);
        await DFT(src, inv, q, k => (k * 2 + 1) * 2, dst, q);
        await DFT(src, inv, q, k => (k * 2) * 2 + 1, dst, q * 2);
        await DFT(src, inv, q, k => (k * 2 + 1) * 2 + 1, dst, q * 3);
    })();
    const h = src.length / 2;
    for (let j = 0; j < 2; j++) {
        for (let k = 0; k < q; k++) {
            const even = dst[h * j + k];
            const odd = dst[h * j + k + q].mul(W(inv * k / h));
            dst[h * j + k] = new Complex(even).add(odd);
            dst[h * j + k + q] = even.sub(odd);
        }
    }
    for (let k = 0; k < m; k++) {
        const even = dst[k];
        const odd = dst[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSulfkuC,'Rf^+cJsk9Lc"?([@q«p(GtADir]H9Z4(;ACE!5dG9ga>M..hM-="i:kAY8WUO,oq%>a;RD)fL8\lc¤0 >:rj2g$Ft=\Z9-ff9g#(iV4:@QCH$k6p`bf0heWJoOq%jPip>X8T"u*r?\4bd_ME5ZRipB9?¤]7,k2JH«A=PaOM;k6 o!8`SC)«R#K1«2pI;-*l+k$uO!u>;NPH0J93n7X-5"l5Ug?admp/M`Tm!\CNrr\NJh?:/UF0O:M¤Q1NA7E5MLK2;j1 $Fk_I.V(rgN/hCY'=bgN-)¤HV4WrdH)N!V-)0"kdLKN9`>YeBTHXgfjL/]'9i;?]Pl_.q8hr$Lh^1.,3WBRi[GTJ)n 9B«c8#q1sCX?KBuG4jU-s$([sFb]Y?.#NpsMlW!Vf;:+uRBX1Y?-tH2;.h?C6r.BUe/=di^Uj.mF-7`T~
GhSup>Ar4L'Ro4H*2,OJ,c¤5_Bc«bs[¤qfQh'>¤STgEO`9B20pOI2>3n_\QWgmec;AZ3Sa53G!kT>,Fen2K,7/EYm2 8S!aOS¤e6i6ad[MH@VX+':XZmee7L%gNkJt9«FKHc@qep1*4nY¤,¤/*69@p#;]Mo+Dju)70i(6DEC?=Y`.Jqd9ONu¤ $Z,WI50-`Y[Pi?BfIp'C,P_oP9.bj]GW8IQM[CF?=]AbP;Q/aO]e7NCY74MTAKE%eMhLV9U2MXHkOR.jS\Q56m\+]U 7«F$YKA3P%FujTI2e*%RX)!¤s6-hFGMHnbqe'1lOo'd4]n;566-EJGm,6pYN*b=!H2%3Gp(mZ«MNJRF^k)3WsXEEks R6,TUOeaN%,6VmSX3VhP[BJ"2"oPSn?j6;*=;5Cd(:^_na1nq«c«E`W]t6W0UkDgZ:7RIRVQG=Fre=3QC=Z*W``je4 +tleT^#r-*rU]7k8J;>%P2@J,_m'Lf8%o9sS9)17]S+rm/5:*R9T,1gpJXlO7Ej`OA-N«NKc[uE6'eSDOn0ZY`4dT] B)i;?8;R7«M(lp-")o4g6RUBtZ^H@>fuLE`F!A);$mR`_5!J]!4nS\7[r9-H:)gc;2c;:iM4.!RRUr0q(7J[q>-gI* 9@i+5MEF/9!H.K7345R;1!^5«LG6«4)EOaBYsa.ToIM[!iom,Y:DiI[:ZJg-)H:XLl=?=lB.GHAgtkGkbRU_KhC)IV 3$2/_):Kkc%#PtLqk5sMOW)_9B«H@bM¤B9['s9/D'@pbB.'c61-WGODBpgBGkBJ2oA)2K+T,h!"']*Tq7I_«FYA$F! .CP^>Cc«cN#nEBKLRR#0.Ul[aKPjp)mbN1e¤5mQM#/7p5//Qb;`j+hfD=N+h:hhUN/d3:mg;^)3BN1>R#?A`p5ot)J 5nQ@C=n51"qs;L5=j?:^WJS)WMT2E-)c'qsGUgnVIkkiQP[3=;bShdo7`f«[#>qQ%[doc1+3I`@O4q/8«d/pB^Lf!U 3c[:::\,MJEQZ9[`YVXIKcq=BkI+IQY:O+1"6uUSD!o:T[_«oDX>anCH1=g+"«Pm9U[*i>3E?jhE[pMoH2B433F>n0 oG?6g#h`W";2t8GYHMK7E^?o;[_U\[Ma[AIrJhYXjYpQ(B:PfLkP!F8NaMed`u#1O~

そうすると、アロー関数の中身はきれいに添え字変換つきのDFTだけになる。 これで2回目の分解も終わりである。

分解3回目

 DFTが倍に、後処理が1段増える。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const m = n / 2;
    const dst = Array(n);
    await (async () => {
        const qq = n / 8;
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2, dst, 0);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2, dst, qq);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2, dst, qq * 2);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2, dst, qq * 3);
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2 + 1, dst, qq * 4);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2 + 1, dst, qq * 5);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2 + 1, dst, qq * 6);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2 + 1, dst, qq * 7);
    })();
    let h = src.length / 4;
    let q = h / 2;
    for (let j = 0; j < 4; j++) {
        for (let k = 0; k < q; k++) {
            const even = dst[h * j + k];
            const odd = dst[h * j + k + q].mul(W(inv * k / h));
            dst[h * j + k] = new Complex(even).add(odd);
            dst[h * j + k + q] = even.sub(odd);
        }
    }
    h *= 2;
    q *= 2;
    for (let j = 0; j < 2; j++) {
        for (let k = 0; k < q; k++) {
            const even = dst[h * j + k];
            const odd = dst[h * j + k + q].mul(W(inv * k / h));
            dst[h * j + k] = new Complex(even).add(odd);
            dst[h * j + k + q] = even.sub(odd);
        }
    }
    for (let k = 0; k < m; k++) {
        const even = dst[k];
        const odd = dst[k + m].mul(W(inv * k / n));
        dst[k] = new Complex(even).add(odd);
        dst[k + m] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSungMVU.¤:O:'k¤FH6'lQep%8@eVTLg4D+"1Mo@-I`-:#j!4«pl21MC,Q«k$3(j%#+e8\LNY?ODn¤5?l3:k7qTS` a/ieb10?/98/tbfCtKeN(lTnkpAf\DaPm=(X>(QDW`=p7?912«Oa/42C:up\iE-U_\9Tbe$'«Z":s^:\>bK:¤F2_Jj /qYNJ`.L7P«8;"m-c7A(\Vph-N2pcD'q==u3W!(JjPNe%TSeu5A;Th-iOcDEWa?TBp=Kf;)M)@U[@B5DY`3"(kCId0 :TN3¤G)`Er%g]#r9fY)]AiJi%«1Dd8=]9«+dn)Z!;B¤ROh`B7eI#!+qDE>¤>flpVGe$:@Xed?dF7R0@iMtN3eb«;S. .nS_VOrpMcGq$m''Z[¤1.VHWO3t.6rMDl?tYf/tT/$/slg3+-b(LJW-c6oe`I+%jn)M1UrSR9ta,Np-*.1ACgkDKUq WgL4hd,7.JfaPuSTZ"J:#6¤^n9(0jKo@/$Y%"BP$((%iH$2sp:5%^2~
GhSuqgMYb*¤:O:S%#fTW@p+f?Zl?*u%>o"$'\I71,XiaUA#nlm0p\b/U)PJ2s1Q=.,jOA5E=>?:3¤>2Z%YmCiU,DZK b;_mmOa?$93XlJZP7«5$¤`BPm`Ob!3ECG¤j2ISjW;tn9>c@o=*17Q\e#dtuI'«`tGT.IZI_Mthm1L>rJm>J%g70j8\ FF^3.XK^fA73JE8FZo/J=]D*I9(F39«k8X9#]ZC/KaJfR/8Yc;$I.@L1k[k2ot`+'D'?3+;)¤!%fV'13^A[TK-I>$, /IY6R9"3\oE%_L«]a\5c2Hdi/3u"8!fX[GIUteElfAD@GVn8%gO$lap8K1ea3@I4RPPH?QE1f>2[!$`AnRsSS¤]1?? D.E%Wh"Qtn'LTFG5iBAcp?W8gms@¤o!WREhG2p3(9N5fr«4K:na)AZM/pj(HLUU:VP8[cdqH6CM0Q$L/Q]LpMTh\gg AA"h71s?a.auf#MhVXl0OL"@q9Ce^rnoX!oa-!Qh"Qt@G^9:r7#$)3:\>GN:MkVs@p*Sp@HFa*0Eg)Ub¤@'M\g;s*! 7H*¤7DA^0K!$jl-Psh#"n2G;+-52kFn((tMc7e$\Hp¤i4]Jjd8V_H-B1¤M48kVa8>YbL_ah3DbY0/eZQ9'r']f1`eg kdo:q¤B'i\nPXWr5eR7.o*'*ub¤YNbYE0dAk`T-5R-rK3$WPT]6l1Yh(QDR+e/MYPZgMR)]8«PQkq]=d.UrnUaI^lh ekeT5,rs8KNOi_jk[:WnEP3HYFsFB[`.?^k«C'YVb7U!M«?D_%_u'h,8CbqmcC\WA.u;>O:%%,XKCTXP-EX#bF2RK= ,8nG'*EY%>$$lqD*Far)SF-tYO6qh/^#BntHV*t.a'Je'FSm\]\nJ:=`E*lCos6[E,5t`9%@"4XC>@P5HH/Y(Z@hg\ a,NbnX/0-H]kcHsY-I:5j.I@ZUtZij[[brdH"(nd*F/@qPT«=R]bkh#`hZYYq1)oi"C41gbEo5o«ZP4af9k!pI2ffI etmkr\hlJCSY:(Y`mVYfpRnto«k[`Fg+4`)Eq9c¤(d+f9KL[a¤O(AloaWTrn+?Ye¤p/QFNo5IKP*;¤g2oBt`lh5C3V H10sFCjliWE)+«+[CKTXIXS6An5iH)P(Xh7[D9Bts3",T#G>sCKu\fs`H-//L[HZ-[-8CV'lh=>_¤Gd/T=bU:Odn0Y `nV$'.o[d!AGX"¤a+W5Xhbg«F^Rm`PH^+tj?*P^«/oR`^O1#^*;.ct2j+q\U`aLQ%f0ar`!j9M/\c~

添え字変換関数がややこしいことになっている。 元々の関数がk=>F(k)だったとしたら、k=>F(k*2)k=>F(k)*2の2通りが考えられる。 これは添え字変換関数を導入したあたりでのDFTの偶奇分解を見れば、k=>(k*2)*2+1k=>(k*2+1)*2+1に分ける操作をしているので、置き換えるのは関数の外側ではなく、引数の方である。 つまり、前者のk=>F(k*2)が正しく、DFT関数の呼び出しを2行に複製して、k(k*2)(k*2+1)で置き換えればよい。 点数は半分になり、出力オフセットは単純に増分が半分になるだけである。

 後処理は既にアロー関数の外に出してある。 処理点数を半分に、ループ回数を倍にすればいい。 あとでまとめてループにする予定なので、点数を示す変数は新たに作るのではなく、後処理が1段終わったら倍にしてそのまま同じ変数を使い回す。

後処理をループにまとめる

 そうすると、後処理の最後の段もループ回数1回のループで囲めば、後処理3段分をひとつのループにまとめられそうに見える。 とりあえず変数を揃えて、

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const dst = Array(n);
    await (async () => {
        const qq = n / 8;
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2, dst, 0);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2, dst, qq);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2, dst, qq * 2);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2, dst, qq * 3);
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2 + 1, dst, qq * 4);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2 + 1, dst, qq * 5);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2 + 1, dst, qq * 6);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2 + 1, dst, qq * 7);
    })();
    let h = src.length / 4;
    let q = h / 2;
    for (let j = 0; j < 4; j++) {
        for (let k = 0; k < q; k++) {
            const even = dst[h * j + k];
            const odd = dst[h * j + k + q].mul(W(inv * k / h));
            dst[h * j + k] = new Complex(even).add(odd);
            dst[h * j + k + q] = even.sub(odd);
        }
    }
    h *= 2;
    q *= 2;
    for (let j = 0; j < 2; j++) {
        for (let k = 0; k < q; k++) {
            const even = dst[h * j + k];
            const odd = dst[h * j + k + q].mul(W(inv * k / h));
            dst[h * j + k] = new Complex(even).add(odd);
            dst[h * j + k + q] = even.sub(odd);
        }
    }
    h *= 2;
    q *= 2;
    for (let k = 0; k < q; k++) {
        const even = dst[k];
        const odd = dst[k + q].mul(W(inv * k / h));
        dst[k] = new Complex(even).add(odd);
        dst[k + q] = even.sub(odd);
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhR::bAMqt%(u0HF55"AH1W7K1_j#kHpOSMWZ3E«9a!$/Q%u+5HYmU4U,;CKD=GbolnF+e3-$+6ke0J',¤29J7bX9? ?'PV4o68U3Z¤=lM@g6l«_GE-rT$tGb96H!B1GTb«e"/U3RPpU!8'[sjr)-C$mslT7%4SMB[Ja@"mrfR.hPjGX3ej") B5fs)V66Sa0[b-=9seXYls(^ehVOsV]]ktKKpkclnbC8,2iVhT3qd,HBj!gaZsL`[#*b]U:f«/gM@`DhB'b`IlE%_> XA7""/A%«VCVBs(pmE`#*VsB9,f^r?MM5DZ\bHa¤HpF]!(b+jn>FatCB.4>rjKN_$jEcI1ZM;Q7«j3t97nFmq9_P?0 ,=Gq$hfN31N4Qf(="sT,CYa0A@;t0¤!NOeYJH~
GhVOd=\md;¤:Vs/(e¤l*`«V¤_>La/g[ABnQ97j/S-n«4A?=L*p]n$`Zs5)H%OnFi'iO)_-)UIb/Zeo!tKQHPfQ?!8[ /9/Y«GnOZ3,7=kQ"2HDp`'!(aYXNl^(4f7f;>])?:P>2D1)a#¤!36-FZ('qG5#¤3??r?@a;P¤e_mYNbF=TDsa0nCWk Xg$p+6BG*"\h«`/1osMq-$b?'jgN%J!aM;!:k>C',u%ffK_;\%TF]KhFMTH¤@Y[KM7)o!Jd9S9u3Ic¤+,d;s!/*N8¤ KW`UCE7aj+;?s@P1WHZ+qqP*OrN)hG;Q_8-o8A'.CD4#H8E=;¤2j%+]5nR9=EI,0M/S,g$g>l;XU)qZZ#Y%0ca51oY c]¤$ioppLMJ\0L%:H+rX(O9?lI!%kg7sERaW+'[G\?t+QT=Ws0Fc)+_[oN-c)IDBKBhijAf]o(NTCHebc)++YGXk2: PU5VWGMHD,s)«?Xo$#"7j>Y/c2)9aCb3*c'chmt':LSVN;\OP;BdH?g0FrhC`#K6ta+.!0*:QT5'k`eYjR],tF70Af )8JQ=''¤tj6u[*j#94Ptcj=i5>peLjo]Yr1$'FWr^^_@$¤9!?n(5CeM5;Ze@SQ=V/F."[BM4.%RRMD]¤@@EnS7^G>k >LqdeAU+[V[a«99i7+(WNorMlFpOiVWhJHmc_d)¤"[*MUJN.u(PS$BuhImn«UI"Q"WZn2oCt0/@@WZP?dF=))25r6" -¤#XK?OUe`DJ-9gm#BJOKq9hBlBt(Y¤2H[tG%poOElGuPpbr0uDU\]Tc0,hrW1dr?-$l:M]kB(`;sBE`3mulkG\X#@ 5Du^'g8ek-ejb$m[6\!HV(BZcOkQ1MHY]%na(V%!FU;Ci-%(Cm]SB.dHh?uu=f9«AZTbl*A9l9(NmPs"FF-¤gH5'«8 DsQB>CngbmeLkC5M"JthFb_htX%,bY«4*]¤n@+D"(iGWYCcU"7qCA]¤i1=r-g.]-I@rV0,K.R;0%3-WN#DU7'TlFm2 R_92POtg_S>LN¤;b7^-m)1U?2¤@7)H#M?Ce,;/4hctMZ[@_$t(QI@qelCg)P*uQ5[A$U_cf=`NGAY[YD«%g8"bPa96 =_>L1]rqoHp*a[6.,a3ko0]dSG(m%p4t>;j3I7_bPYg!L_2k`8¤[K>]V^CMln"7[!SHZjTfED#407ie7U,THR[«ua- L5`H$$N-TDqnfO$C;b,K~

えいやっと書き換える。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const dst = Array(n);
    await (async () => {
        const qq = n / 8;
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2, dst, 0);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2, dst, qq);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2, dst, qq * 2);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2, dst, qq * 3);
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2 + 1, dst, qq * 4);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2 + 1, dst, qq * 5);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2 + 1, dst, qq * 6);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2 + 1, dst, qq * 7);
    })();
    let h = src.length / 4;
    let q = h / 2;
    for (let m = 4; m >= 1; m /= 2) {
        for (let j = 0; j < m; j++) {
            for (let k = 0; k < q; k++) {
                const even = dst[h * j + k];
                const odd = dst[h * j + k + q].mul(W(inv * k / h));
                dst[h * j + k] = new Complex(even).add(odd);
                dst[h * j + k + q] = even.sub(odd);
            }
        }
        h *= 2;
        q *= 2;
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhV8Bh+iSf¤;BRuMK?=M$PcQL$t;b$q?/-OQsNbE8m>EYYA0$bm]7/U@`?o¤2h?OJ1M8IM2@*\r9ZQQI3!JIb6%DE" .RPb¤)-3e3@Q(i0AJ's-P$@^c*#!5pkTllh9Go>6M3*cuN!Z@f>Ka"C;QaY7PZhZ¤R*)T]J*'V«+uRrA.mp.`Y1i5A )cGc3p/UC2R4KkEXcT?;b3D11LeKfeF74W5Mk[qGSpYFRFi*R6>¤Ha-$"DB=n)*jd'.KMmPGM'3G[Y@$BeQ3:d=@q1 cXG>))XObYpsO53kEP7b,fR/%8¤^;q=7+90F>S_dbE-MGLH4YB?WeEcT>n5+E>$a:joMX0GrN;fX*p1ahqZI]q`_sN rV-'9q?t,*2W)%.4SgUh@Bf>_Epq«GkI]40fDIQhG'Bqt«F¤.,jr#6FED%Y~
GhV7\D,8n?¤H:NnEN8`KVFdF$@q]"2BH,Er]h'pF"0/ek(Ligm1i]3CotTs3-`N;aNgGg-TMBmtkBr?Zc«#hag_VO% ¤7p.PM6HM-baD6B'`R_)5C"[>3JW;5Z=C«K"0tFf.k`jo?7s««8DP=«Hf-8X;8Bdp]h6.1\^JfK+nE/sZCKu!N+'a. QHHJLQ6-lnPVUb]\0#«[KjlORO,-rt.%EE«d9RP«E"oUb.F]uOk*E\59XhJ@«[$]B,!-AsitoYh>05+Z(OqkVQI'^[ SJ%7:j_kgf$Nb?]!`s^fN8`(Hg7AqE^6VucEYH!NT6DO7pEA9%6r)o:'3Q>9-pgQODC=¤?V(@7)S(qa]1_6r-_P!T> @c-@Zmj0.!]6rL*i,rh[fm?Q/o:d`:^3CiFinLM-«+g,L\?oS¤Is¤X,>Y?gnhGhluC3YHuB*B0HgZmnt-[])oX/SuN ^8@V]aqXN+>OJ\r^HXn.7IFYXnnVdh*a6@!jRXY5\R:!7;]$Ule:I\H[$"rs«.r+^@Le0K@mDFi$2K:2[BFV1S3P/; n!«I+!DEV=$*Kmm";cj¤^i:2"9L?E3NsRd+4Z`O]6KIhM?j@18O,n^6"9%F1kZ]a;DCb.4\Vq>g6pme/c*V2^9f"W^ -l**JaB\YB/8q6=(2e143-AcAURu2?%UjHG%«Q^dCl/7nl'\1N`%cnA4CQb*4H8CpBi)-lWfq-QX«m^ZROi7RLc0gX MPo*%dFh2KKl6:i/_\IZYY`Zo@6hmJM¤=Y:98/(R@IQ5VjCl%-17QZ=Cnl`b;3^:$qrpLq8Y8V9KJSa6T>a_JmFAQ« C8:/ZS$P$4l`/sl.KsDG`Jl:ci7hPZA3A#6Ok[4eCq\?OHAU=dMh«VrFuuU]E\a]=M/?e2Nr¤6+PaNe\29¤p.ijP`u [mMkXK!la"cR,Xp«0]aMKif)kF+"_6«K«l'Wi?FFfX+Hm_o6YI,O]QQ5;7MO[14WWiF!']3TB-XP(!qqlkg_>22;:O p1>@?c$sceXo(.BgDH"2FQ;?i]WO2KbBb5]O(Dj¤n$DRjCY=GsD+O,Rj3`>ua*,_3rGVs"EjNcDa[_hYLN¤n@*pE>* [+j4#(8,JIEkf?(L\TUR@S¤n0YhANKa=g\«bmgW.0)'UPV0K2uYiEAo>tA,%W?^DaQPY,_M:KkSD+/KF/Fd;l;)MsJ E00;pSf\VdLNQsH%A1C`,^7BI"?U*,oNh1)ohWDP>4k-lZdt0SB13dEIFWH!f3d*XVkX~

3つバラバラだった後処理が、無事にひとつのループになった。 これでだいぶ見慣れたFFTのプログラムに近くなってきた。

ループ変数の整理

 jのループの中にある乗算をなくす。 このループで全体を演算する必要があるから、jの最終値はnになるはずである。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const dst = Array(n);
    await (async () => {
        const qq = n / 8;
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2, dst, 0);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2, dst, qq);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2, dst, qq * 2);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2, dst, qq * 3);
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2 + 1, dst, qq * 4);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2 + 1, dst, qq * 5);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2 + 1, dst, qq * 6);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2 + 1, dst, qq * 7);
    })();
    let h = src.length / 4;
    let q = h / 2;
    for (let m = 4; m >= 1; m /= 2) {
        for (let j = 0; j < n; j += h) {
            for (let k = 0; k < q; k++) {
                const even = dst[j + k];
                const odd = dst[j + k + q].mul(W(inv * k / h));
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + q] = even.sub(odd);
            }
        }
        h *= 2;
        q *= 2;
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS.=5u5?_¤;BTNMKdj][¤$1NU"2%Ti>7l++«7Vt"Dk\cfbQGus6^:1'sQ7\R^\]4o4D=X'`QAM-lPI]P,;?kGEZIg R¤jUuKB`"o\4nlug2)neO$)b5TIP¤CME38@R)Fd2dV^2;?W-nR-Flb\Z%`Q34QrQB0CkOPED1D2P!hp>j«'UpOgOh, #L;;@Kl_hrP1J(%Bk>l*)LAFndE:"G'o$O`HldLI«iL/+^1n^R9d(P]s.:IB69«B'oJkYm7Rk/PhNfgKIM7*a(G(d` [%qX8Y`oQ@-udG[kD\b#GYBK3de+oM^E!!O«1SX^M12c-_>!6tg'jlm~
GhSup>Ar4L'Ro4H*2,OJ¤5q`GY$+;qC0RT-]1I,06\X)JQc:=k+>GIEikOs7\_r,p\i'U0I#bu%o::dKrgZ'%`>ne8 M/;]l*dJ'5aF_9-$h-_M0i'b[9G*o'L$+UDC//Vi,j[p\_15]@=LGpNm50NXEAS-«7D@3,Gq+uf,N=A%*Xn`\!/oo? @*=?$+JfKhD=2YR?_HUTndtmq9nedU3*Me«"cbnb«J?hKla^#r$.EX+Je^i0$5¤=e_Y>_1\!HW,rT:(O?k,n4cmW«7 ?!ZdS[d0*Q%UAA¤4+sH`Zoo_^IEf5FjZ7EC^M-1*D+!_S'02M:«Fchb:k[t#X@B@dUF\d91tej2e,u-ITj,pqZ+##H fV(fu-K"P/Ggfi=Fpk«uP;[!fG?Y2^n2QCr¤UU:gV«)aqPQc9A;.SI/.'fJ0n-[d(;mMI%X$=A5inK¤'«L¤g=%2G_% Q`-H=EbB7fe+j/='*h0nh]p=XJH6`\4qC#NEu4U8J`-o/$udquKd?Bje.[-?F#Z«'!^`«Ka)=D.q't";q6M?O*sp72 ^d8#6iqeL"Y]0@;*/AE5-e*ULR=N](fu«ClOkVsK5+FqY?eb¤kgAZ`Z2IJkR>2H)sR4gUXRS%:_AYrUBR\At«ZTo(# f¤EO3gd/U`DK?=J2.a@oT3')D@«>1#FEI7lJdImn1I`%6UF/bW@FT(O`d']c«,\K\13J=-8J[dFb8GdT1P)N_;66,J CaPQ:[uRR/m«W(UW+TXT5fa(bfVdhl8dbaMi;We=UVR/UCdqd9N=r6Z;5+lpV\/;JI47;l:.I?i'i/tZ_cQA.7lu#S B6D.:7E;/l$-LBu.Tq_PGHdCTI$@S,[72;iqS9u,A-`8t@Z0*0`R1a«oj(VMG?EH/%TuHhr^2Ue!>!SNIa_uq;R«Z% oq;.l"m+QeZ:JOK#`o#hL«8*%U)^d_>o18^Z0F#3j,m%`O?KJ$;709eG]Fm*2)$kmCU#XP!-42NY6imNmOf"Xn%(Tn gZ;SI@«9ccC?r_g*pL8:L3k$u1+Z=!4u;SUF8sN]SAZkgmf5#\4psa+a-4B3P!?6]F=5SSSClVV,AbLckaR@m#g¤!a RNh6GhR-WrXP7o1bEl.QIHJR=EN8lP`16dB3rpKt/6^Tp^]"'sQAPbI]4,:2:L7;KHi,TM%^>l,D>T9u2"9;/\hW-h ?_ZEnQ2~

 mのループはqhを見ていれば終了が分かる。 hはどこまで行くかというと、jのループは最後の後処理で1度しか回らないわけだから、nが正解である。 h(Half、のつもり)なのにnまで行くのは気に入らない分かりづらいので、mで統一する。 「各段のnのようなもの」と捉えられるから、この方が分かりやすい。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const dst = Array(n);
    await (async () => {
        const qq = n / 8;
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2, dst, 0);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2, dst, qq);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2, dst, qq * 2);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2, dst, qq * 3);
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2 + 1, dst, qq * 4);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2 + 1, dst, qq * 5);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2 + 1, dst, qq * 6);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2 + 1, dst, qq * 7);
    })();
    for (let m = n/4; m <= n; m *= 2) {
        const q = m / 2;
        for (let j = 0; j < n; j += m) {
            for (let k = 0; k < q; k++) {
                const even = dst[j + k];
                const odd = dst[j + k + q].mul(W(inv * k / m));
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + q] = even.sub(odd);
            }
        }
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhS^NgMVWt%#46J'`V`X339¤V0.7R`VEVhl>thq>OKKTLN)R?¤lFY@Cf$LFcVF«g0cKF)F*CXo_^-,\NCGSHp+g+%0 Jo.QOAmZsff8m#9¤fZ1?5R;"7iG4¤(`5d,[i*h;k-qbKs_A*p20SL#Ib.2\3UiiG@$7r9O;`n3A4GURS1tYY_;K[¤S ?CMrtbfsEbG«4gZTl!YXl+!Pa4«Us;1"4\]iTl5hU#@g'7c[i?AdP:C-UA44CH:L!K$-u3'%9..%t[BHlFn@npV[#- 8WIG.!G=Qc¤R:LXr0fH"`BboHBoL@`MJNrr?[2qnBfbD?.lXVIF8!QE:8\fskjau'r?(j`)O«?HVDgY/EQ!8KhfJ>( FRPM7V^Wk\h\*P1PcPPR\p_-*T-hST)8NE0P8QnOr#«?_9j9¤P.%#AaC9+c;~
GhSupgMYb*¤:O:S%#fTW@p+Z;Zs0Ws"@_¤Z"ni-GM`q85X6NS;QDdo['.8M6I:l)CVIX8c9oR#T3Q!Z6R@-E.\sAHk V1c4WMGX8^/s8gpCr>5P#k1b=C0aE`po6FmRj;UL\:513(FlL#S'?13K'`i«4KN=C«u*`?ZEYpT;V%jO8#iXmQ,"B" 4:h[U%5rW¤$=jn8)p0QXdg!d6U6!Gbm;¤4A_(jS$C2c"eU2)SUfZe8'6^Y]'+07a`.o^ef.NuP_GAEM:YJ:#!¤nn!7 ('Z@bi7kFfGSnKf_$+`M2OVI85I«RjE6i:.6?X0U?@Nn`Sj%lkrCQ'ZSTg3=b!?\UK*6g3_h!gVdl-8Q0;=q4CP5:" gVnX.jAD9lX;X.s+Th+¤lcq\G)Le`B.c?@aAU\fP;lV2=?A$GUjnRFcgul?tPK@*/rN(_)\5?D8@m%4!nb4DYg2O7] ;@C%uU-3=NL@r6=G1#3>?a@8#YU/B_Z94\'+E«Y34NG!WK?m2XT>K$/(j"D5au$46jYjAsZa2Y7Pb0>o]?8?#Bar>% XfCl^8]q)l!jEZr0*rf;i=/«t-52`mn1GC^/hZ$frc>nhH`(fcB/%?G1¤M79oI_23YiJ*Rmcl[[MbBf)dY8MaZnS0W L@0j$K)!iRT692soK0j**eNH:PrkT/$JAj>0=ZXpa1YYcJJrhM(]n^4U9PS*^VO`VpTq\A¤_3«61MeTh2dM^YJ'«S] ;T13\>BI7sVXR]kK-'Dt?$PSrR5//dM*r?Tp.La+LLouT!0Eu/*lr*8T«O:jW^Y=4-gkUYm0.th4TsplSNMh.52+1m n^U-Vok_qMLqbj*a/K6?pdC[>FfH::mpp(+HDINM1Y$Y!FnA7U_s¤.Q1nmA[;fbY0I=5u/qV(T72-l7\3DTEink]G¤ R?J`L,o1IJD%Le?b^Lm'¤ZP!/XUHZ7C/=**9psc`eEE\S#R\aDd%7Q/=N7W3L]=E/«i8Eo8?aktVodE>(TtK_3bS0; ;d.m:f>«C¤s0YZ._o9!uVcT(1s/kmiee!r-4REDWg#%i@XM#:IW7Ap+^:L0cce/nFH$I6@:eeQWcrXipb.'!!2Q(NT ^XOO]G2U*W¤nuJG:=W*A9rsSVC^4*/Dsk;«q,e4N_C/p\!S^5)o'_'VA.P+2"M59SjH!V!g,JEN]Ld[f$cf6.U«\pH XRC9/qA8/Frd"~

 そうすると、q(Quarter)の方が実はHalfなので、qhに書き換えよう。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const dst = Array(n);
    await (async () => {
        const qq = n / 8;
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2, dst, 0);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2, dst, qq);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2, dst, qq * 2);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2, dst, qq * 3);
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2 + 1, dst, qq * 4);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2 + 1, dst, qq * 5);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2 + 1, dst, qq * 6);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2 + 1, dst, qq * 7);
    })();
    for (let m = n/4; m <= n; m *= 2) {
        const h = m / 2;
        for (let j = 0; j < n; j += m) {
            for (let k = 0; k < h; k++) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(W(inv * k / m));
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhRk6d7V;1'Sc)R'`VaMXKA%F(YPH(lj3eJA/DSokghYs903mA]6e"BD«IDo_E$\)s#E;8RNfDZSL+Y2QdQR`pL,gn >*CgJCTq¤-1:PT,fU;mX#SW6E!#.rK\GWKfFq7X="r..c^6;O?WUb>?^s";qFHK_d)UnmjLbY*E(cCluEOtV:n#1FT .F%hT*«`(Rd3H0bi.BmUOq=3!s,d$eM6lF$l%:l^/(Zo4H5%ki>2Fm4W?Fd0?:@juZ7"e,EC*_]RHJU$l'ag96[s!\ pZDKB^Z!"0YGH\\hE-eL8hh70IiBK!-*\7SY'i>#iX2;bi.pYRkc6A/hiu^~
GhSupgJ6Kg¤:Ml+N1rN¤-B:YG:fJ)>C.hR+2^q]gTVr]7n.L(Blj\W@r-N).Q`MiS.9K1.G\5:GSs_Ssp^?Kq(3B!* ,dPD`7H>c=+\N/«*P`oi`$40E1duf3QC^_ZW-N$f'4E8?N(2=^^q'VCBW!;,LnhT/>NRH,R¤;-23$M*gikZId#-^Xi $Z()B+/G0qYK\(@p'VSj8KK'`WMshL¤'n;*W5-g%U8fs¤_WK^ddtVRZRU?:s[R.nGOYD5Oo[Io1Y229@$-]+-)Hn^D U?a«Mgicu/dXd>3qI«WqD«WL%^=PI^C"JKr`H=95?lS[pRE3c"X«O^.K-E,_@^U4c4V\h=A]S10U0c4%#lTXiNkK;m NZEEkC"rLp!]QlSmqtG!YLBoiph#H"$m+kH;:OGNE+uj¤5NT[d1kps9%n65V¤mjNXECtCogZm,206jn#7b¤er]]#SI ne¤:Y8+(2fIloe?C#\¤Anp=nm0J\1=R4lDgn$u*(T#(1=Ad5bIe%uapId.5fLcLuqMufWrd,R¤/aS%+8aSVl,@QtKs 1Ot)X,fK@3M.#«,#WSFPI@X43_.E`qkGRLX-:F_T$q!"*/m`_[4`pW-[qid«>.)(;(U@Ci¤iH"21s84l/2Y8k'sU"8 ;pOBVMVLS6*i?IMS=6b>EM¤T=%UjHO20@XAjU«(bUM#H-¤4^Z-]Y`ma>)fk725i.%W\S^O1J:c«.a¤]Z,R]1*MH-a= k^5"N_JL;s2;7`;0[S.eg458¤'!Y*Q+4V,2[__tE+@i12aF$`iO@BOVU+"Iuo4^X?94(;PK[Z(W?H#h`D?!U`e^4k5 bul(W?(lM-e0eU_kg#cR(IqsqJZ.^@RGh(W:i0'O6q)i9X4+6hJrQHYgpbsUhe'cS/"_rd%_YSik+r@Vs5=hPjJk7D hRt;7«=/e^LY:1W"Q)2YY,hHYG«T">;^d]+SOeW3s-`2*/aM_ZDK;f"V9GJ[og«]P9Y?fF/sZl=q0nJ%p]'4p[.rt$ le:Z-]56UrT/`\2NUVAGB44>c.+%Z.q(3OchirF.-8p).#dnUO6ZZUsP4ppn7H!]@/6uV!e1usDb?OdEj=4HBZnG,> OAE:u¤@0/U;+g`Ya(M\UHUP)22FTgkqa1,THq90Yj:Ac'cT"Q"cg@_;2?*LSO:Cl2~
変換関数の展開

 あとは8個あるDFT関数をループに直すだけなのだが、そのためには変換関数をどうにかしなければならない。 変換関数は先ほど検討した通り、一段ごとにk(k*2)(k*2+1)で置き換えればいいので、とりあえず文字列で最終形を確認してしまおう。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const dst = Array(n);
    let f = [ "k * 2", "k * 2 + 1" ];
    f = f.flatMap(k => [ k.replace("k", "(k * 2)"), k.replace("k", "(k * 2 + 1)") ]);
    f = f.flatMap(k => [ k.replace("k", "(k * 2)"), k.replace("k", "(k * 2 + 1)") ]);
    console.log(f);
    await (async () => {
        const qq = n / 8;
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2, dst, 0);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2, dst, qq);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2, dst, qq * 2);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2, dst, qq * 3);
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2 + 1, dst, qq * 4);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2 + 1, dst, qq * 5);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2 + 1, dst, qq * 6);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2 + 1, dst, qq * 7);
    })();
    for (let m = n/4; m <= n; m *= 2) {
        const h = m / 2;
        for (let j = 0; j < n; j += m) {
            for (let k = 0; k < h; k++) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(W(inv * k / m));
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhTQe6¤WT6¤;BTO'p"jt>:UF#TiC«rgYTT"LQG6;1eb=%mNVXU-W\TP`sk]Jo?EcK]Xl+)=_%o9I'6Aud)2+Wg^fh" FG0%)-\pPhB]p6IVM2l=!?]e«¤3uaqS9moLE%9bd2b/Qrhs7«g«@0..DZ^tlpCJo'R#C>,5`.nd0NQ."QTMCgQTiS. eX?YYhV5Bg`_+WJR=+4_:tZ'f>;F2arFa`lCZfR¤];'I_ggC*G-:!S\8elgo/UNn,\aVG/)-1«h#@O«9T`~
GhTQ+gMYb*¤:O:S%"pg+Z)kmshQX"!'\d4sTJmuBqHt.E«h^d-''2Z+ihZ(YG=Y=5/^)+NJe`*#jp]R@Gt:J,1/aj2 OH`b]e>H«_.hRn$KqBU*VrDGk8QZ4t4;DWQPNOc)ZM7+u8s/A¤L*P?0+Ndhm_d?u[bk\hQ)3qL.eLFI.74Jd'3:]?L H>KkZ,8/@$J\"NE]M)KC=%PV+'m3o'.ZS#K.l?04'ZIN-L2]Ad)%C#/UKt)>af\J%RAtqQ+KfKSI_J¤B\XgM?RVlM0 +ptpWmXhc)-?h(]le?XgGJTAf7c;;?\>¤(;EkV«(N',dK4Sb>b`@Bb72C/Tld)JU9QtnZ8Q=8404Qopi$ebkp]VY") mu';;3@E?Rm"YkZCMeEA%k@HR%s>ZXVU?U#?'_Lm^#.htDE30W%JO^PP=^5PG!G6L%;o.rj;>cmJ%iS9[l*if«GE\$ «/a;Q79P$hUO2«F:X;U«7CXZ*2.h+hog.Rc=*]gInJ^:@R(\6uLDXg\9$/EY\T0X6KThT;+B4GDH(e+TX'VRahc(?¤ -*kVmD?M(BY0SpkLr1a)3/kmq6J2e]$I6rSS:«[Hg:c*W"=N-dWs=-+Qe]NN%iGh'I93Uo^UQjW«'¤LFRMOJ]5grqJ JKTO#Mr0KT"4TCt2\/4¤5drhp3QLIt5o8$:a)L2Z2Q0?r!_jetgGDWY;/`ZWVu:@,QUFPZqAH^S>FYL@O¤;XH9Q5.b ?A@;YA$YG/3]Qe?@C«I3¤gTJ9+5)gE>0eA0[U¤#Hil'%411Jg3O8=2k)A:#nT`6C>Nj[)VOI$b/C!pn2K\/]PL."[_ ,.bA6rIDk6)>9NBm=$/MI,""`nZ;UBc0o/Ks#0r«Iimb:ME\h8%]VlNU'LNNM"V(%BR2M*MtHX=H0V.fHW7`UB[7ck ZTdB,QIdSMo:$nt_l"Zl]UEQ+_)#l(4m]XkMMcZH_!qD'?!=,:ZqPVA¤,`/O.iMmHFGE`¤D=VJF#t8nNJjQmOpF)>I m-«Og>o4>na\`^]iUWYbLDIMBaP¤O0"H23Q>A/uS@_Eo2=.!:l:%(P1bd>XJ:dlC#:@S[\Lcd%TaQ^cDs6-Jp:7«E7 @PTPD,aJ`V"j]l(9uG=CV-Gq¤'.«(:HE=5::Gk;d¤:«.RM$gIrmoYeeEefWeB^«S!^WE%'¤]*U=Jbd\pN:+^aP-ufW c9s;t;Ud.V4`e'lcS%3C!mC~

実行結果にはFFTの誤差しか出ていないが、ブラウザのログの方に関数の展開結果が出ている。

 通常、配列を何か変換したいときはArray.mapを使う。 要素を削りたい場合はArray.filterだ。 要素を増やすためにはどうすればいいかというと、実はArray.flatMapが使える。 これはa.map().flat()(ただしflatは1段分のみ)と同じなのだが、なぜ別の関数になっているかといえば、mapみたいな処理したいんだけど、要素数が変わる場合に便利だからなんだと思う。 要素を増やしたければ配列を返せばそれをバラして追加してくれるし、空の配列を返すと要素を削除することもできる。

 実際の展開の方は、初期値を["k"]にしてflatMapを3回実行してもいいのだが、そうするとkの周りに余計なカッコがつくので、ソースとの比較のためにこうしてある。 ソースと同じ結果になっているのを確認したら、これを実際の関数に直そう。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const dst = Array(n);
    let f = [ k => k * 2, k => k * 2 + 1 ];
    f = f.flatMap(v => [ k => v(k * 2), k => v(k * 2 + 1) ]);
    f = f.flatMap(v => [ k => v(k * 2), k => v(k * 2 + 1) ]);
    f.forEach(v => console.log(v(0)));
    await (async () => {
        const qq = n / 8;
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2, dst, 0);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2, dst, qq);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2, dst, qq * 2);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2, dst, qq * 3);
        await DFT(src, inv, qq, k => ((k * 2) * 2) * 2 + 1, dst, qq * 4);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2) * 2 + 1, dst, qq * 5);
        await DFT(src, inv, qq, k => ((k * 2) * 2 + 1) * 2 + 1, dst, qq * 6);
        await DFT(src, inv, qq, k => ((k * 2 + 1) * 2 + 1) * 2 + 1, dst, qq * 7);
    })();
    for (let m = n/4; m <= n; m *= 2) {
        const h = m / 2;
        for (let j = 0; j < n; j += m) {
            for (let k = 0; k < h; k++) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(W(inv * k / m));
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhTQ'4\rsL¤Dcq.Shf!;$%r=ZMGf.ZDJX-fK4X%#MIj8W$**dlp`gEN¤0"`WID0pom[\`2L[q-N)\hi/3q2C."m!92 DD3!@h$]XP?l8Np(1g51RDCObo]2[Ycbl%5ot_Z«(%UL2MaqRYKkT1NV+4MR+MILaq1p!FkA2kLT['f«;'A8,;K;HT :Lll*on9TUrCM"VPC*cq¤pZ=3lk_(GoJ$"25I6)NNTr$*%RbVMEReX%¤p\;)LE*#>ieOOZ;¤-6ZdDE¤XZ$reR#OG¤$ qo$H.1"WPOjGm-,^6='9_1ADm+>ebK$2FMco@XN!\Ab>e^¤T75POn~
GhTQ,D/\/e¤H;*)E=httN+F«\g9-h.#0""b"4jkF1A$3-XK5_P`Y.B([,[tOJ+)nr$73k_RqtE#b.P@L??gig6b4%q jJ@C6X9p«S=r0cL,I408FThJ=M-O*`[Xg!Uq)4)O9,am6Hm)W=a`L.m¤aVsujfGWDf«SN?ajsWbV5Rt5Nau:T.Y/J- pqR7p2V@«R.4?W8>pc;A9H+[I8NeihFeE`S"NPIgW52?PMltF¤n1_«j%:P!dIBWpt=NL¤"«a8lA2`+a2cd0=K$YZ6u %L+5lTY¤N¤3/_9emfGGQNG!2P%#(S_DQYl3-$PL;op9+JaW]`eG9Rjh]SikX*-=KKPAmbWE25Y7Uigt4oB7g*'uIJU m:%0[4CWS¤WhJ4/¤U«2;GP2"dqbdrI^j@E7+M1jCZ«;Oh_547orj«^5e(uDba_fN*UkRF1;-B)]9O!#Bp]nNMWS^^T \_'j.)Kq%EqG'EhhL@W%,]sj\Q4t:>nmq$«^eq3)*9Z«5^+Wpg#*qHk\>G7]9[c%bmNr]gFuBGU$M$SiairhGCt/C; Wa^0?fF$P+!0X,U;B^]Q]EWr)GjH31h(gVAipuKuh\S\7pBFCN$p8q1i"tn2gXu++8]+lleSE\gD@mnSehoCh\T,mA Fb_LZ,Z¤jXPP0BWH:Jh70aJ\rG/l`.=«59CJi`+_mX0j#C«EkJ(Pq*0NEouB)dJBorb>8r7.n^p.r6JCD'#]>6¤E`> `S!7r98riaMe-3K^o7PHUSTfq+O-=%cW3H;oc0:M[]>oe$1X>(N+b««;?)/;6)#23o`%«Z\X%LRjQ.+JEGjRRde'NE o::)ZTlM1B7CBBtWF9:HnN1*cJ*m2!YR[o1IV2Mi/DX8`jFfEsA=B*W^-I-5eNU1ngo0qd?@3L]p56JMIFPUn¤0"sQ U%bjRHhYP\9L:n(RiD3[0NtCj7>i3)cQLq;oWdLTE%]T1-e52Wp>S[:lD)%?Em=H:5«$Us-%3AU]R'K«`0](37@SN) cW#Sr(«oYfO¤7kW+Ppm$][qm_YLWTJk[3n8%Z"riJEZi'2MpE`7DT6:b=-Y@Z¤E0o¤-¤ti-M«,Z(!d[KXh44CY^6V6 nkJe¤J`-Q?W1BWe1'lBhCgV\D]:]WU*;6>3U6bMNGU$"0Sg0X+"VT«LC=V,_oo5VR+ta6KQ3j¤a06EAc@;$di6s/T. ;u¤[A``K1;Cld7jWp,RD[cBpGZ10YfrV>Cm\^rhcfUOV!53g/;rd]7DEh5GR]p¤n2B1_ISWsX«'"#a%6)Z~

 念のために書いておくと、fは関数の配列なので、flatMapのコールバック関数の引数vは関数になる。 ここから新しいアロー関数をふたつ作って配列にして返すと、flatMapはそれを展開して、元のfの倍の関数を含む配列を返してくる。 これを繰り返す。

 実際にforEachで回して、関数だから適当に引数を指定して結果を確認すると、いわゆるビット逆順変換をやっていることが分かる。 ビット逆順変換はみな色々と苦労するところで、C言語だと再帰を使うのが一番簡単なのだが、JavaScriptだとflatMapでできることに気づいたのは、ある意味、この記事を書いた収穫である。 ということは、mapflatがある処理系ならば同様に計算できるということでもある。 年をとるとこういうことにふと気づくことがある。 やっぱり経験を重ねてきたのが大きいのかな。

DFTの呼び出しをループにする
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const dst = Array(n);
    let f = [ k => k * 2, k => k * 2 + 1 ];
    f = f.flatMap(v => [ k => v(k * 2), k => v(k * 2 + 1) ]);
    f = f.flatMap(v => [ k => v(k * 2), k => v(k * 2 + 1) ]);
    const qq = n / 8;
    await Promise.all(f.map((f, i) => DFT(src, inv, qq, f, dst, qq * i)));
    for (let m = n/4; m <= n; m *= 2) {
        const h = m / 2;
        for (let j = 0; j < n; j += m) {
            for (let k = 0; k < h; k++) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(W(inv * k / m));
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSum9i$Er¤A@P9GfOtVm"[*[i#3*:2\Q_YbT7[u/m4k«+i\?rmnSEVia.9Zg1b0URl?NKhhUT:`6*d¤Nsjj#/3_M_ l9,U!8>k,-Slh*[!`«-0$[]j4RXpKnT5mm^b6Rj_aENPT*6hEe,N`*Ln`pPjK\^]f,2+qg8*TqX#)«C#kd8]Vkf!=_ «G]d¤)^[d[«.ZtB0lB«R[DNsd.Zn9f,fKi:?KZ$1GJ1is`O1"o/$HPf?A_Wgao=lX'JprS1-S#o8(FloY*"%UZ«ct) m4i^2VTko#%G«?]okVY2:@?esnZ$0uif#GSm+>HH[`;W#;%YiENgXo#=q%8VFZm6aQ)K/+W#ke]ZZHZmLF!Rs@^5IE \09¤.o;u^mZbJ^dE1k""[¤gso2c?%«Zi~
GhSupD/\/e¤H;*)EA7*;N+KuRg9-h.#0""Z"kp+E1A!qAXK3[:N$TsIBZE^¤s5"th.?pnBD.4cVbe¤5iPn+1K/RpA) $+s4@e">2[W^KOh'^eJA+-l-!P->C«Gp9\26=/(/MC#0h.8ArG)NA,`JiICC"b2cU«M"*!AR*AMX*(L:78sU%Qj"T> o5(YU7rU.[J\"OpBuq;.?cd\;-E>GPW"blO^«hJ,:_[2DS\i:uDD["aNV:1,UtS/%VcjCP62])7ID5]$h^tLRVE5WZ Lk>'/qVWnraINjs`¤:c6>rWOi(¤5>N:lO1"Y1DKbOf+6`:O`qd=Q`t#gN'M%F\\[I>/Ppa%+Eeh6-l=`:a^of8))(5 /G#7j.8`IC+!U3g\*iN+W9d_G1]\\dcGJrAWmjc3K*I[Nk4mPG\1`2u9VsbnoTL'Iil5OuN\G>Ij7@VFgX;aq;@0n3 @PupVA_!oA+2%W!E^hMnj-GoKXR^HSJAe?jk;f[k>C9eoi[q558?f"OUu]0\[D.PE*$6g6Tu¤pW(HSU«2)](FSPng? 865iI#XEF;?Q"M¤+)aF(@l!Q^b¤3TnE-h6Gm,(`N_KuYH3/00%No>1!o:'81^ktUp\Z=:n?Ml!n.]mJ`],%eiJZ_(F JKUZ>MdNRK"4TEJ0FpItd#:'KH¤gDuJhF$XNp3.=E'Wa[/C=X\pd=(¤N-^-439bX>qBTf;e$!^BWI**rZH89ilFH4' AoJH/mhk4-QEDI2a$ZFSrK1BJNR;`X)e,(Hd.qBl.4j_)Xf'0"`2«IOiiKRF$'fcSk2cd"O;XliXW@^F«t'9oWnpsr 6PZc-:#iKOYG5,rS,$P8TUq's\FA3*466K«p!3Zg$2$QdYNZ_;BVATD.sbYeK4r$JVj>B=q+*01«Mr%7b@fA+O7qkl 2«uLdjq)ad0ZmpFlmAjDc`/NaQ%gj9#nIF!(keZ"41P0ME8QeN[*Ou¤dLKVPq!r$*(^j-SKdf=feBLf%:G=aHi4R7% o.Fh(]/3TEU49orioY'«S%Cl59kpWKYTJJ6C)K1/==/m.L]?¤UnnCItL11O_;13¤YZ$Y8DHrE\g#l]=.eeoAFK@!kX p@WXC]AYGV]2M2«/`Y.H1«]pqGt)YGs"ch3K=$E=@PTA?A?IB*_O!/P2[;1h=XkH"-!]'Wp/!YbQJ>TQ,GLS:Y$:TF GKkA\pucR9B6-JFF0W[[R*/*KGCSGoQTJKn>Y%_'Bem-+"T8*+2rGLq~

 ここまで来ればDFTの呼び出しをループにまとめるのは比較的簡単である。 ただ、DFTasync関数で、forEachで回すとawaitできないという罠がある。 for ofで回せばよいのだが、qqの計算にiが欲しい。 そこで、iが取れるmapで回して、それをPromise.allで待つという、これまた大変にいい加減なことをしている。 DFT関数はasync関数だから、実際の戻り値はプロミスであり、mapの結果はプロミスの配列になるので、これで動く。 これもasyncawaitとプロミスの関係をきちんと理解していないと思いつかないだろう。

 ただ、JavaScriptはシングルスレッドとは言え、実行順がどうなるか分からないので、STATUSによる表示は使い物にならない。 それぞれのブロックは重なっていないはずなので結果は大丈夫なはずである。 これはマルチスレッド化の可能性も示していることになる。

DFT呼び出しの要素数を1にする
function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const dst = Array(n);
    let f = [ k => k * 2, k => k * 2 + 1 ];
    while (f.length < n)
        f = f.flatMap(v => [ k => v(k * 2), k => v(k * 2 + 1) ]);
    await Promise.all(f.map((f, i) => DFT(src, inv, 1, f, dst, i)));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        for (let j = 0; j < n; j += m) {
            for (let k = 0; k < h; k++) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(W(inv * k / m));
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhT8r5u,«O¤A7`PF0ll6,nWI48PkLFDb(kXTcU9H',S]_M+L_;H.H[X"`XSpfj9#«B6U4$¤7-XA¤cnOr'Er$^[jBP) /Y-c2«i^SuGGThu"*0¤La4h$0b1?7rO0it5GFdMFbL81U.L/DW%sUQm7E6C)3/J4i`]BZEK_7;>.3mZNHsqZJ9P";F _-DA5qHAn:s,ZGtK'ejp//D>/IcX=>LV'^+FC.Jb(oF$^fU3oo\0nmKYs_F%!2[aB+¤a!#q8[_s#NjiUI#Gsh8jPq/ ^ZRUO7ofhIgXY"WTK]6Y:Td.kbJsbbUbVXuReTh*5tCEM8+Mk$C"CDSRU?RbR\\!BO^^\gZ]qW:1MdU6e2+`>o-U1] ~
GhT9#?Z4CI¤:a/H3)r)nCRV@=+@PE?":QG.R'\"XV@'uS-=.?u3:>@7l?DE--_8^u#Q[e,]=to7rBCP*6D[.P$:L^` Aj.e=Y%:4k'^c3VO61¤J'eEin*Q]VRaE¤8,Z0]soD9-6SP6%`hO;=q#iD!«Y'n"\*VNn0@?DOIH7!"a4?3IR@CQ!$O .DSh-nhQL$9!fuiPC6+Wle(00NbbppFPo]SAXDUdGW8n:qCn,L2HYmu,trGrZrB7\BNc(Amn`A`!GnFn$@4/,UoWr- 36O't(BV(SNM^Y2pR11MTs'G)Q6%7%Mi3¤FnRKZ#5=@+?72OXR**b`J,tW$V@E3):nQP¤"L;kp@%1u¤MmL]7McVmk` UAK>q7Gr$"h:"kjfgX^u!QWFFEIN77R5Xe:MbD[]pfTQ2f«$Aur2-TFZA*S5BiU@FFPHaSq1-('NOg[K«*Lo6UM3S+ hl?«7qqIl%;X5$$;I*dsa--cCY%gfJGa9q$5DSJ(bTk1_V_+_XU?3e=@KhOB;a@:ki9H*>]rtJVT"ijFh%J7+!#kr0 Jb!l?OPo[:'J?.X(f5CnUk."$f4SGcM)F`%p>?L:\/iFZ(lFf!(r;^h)g,48CqYKf$s;)\V,o7^C5JrP_ZE,5gP]F0 jrNO;5r3^NdFaaHqK\+K+d1,aJ«'¤hc`o``G\3lUEPeGa15pUoff7^0"q??dbKORO4iQT])eJ?3^shm%KBU.#'^X5m $-\RKfS0S*6nA[WNnG/X[QKQ%20$hslMS¤q'P?ue8:(krrj587#f#l,55Q_=.V6k7«YRs?-))'(d@h=,/DUTn,7q"K mG;H6dal¤hkH6F\T-h'n277/=¤Mcf@0ZYL"]?+X#!VQ6_g«-(K!«;-?kV'T:4?2C"(iB,ShrdG;¤TVF"@>:l«jaZO« s3d86V7f=?c)\:V:%PJFmHWcagfCCk"9aGS/9uoRb-I*>^\Q*]«nhW¤gWHb2_C"m,)f9J8;qJD_r]«ED%#(fKD"#>[ rmf#(6@\kj"%T9Tk*t:UOcYsf4mE[Xc/po@d`l@J«*#9/I>hSfIcd/:nkWbZF%-"3aFphXi«U"gW%"WV>-pXsQl%XQ Nhn`ZGAUK2r;«/A3?Jk">g>#ep>/uf`kA$^Lmp[l'N3%6kLn3f)#3\1S=?E(~

 DFT関数呼び出しの要素数が1になるようにループ回数を調整する。 要素数が1なのだから、DFT関数の呼び出しはn回になり、変換関数もn個必要になるので、n個できるまでループすればいい。 mの初期値はh=m/2という式があるので、最低が1ということはなく、2が正解である。

 ここで、1点DFTというのはどういう演算なのか? と考えると、単純に入力をそのまま返す演算だと分かる。 したがって、DFT関数は呼び出す必要すらなく、入力を並び替えてdstに設定すればよい。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1, n = null, idx = k => k, dst = null, offset = 0) {
    n ??= src.length;
    dst ??= Array(n);
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[idx(i)]));
        dst[k + offset] = sum;
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const dst = Array(n);
    let f = [ k => k * 2, k => k * 2 + 1 ];
    while (f.length < n)
        f = f.flatMap(v => [ k => v(k * 2), k => v(k * 2 + 1) ]);
    f.forEach((v, i) => dst[i] = new Complex(src[v(0)]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        for (let j = 0; j < n; j += m) {
            for (let k = 0; k < h; k++) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(W(inv * k / m));
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhQ.nb>,r/¤A70Vk%H\KTG;E[6'6]SO>q3;Ej1KsXSPf5-3«Z.X«U_s3I%s#a,[bAc/"Ah7D6E@#FW;YK)#g)gDFll ^9KLD!?5Y+UY0\c«$3`eFW/K2:5=lmidegGU;2l?[)U5c,0u4,¤*S!_@qI*)YiFa#MAd0kn9riK>;"?9]M?l/Ug*(' ?1F/s%+X:Tp@P8J^fGuQpQQ'1.ASR2=qdfHWCFniMiB!q*N32h$Qc'-48i0"qO(X\TfCZpE«:2B_io,MVf«hLR2aj5 s"@2'Ul:FH*`jVhIK;3M=.B~
GhS-XD/\/e¤H;*)EA7*;N+LNLY[Npb#"?!0"4e82)>YL1«`UCp`Y'"W[,[tOJ);88acrV1'l6h.h*¤fjHgYE@4-NcP )Gk!F#lA%3a@[iH^u>.nn>`;?««`r«/[+h5VX,*?5D\sOMTW6dP-63OS3W6q\qYE?nW:qH[nKCmgM3)-Z=9)P2(Tru "g'@7?!78#2«kO4aATDoTb:Whdrk"r.Z*J+.!4Aqe4!`sKQ,X,:«"thP9F%de6(Ao``Cf@A)Yg_I_PaP]q7P#c¤XP. d02a`$,K'!ZKDe/4kjc^cgfZXS03d@+,/c!EkV?2N]_dtH2`H;`@gU(]97e>W->Ncbr¤`ccS4fm?CZ9L$gNF"lY2., 01DIg>dA)qp/_M^o[_oJ!EFNL`u+;PfIY=>*_P2«5¤,(_B9¤UeiB@d@bfIhjrN(Wq\4Ki09+9Q«I/46>%Qs$WeCk=1 S;(rZ;*UK0jSei=>?4f3[\VSdQKQEs%'F3/kI?j"n9tc22mA8a`L=5u;Q3jRY_`fo?*+.$i`qpj2JZbJ)qodG«T8iQ T)ia2?ocqZ$"8P2CG«9BAmn$FUX0coDVs=MF2R3dDdWTI0ZDcqr!$*mQqemYo$nWHl5fC@9Oa[C\^laUh72¤"\i2/O ,c4oGlo\XJh?=tl'c7u*h(d2:bRW*L_ZOuhpMPllD:5aV(Pi`f``F#M7]UNW?4@qc;-01W$oa?H2Z51*PR;X'Hj)6V b:'m@F"UnMkWWT>Qb\i@VPKaQ5p.XX\ZkPA"5=K>a4rJ+`1u06!HZoO1$2aZ5?29gCDi$m7kY¤*0SNqU1(-)Z!A[J3 ZB:N;4/4%==L^R`OtjC`bp_7kF¤m$*bapC]Aa'OsUps[gnp4"Fa6A0$_l8op)GU93*HL;ViQQ_fR«ojc`VKCFN8*$9 gO5]70«);m¤;%cBZ[+pB9[cfK^Y-WsZ)¤mHZj7Ne1:+e\9,RKeNp6oLItl5d)O!H\09`+/na,'mc?\«D«RNUFM16k: 7V43]UW1¤JhXHVEQrV#!AUuX]%ZC*p(Sk[RO)63QCRQ\PQH[7\;7,GAkZb-"LU¤:>4h¤Z3qr==8fu8bTa>(mL%K#S. c`auUZ(FciV2_W6R82S¤`Lm/1J'rMl~

 変換関数は点数で決まるため、同じ点数のFFTを繰り返す場合は最初の1度だけ計算すればよく、値もあらかじめ計算しておくのが普通である。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    const n = src.length;
    const dst = Array(n);
    let f = [ k => k * 2, k => k * 2 + 1 ];
    while (f.length < n)
        f = f.flatMap(v => [ k => v(k * 2), k => v(k * 2 + 1) ]);
    const idx = f.map(v => v(0));
    console.log(idx);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        for (let j = 0; j < n; j += m) {
            for (let k = 0; k < h; k++) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(W(inv * k / m));
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhSE\92FS%¤AI=/N-_e!a-LTDBh`«hmW.>U¤Vhik.#Ecqa/SWEd>It«1NAj!KS8j2j2]C«c$V>52>E([n)>_)eu[IG >8¤rq2"8^!*«%sKM[KdH*'^H19mh!VF2).+'mlTI#>=P>4>:GCX#7U;#«_1R]S5-nUi+2E;T_%sl\sbMKK$,=/HMOH SXPll'E'H#d/.*K7BapC%dmC)¤>4rTEG7+oOuWb1rRc5#U$/P@AHbb;8i\1Dd+j.;j1«;-Bgqni2=S,dgH7VS,.rUI XC-Y#$2fC3ga«aGE\>?;b>l"TQ(IF¤G?O9SH=1A«,IOi,b#0acmPQsV"::eJ@F%+.'`Fnrjps..-sB=hCf,/KGf[E4 ,I,I0%dp!7892(ke_rU*4f3SC%!HKY0oR(ZH0b2SD;*.Z^Mn.Nr2YNq#qkYt1fYVYGQ1h/#\BVWSH>!Bdmsst;@?cA +Ec0r6p¤uICi/QqFY(SOkM1E.`R5>X*#-61ZMGNZ).O["F)iO[«;«.O«-6/mI¤u'\dS@.XMBb(US9^>[%«VE_cu40J e$h;`_KTKUUGE^"DO**q"\eH85*\%6%,D9Wg0?W;!WNN3C]~
GhSE`D/\/e¤H;*)EA7*;N,:E^Y[OKq#0""Z"4h«S;Y3=a[¤bNBN$ZW..*"p«s1URPOss%1=!=gohVE6kc]J93lc?"k K.P/MR(qoK.94=[*55/JY;.hTWF6m9i^W=!R/OsUXb^nT-k0B5GrUAm"JSRh(¤UtjgFV#>bG`dQ.I5%h,qBX2_;uY9 :A5Nn/gZ(E$?_¤_P!ZT*^(O]u«E3FHaoq1l:h[[BW:I[,8`@1¤R#GI.P9F%de6¤*tes¤ck0L%BqT?f`Lm2+,8o)jqD «6c#HI;fe=m)93IA"0UY,o@$9$N+.7-m\PLX8:"!o¤:Y[idW08Fk0'k/I,KVk9M%L2-a80Yio=uS9XZFl@^kWC¤?QG 164hhY];;h«sUq\emCRm._\kl%!XF!)Pk^6_R."K\%'(,a1lNM4q««@-Vl>k=U/kbUS;4!R'qG=k7;soo6F]>;L5!F 24>+_M!A,58+44Lgp*l«E$Y)dX1VQ[6/u8¤=b^_^*hPBqH4-3¤`7WO>;Fjq:VB+Y"_J?Zq?3*@r=e[!3)VS2sU%NV@ .@usI(hVY4J,j4¤M's2_oI+Fj\R.9a`Rt«Y@7V=E.`C'\d!2¤)r#U\BTa]`5]t=/\bK/XH\:eXbio:_Z_c/h=dlD5h GXCMl-49TtnPhR$Wc>-Zf*Yg«)RL«4">nsWXlg)A]>-+-J`Zu-¤8$3K"_W^r3=e2E9_3^cS8TmkVK113;hsfB[[=BG B!AD«PbBDt3UI*.nn(*Nl.+$\YMa6#i1sFE¤s.br-E2WX?l8Odgj@¤2Qf,IPPc?88-64oXQlkXT+>T%H0^luHm0LL9 m[-H¤oA1tuF'-$mULd=%-DHa0*@ActO_V¤M-QT*?mariS.bUWKb_Q\uei2i«)F]qp=n0:(c`Q3UGPSTUHUK)6?A3WN ZQc:*7YpNN%6-d6grrT;aOS=cX,d]k+6aK]:QO57R7[]?eb>Ku¤u#D«K:oGOLM%«Z3;3%75@%L)[O`a0kF«(MO4Yi1 ,2_0N!eUH=Tll+0A.[+u37L0_HmXfuLYEilW+5Q7^Peo11*hFI`$aiP+/I_Z9;1Aia5^Y1Qbe_@8[f6$^ncg!c/?Rh OL[ScP*Xlek/OZ;LGVM)]sP0CoC(SiMp2jMBH%6D\^c,JrQO"/]3TD+Pu^nR`)D"ji6T0@(9td~

FFT関数からDFT関数を呼び出すこともなくなったので、DFT関数も元の素朴な形に戻してある。

 変換関数は要するにビット逆順の添え字テーブルなのだから、以下のように生成でき、関数をわざわざ作る必要もない。

function formatNumeric(x) {
    return x.toFixed(6).padStart(11);
}
class Complex {
    constructor(re = 0, im = 0) {
        if (re instanceof Complex) {
            this.re = re.re;
            this.im = re.im;
            return;
        }
        this.re = re;
        this.im = im;
    }
    toString() {
        return `${formatNumeric(this.re)} ${formatNumeric(this.im)}`;
    }
    add(z) {
        this.re += z.re;
        this.im += z.im;
        return this;
    }
    sub(z) {
        this.re -= z.re;
        this.im -= z.im;
        return this;
    }
    mul(z) {
        const re = this.re;
        this.re = re * z.re - this.im * z.im;
        this.im = re * z.im + this.im * z.re;
        return this;
    }
    scale(r) {
        this.re /= r;
        this.im /= r;
        return this;
    }
    abs() {
        return Math.hypot(this.re, this.im);
    }
}
function W(x) {
    return new Complex(
        Math.cos(2 * Math.PI * x),
        Math.sin(2 * Math.PI * x)
    );
}
async function DFT(src, inv = -1) {
    const n = src.length;
    const dst = [];
    for (let k = 0; k < n; k++) {
        await STATUS(inv > 0 ? "inv" : "fwd", k);
        const sum = new Complex();
        for (let i = 0; i < n; i++)
            sum.add(W(inv * i * k / n).mul(src[i]));
        dst.push(sum);
    }
    return dst;
}
async function FFT(src, inv = -1) {
    await STATUS("FFT: doing", src.length);
    const n = src.length;
    const dst = Array(n);
    let idx = [ 0 ];
    while (idx.length < n)
        idx = idx.flatMap(v => [ v, v + idx.length]);
    console.log(idx);
    idx.forEach((v, i) => dst[i] = new Complex(src[v]));
    for (let m = 2; m <= n; m *= 2) {
        const h = m / 2;
        for (let j = 0; j < n; j += m) {
            for (let k = 0; k < h; k++) {
                const even = dst[j + k];
                const odd = dst[j + k + h].mul(W(inv * k / m));
                dst[j + k] = new Complex(even).add(odd);
                dst[j + k + h] = even.sub(odd);
            }
        }
    }
    await STATUS("FFT: done");
    return dst;
}
const src = Array(16).fill(0).map(_ => new Complex(Math.random() - 0.5));
DFT(src).then(async dft => {
    const fft = await FFT(src);
    fft.forEach((z, i) => LOG(z.sub(dft[i]).abs().toFixed(8)));
    STATUS("finished");
});
GhR;'h/@m!'ZJu..o(IAUDR9Kp>K71gO.b>m"t5Y@*rSo(p'Z=p:p¤3](ck-6@jL/4oEab3(>`PjF]Y5p(>=VRtJg[ Ce\eIC%+#TfVZeOPc6B,1^NU,_,'7B2ne>]!Jm:C(:.P7I*Pi\CepFbat1+DjR6es(3URS3b1ZcRZ3#prhFS8bip0Y ;H;_l%/!#aBPC=?28pBO0=9NmQCkf7_uNs+h(=nX@"jZ]1JH.$mW'I>YL(.8Lc)5=JBt«"P8*Mm:DXuno,''PRdZ`1 eR(Yfq>iTC¤)-:tqMlsd¤lJUd.J-«9hl.Ds2+L@@a1gnN$VN'u\m2aE?YpFul«WID`i)rB/r$ej«>kbNa;nU1eQU/0 42eMO*¤J*lFT9/F¤CNVP1)j«@glo>^C"@cH^rW[VX/+QP`o2m3B`/"RQa?;~
GhSE`gMYb*¤:O:S%#fTWI8T#)/W-RmU=_@mJTeLfZKOY`CfQ=LQq3H-d>jF-s5"WNP,X?UX^G?GBBIElB82._X(!;H "_jroaS`_'KU+@IF8.Hj]X0@sX^E6«nl"mE_Hu_>'.$^,dY=l)4Ij2q!c]8](¤V¤IRp>«NAR)7Xk\Y-.c*W!/3*Una kh]^r,uYqc7,:DQU.cL??\pD!aXXMS>2VO`$8"4EA^M7D.3-qLP;1:#1'DBSC(!sH)(jAjo0[Tlq=_jiD?1b75JX3c (B![H#EP%$4o)lfD"A$Z$HBrh!^Ba¤$H[_WCAWE!H$KaBq2«>52X_jE/I,KVk9M%L29`.7=LKhp4cfSs3[)^/g-550 bi:)f9TS«7%"X2j\uR2TU[Q_M"$T4%$aRfIBpu_B1q:1Ua%tH/?O'«¤`'_Fkf"tYH\?+=CVJT\SI)MpG?Z]9tW«OhQ _mO/W4f\;@rS?QmX)s:PWHMD*jbiqP$OYK^*99HBROn(V$%=PcAmMS`[=RGV^caPuLpu!/n2?p9-¤N`fC#J:QmIe4q N#]Q_1\_W\pc*t/0MNXPklU9ufOIge,mO-UnPi]DZ?@g8f+MA5g)iT^lE7>Ao*k;()!3VN4«O]8Q8`j/4^cJffPIUI TdBGWhJEqK7n\.g«ml7Xr@+b[Ju$=NJiOogber«ds')V#8#FXFM%V:)8t//Zn-4ASE5(h5Z#`sMY¤tgAR1!E%K;P)W /$L«gO>KN"aF'"+Ce"I8F2ogE?Z]T\R)@X3;$SM5]gX@gKH3.LlHW\.[JU48Yc\dJ7(D*1C#0g@«hH2$\9884@/rg0 [9(4#(I;Ba¤uo0D#c2tl`>N0!.lkFR3H=e?,U5qJ>"Z.a,;,RCG^Fj)7)La4n¤+[^Tc/q(«#nm0Y,:Ji:";=oiC3d> )f)Ehcr+/A@OHsePRW5]rAm'7==/m.LYlobgS!d@lAWUK#-TNsG->*1S(eE?nGb;OcHqpe!]lU$qM3oNI5p%I8kbdq ^Fe:.cuIeg/,[kZaui0@>$h1u>OU`_LSg)2;AM7HQZ*^¤[l¤,'Op««IMUhZsUk[+lBpmD4$e%h$L-2FqZL4]#h5>`d YU«WV"Jb2pfL)M6O7eYp44#7D8gUN3,W=ffrqloJB9c'~

これが一般的なFFTルーチンになる。 そして、ここからさらに無駄な演算を省く最適化を行うのが普通である。 本当はなんとなく最適化した実FFTまで一気に書くつもりだったが、ていねいにdiffを載せてたらすごく長くなってしまったので、続きは次回。


16 Feb 2026: 新規作成

ご意見・ご要望の送り先は あかもず仮店舗 の末尾をご覧ください。

Copyright (C) 2026 akamoz.jp