From a4f0d8dfa4b2faa4cc8c1f1ef5d7decb2157387e Mon Sep 17 00:00:00 2001 From: Alula Date: Thu, 3 Sep 2020 00:58:11 +0200 Subject: [PATCH] initial organya implementation --- Cargo.toml | 1 + src/builtin/pixtone.pcm | Bin 0 -> 87772 bytes src/builtin_fs.rs | 1 + src/ggez/error.rs | 30 ++- src/main.rs | 2 +- src/sound/mod.rs | 207 ++++++++++++++-- src/sound/organya.rs | 225 ++++++++++++++++++ src/sound/playback.rs | 512 ++++++++++++++++++++++++++++++++++++++++ src/sound/stuff.rs | 36 +++ src/sound/wav.rs | 120 ++++++++++ src/sound/wave_bank.rs | 45 ++++ 11 files changed, 1159 insertions(+), 20 deletions(-) create mode 100644 src/builtin/pixtone.pcm create mode 100644 src/sound/organya.rs create mode 100644 src/sound/playback.rs create mode 100644 src/sound/stuff.rs create mode 100644 src/sound/wav.rs create mode 100644 src/sound/wave_bank.rs diff --git a/Cargo.toml b/Cargo.toml index 065ad59..49f4123 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ approx = "0.3" bitflags = "1" bitvec = "0.17.4" byteorder = "1.3" +cpal = "0.12.1" directories = "2" gfx = "0.18" gfx_core = "0.9" diff --git a/src/builtin/pixtone.pcm b/src/builtin/pixtone.pcm new file mode 100644 index 0000000000000000000000000000000000000000..6bc97f59f1f8f98358ae62b2b2d6f742fac28d4c GIT binary patch literal 87772 zcmeF4cVJXyy8lD(y$YzHsMvd5U3J%rsHg}?7Xri(NgzWqBttR@lQ0RB7-E2c0RaI~ zQBkq%uDk2*y2Xx)2vP*3_uk9z^L^ekNl@H%_j>Q|{&62==FB58>~+^1%lme)JJ-Kl$|2&$#pR&%gX~@#3$({(9-MZ&s{W_3fJP)@|6d zdCRu#J9h2axBt-LqsLCf#8j_Qt9IS`jT$#=-m-O@c4wa5{+x3=o_9f~3op9ll1neU z;)<)TzUG>1ue<*G8*aGqreEBA^Ub&1^2=Y|dh4yX{TSSO>o0G)`Q~5TbmI*-Tz~Dg z*Ia$&6<1t#$t4$cy5Rhd9XoVr-~Oz2XS8n7tZ~EowQE$1iHSK0284iZTQ{v+vuedR zOP4HO{KX;`{LzOW{DXD=_5IVsU;oB}Klte5Pd;1p`4?ZZ#-+eX-Dq?apj}PRH{*U3|&qmtS$^RoAd+ zR_!gCHJ@(9pn=8Sa*M6@`fIPb@`_6@?sR^~bK0NPrgf_p&6_oC%rfiOt6Qgb?OHW! z)Tmy)dbO%ms#KoLiQ~r(AK15R+opBjt^O9$L;0^odBK;!7X~8Y;;)u0UAjzbTn!aj z$fU#DKf#!Z^FY<&hOI9C|B?20R| zzUEpj{>Gbb`b9b1B%0oE{q@&D)vK<&;C1;$%U-5eY=)T>sJ4{#{FmgoP>g~#j&GD4j(#rVBhYYQVn7c zxMS(kuMmz!pMCa;QI&wUw zYV}%m>NjlCw0X-j+O|8ZeTQ?A0O3GLxa{&PuDJ5bt4;i`gYd=+D1a!4FJKAD`SsVq z6W3e=Q%Ke?zR-mIT!g)yguN+JP`7r?>Qzr+{eOmoQ?H>SjvP9$Z}0A1JGMDBxmLom zTv`&2K}CM{DF`_gK5;7W^Dh=JUh;K? zyM24q-+7%7126yzPzF~C2@bBU1lO1tTz=^#PUz2WcSg(RthIqB@`!>7!hg8xQ*rPk zzfPWnRgNAxbPx@?V;eijy0vRof4g!8yrNCy6j=7n@)e%?*syWaR`kc7eFqO8J6WY> zodztwb=!6(1?OLI;e{3CzcLCS;_}NbW2qOM*YVu;XP?v>hV|+|*=qltYy6R= z|M+R)L=3ZM=e8}I)_=EpD4*)dXSr%GKX(*t~t;q2pC*)@Ajr+n&`P)|c=L zF22ZE_?-4Evqkg9jkH(&H!Tt7-nVDZp1phb33hGYEZDT+yKk4n+{|_{Y9GR_G^^mN z#h-t!z47CZKV7uw%f(-R!=AnhYA;7Kn7y!=ErO-3-$)-DH^2^96k4e^i(R{R?%1(y z+tw{xwrtxA_p@c3tZ7gK8EDAn-?m*8zy{jzBH+SK2=jRzg`7*SxQbl?H6Xa*dbl5! zKviCTg(QSu+7ZqH&ONuI;9NFB*ra8PC^T>0q>gq%HbMh%s@)$-ImzC%apOjrCp&iS z+U>!v9a}f7S^3S^wjF9Sw2cr}Ub*VqZ&!OD9GKB0eqUz5c7kqL{Dm1%SVCWZwRHKa zHJi5V*u8K6fddB*9%S1*a_l(9Pt95mYS(Vq@=SJt^BtVWnpwd)9R%lgyx^kCue#wE zx7>Q$?FP5qdh0DWvmLS%UV#E|03w{_;OUBMuDJqyz(Z}$XniWQYEfTOgF=+}O3&|y z_1k9ANE7jvez1BeMZ&)|MI6uuB6B7_y#4TKH0 zgNrc(ue{Riz-!G2#2UKo_Fvy|=RNn_bNAhM-f_pTe|7t9w_){x5ZMM-!WLM37j^=Z z;E-*g{n=-qbymA}ZO&*Zs|c-8AGJ{n3?RfO4FiV`9^AiQ7}&X8I-ZroxvaZ<*`I3J zAAI!jN1rbG0-0bdTeTWH2y=Gpwrx9i?LncluEQ+sm^RR#W&<(2X9md*s$H*s11zDY z%~6JI1!uDni0d!E;_7SJ;jr#lmg&C^=)ZPt&uHDU1twl26uwM63uDtBhi!SIWAvS|un%@6TwZ)|LDM*ufGOW ze-VxjqJ0*EfO^9gkXSd6Ak_T%x#aW|Ky*OO;X}?g+`a|#XYIEumMvMl=#vls{#U`@ z&={XA`r@mv4Ii7g@7#+FvGL(0Rdgv0=-i+Njho^K!vE)>?=QLh>g#U0`PSQiEkMED zdh?CfUwy?TozClUW}8;c<-pdhje%#5PDN+`D~>#)(A!uyuUoTx@uweI#w}zy!P$RXodaWpq{tXs{bq2_wP;u zd}x!!x?20~H%k_O{u!|pEU3jx*X=%dSc@N*dWa-)<7@}Hi9)#ooFnJyyCdyxwSjHV<*{WIP8M!uDR;+ zOFEs?t_A7~Q>$VP&X3x&0|O8nYxllGM~{2NuU*%+KK8z5&CLL4eFi2#+X{OyY8SR^ z$627w88T^FwPZhU-mICnblV5(E7W1~U+pLO{-;MRQHko+Yu9anPb#CSW%DKt>uKYu ziluPmAa>GTVBh}3HuqX}8Yp&i2GbGI%+0J`MK%Q*^T2*UhVF@&sx@jEwuo`HqR(cH z>ea$&G3r*SR--olUy~+H8rQF_laA(9YG4u7uUFHxVotPb)U-vbGupIiV><()Pty~} z4#8I2Hm>`2`PT$52)KQf8<2<+5|!Hf6o5dp#8rBd5_Q)x9TLZzkRzwp!T!1Yj9Tn3$WJ0V!QC{j~)9S zo`39T706n*VdEC9+Mx8=<1W5f9w47Ouz%hU`=xX4?8S18aE|bf{wWY3!+2`iux^bX z(VPc%Y~COr?Z+wSAw*gmiLJD6{n(oRz4O&-8vV~~-|>Qf1v2N)Y2U6@3{wGy<(GSISd{Te?P zUZVXR+Pi(d3CEAi&S|f##w%X8&O%_jj>goe+qjiwJ$`&`r&-h4*u8C9H*ff}FDkmY zpXd8HINGF$;Hs4?6%_mdY}|FYUgK7TJ&e&$r^?Rn*t$vWpJz7z3wl`%9?D_W%9xl_ zs4hbPFPLO>3Kk0VJ~l;9>bq$-X|c%9p^US+t{#k|iyS1G-gO|hu4L!(fo zN_C;iqGrtvWC>#mV+%J_62K+?uw4n8oIW9@;1_P1V)yI5Rov&(kL9S5#>GFeIG5W9 zEhEfT{92a|#nu1#lg}4_O>B7ON*4(w*5|=@r-OAa4rqZuf=QPC!7i>{uW|FX?a#jm z_vgABZce^10H`8> z3VsT35S8pv{Lg`Nhsr;-Li{2vlE`2KqMS7FebWIiq=+jS|_7CB8N z67jyceA!oDc{y%zM-dq^p9PUmS-hM=f}t(t3PM^K-vWhD_g_u~mz44c;{Rkhq3Q+? zHfjT+RgT|MUbVFHsowr$%&q)k}~ zgH4;ZY(HGDX{$3l$CxkH-71C*5tCCn!-*pYEa@a~M&vPq!L)?mv9;04!n7k|0v zQwNs({(6~Q3Z+m;pQB~SAbj)f#{IZc7{860w?4D|d7Um+{)PlL>1!ptO!hCfQV0qD zl~<6_xIn6|5+EuRv?v7X{Mov7l+^eytgq5i|I0bSMSu&| zZQitY^`ATGaq7BcbXn-VP936W3bfjmp@1OXepL!-*d~t&%=wqfJV?xB$2Q9mn#NRm zPci7tJw6jRa~lYCy)Z8Zk826A^GXiSkkt(gT(iB|3gGMpYpV8W9Z? zD6nr?{;JY%dv{Q%wRXjF!l>G>wPPzChdt>52u93Ew13Hkub07QI}RO-QL&ZFvsxk4 zzlyB704MRyrY+i>O*sbHSm@HaxicKh1XJ}=6kPniZe`0&H5~@d0_2dyUaK=ZoOj_R zs%`?VxUhZO=Jjhf&kEp9vrBz}9k0h4JAP2f{-+==p zXsw{@7@a#fbdcR(#}3l?#Cs8dE=UgjJGcs$vGP z*tkj!bC`7wzOEdZW;E_N!eYr$2}tWG*h|8ToOn4{?u*3cG#^!D*p4{;?8N)wa11|}@2XSR#lBlm@?c)Ds-G^tRJ$Jbu*w|`ZoZk42NgL{ z33a33TF)YOS8u!?3(p)s21+?7X2fI~ zHEG_u%~|b9KC1|m$p}=3p+r39DM@ImQ@F-1g}OeII7AanO*tEs97REKcu}kzBphg% zx&jo9ZB&Y1Q-4VPi30-bTIiWk85{JnuAb7w*=Dm0Q+n-stiSz-C;%~+7sQkEl-ElJ5Fx=tYumOh zJ|UTK6$L6NjAf+y4hmfiQ~*gV*y>{-bqC5k47|(;1`jon^#u(8J{vYvWtd6@L@0`2 zqyt(0v19Dk)IDHq%CC`ogLNzOQ3Vj%uGp#AxaF=JxXL#>y#fc`AZj1NBPt6JQlvfIC&dvh zR4Hmo1jT%QdC(Yc#5gitEp>e^MG~@$u&vuf^GIY)T4^t8h>b1&0|8ZQ04hSV3IxL! zvu&kXq>AZrgh^>u<(aTM)}#RyO9nFDF)^EA?VD6pIe9b+aFbGMzyZ}5ZCbwuO(Cu| z&OkF@2T3zHpK>co6&*`q9NL_G8!YPCHdRr7_3-o&&30#ZP=TP82a*2tZ3+ZdcTwVuq1cD5zd37C6;2e9OuZ`bcUc(Qhr7UcIGZdph3#`V#=s9pTAozm)L zQp7*VhO2O4b?kShrIDN}mS{t7Tj4R&^$svx*a2YeN}Jq|9+G{liyPLw6+A`pWd~}< zRDy+m>-4?kiZmcb>5l^mnSTvS0sH-~Tve)C-y91(F5$ zAJvQxTDy9c6G!&%*!M5)MN)|X8v~}fjJtO2JxECdHz@CaaJT7^&0BY3X;hTIV-U+P*hfxbnZgZ9Eq}J?i`Cl? z9IMlkiU&47wyU$wP+?lN7_+NLjfv%D0s@U2C^;%JTFtO&wR&}w9wmqZiy(S(fU!-9 zLBfMO_Uxm!ltug~(<21D!39U-x~WqV4oR#cSoZ)&XHaRXSQcjJ-gB?iU!#RN_hJ zN1gg9Wg&g2&`CQgqE$X3wlF(Z1_1a*QD_TdqhP1NX7$R5y?vq_iLO}1vt8;|x&ig9 z+`X#zOyx@FmTOSURy1o=x0Z6_XhbUjJWjZEkAe$;#Rds-umLH2X39h+rqd^zzEc80 zTYMDEp2eEdR-+oD3fR3{T96q~+BhTzgw!xX2^(Q5AC9Sm_8=?oHedD^?GEg9rjWe=EJ$Qe&@O=FNVKYH z*MXhce6exO?udS<)xm=Y4w%x_o`H?Ak!b-ubg}Cg`O-Raj#MKE$5qC%(2dBqYTl>; z#uvaoZ*pPtb=#0Lu0?Qx6AqQPgDo5{EM+s=u+!Kgsz;SAh3X`#U7;%bM$8HG3XYwq z!UpXOIx9sop@7)T|$_Vse`Kr|@#DmV}^m_8Xu)b9UK=`ov8KgSepF{cbY2h3h+dS^l zxPF~VQfbOhAvBml4-WtxegFhb5qxK;yQ;AYwJ7%w4|MN7iHrd;VM;!UUJDc$UAaQ> z%5|m_S1n&k;rBA)!Afo_jsYIP!E)Ltu(MQ!&N69i^I!Mfw`(_0>+cGG@hf3$aJ+}Q ze=h(~wR-Kw*ksU>JO;6Y4i{3j&15liI$m_`ltjC9z&CS5_+zkVm5al2iz~V7L8dVf+j)!dw zyKow;avK?T16m6h#yJMUlw=hRVk(4SDQ*C%5qxmsev20`{%XUX>h+p5MTyjiIl6B< z=JB?j%msO_4Yvv;EeJvi3@;#$9 zmCgdvXyn#Ao_~Ia=F}Il-s2EK&s}KY*z)A(2r#G~$GjtzYg%!B`IBoFVmO3Nwr%tJ zH7ltyBCxV_1<|{mLVm>-e_|h-=XM04JhpRa2g-fz+VXNFhxU`y9PQgdRUkH@JtIPK zQ-QP|3q**u)LE>n{%bZa=+#0K9E6BX&mnm z5AiH2XJU4(U-PvJ`xbw*cJr=-q&RT>O;dZ`C)}d)dzW)18;LD!J*d~H4Jxzde-LE% zI54}X{NvKf){vF^JEOO4>t+pVR>3;mxpmVj0`baY5hg@wtyr^ui^7x?Cn@-*%dY8!?kAiHp3XGJar(%L~ z)Gs9D#SA;NZ_}*)@$&T1>NV@O9IbUG&pHU#<;)^^0|`*&hT;D-smqx6Q#yHoOX7}2%~;i$`qL)tuOjjyhd9Rh*3 z+16|!_kk--37x!)7&i9W zE{EEIHnsM!p#d9%%0Hdct;~)dcO$M`OLjj&4E)}5AJt5GJ;;GR1~4osMvvNwyZ2DQ z$-@ln4=6W#dW{-DBCzNr{BwMAp6$TI-+KTLx>han6IP)^L<~@9L80J=^=ehc5ZbS2 zr#-vT#YYG;ZQZr+SWK;YE^+8c^21L>#H+^~JFr_LJG;|$uv!$_x>LF1<5gnD~Bypi4T?a6RBo8bre1&g2y~eq!pm5(8d>7so%L;>6vZXXhYLu=%9_fhMqtt ztL&oojB8sXgGWJQsxZMSW_Uym=R}3Y#H|__F7ZA)*aJhBN|(ZnDhhj=kXWrfAli(w z08C%cHpU#VT(5F3db+1P9!VHiMy6$c6bnB@O)Kt!i$aN)8`iIb+!1akT;th5gjIQ@ zw^~geXxDQL+C>bMq;5fVa~-k!;r+X|Zd{8M>5z&gxr!%laHkI(IcZzHqR*ZsYgKN> z5slyoFZ8b`Q7l4i&y<)JvtYUc&zdqzIw?cW9I7KckEK|m!mLpc)7w)mT*6$yrEsu*NTD4!+x+l_;faKH0K}1f?3n@N(~hl>>iZI;qiM-)e@dJ7eThK zlD*DZH#J0fd<*ek1(XHaU7ZhW2BE97mWk{_fAU+S(AE^ZmG|2=6WxcXX`$NRLa%1m zHu3758kk`$^I1zYL|GOBx|{-7{FFc$Q~_P(AnL;{mhZ7W40GR><)yFnxa#K3-j1gl z6M6S4vr@r6(8wYs_2q9yxkVN!qDVykqUPDu*>JXoq&s8CrTqow|BJE3Q$<0}ZcKxkbR;6S7tz zKtM|>f}t!XOsqvg&RYum`X>TYc7{o2iUyQSWb_Um-8)BS4Zojg{@JHP=jgvcf^ zFw7{rgx$wp8Vx4!G6hwcG;7kZe$AM}dtnY&8Y;UAL*aOJp5m9g>7>JZ9;bpAEc?X6 zdBp!Lu?pij3sfbaUi!;IJrr4;Jj^pKCL^%035VF%625vc*;rEPSD2B^^S&c7H5xP* z%eR#qCl5FVrfA)i=XR>c5Qb|wtp2GNKmEs0Hgs9B^>9eJ`2Oy zMk9SEjR(lSjH*3!Ub)>Rb!X)eXiT-*prsj4^$A=lgr3}Un%Gp5 zsUw?)n?O|YqBdaRZCg}ohOA(3tlPMK-!Y{fReRr#ZLKqOz@Mw+)nB6_D&oaIbz)e4-Ooj#poE9pK*XqT)|8T+Z)kg@43^WJf5^rpzYckNVmarRLou zQa@=4r*Gsr!F50^mP>1%#o;wHmt1z`)jV!~^Dl3`{a3&K_3glIx7>WgwO3t!(RuC9 zY)v3a8@mHuU(lfW8SP!hT(7U;In|qP{^jj=+;Qh!cinl%ZMWQb%@r4&Pt>tR6J;JX z(t;;IOJkvsUY~5CF zNAg*jUS&g1S*Zq;wM{5kPLpIsfD614zkJ_;qga^K+~_HAi)=%2Jt{|#$97aj&Sios ztnBt$>?+&7FOzM3VLENk@O;96vMvYavk!lS~O76DHL^T(;}yQ2#ZcD$S5>V z0Z@ z_t1$N77LVN)~F%wtWChMg9)=8zTz39Dz%)Mh~JFQE+bz7AXfn*`&f1Vo7S#ezI^#g zFBk6!y=B{;Bh@HTMn6%MNq*89g8Jm;3w2`-SjG?$+$o8Rf`wOcn|VBz*IHR)8I6Lb zD$qxHk$iAT5yA(OUxlSSkY7W_1=gSnPEzTFfj5~Lcqk-huKlelX;?kLsfrjm6+BX* zQf%o4*?u4&Ke-BtR4PjV4K16m;$=t>pUj7d?+Ae@57JOZ->FnVPj66MN9y9Rd4Jep zJc23{3>Vh2%g#+A(}ejgAA`UICYV{j$dkxqa!yV83Ns+Tvs@q17zHZy4kXn=DOn}& z1}SwQwS;6~@j7YuxD_8qV04Q%#rab*&2%c~oF8PHl>3nPhm;wdZl^e|(9y1qte3L) zuPNR7ng?jN>^>Azs~NQxXSS?gE9MB9t97fsS@OlFpDy}*$*L{;Ycy!lTDi7nB&9J{ zc5GO?lBD+MpD$jvdi|DNyeq7Fy~eHE*_PF^>CgY`H3BKPz|v*B3^>l3A^UI*|=~&(TE}oQ;uYc{;-Q-@mQWZ$0EpNr>lSnw1C_D??e% zFqK;Wn}?QC{AVAwy-Gw4RG(v@(gz1p!MlV5mvGThC6J@&O(`njKm4`^@+A#UU4i-I z(GR<%PzO-CnaA*)GDbgjJx(!KoRhfKtZBcGSzQu&vEyjb7VxYjyIrgL#5VVCS&N6} z;$No(z*OD{EKzBr)rLvu=oL?U38^ZfW!{?0n|s?>bnE)>`BviF+we~J<)RCoZ6f=T z=oR&fwQE*A4sWs@h`C$<6$eydSAw?|Q$ux=cA_k)cFbWiR_>*5o<&RY*Q@a7>yygB zZ}P$b)rVh@EaQz|_6j(A|CwGJWzP|*(!sqSMVUoY`AT|QF?L-=#4$xJL<&t@*wupI zr+l1e&k3Az>NG#jCF#(C@Twl2QAwO0fRgxN0f+~*{~eZm5{9-C4E&?Lhhpk9Hyf-? z3weCnFf10x-s$`jXCY|MR>g##4p}LS-xa}rj} zz0y5b2Cgg|t^5JRkvk8r%ym@G0^7-adBfEiRx$~;NElQq3?yXV@m%+u1pBuYqZ2@n zIzZ~hA{R1=v={Q;22Vhh`6UjAohn$nX&g{~m zUIFaKXdSa`ShiAz(Lmgy4|l9bdSnXg%s&J3f^0L@h6{;ojQQEMVD9tq8a^ z)5{BFW8zHLq$H%4_tcW+3;kt;JKMu^3z&=|eTIrkA3wTu*{?DIl*SPy@JL8mQ)sE1KVZNh$Gf*(wo1qVXaP>8d@H8Q4$4zW+%lSqJT$|lgGC4 zfaS6!i#~R*&;7?opDtOwZp${k1JY>aO%HAqfZdv~WBHt=7NoOPdSsl0+`<+R93%*2 zb22As5(z#ncrk!AGK; z%F`*H$7bPh&()VAlyyVtm4wwqK_8ZIIlP~!ic0r=ye-v9eYpMJSy`L`Q3@7{;!Bn`pafb|~mj+8_weQLE3rxZh|?zNM>8B}|_ z6~-|Rn*}bedlOXo)9-FOa9iPjhK-+mmv-86uq`^KMjdfA;=n@}4>_@{x}~mudIA2=FtPv9E({A%41fd$Da;a*c~GxBD-Fve8POKzr$)YWn^3bN>0x?uo%}kq4kOkMedLDk-;kc*7j zA0vlJFQz^2ikK_CoJ`6|9O{57T+GbcwcDLl$H?bC>YgoURACIFJkiFDE&E}0xUfm> z6YWl5HJ;eCVGAk3T}^g1ZoI3}&K(UKHiv?Z6|e!AI`Ki(fGFJ9VIMojfsP#yb!>Uw zRwXjvd@q5qTd~lC2VFtkrI&zXgXXYN({<}W+y+&?>ltHB@a#5pEU++)<-?h%#af#m z+HHyKXar{$k@)RZw{5G+fCTaP#vD@4`^dI!hYsz;TiH$7{hs~%_Yw)+x387VR$wn7 z3&&I(RIQ5qp1FVDfo=P?@&B4NTi5K{w{`0(R@YjJUr9+lys%}ENO2)eoO@_?+D}cGWs}*RdUqkDt@1Zo}qB>mS|LxL${P+ZxuZ z*Wh>+{%h1;BGXuwqrrKuV?K{JK3=;~lTDlIH{G=9+v7DGG;eU?TRs|nTcZ()&;~VX zHmJF#X3fSm)-kQ-^_$hN-^3w^PnCnU#>S8oRU=&Y@V$54HJ_AlkK4Q7Ipj%SdnKxI zSz^xV9%KKi#MF+7ecbnWm^Nd_=8VfPc&fPg=}A)}PZd2~I^~&{=D)Jw`4?V%>z&{H z_RY86Ua;V`-vV#H{WgCm-XQ!3DG_P#Rr2KKYV}luy zym8stV}qH#k%>C*Y$2k!53f0vtXeBglx?!TWu z^l@+JU;XOgN4h=Q{f;|s`_*0d+z{lEPF z_piUc@QvU9Zow-rzcgpotFO$RGoh#;f9mAv!OV%pqf`9;tc=ju#Ni_neaS;(M-1=w zQ18e3^?l^wJMVb#fzJ0l^3Wp>-`Dw8ot+(EH-vbZ)`VRgg$?Xq!d+3on?!4p9 z9z7rI+V!DceS7!o_xRwUPbMWLj|rxa$;!&g&KmpFgvq5-%ATGy>$y3z=e{)mg%@9b z<<-~bzr5h}-~aKCZ@=>{@H@d@{_^{G-}@s!-g@)(1;2Uwtv6nO?bSKY&6@q()G4Kt zOC}UfC@jd$31tU^nc#CoV#2Vvq~U`G^dB^^fB(MSAARKEt`By;_rA{e-h0p8cizbh zvVX<9vU!i}?YIAmoaB97?!UKlm-`>=`oMz^J=*iJ-u(v-8WcM;c1YZ?gv1g4v@sc3 zSz~i@a`Ov{z-Z|+(`L<{JMX#K&(B-%+8YbshSvJ`+uyzW?wo30W{G1o&&6z#(nbOGq!GiPjDKQi?4SXCdp*{p zXZLOp|Nbubbz!>q+{J9~yaQ0e_4eDC@2~E>`|f-1Wy%jc*!7`Dx-s28{h<5M__+8d z6Gx_`rDp^)gTd_3*xc~=g5swqPM$LLnd#5Ym^}x^eDS51Us>?xo6OfH{97Sb7=9P- z)SQ3w&f9Ol$$VdXdH#!<@2nZmPMUMy4Xd z^Q6gB%BIbjIdk@$xi2DCuP%7~jfKdRhiN8U4$Sncq(BIuJP|)^c+!aE6#wXe%|DbgE*wEA7~adK&(Qqml`ml7TW`PZ zP+xfg9_~T@TW>D(mhjTN=jYCzHFL(avZ<5Z8Y1CwIktw3K)Szt5km$I9MHc{?_Q5} zf3(}fZWWFM6*Q<=hSmWY?(5P;yaN@w_v{7t3>av1h%Uqk0RtH&w310vrj|{cK64f< zG|yN_#CT)jTTsKu;aJG{h!5BZYS>!dc;od2ucA$0|GBfDn>pjzvS+50PAYl2xTqjf zP7;5?fT>eNJ#inhFq2SWT3x9_;$?BT%M?R@8UE z<1A@#T>P+v;Uh-CT>f-N9W5#xi4;CHp}1tyC_EN~%odwl_GeC6er-69bumMLYRb&7%$iV6!@>9}0uNv$+JZ8S?w9+{LV>53aV zBzDli#|PMIdp+jX3q2oV#a$m1QSV3KDg!4Tu%)`Sc6;^*oQc_2!1&pX#awvCPUjF!k!lzIi@M|esaakEkZ|2PBkiWS`-j{6Uue=ICWv%}d z;EbpWUA5?!jJD60uX}pgGume+Pn@WwKUGjL9{$Y@jg>&M_%VUB(f$-xKVn4U@ZqAh zh&>ntJU*abUt?!5(4%|zM<4A562QVk`Ufgb0i6aO5hFj^y?YPVZwTny4`pL4EtNNs~~cQ<2_jh77U#oVm|GKX0Bm z0alp*vfvdM;uLrV90=wcB3=}mKmYt(kRdLZIb+6j_`u;~lJR^|5!*u~KQEli7VL0h zNJ&rg`%{IMkx5BOiBCQ`EIvLyE^g>xgxq+ee?NzuUcJ!5J%k@aP&WV^2n`kRsDY4z z5C|{5V2wU~`}Tu73^y={VF&a)IXp4RxMb9*RQMz_y2unr9 z#lPmBP#NG1q>TV+FjGQ8d_4R#bjT3&2SPOvu^ND2 z0e$-by+H*Cd0`!Z zI^1CdVFjT;!{Pk=2wGI=gUgUO^vq<0ZVIMGSsD5U97_0R3X8%cAXNS+a9s_bn+3pM zGiDg#z?@JwwG_cKYm;+`-JCL}!h zB*THj5y{EEQD`an&tpKxfk7F)V?)M-a3NX?Ou~plrEpmSGa|I5!Y2G^=$wZ5f>8bk zs{$G{q>B}5SKfx>5u|EACE}KmmWZ);Sp9uhG9mu9C9p% zMAwaS==2MplISrR9!7)UG&_5&1Unbe9v23y`QyPW0uExqtWb-aChQt|;Z|YTI2J4m zKoTHyRR%lh8X&=d-6eLMJV~f@$Ye{Hz#xt>3nbcxQp6ki&drgTkBoy@cvO&?k&!V* zjG8Xwf?c>IB}FnHMKF?YY|CB(paBG6MfesMM*)QW5TX~wk`imS3-uy=3ZtO39H}s= z;WZ^CRnqQIn=a&n-E#CA&kDn!7&piv87zZl@I2l)xIoAjwuNpX+)!>v2k(Y?kPjHv zq95**_i63;fK-5b@X#uOFJu6C71Rp1`3!F8MF9xI!ZEBX97mBXJj24mw6U=v8*Gc2 zJ%p#gQZO}0cbHFx&jfHBAT&7uXFm>xOLvF()BrI7=+v+~3ff8$Xkg)3{F}yfz%$@b z4X(96ML|je5XKF(LKbS6hrtEHfH2`mATk&TM^OM}{~BlDV$eAx|YCVgn7fgp%~w_e(|LCh5c z#?GDkRB&Y4*k^w?X=MI$(@KB;-NKyW8RJua5p(Wc=fJhMV}Cv*$N+&yduv^n!L` zkAx82XAHG6ZLaK*@zl(BKKo|<9+epYM-T4Vae#^x2M2izlwjA|UG7As&)q!gxQF(8 zt3Ah<)bf`@+!t12lXc*PExyb%)Srp zKIHx4%?A6A5K^Y5!-GRS8+wF?%vG@D;KXqv##>iY^R#TmlF$GChj)Lw@YPup3&UfQ zhho}3(&gq$&g1doCPep@S)l5KXIHCLt){YC>R)P75LBgFRjNK}ShZz!5)id1f~rGh za}Ba1$`z6auUW5gn~oP=cGY#a+`65&lv%L zYW?;DOiQtAffde^dg8z+bb@2M%BQM1j*^U5vfZ^iPBXQ$FV_d3$9Naio{h^F{o_x+ zC&Kf)KfeFrN1uH3;h%r|%5xLPr9C;g?_=G2_8u^JNZjB-kN5BS@Lj*StV7fCM^UQR zX~drsYTKrH!&8bIUHttgeO9a0sP$RrU3l5$ygvN$%dfij7q{Pie~-t9r3CYzDj_sn zHjMzt?71(#y70HZfA8;~e#PIT503MQ8Wj5QFTY#(x`jy= z5-51>)p;`~7mOV};)y}51Rsq+;KL7heW3H*xAXV8uDSAZ{$d@Ebm|QpZQJS(4(grM zjr8K^e*${XGtE6;r2#-k{A-reP)M|<^s zJT@*K$8Q9|p3&y-SzJJdc9?KTL7~|sMMZ@Ko^lA>=MkjCR*{WSTvRwd913QP$uxBz z%*@WoFM8U79Jq#)N{Wl*OqWca`t0nv1hR-Gy!iZ_nbV(y1}mN;2@GOI1?AU&G_iN?mu|RlExP$}6wp?dRA0 z;>H_py5+8}_uO{peO(`Yh_FiMd+vXv>jMux+Jg|~z@d-#!Qtz#2t=>meQ|RhHxIKD#IX2Qt{y*~R&v3+Xte*r>$9gIQRC>``oFZl|(v6V{(x zGWmocOO{*-f#+xNrscBW2g((kGw;Ru^XI?%#@oMp_YZ&g;~)R{9^vvo|K%@#`SYLc z;Qjai`u<;Z_}+Uc0V2_F5juy%i0Hoj3NhX16%Q52x3Nf~3qHs^)Tm}O5}F}4gXQA6 z0TB$x{BSPOUl}PfOPo_P+6Ald4n3pTEI#MD$@X(ThtE7XEH~LHm>;9l$3QcpE!jj` z7$IM=J`V`J$eEKbhktM2MPZ!HTk^E$x+?Af*U6KG3-JGCJ+YmFS-A;{oOs@Wi(1OR zk27Dl3GV(1I;_z%$9r16R7R-u0 z@6x4AGk+9@=)WebU`Q_}$H+3Ka$^&xPcKDWl62S{Bq^l5;^oK`(VcO#};_Bi( z5dm#iEPBzCi_|(#Av!ms46zei6?`md@nTT~0HT-xDQL=Z;oKZWsK&@Gv>b;gM+lM& zJQ~BAO>G9Y?6GSu_}BJWW7(P_m`=jXro-g~oV&3x%a z6fR*O7$b$G2aGyBgF*WB?bWMik4LkD8H8L@NA<=|c$mmTd_2jfUzx2xhEx-g;>4uT zxJUsBj%m|SMl(?HY!c+@jn0eKg;@R z+arYTlM?&%Mu$Gy?TNU%?;bsR)Tq1e8lCn?x7fkG`;5);j}GT&W(`Y78J$L;ATw($ zVfc(-){FCBewC=Ji_^XYurU1o-~J{rHhcQC*KiL2)4q2CCY2ZLjXtB?1MOkx-9 z3NI5W9mv$=HTZmwJkt5T9zDlo&pr1XF(BBJ&^D<(@-bj@<_x0PrIXXrGRCB9w1>L(?Ea`?l4M?n4C()9 zw;nxma{7-MGcvj7V-Mzz3ufhy@6$Iey$9LG_=MquhsF^>CA7-sT2RRDhm@tIlS#m3 zefj123m49uSvo%dnW?j%E1f){=*@-2nPVmtz4StO9IGRe0fN&=9*k0=b8u=(-}onz z#*OQ8UuGyYU_fjz_+;C%}Blq+#CCh+{+=Vpz~o>Y<%C{qA<@~}7(Kjgss_8BxVb`Uv|UXP`w+Q88a$%@)T73v}Y%m<`h2l!V6@g z3kqIcF!y=+sO%i`fZzP)9fXl2D7q9Womj$_IVM1s2F)GJ977D6j0_mT@)#EX1lbtk zOGN7Mkr4q#C$>%oqEBClnUf0^2&u;o8Tte}3CW~kv9S}1eWQ{_Lbtg{jXxqoB{LufD4M<@oV0zdUosi!T(j55Z8VY~_V!JUflBb}*RcPkr*qVPr_K z7nJa0Bk(2n?e<7!M(UU`-Mc3wJn+C{k3IBoLISb(@V+otGcT@TVn> z3O(sdFeRyjK?bi!B4sbVR91%6lsxww%8*5umBIR;g!ILn=g?*( zZYED++h#&ThYTKs!7#2*@2tMP2MtWi&gwplOkKBbg9i2K6CY2kci6CDO2&YfM+o>T z`$C{n3AZd5rKmU1m<8#89i zojqGISTwBy*C;atE_D=P>X9RuLnt(ET*@eN)Be;ne?Ru1zI`ZTNEzi%B{w4bBPE6C zGn?Ij0T$Ni)$8GhyZ7kb2mTzMI3y#pPoI8lr0MBs?a27z;_>4rJUucqI6fglq{>rlk$b%IeuOe$=SA9zBK+9ZI$!x&MG(14pL?vz}CZJSQhB zYhWzdD~ub`R*I4%N#tden#srrIw8XLVBbV#73by8ob~*?xp{fBpL^!H=ZYs56$Nv1 z=S9ZDz*snJzu|G%F)}2kOrDIv0af8=_D&K|#*G`55(o^#SV)IB>}wV?9X^~bE`wB& zVxf8Y7@1hJhzj-;oCbFlvcYnourN_H1o639)b>34ENzIo2Qxv`#1e#m^k~I`Lm>nK zOAmV_J8g7UVq*L#U&@FPgucd%A*_r2!K?=kBq%v(P(nig{{8)atW$#JLx#8rBGJX< zK_u&k#`#Alq@^b($ETzw`y!b^){%kD6p3VF^yKG{8<(FM9K)yI4{8*2g(EQwA`!NX z>}`$pP81Hn4BDR)*Q-3YAPc`A?bt!&`Ic+z%-vk__6ko z7Z?)8Cguq*pwO5&dvjWDZeeb2dVYQ`TKB1}Ea<^C&`u0KFAuZ6^y#NFGSVhY2*Ucg zxk>?LXJcjN4}NS`T40S0k(4AsJy(`TrC zFx>)U$7bf{24xx$=45tinVFfMl@%K6_lLt78NRHnQNFYhBeK)dMvffiQx_RX#&BvP zpq$Z@S&t&N&E1g6$MkF`pd%(|5$vq2(WB)HWoBaSv98gibP*>l5i4DT=jNuS2AKwi z6g#Y@6b!P+NQ7RL`XYmvK>?MrhuF?pSzHSQ@?g=tNaQK3l7ftkPzd|hTuPQ04rgW{ z%j22s`0+t;b_mxEF63?o;FGblvch4y42~HyE)-(+sk9<1fYT5jr>L>)eVWNf6{;~+ zH3R0t304Ebn5EDXfcWs`SU59-`_K%0imYQpw6?rVC}-?gDQRDJc53RVfPb_3$DvOp+H6UAjs!~J7eEQ5+ca&kG)((>||yMV>uMX^3cK{PnZ$Y9O+`82XS za!^Jjne+3Zp_i4QDd%Xf)7FwPnIYYAm$8Hh!K(az+M3fGVtkGvZ)z${F*V#hnq*1} zs%+G#Aj?f>b7ejBWFPRp^4{s(DD)VEWM+Z`mY%{uFshCi3Dl#Y>{JMHKm(yQGjm*6 z{3Y5sywHLr8;|R<&?8xLYXmuCAu=)pH^}u5hlN`^37=;%4lP(RpF$45IScDp`iC!i zqASji8wcwNqcA$YR3N~hY_W3JG$LrHB|Xp)7nxd``=KNg>!l%L!&zubL(W(y0eB__ z&Sx%&5vZf=Mn0IdnSPIf`?MS#i3NlkGF$XU4~{fYn)@_)_Ex&)8mFLshE#U0=0Pcs zrpAn!C{3<^_PMm(=O-9p{+E zFoIN(xHL$KFJP^>OY7rn3|JbS#=U82@Ea|ep#~G-yvc|kGFX=wKAX)cjZHBZ91B(a zMjc{ijSKUO)w5J~6c|FmE+aok$Mr#WvO$Xg#-mgxm)lugKy;xYKeR9|ySt2$&@j^H z+{z`+)08tgIl7g}ah^}%2csqpT6*lJ-NL4)o{e9}uuKz1+8e8hkKjRTNylTyf-#Vc z9?>*m3~0!lL|3tqh^Mw#DqL3D9CC~8*m}~^=v1o^V*wnwg?Nk0Nl&MNAsmu1e@%`S zFqT_1Yi1?f$tQE+s;Ljce9^5zy>mw(kPUueY;h3;?Y2prEUj|sU+F9EXWLVa)&Xpc#rlp zX6A-9(`1?*bb)GOQ1z&}Xk_++0PM*LPTN>^Ni7`8tv``ROEJpWtgIJKd);`m(`^iH z0wS4l%hSprE}Geh$Om08nzV+ED}K="+Hbvx<{1N`P@lRCqN5g69dDPwh(EaKXH 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 literal 0 HcmV?d00001 diff --git a/src/builtin_fs.rs b/src/builtin_fs.rs index 1007353..d253431 100644 --- a/src/builtin_fs.rs +++ b/src/builtin_fs.rs @@ -110,6 +110,7 @@ impl BuiltinFS { FSNode::File("builtin_font.fnt", include_bytes!("builtin/builtin_font.fnt")), FSNode::File("builtin_font_0.png", include_bytes!("builtin/builtin_font_0.png")), FSNode::File("builtin_font_1.png", include_bytes!("builtin/builtin_font_1.png")), + FSNode::File("pixtone.pcm", include_bytes!("builtin/pixtone.pcm")), ]) ], } diff --git a/src/ggez/error.rs b/src/ggez/error.rs index 9feb314..d8a2de9 100644 --- a/src/ggez/error.rs +++ b/src/ggez/error.rs @@ -4,7 +4,7 @@ use std::error::Error; use std::fmt; use std::string::FromUtf8Error; -use std::sync::Arc; +use std::sync::{Arc, PoisonError}; /// An enum containing all kinds of game framework errors. #[derive(Debug, Clone)] @@ -232,3 +232,31 @@ impl From for GameError { GameError::ParseError(errstr) } } + +impl From for GameError { + fn from(s: cpal::DefaultStreamConfigError) -> GameError { + let errstr = format!("Default stream config error: {}", s); + GameError::AudioError(errstr) + } +} + +impl From for GameError { + fn from(s: cpal::PlayStreamError) -> GameError { + let errstr = format!("Play stream error: {}", s); + GameError::AudioError(errstr) + } +} + +impl From for GameError { + fn from(s: cpal::BuildStreamError) -> GameError { + let errstr = format!("Build stream error: {}", s); + GameError::AudioError(errstr) + } +} + +impl From> for GameError { + fn from(s: PoisonError) -> GameError { + let errstr = format!("Poison error: {}", s); + GameError::EventLoopError(errstr) + } +} diff --git a/src/main.rs b/src/main.rs index 1af8948..363f406 100644 --- a/src/main.rs +++ b/src/main.rs @@ -187,7 +187,7 @@ impl Game { texture_set: TextureSet::new(base_path), base_path: str!(base_path), stages: Vec::with_capacity(96), - sound_manager: SoundManager::new(ctx), + sound_manager: SoundManager::new(ctx)?, constants, scale, screen_size, diff --git a/src/sound/mod.rs b/src/sound/mod.rs index d17ed92..4247040 100644 --- a/src/sound/mod.rs +++ b/src/sound/mod.rs @@ -1,33 +1,204 @@ -use crate::ggez::{Context, GameResult}; +use std::sync::{mpsc, RwLock}; +use std::sync::mpsc::{Receiver, Sender}; + +use bitflags::_core::time::Duration; +use cpal::Sample; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; + +use crate::engine_constants::EngineConstants; +use crate::ggez::{Context, filesystem, GameResult}; +use crate::ggez::GameError::AudioError; +use crate::sound::organya::Song; +use crate::sound::playback::PlaybackEngine; +use crate::sound::wave_bank::SoundBank; +use crate::str; pub mod pixtone; +mod wave_bank; +mod organya; +mod playback; +mod stuff; +mod wav; pub struct SoundManager { - intro: Vec, - sloop: Vec, + tx: Sender, + current_song_id: usize, } -//unsafe impl Send for SoundManager {} +static SONGS: [&'static str; 42] = [ + "XXXX", + "WANPAKU", + "ANZEN", + "GAMEOVER", + "GRAVITY", + "WEED", + "MDOWN2", + "FIREEYE", + "VIVI", + "MURA", + "FANFALE1", + "GINSUKE", + "CEMETERY", + "PLANT", + "KODOU", + "FANFALE3", + "FANFALE2", + "DR", + "ESCAPE", + "JENKA", + "MAZE", + "ACCESS", + "IRONH", + "GRAND", + "Curly", + "OSIDE", + "REQUIEM", + "WANPAK2", + "QUIET", + "LASTCAVE", + "BALCONY", + "LASTBTL", + "LASTBT3", + "ENDING", + "ZONBIE", + "BDOWN", + "HELL", + "JENKA2", + "MARINE", + "BALLOS", + "TOROKO", + "WHITE" +]; impl SoundManager { - pub fn new(ctx: &mut Context) -> SoundManager { - SoundManager { - intro: Vec::new(), - sloop: Vec::new(), - } + pub fn new(ctx: &mut Context) -> GameResult { + let (tx, rx): (Sender, Receiver) = mpsc::channel(); + + let host = cpal::default_host(); + let device = host.default_output_device().ok_or_else(|| AudioError(str!("Error initializing audio device.")))?; + let config = device.default_output_config()?; + + let bnk = wave_bank::SoundBank::load_from(filesystem::open(ctx, "/builtin/pixtone.pcm")?)?; + + std::thread::spawn(move || { + if let Err(err) = match config.sample_format() { + cpal::SampleFormat::F32 => run::(rx, bnk, &device, &config.into()), + cpal::SampleFormat::I16 => run::(rx, bnk, &device, &config.into()), + cpal::SampleFormat::U16 => run::(rx, bnk, &device, &config.into()), + } { + log::error!("Something went wrong in audio thread: {}", err); + } + }); + + Ok(SoundManager { + tx: tx.clone(), + current_song_id: 0, + }) } - pub fn play_song(&mut self, ctx: &mut Context) -> GameResult { - /*self.intro.clear(); - self.sloop.clear(); - ggez::filesystem::open(ctx, "/base/Ogg11/curly_intro.ogg")?.read_to_end(&mut self.intro)?; - ggez::filesystem::open(ctx, "/base/Ogg11/curly_loop.ogg")?.read_to_end(&mut self.sloop)?; + pub fn play_song(&mut self, song_id: usize, constants: &EngineConstants, ctx: &mut Context) -> GameResult { + if self.current_song_id == song_id { + return Ok(()); + } - let sink = Sink::new(ctx.audio_context.device()); - sink.append(rodio::Decoder::new(Cursor::new(self.intro.clone()))?); - sink.append(rodio::Decoder::new(Cursor::new(self.sloop.clone()))?); - sink.detach();*/ + if let Some(song_name) = SONGS.get(song_id) { + let org = organya::Song::load_from(filesystem::open(ctx, ["/base/Org/", &song_name.to_lowercase(), ".org"].join(""))?)?; + log::info!("Playing song: {}", song_name); + self.current_song_id = song_id; + self.tx.send(PlaybackMessage::PlaySong(org)); + } Ok(()) } } + +enum PlaybackMessage { + Stop, + PlaySong(Song), +} + +#[derive(PartialEq, Eq)] +enum PlaybackState { + Stopped, + Playing, +} + +fn run(rx: Receiver, bank: SoundBank, + device: &cpal::Device, config: &cpal::StreamConfig) -> GameResult where + T: cpal::Sample, +{ + let sample_rate = config.sample_rate.0 as f32; + let channels = config.channels as usize; + let mut state = PlaybackState::Stopped; + let mut new_song: Option = None; + let mut engine = PlaybackEngine::new(Song::empty(), &bank); + + log::info!("Audio format: {} {}", sample_rate, channels); + engine.set_sample_rate(sample_rate as usize); + engine.loops = usize::MAX; + + let mut buf = vec![0x8080; 441]; + let mut index = 0; + let mut frames = engine.render_to(&mut buf); + + let err_fn = |err| eprintln!("an error occurred on stream: {}", err); + + let stream = device.build_output_stream( + config, + move |data: &mut [T], _: &cpal::OutputCallbackInfo| { + match rx.try_recv() { + Ok(PlaybackMessage::PlaySong(song)) => { + engine.start_song(song, &bank); + + for i in &mut buf[0..frames] { *i = 0x8080 }; + frames = engine.render_to(&mut buf); + index = 0; + + state = PlaybackState::Playing; + } + _ => {} + } + + for frame in data.chunks_mut(channels) { + let sample: u16 = { + if state == PlaybackState::Stopped { + 0x8000 + } else if index < frames { + let sample = buf[index]; + index += 1; + if index & 1 == 0 { (sample & 0xff) << 8 } else { sample & 0xff00 } + } else { + for i in &mut buf[0..frames] { *i = 0x8080 }; + frames = engine.render_to(&mut buf); + index = 0; + let sample = buf[0]; + (sample & 0xff) << 8 + } + }; + + let value: T = cpal::Sample::from::(&sample); + for sample in frame.iter_mut() { + *sample = value; + } + } + }, + err_fn, + )?; + stream.play()?; + + loop { + std::thread::sleep(Duration::from_millis(10)); + } +} + +fn write_data(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> u16) + where + T: cpal::Sample, +{ + for frame in output.chunks_mut(channels) { + let value: T = cpal::Sample::from::(&next_sample()); + for sample in frame.iter_mut() { + *sample = value; + } + } +} diff --git a/src/sound/organya.rs b/src/sound/organya.rs new file mode 100644 index 0000000..bb90c6b --- /dev/null +++ b/src/sound/organya.rs @@ -0,0 +1,225 @@ +#[repr(u8)] +#[derive(Debug, Copy, Clone)] +pub enum Version { + // Can't find any files with this signature, + // But apparently these files had no Pi flag. + Beta = b'1', + Main = b'2', + // OrgMaker 2.05 Extended Drums + Extended = b'3' +} + +#[derive(Debug, Copy, Clone)] +pub struct LoopRange { + // inclusive + pub start: i32, + // exclusive + pub end: i32 +} + +#[derive(Debug, Copy, Clone)] +pub struct Display { + pub beats: u8, + pub steps: u8 +} + +#[derive(Debug, Copy, Clone)] +pub struct Timing { + pub wait: u16, + pub loop_range: LoopRange +} + +#[derive(Copy, Clone)] +#[derive(Debug)] +pub struct Instrument { + pub freq: u16, + pub inst: u8, + pub pipi: u8, + pub notes: u16 +} + +pub struct Track { + pub inst: Instrument, + pub notes: Vec +} + +impl Clone for Track { + fn clone(&self) -> Track { + Track { + inst: self.inst, + notes: self.notes.clone(), + } + } +} + +impl std::fmt::Debug for Track { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + self.inst.fmt(f) + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct Note { + pub pos: i32, + pub key: u8, + pub len: u8, + pub vol: u8, + pub pan: u8 +} + +#[derive(Debug)] +pub struct Song { + pub version: Version, + pub time: Timing, + pub tracks: [Track; 16] +} + +impl Clone for Song { + fn clone(&self) -> Song { + Song { + version: self.version, + time: self.time, + tracks: self.tracks.clone(), + } + } +} + +use byteorder::{LE, ReadBytesExt}; +use std::io; + +impl Song { + pub fn empty() -> Song { + Song { + version: Version::Main, + time: Timing { wait: 8, loop_range: LoopRange { start: 0, end: 1 } }, + tracks: [ + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] }, + ] + } + } + + pub fn load_from(mut f: R) -> io::Result { + let mut magic = [0; 6]; + + f.read_exact(&mut magic)?; + + let version = + match &magic { + b"Org-01" => Version::Beta, + b"Org-02" => Version::Main, + b"Org-03" => Version::Extended, + _ => return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid magic number")) + }; + + let wait = f.read_u16::()?; + let _bpm = f.read_u8()?; + let _spb = f.read_u8()?; + let start = f.read_i32::()?; + let end = f.read_i32::()?; + + use std::mem::MaybeUninit as Mu; + + let mut insts: [Mu; 16] = unsafe { + Mu::uninit().assume_init() + }; + + for i in insts.iter_mut() { + let freq = f.read_u16::()?; + let inst = f.read_u8()?; + let pipi = f.read_u8()?; + let notes = f.read_u16::()?; + + *i = Mu::new(Instrument { + freq, + inst, + pipi, + notes + }); + } + + let insts: [Instrument; 16] = unsafe { + std::mem::transmute(insts) + }; + + let mut tracks: [Mu; 16] = unsafe { + Mu::uninit().assume_init() + }; + + for (i, t) in tracks.iter_mut().enumerate() { + let count = insts[i].notes as usize; + + #[repr(C)] + #[derive(Copy, Clone)] + struct UninitNote { + pos: Mu, + key: Mu, + len: Mu, + vol: Mu, + pan: Mu + } + + let mut notes: Vec = unsafe { + vec![Mu::uninit().assume_init(); count] + }; + + for note in notes.iter_mut() { + note.pos = Mu::new(f.read_i32::()?); + } + + for note in notes.iter_mut() { + note.key = Mu::new(f.read_u8()?); + } + + for note in notes.iter_mut() { + note.len = Mu::new(f.read_u8()?); + } + + for note in notes.iter_mut() { + note.vol = Mu::new(f.read_u8()?); + } + + for note in notes.iter_mut() { + note.pan = Mu::new(f.read_u8()?); + } + + *t = Mu::new(Track { + inst: insts[i], + notes: unsafe { std::mem::transmute(notes) } + }); + } + + let tracks = unsafe { + std::mem::transmute(tracks) + }; + + let song = Song { + version, + time: Timing { + wait, + loop_range: LoopRange { + start, + end + } + }, + tracks + }; + + Ok(song) + } +} diff --git a/src/sound/playback.rs b/src/sound/playback.rs new file mode 100644 index 0000000..4c17f4b --- /dev/null +++ b/src/sound/playback.rs @@ -0,0 +1,512 @@ +use std::mem::MaybeUninit; + +use crate::sound::organya::Song as Organya; +use crate::sound::stuff::*; +use crate::sound::wav::*; +use crate::sound::wave_bank::SoundBank; + +pub struct PlaybackEngine { + song: Organya, + lengths: [u8; 8], + swaps: [usize; 8], + keys: [u8; 8], + track_buffers: [RenderBuffer; 136], + output_format: WavFormat, + play_pos: i32, + frames_this_tick: usize, + frames_per_tick: usize, + pub loops: usize, +} + +pub struct PlaybackState { + song: Organya, + play_pos: i32, +} + +impl PlaybackEngine { + pub fn new(song: Organya, samples: &SoundBank) -> Self { + + // Octave 0 Track 0 Swap 0 + // Octave 0 Track 1 Swap 0 + // ... + // Octave 1 Track 0 Swap 0 + // ... + // Octave 0 Track 0 Swap 1 + // octave * 8 + track + swap + // 128..136: Drum Tracks + let mut buffers: [MaybeUninit; 136] = unsafe { + MaybeUninit::uninit().assume_init() + }; + + // track + for i in 0..8 { + let sound_index = song.tracks[i].inst.inst as usize; + + // WAVE100 uses 8-bit signed audio, but wav audio wants 8-bit unsigned. + // On 2s complement system, we can simply flip the top bit + // No need to cast to u8 here because the sound bank data is one big &[u8]. + let sound = samples.get_wave(sound_index) + .iter() + .map(|&x| x ^ 128) + .collect(); + + let format = WavFormat { channels: 1, sample_rate: 22050, bit_depth: 8 }; + + let rbuf = RenderBuffer::new_organya(WavSample { format, data: sound }); + + // octave + for j in 0..8 { + // swap + for &k in &[0, 64] { + buffers[i + (j * 8) + k] = MaybeUninit::new(rbuf.clone()); + } + } + } + + for (inst, buf) in song.tracks[8..].iter().zip(buffers[128..].iter_mut()) { + *buf = + MaybeUninit::new( + RenderBuffer::new( + // FIXME: *frustrated screaming* + samples.samples[inst.inst.inst as usize].clone() + ) + ); + } + + let frames_per_tick = (44100 / 1000) * song.time.wait as usize; + + PlaybackEngine { + song, + lengths: [0; 8], + swaps: [0; 8], + keys: [255; 8], + track_buffers: unsafe { std::mem::transmute(buffers) }, + play_pos: 0, + output_format: WavFormat { + channels: 2, + sample_rate: 44100, + bit_depth: 16, + }, + frames_this_tick: 0, + frames_per_tick, + loops: 1, + } + } + + pub fn set_sample_rate(&mut self, sample_rate: usize) { + self.output_format.sample_rate = sample_rate as u32; + self.frames_per_tick = (sample_rate / 1000) * self.song.time.wait as usize; + } + + pub fn get_state(&self) -> PlaybackState { + PlaybackState { + song: self.song.clone(), + play_pos: self.play_pos, + } + } + + pub fn set_state(&mut self, state: PlaybackState, samples: &SoundBank) { + self.start_song(state.song, samples); + self.play_pos = state.play_pos; + } + + pub fn start_song(&mut self, song: Organya, samples: &SoundBank) { + for i in 0..8 { + let sound_index = song.tracks[i].inst.inst as usize; + let sound = samples.get_wave(sound_index) + .iter() + .map(|&x| x ^ 128) + .collect(); + + let format = WavFormat { channels: 1, sample_rate: 22050, bit_depth: 8 }; + + let rbuf = RenderBuffer::new_organya(WavSample { format, data: sound }); + + for j in 0..8 { + for &k in &[0, 64] { + self.track_buffers[i + (j * 8) + k] = rbuf.clone(); + } + } + } + + for (inst, buf) in song.tracks[8..].iter().zip(self.track_buffers[128..].iter_mut()) { + *buf = RenderBuffer::new(samples.samples[inst.inst.inst as usize].clone()); + } + + self.song = song; + self.play_pos = 0; + self.frames_per_tick = (self.output_format.sample_rate as usize / 1000) * self.song.time.wait as usize; + self.frames_this_tick = 0; + for i in self.lengths.iter_mut() { *i = 0 }; + for i in self.swaps.iter_mut() { *i = 0 }; + for i in self.keys.iter_mut() { *i = 255 }; + } + + #[allow(unused)] + pub fn set_position(&mut self, position: i32) { + self.play_pos = position; + } + + pub fn get_total_samples(&self) -> u32 { + let ticks_intro = self.song.time.loop_range.start; + let ticks_loop = self.song.time.loop_range.end - self.song.time.loop_range.start; + let ticks_total = ticks_intro + ticks_loop + (ticks_loop * self.loops as i32); + + self.frames_per_tick as u32 * ticks_total as u32 + } + + fn update_play_state(&mut self) { + for track in 0..8 { + if let Some(note) = + self.song.tracks[track].notes.iter().find(|x| x.pos == self.play_pos) { + // New note + //eprintln!("{:?}", &self.keys); + if note.key != 255 { + if self.keys[track] == 255 { // New + let octave = (note.key / 12) * 8; + let j = octave as usize + track + self.swaps[track]; + for k in 0..16 { + let swap = if k >= 8 { 64 } else { 0 }; + let key = note.key % 12; + let p_oct = k % 8; + + let freq = org_key_to_freq(key + p_oct * 12, self.song.tracks[track].inst.freq as i16); + + let l = p_oct as usize * 8 + track + swap; + self.track_buffers[l].set_frequency(freq as u32); + self.track_buffers[l].organya_select_octave(p_oct as usize, self.song.tracks[track].inst.pipi != 0); + } + self.track_buffers[j].looping = true; + self.track_buffers[j].playing = true; + // last playing key + self.keys[track] = note.key; + } else if self.keys[track] == note.key { // Same + //assert!(self.lengths[track] == 0); + let octave = (self.keys[track] / 12) * 8; + let j = octave as usize + track + self.swaps[track]; + if self.song.tracks[track].inst.pipi == 0 { + self.track_buffers[j].looping = false; + } + self.swaps[track] += 64; + self.swaps[track] %= 128; + let j = octave as usize + track + self.swaps[track]; + self.track_buffers[j].organya_select_octave(note.key as usize / 12, self.song.tracks[track].inst.pipi != 0); + self.track_buffers[j].looping = true; + self.track_buffers[j].playing = true; + } else { // change + let octave = (self.keys[track] / 12) * 8; + let j = octave as usize + track + self.swaps[track]; + if self.song.tracks[track].inst.pipi == 0 { + self.track_buffers[j].looping = false; + } + self.swaps[track] += 64; + self.swaps[track] %= 128; + let octave = (note.key / 12) * 8; + let j = octave as usize + track + self.swaps[track]; + for k in 0..16 { + let swap = if k >= 8 { 64 } else { 0 }; + let key = note.key % 12; + let p_oct = k % 8; + + let freq = org_key_to_freq(key + p_oct * 12, self.song.tracks[track].inst.freq as i16); + let l = p_oct as usize * 8 + track + swap; + self.track_buffers[l].set_frequency(freq as u32); + self.track_buffers[l].organya_select_octave(p_oct as usize, self.song.tracks[track].inst.pipi != 0); + } + self.track_buffers[j].looping = true; + self.track_buffers[j].playing = true; + self.keys[track] = note.key; + } + + self.lengths[track] = note.len; + } + + if self.keys[track] != 255 { + let octave = (self.keys[track] / 12) * 8; + let j = octave as usize + track + self.swaps[track]; + + if note.vol != 255 { + let vol = org_vol_to_vol(note.vol); + self.track_buffers[j].set_volume(vol); + } + + if note.pan != 255 { + let pan = org_pan_to_pan(note.pan); + self.track_buffers[j].set_pan(pan); + } + } + } + + if self.lengths[track] == 0 { + if self.keys[track] != 255 { + let octave = (self.keys[track] / 12) * 8; + let j = octave as usize + track + self.swaps[track]; + if self.song.tracks[track].inst.pipi == 0 { + self.track_buffers[j].looping = false; + } + self.keys[track] = 255; + } + } + + self.lengths[track] = self.lengths[track].saturating_sub(1); + } + + for i in 8..16 { + let j = i + 120; + + let notes = &self.song.tracks[i].notes; + + // start a new note + // note (hah) that drums are unaffected by length and pi values. This is the only case we have to handle. + if let Some(note) = + notes.iter().find(|x| x.pos == self.play_pos) { + + // FIXME: Add constants for dummy values + if note.key != 255 { + let freq = org_key_to_drum_freq(note.key); + self.track_buffers[j].set_frequency(freq as u32); + self.track_buffers[j].set_position(0); + self.track_buffers[j].playing = true; + } + + if note.vol != 255 { + let vol = org_vol_to_vol(note.vol); + self.track_buffers[j].set_volume(vol); + } + + if note.pan != 255 { + let pan = org_pan_to_pan(note.pan); + self.track_buffers[j].set_pan(pan); + } + } + } + } + + pub fn render_to(&mut self, buf: &mut [u16]) -> usize { + for (i, frame) in buf.iter_mut().enumerate() { + if self.frames_this_tick == 0 { + self.update_play_state() + } + + mix(std::slice::from_mut(frame), self.output_format, &mut self.track_buffers); + + self.frames_this_tick += 1; + + if self.frames_this_tick == self.frames_per_tick { + self.play_pos += 1; + + if self.play_pos == self.song.time.loop_range.end { + self.play_pos = self.song.time.loop_range.start; + + if self.loops == 0 { + return i + 1; + } + + self.loops -= 1; + } + + self.frames_this_tick = 0; + } + } + + buf.len() + } +} + +// TODO: Create a MixingBuffer or something... +fn mix(dst: &mut [u16], dst_fmt: WavFormat, srcs: &mut [RenderBuffer]) { + let freq = dst_fmt.sample_rate as f64; + + for buf in srcs { + if buf.playing { + // index into sound samples + let advance = buf.frequency as f64 / freq; + + let vol = centibel_to_scale(buf.volume); + + let (pan_l, pan_r) = + match buf.pan.signum() { + 0 => (1.0, 1.0), + 1 => (centibel_to_scale(-buf.pan), 1.0), + -1 => (1.0, centibel_to_scale(buf.pan)), + _ => unsafe { std::hint::unreachable_unchecked() } + }; + + fn clamp(v: T, limit: T) -> T { + if v > limit { + limit + } else { + v + } + } + + // s1: sample 1 + // s2: sample 2 + // sp: previous sample (before s1) + // sn: next sample (after s2) + // mu: position to interpolate for + fn cubic_interp(s1: f32, s2: f32, sp: f32, sn: f32, mu: f32) -> f32 { + let mu2 = mu * mu; + let a0 = sn - s2 - sp + s1; + let a1 = sp - s1 - a0; + let a2 = s2 - sp; + let a3 = s1; + + a0 * mu * mu2 + a1 * mu2 + a2 * mu + a3 + } + + #[allow(unused_variables)] + + for frame in dst.iter_mut() { + let pos = buf.position as usize + buf.base_pos; + // -1..1 + let s1 = (buf.sample.data[pos] as f32 - 128.0) / 128.0; + let s2 = (buf.sample.data[clamp(pos + 1, buf.base_pos + buf.len - 1)] as f32 - 128.0) / 128.0; + let s3 = (buf.sample.data[clamp(pos + 2, buf.base_pos + buf.len - 1)] as f32 - 128.0) / 128.0; + let s4 = (buf.sample.data[pos.saturating_sub(1)] as f32 - 128.0) / 128.0; + + use std::f32::consts::PI; + + let r1 = buf.position.fract() as f32; + let r2 = (1.0 - f32::cos(r1 * PI)) / 2.0; + + //let s = s1; // No interp + //let s = s1 + (s2 - s1) * r1; // Linear interp + //let s = s1 * (1.0 - r2) + s2 * r2; // Cosine interp + let s = cubic_interp(s1, s2, s4, s3, r1); // Cubic interp + // Ideally we want sinc/lanczos interpolation, since that's what DirectSound appears to use. + + // -128..128 + let sl = s * pan_l * vol * 128.0; + let sr = s * pan_r * vol * 128.0; + + buf.position += advance; + + if buf.position as usize >= buf.len { + if buf.looping && buf.nloops != 1 { + buf.position %= buf.len as f64; + if buf.nloops != -1 { + buf.nloops -= 1; + } + } else { + buf.position = 0.0; + buf.playing = false; + break; + } + } + + let [mut l, mut r] = frame.to_le_bytes(); + // -128..127 + let xl = (l ^ 128) as i8; + let xr = (r ^ 128) as i8; + + // 0..255 + l = xl.saturating_add(sl as i8) as u8 ^ 128; + r = xr.saturating_add(sr as i8) as u8 ^ 128; + + *frame = u16::from_le_bytes([l, r]); + } + } + } +} + +pub fn centibel_to_scale(a: i32) -> f32 { + f32::powf(10.0, a as f32 / 2000.0) +} + +#[derive(Clone)] +pub struct RenderBuffer { + pub position: f64, + pub frequency: u32, + pub volume: i32, + pub pan: i32, + pub sample: WavSample, + pub playing: bool, + pub looping: bool, + pub base_pos: usize, + pub len: usize, + // -1 = infinite + pub nloops: i32, +} + +impl RenderBuffer { + pub fn new(sample: WavSample) -> RenderBuffer { + RenderBuffer { + position: 0.0, + frequency: sample.format.sample_rate, + volume: 0, + pan: 0, + len: sample.data.len(), + sample, + playing: false, + looping: false, + base_pos: 0, + nloops: -1, + } + } + + pub fn new_organya(mut sample: WavSample) -> RenderBuffer { + let wave = sample.data.clone(); + sample.data.clear(); + + for size in &[256_usize, 256, 128, 128, 64, 32, 16, 8] { + let step = 256 / size; + let mut acc = 0; + + for _ in 0..*size { + sample.data.push(wave[acc]); + acc += step; + + if acc >= 256 { + acc = 0; + } + } + } + + RenderBuffer::new(sample) + } + + #[inline] + pub fn organya_select_octave(&mut self, octave: usize, pipi: bool) { + const OFFS: &[usize] = &[0x000, 0x100, + 0x200, 0x280, + 0x300, 0x340, + 0x360, 0x370]; + const LENS: &[usize] = &[256_usize, 256, 128, 128, 64, 32, 16, 8]; + self.base_pos = OFFS[octave]; + self.len = LENS[octave]; + self.position %= self.len as f64; + if pipi && !self.playing { + self.nloops = ((octave + 1) * 4) as i32; + } + } + + #[inline] + pub fn set_frequency(&mut self, frequency: u32) { + //assert!(frequency >= 100 && frequency <= 100000); + //dbg!(frequency); + self.frequency = frequency; + } + + #[inline] + pub fn set_volume(&mut self, volume: i32) { + assert!(volume >= -10000 && volume <= 0); + + self.volume = volume; + } + + #[inline] + pub fn set_pan(&mut self, pan: i32) { + assert!(pan >= -10000 && pan <= 10000); + + self.pan = pan; + } + + #[inline] + #[allow(unused)] + pub fn set_position(&mut self, position: u32) { + assert!(position < self.sample.data.len() as u32 / self.sample.format.bit_depth as u32); + + self.position = position as f64; + } +} diff --git a/src/sound/stuff.rs b/src/sound/stuff.rs new file mode 100644 index 0000000..6285f58 --- /dev/null +++ b/src/sound/stuff.rs @@ -0,0 +1,36 @@ +pub const FRQ_TBL: [i16; 12] = [ + 261,278,294,311,329,349,371,391,414,440,466,494 +]; + +pub const PAN_TBL: [i16; 13] = [ + 0,43,86,129,172,215,256,297,340,383,426,469,512 +]; + +pub const OCT_TBL: [i16; 8] = [ + 32,64,64,128,128,128,128,128 +]; + +pub fn org_key_to_freq(key: u8, a: i16) -> i32 { + let (oct, pitch) = org_key_to_oct_pitch(key); + + let freq = FRQ_TBL[pitch as usize] as f32; + let oct = OCT_TBL[oct as usize] as f32; + + (freq * oct) as i32 + (a as i32 - 1000) +} + +pub fn org_key_to_drum_freq(key: u8) -> i32 { + key as i32 * 800 + 100 +} + +pub fn org_pan_to_pan(pan: u8) -> i32 { + (PAN_TBL[pan as usize] as i32 - 256) * 10 +} + +pub fn org_vol_to_vol(vol: u8) -> i32 { + (vol as i32 - 255) * 8 +} + +pub fn org_key_to_oct_pitch(key: u8) -> (u8, u8) { + (key/12, key%12) +} diff --git a/src/sound/wav.rs b/src/sound/wav.rs new file mode 100644 index 0000000..2fe9333 --- /dev/null +++ b/src/sound/wav.rs @@ -0,0 +1,120 @@ +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct RiffChunk { + id: [u8; 4], + length: u32 +} + +use std::fmt; + +impl fmt::Display for RiffChunk { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use std::ascii::escape_default as esc; + + write!(f, "chunk \"{}{}{}{}\", length: {}", + esc(self.id[0]), + esc(self.id[1]), + esc(self.id[2]), + esc(self.id[3]), + self.length + ) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct WavFormat { + pub channels: u16, + pub sample_rate: u32, + pub bit_depth: u16 +} + +impl fmt::Display for WavFormat { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} channels, {} Hz, {}-bit", + self.channels, + self.sample_rate, + self.bit_depth + ) + } +} + +#[derive(Clone)] +pub struct WavSample { + pub format: WavFormat, + pub data: Vec +} + +impl fmt::Display for WavSample { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}, {} samples", + self.format, + // num_bytes / bytes_per_sample + self.data.len() / ((self.format.bit_depth / 8) * self.format.channels) as usize + ) + } +} + +use byteorder::{LE, ReadBytesExt}; +use std::io; + +impl RiffChunk { + pub fn read_from(mut f: R) -> io::Result { + let mut id = [0; 4]; + + f.read_exact(&mut id)?; + let length = f.read_u32::()?; + + Ok(RiffChunk { id, length }) + } +} + +impl WavSample { + pub fn read_from(mut f: R) -> io::Result { + let riff = RiffChunk::read_from(&mut f)?; + + match &riff.id { + b"RIFF" => {}, + b"RIFX" => panic!("Cannot handle RIFX data!"), + _ => panic!("Expected RIFF signature, found {}", riff) + } + + let mut rfmt = [0; 4]; + + f.read_exact(&mut rfmt)?; + + assert_eq!(rfmt, *b"WAVE"); + + let fmt = RiffChunk::read_from(&mut f)?; + + assert_eq!(fmt.id, *b"fmt "); + //assert_eq!(fmt.length, 16); + + let afmt = f.read_u16::()?; + + debug_assert!(afmt == 1); + + let channels = f.read_u16::()?; + let samples = f.read_u32::()?; + let _brate = f.read_u32::()?; + let _balgn = f.read_u16::()?; + let bits = f.read_u16::()?; + + let data = RiffChunk::read_from(&mut f)?; + + assert_eq!(data.id, *b"data"); + + let mut buf = vec![0; data.length as usize]; + + f.read_exact(&mut buf)?; + + Ok( + WavSample { + format: WavFormat { + channels, + sample_rate: samples, + bit_depth: bits + }, + data: buf + } + ) + } +} diff --git a/src/sound/wave_bank.rs b/src/sound/wave_bank.rs new file mode 100644 index 0000000..c32f142 --- /dev/null +++ b/src/sound/wave_bank.rs @@ -0,0 +1,45 @@ +use crate::sound::wav; + +use std::io; +use std::fmt; + +pub struct SoundBank { + // FIXME: would prefer Box<[u8; 25600]> + pub wave100: Box<[u8]>, + + pub samples: Vec +} + +impl fmt::Display for SoundBank { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "WAVE100: {:2X?}...", &self.wave100[..8])?; + + for sample in self.samples.iter() { + writeln!(f, "{}", sample)?; + } + + Ok(()) + } +} + +impl SoundBank { + pub fn load_from(mut f: R) -> io::Result { + // no box [0; 25600] yet + let mut wave100 = vec![0; 25600].into_boxed_slice(); + + f.read_exact(&mut *wave100)?; + + let mut samples = Vec::with_capacity(16); + + loop { + match wav::WavSample::read_from(&mut f) { + Ok(sample) => samples.push(sample), + Err(_) => return Ok(SoundBank { wave100, samples }) + } + } + } + + pub fn get_wave(&self, index: usize) -> &[u8] { + &self.wave100[index*256..(index+1)*256] + } +}