From 87d8816881d146d73a55368759af0c437ef0a8d2 Mon Sep 17 00:00:00 2001 From: f4exb Date: Sat, 21 Nov 2020 11:15:06 +0100 Subject: [PATCH] Audio output plugin --- doc/img/AudioOutput_plugin.png | Bin 0 -> 12962 bytes doc/img/AudioOutput_plugin.xcf | Bin 0 -> 42620 bytes plugins/samplesink/CMakeLists.txt | 1 + plugins/samplesink/audiooutput/CMakeLists.txt | 57 +++ .../samplesink/audiooutput/audiooutput.cpp | 444 ++++++++++++++++++ plugins/samplesink/audiooutput/audiooutput.h | 153 ++++++ .../samplesink/audiooutput/audiooutputgui.cpp | 239 ++++++++++ .../samplesink/audiooutput/audiooutputgui.h | 79 ++++ .../samplesink/audiooutput/audiooutputgui.ui | 290 ++++++++++++ .../audiooutput/audiooutputplugin.cpp | 148 ++++++ .../audiooutput/audiooutputplugin.h | 56 +++ .../audiooutput/audiooutputsettings.cpp | 93 ++++ .../audiooutput/audiooutputsettings.h | 58 +++ .../audiooutput/audiooutputwebapiadapter.cpp | 49 ++ .../audiooutput/audiooutputwebapiadapter.h | 41 ++ .../audiooutput/audiooutputworker.cpp | 157 +++++++ .../audiooutput/audiooutputworker.h | 66 +++ plugins/samplesink/audiooutput/readme.md | 36 ++ sdrbase/audio/audiofifo.cpp | 21 +- sdrbase/resources/webapi.qrc | 1 + sdrbase/resources/webapi/doc/html2/index.html | 35 +- .../doc/swagger/include/AudioOutput.yaml | 24 + .../doc/swagger/include/DeviceSettings.yaml | 2 + sdrbase/webapi/webapirequestmapper.cpp | 5 + sdrbase/webapi/webapiutils.cpp | 4 +- .../api/swagger/include/AudioOutput.yaml | 24 + .../api/swagger/include/DeviceSettings.yaml | 2 + swagger/sdrangel/code/html2/index.html | 35 +- .../qt5/client/SWGAudioOutputSettings.cpp | 250 ++++++++++ .../code/qt5/client/SWGAudioOutputSettings.h | 95 ++++ .../code/qt5/client/SWGDeviceSettings.cpp | 25 + .../code/qt5/client/SWGDeviceSettings.h | 7 + .../code/qt5/client/SWGModelFactory.h | 4 + 33 files changed, 2490 insertions(+), 11 deletions(-) create mode 100644 doc/img/AudioOutput_plugin.png create mode 100644 doc/img/AudioOutput_plugin.xcf create mode 100644 plugins/samplesink/audiooutput/CMakeLists.txt create mode 100644 plugins/samplesink/audiooutput/audiooutput.cpp create mode 100644 plugins/samplesink/audiooutput/audiooutput.h create mode 100644 plugins/samplesink/audiooutput/audiooutputgui.cpp create mode 100644 plugins/samplesink/audiooutput/audiooutputgui.h create mode 100644 plugins/samplesink/audiooutput/audiooutputgui.ui create mode 100644 plugins/samplesink/audiooutput/audiooutputplugin.cpp create mode 100644 plugins/samplesink/audiooutput/audiooutputplugin.h create mode 100644 plugins/samplesink/audiooutput/audiooutputsettings.cpp create mode 100644 plugins/samplesink/audiooutput/audiooutputsettings.h create mode 100644 plugins/samplesink/audiooutput/audiooutputwebapiadapter.cpp create mode 100644 plugins/samplesink/audiooutput/audiooutputwebapiadapter.h create mode 100644 plugins/samplesink/audiooutput/audiooutputworker.cpp create mode 100644 plugins/samplesink/audiooutput/audiooutputworker.h create mode 100644 plugins/samplesink/audiooutput/readme.md create mode 100644 sdrbase/resources/webapi/doc/swagger/include/AudioOutput.yaml create mode 100644 swagger/sdrangel/api/swagger/include/AudioOutput.yaml create mode 100644 swagger/sdrangel/code/qt5/client/SWGAudioOutputSettings.cpp create mode 100644 swagger/sdrangel/code/qt5/client/SWGAudioOutputSettings.h diff --git a/doc/img/AudioOutput_plugin.png b/doc/img/AudioOutput_plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..fdcc2b7e3a9c7bced35844c374ab49f2a3df8818 GIT binary patch literal 12962 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 PK#>-g6DxSC>-+xz6{&85 literal 0 HcmV?d00001 diff --git a/doc/img/AudioOutput_plugin.xcf b/doc/img/AudioOutput_plugin.xcf new file mode 100644 index 0000000000000000000000000000000000000000..371b2449de7602dbd1cae0b47024b459be3c834e GIT binary patch literal 42620 zcmeHQ30zdw_kYX`o4c0T9x75IC|nsqL54+eLq*&*D^#>e!~)E%Uyw`t&wtv!32JV* zWtIz?j%j8wZe?1jX@Xmd2C1ww?|;s_=gqu<8Iei-;qT}3bLV@{x$C>{ob&Fv_wpR4 zpYW{Ll+k0oo(~8N6a>NLM+kR81h4lXG=<>e0zb&~hz1r0xbB9afzSlPJru`$%^~~^ zq2V~F+JE%K2@{f1(t#E*u~~F#Qet}27_Z6t^ryYL_v{moDNUrT`U#2SlKjV|>c=1t zbFX|XEq!Wol2_WqWc`@7^&T5LZuG?DiK$+XN#LJ4Zd9UIfb=i0EhLY!lN{(J{lnxb zY0oB()~Af~3Jd^hstrYi|D+Usx|cp>^wUXcSYqPXv1v)^UQ&KVoJValW1kh5%SleS zy}+^YM2>e6|Dg_?ZuKt5c6yE-{^S^PiDTCh9AjE>>^YcY0`Z6Dae97lj`HuK=j9k+ z3kdj#a7@)+o#0n+gMd#@gwPV>x=i?mCkz4knAMZw00K6Te7qinAjj(|;$Dh)eMP*1 zBHmCDZwzs0&m|e~3kdF%hV{3hIDp`daS<-S){3|+fmqM|$cLbU021heb#bvuPnweM zpPV=~DHT}KeVu;{d0wr&I(fB%r#yCI3J?PlQ_}p?l2Y|!WuAYUep-@OUl4$wrb+^noR~6h5=gOEtI=4?sKm4+|1tX1q|xd6i78&m>8Zf1 zD#9S30qsKqY_A}YImr9Wq_lMX*r{I0Nn_KozKK)(CrliZdN(4*AgaLSjQ2DHXcW_A#p32G_032veW;;w?b4I2kepf~!y5W)$~e8B8n7+r4X#(YOmbCDCfZ z#VF?4P7kLLYM9kjbR@5Pty|zAY44%#FOXO2&PxX z7wXAG=^8O?=D8B%lddwyv_)J3^<`K}UtK4j@7JhVVrf}3wI$R0V!B!|y(@OJvYsSX zw1suGNzsLV4VxyGmNX$A-2Et5QB0%C-GuUfyRKZ>HIQ1jy;n}bIEaQ8pw5=J4Vs(! z{hm)s1x0-_psracSpl53P>n1#ND=o8D{UD9QbYH3JgqSq^u|Mo9&M)#usqeA?@2Fh zPqdJ5d3Gd>CLBOW^?0TcJ(KWl!bODZ2zLj}V?Gtl-$VKH&p|?Fd6T3PJ|VU~Xy9WC|I| z@ZZ+H`dgt3n0sYVi=kllZ(dNg)ddV)gph%~0Hdr7Y9sXECI)3&U5rvM@YzC7#E8zB z@V?3H4CBIo(b5|_0aG&hp|=M}9ai22-WMNjgYdBEr3g3gS#vp3wuSKwO(Kjm^uh>~ zCEVQSt+uoyw<+~PdgcbfD#SeK5JdJGg0;zhrbKn7GTYX1x zzeH#pUW78rzM_mWU-lU})&qYLnh>Eo63VpIx0Lhq^3Xc@s~tp#z88#E6L4P6T3n?n zq^XJcmc_BMAxfKwC(f1p{Gx|VQS)w3i<4E-I7ip`oL>0bqP}1*{>@&3 zMkty6uMQ2G4?HeTw$vql;wqZIuVSNdT^adaov5& zN>uf-n%~FM9wP7HA=ELr9wP4rl#@TZQ z_Q+-)?{!oCwyHj%ZQF8>#E6G7y})mJ2zthUCGY8eVBSvNQ>ML%zX9(;d24vH(v3@Q zM5b_Gb{j>wb;6;xX&3vDp*{@)0zDFevV@!M=qc-0wu^DONB4qD(nh!!TrK&x`R(Ou z*^>;o7rG1irsxLI|H4P{Pc51zvu>?J-AdmL+fw*$7>DZ|gx4?v*Y){tL725J`ERSC zKZ77RJBg|fWvlvc?SSWCGtH&3yqOkMH~hEu9RpdX&RVPU77QL;FzXx?7-;2bffpDQ z5*);=yv{dc!|f58x4{5f{W$XR9TyZn>F&E++ zKB5$Z;q);*%DFI{{yshy!o{ajAOeQdr{YnWg5mV`X(L!kvriisPRa`%Z0q48J_?=n zf%s!`9EOr|*o(=Xgf9Cy^jhH#nx7ZnBF_`3<)kq7i#r-66(b~w<+Qks|Cmd;~46xeZF$zT5zi$&pbR6O1gvo?25WY$H z3E@hPk9rdhC8T_h9pdz3rwOleY>RJ2xZ2)BIE(Oe!lN90n-hX2;pHo>O90Y+`r_LR z`ALM62xk+%OSqWuE5dIHe!8b8zpoG&-_Rzg<6(a6RPYxv zu)bWVPZ89)P@hwQ>hpp66hZ9+^*I%IePTpFW<&6g0z~;&a{c^)d5c^>>_-Gl7KM1p zE93E8MchY@>oPNhOc8$7FuD8(Wt@GYSdQ9t>IuqEp6YPwsmV_p>j(!&B^-RRa53x_ zdt)`aMw-lPx;_mhowC)nj7A0l_%O0ceF@AZK8%)9?gM=|Um>Ih6k4Suqika?9hWXS zCeIIG79QU7%k2Y&2rrO8`gwp(Q$f=1dZlaEo#$2MiYGh1oaK(%DMb_R2F#$0I$23gbRV!(48HXerC%<~>I+0W$3} zQYPr2R1m5Vc@=n2v;4sBJUDyF+k#f}c+9ldh&Ap5$L{(MUxK@%>b3I3)gvFZvTo#s z!Dd@=JIiKUNk~rI10KH1#1E|db*;q3L_mLC+bk3Z6AAb7xJoo18%5P|+H~b@cWZu6 z|Ape)M8ZDcu?rN3+f?P35&^^b>Qw*wjXJFqw^>+XHgv5h&_b6spYlUd>96m4QESdP zRdvn~$%5{zA@Yzb;22P7rK>tsfh~Dj2<=~m940#7P8nda=Qy9eL^zjl0pVK0?Suyi z{~#>l7)1MeTFR%Td|JwYdJ30o5G;ut)ha5~`|gk(j6R}j6KknS`12+`*W zD>#PKCwzdg9bqV8Z^99T;|Zq|zCrjg;R?dd9EFOOP4G)oO{dx z=K~H);$cj2A-E+FFIkYCmG!B7R1&865XF>wf=jk29iN0hf<#f(p)V!p<60QJIyUy%7Ge4Wd$eS-d@K9QKmvfT;cRm-d}T z`CKLcJ3h4tjfv175tV`For|KH?2f(aSRbKlIk7DcWjrP}}F&X)a|FZNCvU4zgp+fNa|70PxdDC@v@hHD{=F4Qgp zWeLtYa>_a|F4x`^Cdhs;I~OzM-$u8}#qtxVDY<3p1y0%V-fy&yDD?v4H@g0s>Z@z* zgKXV)f3g)Qz*W~=hEV8}v#)QQ2Rr351l9?c;TCS(bPvI2PnY~>Sv0fhK2S~ol+z&E zozB-NH*Q=7jWnOu!1pb1E-j-6{B0A`&Z7L?3>lSkaO=hmd|zTup2`Rf!5#{1E>NsI z$CwA-%0M=>|Bn#bm$qkQgnrPLOby?{a1|=rPB)4LG%ttlY1E>EELR_z9duMq_7KcB zH4dM)*a?3f2Dd_9f(_}+)W6Qz4q0ZMwH+kNjMhikfBj!FNe@Rh_TzMQglb1zjl%sEUGeR=$mulK*co7zYag8ip~^{4g-F4 z&r$jpep=Uc`>RX0zIAlcFVnk4M+1LoRQknpmw&$N!R@nC4yT=ab<6zNsHmY5|M18) z3k2Xl`Ey#q`#I}+O8g;_>HGZNUm!eSm^IM&(w;@1Z0;EuImFu=`TieZ{#pIMe`V+L z_qTBV;D|Kh&+MN!bN7-Dw)BXI80_smSeJTu*y_cQf99|Qla7A0Xnm|sC-GBG9X`MR zUf?JFnsV;Vb&I=6{6XP8$3OSNp3ilLS^u}|r4y6)&5Q^SALQ*lNE3c})K7*VU;g&f zrF)(|GDa0%3EY92u9w484qu#^HsGC zm`V3|FIAW+yl<>JtRlg?ZTH?yL(6-2Z|mpZy=g*u_x66QUE8MKrnY|U&U3r--0tYx zt<$#*)ng*OW>Uazt|8mmQ2D+=UR`?0;BFNLXJ0b%ixck)u3`H*;;&^8)SNe*UN-X| z<@9{%yr&?{*KrzuEoC4+{4>r^T1H4aN@E*wdTcl$4mMoRwC6aD^3OWQ=~sT^_y)Q8 zbGLK)gV7v6#upx39|sWjAbg5&3P<=xv9>L{J5$t#8_ZfmS9Yx*6*K$!HuE<7`nP7K zS_6|_n@t|C#MfpkR)!pSdp3(T!P~PHD@WRU_}ww);odSe0;mxdYWUsV6lw4xHTUS% zP{|smj;`oFbLse)`qAcLmSVsy>$kPy)XL$po=W~ubyP{e?PgKD_<3rKdz5*orNj_* zq^YmLtP(3s=awf%xkj3YR87-}6_uA(B}J)~X@k`fralI7kA=54Mz|6kcj{ zWR*0XDUP>xcG;7Wt~#4?x)Ml(KCXE#%3YZBZ@OiP!w;g zqBQ1;(%PaZO(jKXEh`G2ZF-`*$UK;DR!H+r4^*4hOw!d@&{e%ztH(|{{*k(1(N%pl zT?LMft^%#lRiv!C%F|@Ulqg$WMOJlPC2locMZSGq#S}-nsz|G*tC(hAR~0GMbd{&s z*HuchqpO%=PgjXwMOTqqMOTq!)m5adx(bwyt^#eNt9p2>3SD*l_$A@6tE+mux~g}q zt9r+}3P}!hRqt3=A<3bx>K*ASB~;hdYYjoA7RC>C6#;bBZeCkm#Q~RmLFuK2x(Xc0 zCB&B+>Lt*!XK2w?Xl=u0oOnT@@VbDkM47Rl$+2QbKiI zz19#^YGM3PR}ny0?dG-BRUB|zT?LMft^!?6SCLm$SMSPfr_~uU8W@^DuUe= z3mm--M*xnX-sXd!TC!`F_X=WwCqKLB1)Q z6#3-!W;vhv(gYPd7!{*03Sn%vKkYei6({jXaK(u%cQ@M6cQTMCQ6=)peT#k!S6 z54KoPCqj>}sbs{^|(j!o+SG8oj|f%WAk>YH!SyxnV4N z?Lp|!DHxfI)ty*UQO9?_W9+*Htm9jmygloB`HTn;bl}zOx8d6_UM^f63-7rrD;Nv= z@@&B`&#>W_E(FJJDJb|lnhI*d%W*-SiZx)Spi?97B3}_^Ddo7X{*ulER{$C?^Z)I+ zTTXNBS|P2jOG))uW!Qy-edI)1X7|Lk0dhp8b#`1EAX-{($F%{;cb4Wb;K{c2liX~a zsu<{}W>2{09M;<8SZBBmT}*eaA()j0_ne%yakJLYiJ77Y8&o?@JG2HZtB6kB^^HNb z9ZCeTqToThwr<^KFl<99{4QI=%)h_t%1k}Q5HD8IW81bZU#quDYanTbom9+}SQ5@t zA7K^RN!xQbYqm(K+CL|$*wM9X*z98`4(S;b?c%R% zeYJVxM)fAsMl4#=v!WN9b(k$(qhe=%e_m1a=v2lEQW-n{)YP-^4A#zfW0f%*H?3E1 zsN9&RHGsf=^C3Gpi*1?@CHK9mEIE;}d~it4je5CEb=q9G3L1@EyydH}RO>4bd?*!7 zIX&n~7+Z{uo=AtHeZ*kK{+P;+T8cjVdwpiozPwfSdHENcw>*cX4hNmt1>!oos27`1 z908>|R}5iKR4Pk_T})wJR*iIHmjegvShIS~n%qs3wFV6{y(bP;vEOGMJ^s^Z99lN# z#CHpSn5JltjohD~zaR*1GN9|L+tjNn4}6SIx6&_W(bwt~rXP^F65!kOlO5|;En9|C zaJ9W}%$S^Q-)=?mDDn3Vh>Gp?Bub6gLFHT7uD#X(lGMJPudmh+#!P`0sg>nSDajI# zJiF@F7o9sh)>mR%cJmxtU(9zs)|c|t-uhxer>w7Bu-4WW3)b5DN`-1|eR(NotS^>y z%KAzL9a&#YbYy*{G)LAK6RTQZ;MrJTplz%#U+uA2Uz_ENn%6dJ&QX0uM7F+qtM%0@ ztgl{aef2ih7y9nd`a;?@tS_WGYkgsWoVC7C)LHARci#G9(b`*IDCMm6)jMl_;ptkf zFAxgr3sPXUz91eJ>kCpNTVK6wef5&{wGrLwV%HU3`jW3+-TETij`fw;*I|7z-}P8u z%2#{qiv^vszH-4@TVE_#YwIf&skFlvwZ4M0))$_x)%pUVu)ZJ#R_hDmVX?j-HL~>;Wa}$P z*4IXKtBYM%mLbf8>Ao1}&P@9A2OF`9{A4!raSvDoJKc;D%u81{miQ}P_F$Lwr>%aG zhW#4!+1X!bi*ejRVsnd&jBL&yXMag#vJ;h`Jg2m9shVAi+;QgMJExh|GXj4&{cI%5 z7URfAV#}AXC$4m7?;cZ>X2zGn7hk#~sI&1jv-(DAR>_v-l=p~(AJ0A;o3}}D4YvhzFovPbd*jPuEN5d7yW$jH&eC3g~_m|Y+Kl6r3n*w4vtxZb zfRI6S(}tbn0({+=X~vZ|!Ka*lqUhiVFLrfv?xv0Fb5}+Aw%4%AWt$GX>B`Cm{{3Xf z*=Mwmu&bLkZdm`-IzxBgc91%f?SfSE$LqkOJv9zeH$&p*tH15l4yrXvX`AOk+OF3h zVOPHYaqF7ZD-BPz_YH(HQsU0nA#v+GNPKS654kH>?TGfZCgx(v-LFC7XaDz#amCv8 zvDU=0559tBs6UgS$z|`poVPspoe*E%q>5cl9mM)yP7IoJG6<5VPd08?y8(*&YnV$} zv8l3b1v~U6W0Q{;?;QkpyW`--wOiu?e0>A7tYq!yxf|kwA#W#Ey5p;NVu9T*7}hFc zLR7xyc7ef6<16}iWEgfpXfF*PdQl7M1H=$DGw1#d3*LYIt0glZp6tpl<;*;G_$aa0L$JFg>fG30 zM`7Ox_AmQvJp9wC@7eRes#t}X5GCp$LrBqEYF0R5E_6!Dvp+kC%~R%in4OtXHm7IU z^x`RUo>Tu~y?^;hQO_gnZ0doMzm2ePOVvz0b2K;cuQk@DGSn}zj5|YgXNYdR6uI1s zhpXFo-vcWoaQe&p@q3sF4t=$Wy@#2-hx^1JjNmZbryEkke4-^#N1G454W}oHrZ`vN`WaXi`k-umzSH2SoRYsbB&elYpd+kDG@P+a635fjU zkjnn7#|}KLgeA;}=EC6#DR=&xn#c?3<&kXPO!?SDesa*Ci~55%j0Y$&YlOLIYW)cF zo@`A->C5>Mu;PG+9j?G(2k1%P6=zSqj6E5w)>R&QOLrK0^0uyMN`0N_lao!5^m%mIc#%}hxpL$Du^-+bsTI2}a8}AI2P~G0#kre4+-jS3$l5)qUIA>FC*RWrL++@cJrw zTep3gP8V@#aYLO;Uk_b5Pk}$9+rCVtGd*2CvW0~gZZ<4~SJI|EEyBwONfkV5iF7R= z$T^lsSb2iP@d+)1lW@|wC6wWxoZ|@M={IRuYatD_JUZ*-dE=T7>(-n@mKwcPYV;PV z(aTaJMNn!W!lecxTtf8P>QV!|YEpwd2U4Tg)*v-d*0Iz;q64YXYaK}ql&K~)z^f`X zaFbkWAi{-38R1d`qeW5!F_IdHk<`>m5Ndf~*vX^Tl1yClh;q$2WT_FXQX^QTMv$dO zilEd$gi8%XxYP)Cqy~7^qy~8oq(-PoYM`uRsewcXQX@E&8YojuYJgW&YTzch)Ifww zjWWWe21bjd24W;N5F@F%enD_}p7FmW0sk^xr?q!x=)a1Gf7FY&S>s;)?VSbx9#W^x zs1K`W@Khy)+m`SK@@w&;Hr$qfE9GZs|KI;NN`89jCa>QXI{7~Qj~4S2+H2uc92+1E zp-*y*0BlEbf57HEZuxpAe($S(RO0CI<5DM1N*RNxb?NIXytmGOeTBZPB3F~uSB}A# zRDOhT7tr+8m8=;&Z4v!{1dE;^`WZWAfRzp9@yb3)Sj=$&eeq>MGoo7&GQudr1j12- zsf05K=MgR>{DSZs!XF7w5*Bk@s3L4e*ou%5MiC|ujv`DYoIyB`a3SFrgx?VUNO+R4 znByW9VKc&3gp4qXBYfP`_H%#YO{X>14|Qf}GH-r+RqyXk`^t}{idDWRfAeXV!SJ5} zSps~9O2985y#@iVbO_y~m>_fkB)PTNiN*txl9NWK>nEmo^+`;VJ{Cx(!(uWspcwvt zL>e8h#{=UK(8_vJ96+jCE{k6+l;d8CIDW)QE(?MrkSX(lXafk!>uw=- zMR=L8l(3TH5)Q%=ykEGM;1DCym@L7eLK?>e@m@k4X1JDkNgyooCVY(0k1&7`#tyF~ vA%tOs5ro|cdl1GE#uEF#F!3_ literal 0 HcmV?d00001 diff --git a/plugins/samplesink/CMakeLists.txt b/plugins/samplesink/CMakeLists.txt index c8180d5eb..c86461bcf 100644 --- a/plugins/samplesink/CMakeLists.txt +++ b/plugins/samplesink/CMakeLists.txt @@ -3,6 +3,7 @@ project(samplesink) add_subdirectory(testsink) add_subdirectory(fileoutput) add_subdirectory(localoutput) +add_subdirectory(audiooutput) if(CM256CC_FOUND) add_subdirectory(remoteoutput) diff --git a/plugins/samplesink/audiooutput/CMakeLists.txt b/plugins/samplesink/audiooutput/CMakeLists.txt new file mode 100644 index 000000000..72c7d8924 --- /dev/null +++ b/plugins/samplesink/audiooutput/CMakeLists.txt @@ -0,0 +1,57 @@ +project(audiooutput) + +set(audiooutput_SOURCES + audiooutput.cpp + audiooutputplugin.cpp + audiooutputsettings.cpp + # audiooutputwebapiadapter.cpp + audiooutputworker.cpp +) + +set(audiooutput_HEADERS + audiooutput.h + audiooutputplugin.h + audiooutputsettings.h + # audiooutputwebapiadapter.h + audiooutputworker.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(audiooutput_SOURCES + ${audiooutput_SOURCES} + audiooutputgui.cpp + audiooutputgui.ui + ) + set(audiooutput_HEADERS + ${audiooutput_HEADERS} + audiooutputgui.h + ) + + set(TARGET_NAME outputaudio) + set(TARGET_LIB "Qt5::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME outputaudiosrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${audiooutput_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} + swagger +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) diff --git a/plugins/samplesink/audiooutput/audiooutput.cpp b/plugins/samplesink/audiooutput/audiooutput.cpp new file mode 100644 index 000000000..8a7be74ac --- /dev/null +++ b/plugins/samplesink/audiooutput/audiooutput.cpp @@ -0,0 +1,444 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include + +#include "SWGDeviceSettings.h" +#include "SWGDeviceState.h" + +#include "device/deviceapi.h" +#include "audio/audiodevicemanager.h" +#include "dsp/dspcommands.h" +#include "dsp/dspengine.h" + +#include "audiooutputworker.h" +#include "audiooutput.h" + +MESSAGE_CLASS_DEFINITION(AudioOutput::MsgConfigureAudioOutput, Message) +MESSAGE_CLASS_DEFINITION(AudioOutput::MsgStartStop, Message) + +AudioOutput::AudioOutput(DeviceAPI *deviceAPI) : + m_deviceAPI(deviceAPI), + m_audioFifo(48000), + m_settings(), + m_audioDeviceIndex(-1), + m_centerFrequency(0), + m_worker(nullptr), + m_deviceDescription("AudioOutput") +{ + m_deviceAPI->setNbSinkStreams(1); + AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); + m_sampleRate = audioDeviceManager->getOutputSampleRate(m_audioDeviceIndex); + m_settings.m_deviceName = AudioDeviceManager::m_defaultDeviceName; + m_sampleSourceFifo.resize(SampleSourceFifo::getSizePolicy(48000)); +} + +AudioOutput::~AudioOutput() +{ + stop(); +} + +void AudioOutput::destroy() +{ + delete this; +} + +void AudioOutput::init() +{ + applySettings(m_settings, true); +} + +bool AudioOutput::start() +{ + QMutexLocker mutexLocker(&m_mutex); + qDebug("AudioOutput::start"); + + AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); + audioDeviceManager->addAudioSink(&m_audioFifo, getInputMessageQueue(), m_audioDeviceIndex); + + m_worker = new AudioOutputWorker(&m_sampleSourceFifo, &m_audioFifo); + m_worker->moveToThread(&m_workerThread); + m_worker->setSamplerate(m_sampleRate); + m_worker->setIQMapping(m_settings.m_iqMapping); + m_worker->connectTimer(m_deviceAPI->getMasterTimer()); + m_worker->startWork(); + m_workerThread.start(); + + mutexLocker.unlock(); + + qDebug("AudioOutput::start: started"); + + return true; +} + +void AudioOutput::stop() +{ + qDebug("AudioOutput::stop"); + if (m_worker) + { + m_worker->stopWork(); + m_workerThread.quit(); + m_workerThread.wait(); + delete m_worker; + m_worker = nullptr; + } + + AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); + audioDeviceManager->removeAudioSink(&m_audioFifo); + + m_running = false; + qDebug("AudioOutput::stop: stopped"); +} + +QByteArray AudioOutput::serialize() const +{ + return m_settings.serialize(); +} + +bool AudioOutput::deserialize(const QByteArray& data) +{ + bool success = true; + + if (!m_settings.deserialize(data)) + { + m_settings.resetToDefaults(); + success = false; + } + + MsgConfigureAudioOutput* message = MsgConfigureAudioOutput::create(m_settings, true); + m_inputMessageQueue.push(message); + + if (m_guiMessageQueue) + { + MsgConfigureAudioOutput* messageToGUI = MsgConfigureAudioOutput::create(m_settings, true); + m_guiMessageQueue->push(messageToGUI); + } + + return success; +} + +const QString& AudioOutput::getDeviceDescription() const +{ + return m_deviceDescription; +} + +int AudioOutput::getSampleRate() const +{ + return m_sampleRate; +} + +quint64 AudioOutput::getCenterFrequency() const +{ + return m_centerFrequency; +} + +bool AudioOutput::handleMessage(const Message& message) +{ + if(MsgConfigureAudioOutput::match(message)) + { + qDebug() << "AudioOutput::handleMessage: MsgConfigureAudioOutput"; + MsgConfigureAudioOutput& conf = (MsgConfigureAudioOutput&) message; + applySettings(conf.getSettings(), conf.getForce()); + return true; + } + else if (MsgStartStop::match(message)) + { + MsgStartStop& cmd = (MsgStartStop&) message; + qDebug() << "AudioOutput::handleMessage: MsgStartStop: " << (cmd.getStartStop() ? "start" : "stop"); + + if (cmd.getStartStop()) + { + if (m_deviceAPI->initDeviceEngine()) { + m_deviceAPI->startDeviceEngine(); + } + } + else + { + m_deviceAPI->stopDeviceEngine(); + } + + if (m_settings.m_useReverseAPI) { + webapiReverseSendStartStop(cmd.getStartStop()); + } + + return true; + } + else + { + return false; + } +} + +void AudioOutput::applySettings(const AudioOutputSettings& settings, bool force) +{ + bool forwardChange = false; + QList reverseAPIKeys; + + if ((m_settings.m_deviceName != settings.m_deviceName) || force) + { + reverseAPIKeys.append("deviceName"); + AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); + m_audioDeviceIndex = audioDeviceManager->getOutputDeviceIndex(settings.m_deviceName); + //qDebug("AMDemod::applySettings: audioDeviceName: %s audioDeviceIndex: %d", qPrintable(settings.m_audioDeviceName), audioDeviceIndex); + audioDeviceManager->removeAudioSink(&m_audioFifo); + audioDeviceManager->addAudioSink(&m_audioFifo, getInputMessageQueue(), m_audioDeviceIndex); + m_sampleRate = audioDeviceManager->getOutputSampleRate(m_audioDeviceIndex); + forwardChange = true; + } + + if ((m_settings.m_volume != settings.m_volume) || force) + { + reverseAPIKeys.append("volume"); + m_audioOutputDevice.setVolume(settings.m_volume); + qDebug() << "AudioOutput::applySettings: set volume to " << settings.m_volume; + } + + if ((m_settings.m_iqMapping != settings.m_iqMapping) || force) + { + reverseAPIKeys.append("iqMapping"); + forwardChange = true; + + if (m_worker) { + m_worker->setIQMapping(settings.m_iqMapping); + } + } + + if (settings.m_useReverseAPI) + { + bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || + (m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) || + (m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) || + (m_settings.m_reverseAPIDeviceIndex != settings.m_reverseAPIDeviceIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + m_settings = settings; + + if (forwardChange) + { + if (m_worker) { + m_worker->setSamplerate(m_sampleRate); + } + + DSPSignalNotification *notif = new DSPSignalNotification(m_sampleRate, 0); + m_centerFrequency = 0; + m_deviceAPI->getDeviceEngineInputMessageQueue()->push(notif); + } +} + +int AudioOutput::webapiRunGet( + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage) +{ + (void) errorMessage; + m_deviceAPI->getDeviceEngineStateStr(*response.getState()); + return 200; +} + +int AudioOutput::webapiRun( + bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage) +{ + (void) errorMessage; + m_deviceAPI->getDeviceEngineStateStr(*response.getState()); + MsgStartStop *message = MsgStartStop::create(run); + m_inputMessageQueue.push(message); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgStartStop *msgToGUI = MsgStartStop::create(run); + m_guiMessageQueue->push(msgToGUI); + } + + return 200; +} + +int AudioOutput::webapiSettingsGet( + SWGSDRangel::SWGDeviceSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAudioOutputSettings(new SWGSDRangel::SWGAudioOutputSettings()); + response.getAudioOutputSettings()->init(); + webapiFormatDeviceSettings(response, m_settings); + + return 200; +} + +int AudioOutput::webapiSettingsPutPatch( + bool force, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response, // query + response + QString& errorMessage) +{ + (void) errorMessage; + AudioOutputSettings settings = m_settings; + webapiUpdateDeviceSettings(settings, deviceSettingsKeys, response); + + MsgConfigureAudioOutput *msg = MsgConfigureAudioOutput::create(settings, force); + m_inputMessageQueue.push(msg); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureAudioOutput *msgToGUI = MsgConfigureAudioOutput::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatDeviceSettings(response, settings); + return 200; +} + +void AudioOutput::webapiUpdateDeviceSettings( + AudioOutputSettings& settings, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response) +{ + if (deviceSettingsKeys.contains("deviceName")) { + settings.m_deviceName = *response.getAudioOutputSettings()->getDeviceName(); + } + if (deviceSettingsKeys.contains("volume")) { + settings.m_volume = response.getAudioOutputSettings()->getVolume(); + } + if (deviceSettingsKeys.contains("iqMapping")) { + settings.m_iqMapping = (AudioOutputSettings::IQMapping) response.getAudioOutputSettings()->getIqMapping(); + } + if (deviceSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getAudioOutputSettings()->getUseReverseApi() != 0; + } + if (deviceSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getAudioOutputSettings()->getReverseApiAddress(); + } + if (deviceSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getAudioOutputSettings()->getReverseApiPort(); + } + if (deviceSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getAudioOutputSettings()->getReverseApiDeviceIndex(); + } +} + +void AudioOutput::webapiFormatDeviceSettings(SWGSDRangel::SWGDeviceSettings& response, const AudioOutputSettings& settings) +{ + response.getAudioOutputSettings()->setDeviceName(new QString(settings.m_deviceName)); + response.getAudioOutputSettings()->setVolume(settings.m_volume); + response.getAudioOutputSettings()->setIqMapping((int) settings.m_iqMapping); + + response.getAudioOutputSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getAudioOutputSettings()->getReverseApiAddress()) { + *response.getAudioOutputSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getAudioOutputSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getAudioOutputSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getAudioOutputSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); +} + +void AudioOutput::webapiReverseSendSettings(QList& deviceSettingsKeys, const AudioOutputSettings& settings, bool force) +{ + SWGSDRangel::SWGDeviceSettings *swgDeviceSettings = new SWGSDRangel::SWGDeviceSettings(); + swgDeviceSettings->setDirection(1); // single Tx + swgDeviceSettings->setOriginatorIndex(m_deviceAPI->getDeviceSetIndex()); + swgDeviceSettings->setDeviceHwType(new QString("AudioOutput")); + swgDeviceSettings->setAudioOutputSettings(new SWGSDRangel::SWGAudioOutputSettings()); + SWGSDRangel::SWGAudioOutputSettings *swgAudioOutputSettings = swgDeviceSettings->getAudioOutputSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (deviceSettingsKeys.contains("deviceName") || force) { + swgAudioOutputSettings->setDeviceName(new QString(settings.m_deviceName)); + } + if (deviceSettingsKeys.contains("volume") || force) { + swgAudioOutputSettings->setVolume(settings.m_volume); + } + if (deviceSettingsKeys.contains("iqMapping") || force) { + swgAudioOutputSettings->setIqMapping(settings.m_iqMapping); + } + + QString deviceSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/device/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIDeviceIndex); + m_networkRequest.setUrl(QUrl(deviceSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgDeviceSettings->asJson().toUtf8()); + buffer->seek(0); + + // Always use PATCH to avoid passing reverse API settings + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); + + delete swgDeviceSettings; +} + +void AudioOutput::webapiReverseSendStartStop(bool start) +{ + SWGSDRangel::SWGDeviceSettings *swgDeviceSettings = new SWGSDRangel::SWGDeviceSettings(); + swgDeviceSettings->setDirection(1); // single Tx + swgDeviceSettings->setOriginatorIndex(m_deviceAPI->getDeviceSetIndex()); + swgDeviceSettings->setDeviceHwType(new QString("AudioOutput")); + + QString deviceSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/device/run") + .arg(m_settings.m_reverseAPIAddress) + .arg(m_settings.m_reverseAPIPort) + .arg(m_settings.m_reverseAPIDeviceIndex); + m_networkRequest.setUrl(QUrl(deviceSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgDeviceSettings->asJson().toUtf8()); + buffer->seek(0); + QNetworkReply *reply; + + if (start) { + reply = m_networkManager->sendCustomRequest(m_networkRequest, "POST", buffer); + } else { + reply = m_networkManager->sendCustomRequest(m_networkRequest, "DELETE", buffer); + } + + buffer->setParent(reply); + delete swgDeviceSettings; +} + +void AudioOutput::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "AudioOutput::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("AudioOutput::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + diff --git a/plugins/samplesink/audiooutput/audiooutput.h b/plugins/samplesink/audiooutput/audiooutput.h new file mode 100644 index 000000000..01b2ff309 --- /dev/null +++ b/plugins/samplesink/audiooutput/audiooutput.h @@ -0,0 +1,153 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef _AUDIOOUTPUT_AUDIOOUTPUT_H_ +#define _AUDIOOUTPUT_AUDIOOUTPUT_H_ + +#include +#include + +#include "dsp/devicesamplesink.h" +#include "audio/audiooutputdevice.h" +#include "audio/audiofifo.h" + +#include "audiooutputsettings.h" + +class QNetworkAccessManager; +class QNetworkReply; +class AudioOutputWorker; +class DeviceAPI; + +class AudioOutput : public DeviceSampleSink { +public: + class MsgConfigureAudioOutput : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const AudioOutputSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureAudioOutput* create(const AudioOutputSettings& settings, bool force) + { + return new MsgConfigureAudioOutput(settings, force); + } + + private: + AudioOutputSettings m_settings; + bool m_force; + + MsgConfigureAudioOutput(const AudioOutputSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgStartStop : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool getStartStop() const { return m_startStop; } + + static MsgStartStop* create(bool startStop) { + return new MsgStartStop(startStop); + } + + protected: + bool m_startStop; + + MsgStartStop(bool startStop) : + Message(), + m_startStop(startStop) + { } + }; + + AudioOutput(DeviceAPI *deviceAPI); + virtual ~AudioOutput(); + virtual void destroy(); + + virtual void init(); + virtual bool start(); + virtual void stop(); + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual void setMessageQueueToGUI(MessageQueue *queue) { m_guiMessageQueue = queue; } + virtual const QString& getDeviceDescription() const; + virtual int getSampleRate() const; + virtual void setSampleRate(int sampleRate) { (void) sampleRate; } + virtual quint64 getCenterFrequency() const; + virtual void setCenterFrequency(qint64 centerFrequency) { (void) centerFrequency; } + const QString& getDeviceName() const { return m_settings.m_deviceName; } + virtual bool handleMessage(const Message& message); + + virtual int webapiSettingsGet( + SWGSDRangel::SWGDeviceSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response, // query + response + QString& errorMessage); + + virtual int webapiRunGet( + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage); + + virtual int webapiRun( + bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage); + + static void webapiFormatDeviceSettings( + SWGSDRangel::SWGDeviceSettings& response, + const AudioOutputSettings& settings); + + static void webapiUpdateDeviceSettings( + AudioOutputSettings& settings, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response); + +private: + DeviceAPI *m_deviceAPI; + AudioOutputDevice m_audioOutputDevice; + AudioFifo m_audioFifo; + QMutex m_mutex; + AudioOutputSettings m_settings; + int m_audioDeviceIndex; + int m_sampleRate; + qint64 m_centerFrequency; + AudioOutputWorker* m_worker; + QThread m_workerThread; + QString m_deviceDescription; + bool m_running; + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void applySettings(const AudioOutputSettings& settings, bool force); + + void webapiReverseSendSettings(QList& deviceSettingsKeys, const AudioOutputSettings& settings, bool force); + void webapiReverseSendStartStop(bool start); + +private slots: + void networkManagerFinished(QNetworkReply *reply); +}; + + +#endif diff --git a/plugins/samplesink/audiooutput/audiooutputgui.cpp b/plugins/samplesink/audiooutput/audiooutputgui.cpp new file mode 100644 index 000000000..be4df2bb1 --- /dev/null +++ b/plugins/samplesink/audiooutput/audiooutputgui.cpp @@ -0,0 +1,239 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "ui_audiooutputgui.h" +#include "gui/colormapper.h" +#include "gui/glspectrum.h" +#include "gui/crightclickenabler.h" +#include "gui/basicdevicesettingsdialog.h" +#include "gui/audioselectdialog.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "audiooutputgui.h" + +#include "device/deviceapi.h" +#include "device/deviceuiset.h" + +AudioOutputGui::AudioOutputGui(DeviceUISet *deviceUISet, QWidget* parent) : + DeviceGUI(parent), + ui(new Ui::AudioOutputGui), + m_deviceUISet(deviceUISet), + m_doApplySettings(true), + m_forceSettings(true), + m_settings(), + m_centerFrequency(0) +{ + m_audioOutput = (AudioOutput*) m_deviceUISet->m_deviceAPI->getSampleSink(); + + ui->setupUi(this); + + connect(&m_updateTimer, SIGNAL(timeout()), this, SLOT(updateHardware())); + + CRightClickEnabler *startStopRightClickEnabler = new CRightClickEnabler(ui->startStop); + connect(startStopRightClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(openDeviceSettingsDialog(const QPoint &))); + + m_sampleRate = m_audioOutput->getSampleRate(); + m_centerFrequency = m_audioOutput->getCenterFrequency(); + m_settings.m_deviceName = m_audioOutput->getDeviceName(); + updateSampleRateAndFrequency(); + displaySettings(); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()), Qt::QueuedConnection); + m_audioOutput->setMessageQueueToGUI(&m_inputMessageQueue); +} + +AudioOutputGui::~AudioOutputGui() +{ + delete ui; +} + +void AudioOutputGui::destroy() +{ + delete this; +} + +void AudioOutputGui::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + sendSettings(); +} + +QByteArray AudioOutputGui::serialize() const +{ + return m_settings.serialize(); +} + +bool AudioOutputGui::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) + { + displaySettings(); + m_forceSettings = true; + sendSettings(); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool AudioOutputGui::handleMessage(const Message& message) +{ + if (AudioOutput::MsgConfigureAudioOutput::match(message)) + { + const AudioOutput::MsgConfigureAudioOutput& cfg = (AudioOutput::MsgConfigureAudioOutput&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + return true; + } + else if (AudioOutput::MsgStartStop::match(message)) + { + AudioOutput::MsgStartStop& notif = (AudioOutput::MsgStartStop&) message; + blockApplySettings(true); + ui->startStop->setChecked(notif.getStartStop()); + blockApplySettings(false); + return true; + } + else + { + return false; + } +} + +void AudioOutputGui::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != 0) + { + qDebug("AudioOutputGui::handleInputMessages: message: %s", message->getIdentifier()); + + if (DSPSignalNotification::match(*message)) + { + DSPSignalNotification* notif = (DSPSignalNotification*) message; + m_sampleRate = notif->getSampleRate(); + m_centerFrequency = notif->getCenterFrequency(); + qDebug("AudioOutputGui::handleInputMessages: DSPSignalNotification: SampleRate: %d", notif->getSampleRate()); + updateSampleRateAndFrequency(); + + delete message; + } + else + { + if (handleMessage(*message)) { + delete message; + } + } + } +} + +void AudioOutputGui::updateSampleRateAndFrequency() +{ + m_deviceUISet->getSpectrum()->setSampleRate(m_sampleRate); + m_deviceUISet->getSpectrum()->setCenterFrequency(m_centerFrequency); + m_deviceUISet->getSpectrum()->setSsbSpectrum(false); + m_deviceUISet->getSpectrum()->setLsbDisplay(false); + ui->deviceRateText->setText(tr("%1k").arg((float)m_sampleRate / 1000)); +} + +void AudioOutputGui::displaySettings() +{ + ui->deviceLabel->setText(m_settings.m_deviceName); + ui->volume->setValue((int)(m_settings.m_volume*10.0f)); + ui->volumeText->setText(QString("%1").arg(m_settings.m_volume, 3, 'f', 1)); + ui->channels->setCurrentIndex((int)m_settings.m_iqMapping); +} + +void AudioOutputGui::on_deviceSelect_clicked() +{ + AudioSelectDialog audioSelect(DSPEngine::instance()->getAudioDeviceManager(), m_settings.m_deviceName, false, this); + audioSelect.exec(); + + if (audioSelect.m_selected) + { + m_settings.m_deviceName = audioSelect.m_audioDeviceName; + ui->deviceLabel->setText(m_settings.m_deviceName); + sendSettings(); + } +} + +void AudioOutputGui::on_volume_valueChanged(int value) +{ + m_settings.m_volume = value/10.0f; + ui->volumeText->setText(QString("%1").arg(m_settings.m_volume, 3, 'f', 1)); + sendSettings(); +} + +void AudioOutputGui::on_channels_currentIndexChanged(int index) +{ + m_settings.m_iqMapping = (AudioOutputSettings::IQMapping) index; + updateSampleRateAndFrequency(); + sendSettings(); +} + +void AudioOutputGui::on_startStop_toggled(bool checked) +{ + if (m_doApplySettings) + { + AudioOutput::MsgStartStop *message = AudioOutput::MsgStartStop::create(checked); + m_audioOutput->getInputMessageQueue()->push(message); + } +} + +void AudioOutputGui::sendSettings() +{ + if(!m_updateTimer.isActive()) + m_updateTimer.start(100); +} + +void AudioOutputGui::updateHardware() +{ + if (m_doApplySettings) + { + AudioOutput::MsgConfigureAudioOutput* message = AudioOutput::MsgConfigureAudioOutput::create(m_settings, m_forceSettings); + m_audioOutput->getInputMessageQueue()->push(message); + m_forceSettings = false; + m_updateTimer.stop(); + } +} + +void AudioOutputGui::openDeviceSettingsDialog(const QPoint& p) +{ + BasicDeviceSettingsDialog dialog(this); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex); + + dialog.move(p); + dialog.exec(); + + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + + sendSettings(); +} diff --git a/plugins/samplesink/audiooutput/audiooutputgui.h b/plugins/samplesink/audiooutput/audiooutputgui.h new file mode 100644 index 000000000..84e3e92a9 --- /dev/null +++ b/plugins/samplesink/audiooutput/audiooutputgui.h @@ -0,0 +1,79 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AUDIOOUTPUTGUI_H +#define INCLUDE_AUDIOOUTPUTGUI_H + +#include +#include + +#include "device/devicegui.h" +#include "util/messagequeue.h" + +#include "audiooutput.h" + +class QWidget; +class DeviceUISet; + +namespace Ui { + class AudioOutputGui; +} + +class AudioOutputGui : public DeviceGUI { + Q_OBJECT + +public: + explicit AudioOutputGui(DeviceUISet *deviceUISet, QWidget* parent = nullptr); + virtual ~AudioOutputGui(); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + +private: + Ui::AudioOutputGui* ui; + + DeviceUISet* m_deviceUISet; + AudioOutput* m_audioOutput; + bool m_doApplySettings; + bool m_forceSettings; + AudioOutputSettings m_settings; + QTimer m_updateTimer; + int m_sampleRate; + qint64 m_centerFrequency; + + MessageQueue m_inputMessageQueue; + + void blockApplySettings(bool block) { m_doApplySettings = !block; } + void displaySettings(); + void sendSettings(); + void updateSampleRateAndFrequency(); + bool handleMessage(const Message& message); + +private slots: + void handleInputMessages(); + void on_deviceSelect_clicked(); + void on_volume_valueChanged(int value); + void on_channels_currentIndexChanged(int index); + void on_startStop_toggled(bool checked); + void updateHardware(); + void openDeviceSettingsDialog(const QPoint& p); +}; + +#endif // INCLUDE_AUDIOINPUTGUI_H diff --git a/plugins/samplesink/audiooutput/audiooutputgui.ui b/plugins/samplesink/audiooutput/audiooutputgui.ui new file mode 100644 index 000000000..291a3b123 --- /dev/null +++ b/plugins/samplesink/audiooutput/audiooutputgui.ui @@ -0,0 +1,290 @@ + + + AudioOutputGui + + + + 0 + 0 + 320 + 200 + + + + + 0 + 0 + + + + + 320 + 200 + + + + + Liberation Sans + 9 + + + + Audio Output + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 4 + + + + + + + + + start/stop acquisition + + + + + + + :/play.png + :/stop.png:/play.png + + + + + + + + + + + Baseband I/Q sample rate kS/s + + + 00000k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + Qt::Horizontal + + + + + + + + + + 24 + 24 + + + + Open dialog to select output device + + + + + + + :/sound_on.png:/sound_on.png + + + + + + + + 0 + 0 + + + + Output device selected + + + ... + + + + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + + + + + + Volume + + + + + + + + 24 + 0 + + + + + 24 + 24 + + + + Audio volume. Not supported by all devices + + + 10 + + + 10 + + + + + + + 1.0 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Channels + + + + + + + + 80 + 0 + + + + How audio channels map to IQ data + + + 0 + + + + I=L, Q=R + + + + + I=R, Q=L + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+
+ + + + +
diff --git a/plugins/samplesink/audiooutput/audiooutputplugin.cpp b/plugins/samplesink/audiooutput/audiooutputplugin.cpp new file mode 100644 index 000000000..ec36a0476 --- /dev/null +++ b/plugins/samplesink/audiooutput/audiooutputplugin.cpp @@ -0,0 +1,148 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "audiooutputplugin.h" +#include "audiooutputwebapiadapter.h" + +#ifdef SERVER_MODE +#include "audiooutput.h" +#else +#include "audiooutputgui.h" +#endif + +const PluginDescriptor AudioOutputPlugin::m_pluginDescriptor = { + QString("AudioOutput"), + QString("Audio output"), + QString("6.1.0"), + QString("(c) Edouard Griffiths, F4EXB"), + QString("https://github.com/f4exb/sdrangel"), + true, + QString("https://github.com/f4exb/sdrangel") +}; + +const QString AudioOutputPlugin::m_hardwareID = "AudioOutput"; +const QString AudioOutputPlugin::m_deviceTypeID = AUDIOOUTPUT_DEVICE_TYPE_ID; + +AudioOutputPlugin::AudioOutputPlugin(QObject* parent) : + QObject(parent) +{ +} + +const PluginDescriptor& AudioOutputPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void AudioOutputPlugin::initPlugin(PluginAPI* pluginAPI) +{ + pluginAPI->registerSampleSink(m_deviceTypeID, this); +} + +void AudioOutputPlugin::enumOriginDevices(QStringList& listedHwIds, OriginDevices& originDevices) +{ + if (listedHwIds.contains(m_hardwareID)) { // check if it was done + return; + } + + originDevices.append(OriginDevice( + "AudioOutput", + m_hardwareID, + QString(), + 0, // Sequence + 0, // nb Rx + 1 // nb Tx + )); + + listedHwIds.append(m_hardwareID); +} + +PluginInterface::SamplingDevices AudioOutputPlugin::enumSampleSinks(const OriginDevices& originDevices) +{ + SamplingDevices result; + + for (OriginDevices::const_iterator it = originDevices.begin(); it != originDevices.end(); ++it) + { + if (it->hardwareId == m_hardwareID) + { + result.append(SamplingDevice( + it->displayableName, + it->hardwareId, + m_deviceTypeID, + it->serial, + it->sequence, + PluginInterface::SamplingDevice::BuiltInDevice, + PluginInterface::SamplingDevice::StreamSingleTx, + 1, + 0 + )); + } + } + + return result; +} + +#ifdef SERVER_MODE +DeviceGUI* AudioOutputPlugin::createSampleSinkPluginInstanceGUI( + const QString& sinkId, + QWidget **widget, + DeviceUISet *deviceUISet) +{ + (void) sinkId; + (void) widget; + (void) deviceUISet; + return nullptr; +} +#else +DeviceGUI* AudioOutputPlugin::createSampleSinkPluginInstanceGUI( + const QString& sinkId, + QWidget **widget, + DeviceUISet *deviceUISet) +{ + if (sinkId == m_deviceTypeID) + { + AudioOutputGui* gui = new AudioOutputGui(deviceUISet); + *widget = gui; + return gui; + } + else + { + return nullptr; + } +} +#endif + +DeviceSampleSink* AudioOutputPlugin::createSampleSinkPluginInstance(const QString& sinkId, DeviceAPI *deviceAPI) +{ + if(sinkId == m_deviceTypeID) + { + AudioOutput* output = new AudioOutput(deviceAPI); + return output; + } + else + { + return nullptr; + } +} + +DeviceWebAPIAdapter *AudioOutputPlugin::createDeviceWebAPIAdapter() const +{ + return new AudioOutputWebAPIAdapter(); +} diff --git a/plugins/samplesink/audiooutput/audiooutputplugin.h b/plugins/samplesink/audiooutput/audiooutputplugin.h new file mode 100644 index 000000000..030ec3000 --- /dev/null +++ b/plugins/samplesink/audiooutput/audiooutputplugin.h @@ -0,0 +1,56 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AUDIOOUTPUTPLUGIN_H +#define INCLUDE_AUDIOOUTPUTPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +#define AUDIOOUTPUT_DEVICE_TYPE_ID "sdrangel.samplesink.audiooutput" + +class PluginAPI; +class DeviceAPI; + +class AudioOutputPlugin : public QObject, public PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID AUDIOOUTPUT_DEVICE_TYPE_ID) + +public: + explicit AudioOutputPlugin(QObject* parent = nullptr); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual void enumOriginDevices(QStringList& listedHwIds, OriginDevices& originDevices); + virtual SamplingDevices enumSampleSinks(const OriginDevices& originDevices); + virtual DeviceGUI* createSampleSinkPluginInstanceGUI( + const QString& sinkId, + QWidget **widget, + DeviceUISet *deviceUISet); + virtual DeviceSampleSink* createSampleSinkPluginInstance(const QString& sinkId, DeviceAPI *deviceAPI); + virtual DeviceWebAPIAdapter* createDeviceWebAPIAdapter() const; + + static const QString m_hardwareID; + static const QString m_deviceTypeID; + +private: + static const PluginDescriptor m_pluginDescriptor; +}; + +#endif // INCLUDE_AUDIOOUTPUTPLUGIN_H diff --git a/plugins/samplesink/audiooutput/audiooutputsettings.cpp b/plugins/samplesink/audiooutput/audiooutputsettings.cpp new file mode 100644 index 000000000..e6d39f949 --- /dev/null +++ b/plugins/samplesink/audiooutput/audiooutputsettings.cpp @@ -0,0 +1,93 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "util/simpleserializer.h" +#include "audiooutputsettings.h" + +AudioOutputSettings::AudioOutputSettings() +{ + resetToDefaults(); +} + +void AudioOutputSettings::resetToDefaults() +{ + m_deviceName = ""; + m_volume = 1.0f; + m_iqMapping = LR; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; +} + +QByteArray AudioOutputSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeString(1, m_deviceName); + s.writeFloat(3, m_volume); + s.writeS32(5, (int)m_iqMapping); + + s.writeBool(24, m_useReverseAPI); + s.writeString(25, m_reverseAPIAddress); + s.writeU32(26, m_reverseAPIPort); + s.writeU32(27, m_reverseAPIDeviceIndex); + + return s.final(); +} + +bool AudioOutputSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + uint32_t uintval; + + d.readString(1, &m_deviceName, ""); + d.readFloat(3, &m_volume, 1.0f); + d.readS32(5, (int *)&m_iqMapping, IQMapping::LR); + + d.readBool(24, &m_useReverseAPI, false); + d.readString(25, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(26, &uintval, 0); + + if ((uintval > 1023) && (uintval < 65535)) { + m_reverseAPIPort = uintval; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(27, &uintval, 0); + m_reverseAPIDeviceIndex = uintval > 99 ? 99 : uintval; + + return true; + } + else + { + resetToDefaults(); + return false; + } +} diff --git a/plugins/samplesink/audiooutput/audiooutputsettings.h b/plugins/samplesink/audiooutput/audiooutputsettings.h new file mode 100644 index 000000000..d2f9e7634 --- /dev/null +++ b/plugins/samplesink/audiooutput/audiooutputsettings.h @@ -0,0 +1,58 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef _AUDIOOUTPUT_AUDIOOUTPUTSETTINGS_H_ +#define _AUDIOOUTPUT_AUDIOOUTPUTSETTINGS_H_ + +#include +#include + +struct AudioOutputSettings { + + QString m_deviceName; // Including realm, as from getFullDeviceName below + float m_volume; + enum IQMapping { + LR, + RL + } m_iqMapping; + + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIDeviceIndex; + + AudioOutputSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + + // Append realm to device names, because there may be multiple devices with the same name on Windows + static QString getFullDeviceName(const QAudioDeviceInfo &deviceInfo) + { +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + return deviceInfo.deviceName(); +#else + QString realm = deviceInfo.realm(); + if (realm != "" && realm != "default" && realm != "alsa") + return deviceInfo.deviceName() + " " + realm; + else + return deviceInfo.deviceName(); +#endif + } +}; + +#endif /* _AUDIOOUTPUT_AUDIOOUTPUTSETTINGS_H_ */ diff --git a/plugins/samplesink/audiooutput/audiooutputwebapiadapter.cpp b/plugins/samplesink/audiooutput/audiooutputwebapiadapter.cpp new file mode 100644 index 000000000..5bafb0148 --- /dev/null +++ b/plugins/samplesink/audiooutput/audiooutputwebapiadapter.cpp @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGDeviceSettings.h" +#include "audiooutput.h" +#include "audiooutputwebapiadapter.h" + +AudioOutputWebAPIAdapter::AudioOutputWebAPIAdapter() +{} + +AudioOutputWebAPIAdapter::~AudioOutputWebAPIAdapter() +{} + +int AudioOutputWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGDeviceSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAirspyHfSettings(new SWGSDRangel::SWGAirspyHFSettings()); + response.getAirspyHfSettings()->init(); + AudioOutput::webapiFormatDeviceSettings(response, m_settings); + return 200; +} + +int AudioOutputWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response, // query + response + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + AudioOutput::webapiUpdateDeviceSettings(m_settings, deviceSettingsKeys, response); + return 200; +} diff --git a/plugins/samplesink/audiooutput/audiooutputwebapiadapter.h b/plugins/samplesink/audiooutput/audiooutputwebapiadapter.h new file mode 100644 index 000000000..0660398ba --- /dev/null +++ b/plugins/samplesink/audiooutput/audiooutputwebapiadapter.h @@ -0,0 +1,41 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "device/devicewebapiadapter.h" +#include "audiooutputsettings.h" + +class AudioOutputWebAPIAdapter : public DeviceWebAPIAdapter +{ +public: + AudioOutputWebAPIAdapter(); + virtual ~AudioOutputWebAPIAdapter(); + virtual QByteArray serialize() { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGDeviceSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& deviceSettingsKeys, + SWGSDRangel::SWGDeviceSettings& response, // query + response + QString& errorMessage); + +private: + AudioOutputSettings m_settings; +}; diff --git a/plugins/samplesink/audiooutput/audiooutputworker.cpp b/plugins/samplesink/audiooutput/audiooutputworker.cpp new file mode 100644 index 000000000..01da2559d --- /dev/null +++ b/plugins/samplesink/audiooutput/audiooutputworker.cpp @@ -0,0 +1,157 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "audio/audiofifo.h" + +#include "audiooutputworker.h" + +#define AUDIOOUTPUT_THROTTLE_MS 50 + +AudioOutputWorker::AudioOutputWorker(SampleSourceFifo* sampleFifo, AudioFifo *fifo, QObject* parent) : + QObject(parent), + m_running(false), + m_samplerate(0), + m_throttlems(AUDIOOUTPUT_THROTTLE_MS), + m_maxThrottlems(50), + m_throttleToggle(false), + m_iqMapping(AudioOutputSettings::IQMapping::LR), + m_buf(nullptr), + m_samplesChunkSize(0), + m_sampleFifo(sampleFifo), + m_audioFifo(fifo) +{ + m_audioBuffer.resize(1<<14); + m_audioBufferFill = 0; +} + +AudioOutputWorker::~AudioOutputWorker() +{ +} + +void AudioOutputWorker::startWork() +{ + qDebug("AudioOutputWorker::startWork"); + m_running = true; +} + +void AudioOutputWorker::stopWork() +{ + qDebug("AudioOutputWorker::stopWork"); + m_running = false; +} + +void AudioOutputWorker::connectTimer(const QTimer& timer) +{ + qDebug() << "AudioOutputWorker::connectTimer"; + connect(&timer, SIGNAL(timeout()), this, SLOT(tick())); +} + +void AudioOutputWorker::setSamplerate(int samplerate) +{ + if (samplerate != m_samplerate) + { + bool wasRunning = false; + + if (m_running) + { + stopWork(); + wasRunning = true; + } + + // resize sample FIFO + if (m_sampleFifo) { + m_sampleFifo->resize(SampleSourceFifo::getSizePolicy(samplerate)); // 1s buffer + } + + qDebug() << "AudioOutputWorker::setSamplerate:" + << " new:" << samplerate + << " old:" << m_samplerate + << " m_sampleFifo size:" << m_sampleFifo->size() + << " m_audioFifo size:" << m_audioFifo->size() + << " sample i/q size" << sizeof(FixReal); + + // resize output buffer + if (m_buf) { + delete[] m_buf; + } + + m_buf = new int16_t[samplerate*2]; + + m_samplerate = samplerate; + m_samplesChunkSize = (m_samplerate * m_throttlems) / 1000; + + if (wasRunning) { + startWork(); + } + } +} + +void AudioOutputWorker::tick() +{ + if (m_running) + { + qint64 throttlems = m_elapsedTimer.restart(); + + if (throttlems != m_throttlems) + { + m_throttlems = throttlems; + m_samplesChunkSize = (m_samplerate * (m_throttlems+(m_throttleToggle ? 1 : 0))) / 1000; + m_throttleToggle = !m_throttleToggle; + } + + unsigned int iPart1Begin, iPart1End, iPart2Begin, iPart2End; + SampleVector& data = m_sampleFifo->getData(); + m_sampleFifo->read(m_samplesChunkSize, iPart1Begin, iPart1End, iPart2Begin, iPart2End); + + if (iPart1Begin != iPart1End) { + callbackPart(data, iPart1Begin, iPart1End); + } + + if (iPart2Begin != iPart2End) { + callbackPart(data, iPart2Begin, iPart2End); + } + + // qDebug("AudioOutputWorker::tick: %d samples fill: %u", m_samplesChunkSize, m_audioFifo->fill()); + } +} + +void AudioOutputWorker::callbackPart(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + for (unsigned int i = iBegin; i < iEnd; i++) + { + m_audioBuffer[m_audioBufferFill].l = m_iqMapping == AudioOutputSettings::LR ? data[i].m_real : data[i].m_imag; + m_audioBuffer[m_audioBufferFill].r = m_iqMapping == AudioOutputSettings::LR ? data[i].m_imag : data[i].m_real; + m_audioBufferFill++; + + if (m_audioBufferFill >= m_audioBuffer.size()) + { + uint res = m_audioFifo->write((const quint8*)&m_audioBuffer[0], m_audioBufferFill); + + if (res != m_audioBufferFill) + { + qDebug("AudioOutputWorker::callbackPart: %u/%u audio samples written", res, m_audioBufferFill); + m_audioFifo->clear(); + } + + m_audioBufferFill = 0; + } + } +} diff --git a/plugins/samplesink/audiooutput/audiooutputworker.h b/plugins/samplesink/audiooutput/audiooutputworker.h new file mode 100644 index 000000000..5c1ca93a3 --- /dev/null +++ b/plugins/samplesink/audiooutput/audiooutputworker.h @@ -0,0 +1,66 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AUDIOOUTPUTWORKER_H +#define INCLUDE_AUDIOOUTPUTWORKER_H + +#include +#include + +#include "dsp/interpolators.h" +#include "audiooutputsettings.h" + +class QTimer; +class SampleSourceFifo; +class AudioFifo; + +class AudioOutputWorker : public QObject { + Q_OBJECT + +public: + AudioOutputWorker(SampleSourceFifo* sampleFifo, AudioFifo *fifo, QObject* parent = nullptr); + ~AudioOutputWorker(); + + void startWork(); + void stopWork(); + void setSamplerate(int samplerate); + void setIQMapping(AudioOutputSettings::IQMapping iqMapping) {m_iqMapping = iqMapping;} + void connectTimer(const QTimer& timer); + +private: + bool m_running; + int m_samplerate; + int m_throttlems; + int m_maxThrottlems; + QElapsedTimer m_elapsedTimer; + bool m_throttleToggle; + AudioOutputSettings::IQMapping m_iqMapping; + AudioVector m_audioBuffer; + uint32_t m_audioBufferFill; + qint16 *m_buf; // stereo (I, Q) + unsigned int m_samplesChunkSize; + SampleSourceFifo* m_sampleFifo; + AudioFifo* m_audioFifo; + Interpolators m_interpolators; + + void callbackPart(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + +private slots: + void tick(); +}; + +#endif // INCLUDE_AUDIOOUTPUTWORKER_H diff --git a/plugins/samplesink/audiooutput/readme.md b/plugins/samplesink/audiooutput/readme.md new file mode 100644 index 000000000..7275c48a9 --- /dev/null +++ b/plugins/samplesink/audiooutput/readme.md @@ -0,0 +1,36 @@ +

Audio output plugin

+ +

Introduction

+ +This output plugin sends its samples to an audio device. + +

Interface

+ +![Audio output plugin GUI](../../../doc/img/AudioOutput_plugin.png) + +

1: Start/Stop

+ +Device start / stop button. Use this switch button to play or stop audio playback + +

2: Audio sample rate

+ +Audio sample rate in Hz (Sa/s) with multiplier indicator (k). + +

3: Select audio device

+ +Use this push button to open a dialog that lets you choose the audio playback device. See [audio management documentation](../../../sdrgui/audio.md) for details. + +

4: Audio device

+ +The name of the audio device in use. + +

5: Volume

+ +A control to set the output volume. This is not supported by all output audio devices. + +

6: Channel Map

+ +This controls how the left and right audio channels map on to the IQ channels. + +* I=L, Q=R - The left audio channel is driven to the I channel. The right audio channel is driven to the Q channel for a complex (analytic signal)input. +* I=R, Q=L - The right audio channel is driven to the I channel. The left audio channel is driven to the Q channel for a complex (analytic signal)input. diff --git a/sdrbase/audio/audiofifo.cpp b/sdrbase/audio/audiofifo.cpp index 85347717f..01a780648 100644 --- a/sdrbase/audio/audiofifo.cpp +++ b/sdrbase/audio/audiofifo.cpp @@ -25,7 +25,7 @@ #define MIN(x, y) ((x) < (y) ? (x) : (y)) AudioFifo::AudioFifo() : - m_fifo(0), + m_fifo(nullptr), m_sampleSize(sizeof(AudioSample)) { m_size = 0; @@ -35,7 +35,7 @@ AudioFifo::AudioFifo() : } AudioFifo::AudioFifo(uint32_t numSamples) : - m_fifo(0), + m_fifo(nullptr), m_sampleSize(sizeof(AudioSample)) { QMutexLocker mutexLocker(&m_mutex); @@ -47,10 +47,10 @@ AudioFifo::~AudioFifo() { QMutexLocker mutexLocker(&m_mutex); - if (m_fifo != 0) + if (m_fifo) { delete[] m_fifo; - m_fifo = 0; + m_fifo = nullptr; } m_size = 0; @@ -69,7 +69,7 @@ uint AudioFifo::write(const quint8* data, uint32_t numSamples) uint32_t remaining; uint32_t copyLen; - if (m_fifo == 0) { + if (!m_fifo) { return 0; } @@ -83,6 +83,11 @@ uint AudioFifo::write(const quint8* data, uint32_t numSamples) if (isFull()) { m_mutex.unlock(); + + if (total - remaining > 0) { + emit dataReady(); + } + return total - remaining; // written so far } @@ -107,7 +112,7 @@ uint AudioFifo::read(quint8* data, uint32_t numSamples) uint32_t remaining; uint32_t copyLen; - if (m_fifo == 0) { + if (!m_fifo) { return 0; } @@ -165,10 +170,10 @@ void AudioFifo::clear() bool AudioFifo::create(uint32_t numSamples) { - if(m_fifo != 0) + if (m_fifo) { delete[] m_fifo; - m_fifo = 0; + m_fifo = nullptr; } m_fill = 0; diff --git a/sdrbase/resources/webapi.qrc b/sdrbase/resources/webapi.qrc index f6d580f0c..79f167f6d 100644 --- a/sdrbase/resources/webapi.qrc +++ b/sdrbase/resources/webapi.qrc @@ -11,6 +11,7 @@ webapi/doc/swagger/include/ATVDemod.yaml webapi/doc/swagger/include/ATVMod.yaml webapi/doc/swagger/include/AudioInput.yaml + webapi/doc/swagger/include/AudioOutput.yaml webapi/doc/swagger/include/BeamSteeringCWMod.yaml webapi/doc/swagger/include/BFMDemod.yaml webapi/doc/swagger/include/BladeRF1.yaml diff --git a/sdrbase/resources/webapi/doc/html2/index.html b/sdrbase/resources/webapi/doc/html2/index.html index e13223fa7..78b316629 100644 --- a/sdrbase/resources/webapi/doc/html2/index.html +++ b/sdrbase/resources/webapi/doc/html2/index.html @@ -1665,6 +1665,36 @@ margin-bottom: 20px; } }, "description" : "Audio output device" +}; + defs.AudioOutputSettings = { + "properties" : { + "deviceName" : { + "type" : "string", + "description" : "The name of the audio device" + }, + "volume" : { + "type" : "number", + "format" : "float" + }, + "iqMapping" : { + "type" : "integer", + "description" : "Audio channel to IQ mapping\n * 0 - I=L, Q=R\n * 1 - I=R, Q=L\n" + }, + "useReverseAPI" : { + "type" : "integer", + "description" : "Synchronize with reverse API (1 for yes, 0 for no)" + }, + "reverseAPIAddress" : { + "type" : "string" + }, + "reverseAPIPort" : { + "type" : "integer" + }, + "reverseAPIDeviceIndex" : { + "type" : "integer" + } + }, + "description" : "AudioOutput" }; defs.BFMDemodReport = { "properties" : { @@ -3600,6 +3630,9 @@ margin-bottom: 20px; "audioInputSettings" : { "$ref" : "#/definitions/AudioInputSettings" }, + "audioOutputSettings" : { + "$ref" : "#/definitions/AudioOutputSettings" + }, "bladeRF1InputSettings" : { "$ref" : "#/definitions/BladeRF1InputSettings" }, @@ -44596,7 +44629,7 @@ except ApiException as e:
- Generated 2020-11-11T13:32:52.276+01:00 + Generated 2020-11-21T10:29:19.215+01:00
diff --git a/sdrbase/resources/webapi/doc/swagger/include/AudioOutput.yaml b/sdrbase/resources/webapi/doc/swagger/include/AudioOutput.yaml new file mode 100644 index 000000000..15f2f46db --- /dev/null +++ b/sdrbase/resources/webapi/doc/swagger/include/AudioOutput.yaml @@ -0,0 +1,24 @@ +AudioOutputSettings: + description: AudioOutput + properties: + deviceName: + description: The name of the audio device + type: string + volume: + type: number + format: float + iqMapping: + type: integer + description: > + Audio channel to IQ mapping + * 0 - I=L, Q=R + * 1 - I=R, Q=L + useReverseAPI: + description: Synchronize with reverse API (1 for yes, 0 for no) + type: integer + reverseAPIAddress: + type: string + reverseAPIPort: + type: integer + reverseAPIDeviceIndex: + type: integer diff --git a/sdrbase/resources/webapi/doc/swagger/include/DeviceSettings.yaml b/sdrbase/resources/webapi/doc/swagger/include/DeviceSettings.yaml index 4a75ffc72..35f0af122 100644 --- a/sdrbase/resources/webapi/doc/swagger/include/DeviceSettings.yaml +++ b/sdrbase/resources/webapi/doc/swagger/include/DeviceSettings.yaml @@ -20,6 +20,8 @@ DeviceSettings: $ref: "/doc/swagger/include/AirspyHF.yaml#/AirspyHFSettings" audioInputSettings: $ref: "/doc/swagger/include/AudioInput.yaml#/AudioInputSettings" + audioOutputSettings: + $ref: "/doc/swagger/include/AudioOutput.yaml#/AudioOutputSettings" bladeRF1InputSettings: $ref: "/doc/swagger/include/BladeRF1.yaml#/BladeRF1InputSettings" bladeRF2InputSettings: diff --git a/sdrbase/webapi/webapirequestmapper.cpp b/sdrbase/webapi/webapirequestmapper.cpp index 9b8a85513..2556a07e4 100644 --- a/sdrbase/webapi/webapirequestmapper.cpp +++ b/sdrbase/webapi/webapirequestmapper.cpp @@ -4096,6 +4096,11 @@ bool WebAPIRequestMapper::getDeviceSettings( deviceSettings->setAudioInputSettings(new SWGSDRangel::SWGAudioInputSettings()); deviceSettings->getAudioInputSettings()->fromJsonObject(settingsJsonObject); } + else if (deviceSettingsKey == "audioOutputSettings") + { + deviceSettings->setAudioOutputSettings(new SWGSDRangel::SWGAudioOutputSettings()); + deviceSettings->getAudioOutputSettings()->fromJsonObject(settingsJsonObject); + } else if (deviceSettingsKey == "bladeRF1InputSettings") { deviceSettings->setBladeRf1InputSettings(new SWGSDRangel::SWGBladeRF1InputSettings()); diff --git a/sdrbase/webapi/webapiutils.cpp b/sdrbase/webapi/webapiutils.cpp index e4265d02a..86abdc6c8 100644 --- a/sdrbase/webapi/webapiutils.cpp +++ b/sdrbase/webapi/webapiutils.cpp @@ -68,7 +68,8 @@ const QMap WebAPIUtils::m_channelURIToSettingsKey = { const QMap WebAPIUtils::m_deviceIdToSettingsKey = { {"sdrangel.samplesource.airspy", "airspySettings"}, {"sdrangel.samplesource.airspyhf", "airspyHFSettings"}, - {"sdrangel.samplesource.audio", "audioInputSettings"}, + {"sdrangel.samplesource.audioinput", "audioInputSettings"}, + {"sdrangel.samplesink.audiooutput", "audioOutputSettings"}, {"sdrangel.samplesource.bladerf1input", "bladeRF1InputSettings"}, {"sdrangel.samplesource.bladerf", "bladeRF1InputSettings"}, // remap {"sdrangel.samplesink.bladerf1output", "bladeRF1OutputSettings"}, @@ -204,6 +205,7 @@ const QMap WebAPIUtils::m_sourceDeviceHwIdToActionsKey = { }; const QMap WebAPIUtils::m_sinkDeviceHwIdToSettingsKey = { + {"AudioOutput", "AudioOutputSettings"}, {"BladeRF1", "bladeRF1OutputSettings"}, {"BladeRF2", "bladeRF2OutputSettings"}, {"HackRF", "hackRFOutputSettings"}, diff --git a/swagger/sdrangel/api/swagger/include/AudioOutput.yaml b/swagger/sdrangel/api/swagger/include/AudioOutput.yaml new file mode 100644 index 000000000..15f2f46db --- /dev/null +++ b/swagger/sdrangel/api/swagger/include/AudioOutput.yaml @@ -0,0 +1,24 @@ +AudioOutputSettings: + description: AudioOutput + properties: + deviceName: + description: The name of the audio device + type: string + volume: + type: number + format: float + iqMapping: + type: integer + description: > + Audio channel to IQ mapping + * 0 - I=L, Q=R + * 1 - I=R, Q=L + useReverseAPI: + description: Synchronize with reverse API (1 for yes, 0 for no) + type: integer + reverseAPIAddress: + type: string + reverseAPIPort: + type: integer + reverseAPIDeviceIndex: + type: integer diff --git a/swagger/sdrangel/api/swagger/include/DeviceSettings.yaml b/swagger/sdrangel/api/swagger/include/DeviceSettings.yaml index 74cec9986..03a6922ee 100644 --- a/swagger/sdrangel/api/swagger/include/DeviceSettings.yaml +++ b/swagger/sdrangel/api/swagger/include/DeviceSettings.yaml @@ -20,6 +20,8 @@ DeviceSettings: $ref: "http://swgserver:8081/api/swagger/include/AirspyHF.yaml#/AirspyHFSettings" audioInputSettings: $ref: "http://swgserver:8081/api/swagger/include/AudioInput.yaml#/AudioInputSettings" + audioOutputSettings: + $ref: "http://swgserver:8081/api/swagger/include/AudioOutput.yaml#/AudioOutputSettings" bladeRF1InputSettings: $ref: "http://swgserver:8081/api/swagger/include/BladeRF1.yaml#/BladeRF1InputSettings" bladeRF2InputSettings: diff --git a/swagger/sdrangel/code/html2/index.html b/swagger/sdrangel/code/html2/index.html index e13223fa7..78b316629 100644 --- a/swagger/sdrangel/code/html2/index.html +++ b/swagger/sdrangel/code/html2/index.html @@ -1665,6 +1665,36 @@ margin-bottom: 20px; } }, "description" : "Audio output device" +}; + defs.AudioOutputSettings = { + "properties" : { + "deviceName" : { + "type" : "string", + "description" : "The name of the audio device" + }, + "volume" : { + "type" : "number", + "format" : "float" + }, + "iqMapping" : { + "type" : "integer", + "description" : "Audio channel to IQ mapping\n * 0 - I=L, Q=R\n * 1 - I=R, Q=L\n" + }, + "useReverseAPI" : { + "type" : "integer", + "description" : "Synchronize with reverse API (1 for yes, 0 for no)" + }, + "reverseAPIAddress" : { + "type" : "string" + }, + "reverseAPIPort" : { + "type" : "integer" + }, + "reverseAPIDeviceIndex" : { + "type" : "integer" + } + }, + "description" : "AudioOutput" }; defs.BFMDemodReport = { "properties" : { @@ -3600,6 +3630,9 @@ margin-bottom: 20px; "audioInputSettings" : { "$ref" : "#/definitions/AudioInputSettings" }, + "audioOutputSettings" : { + "$ref" : "#/definitions/AudioOutputSettings" + }, "bladeRF1InputSettings" : { "$ref" : "#/definitions/BladeRF1InputSettings" }, @@ -44596,7 +44629,7 @@ except ApiException as e:
- Generated 2020-11-11T13:32:52.276+01:00 + Generated 2020-11-21T10:29:19.215+01:00
diff --git a/swagger/sdrangel/code/qt5/client/SWGAudioOutputSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGAudioOutputSettings.cpp new file mode 100644 index 000000000..8404fb3a9 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAudioOutputSettings.cpp @@ -0,0 +1,250 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGAudioOutputSettings.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGAudioOutputSettings::SWGAudioOutputSettings(QString* json) { + init(); + this->fromJson(*json); +} + +SWGAudioOutputSettings::SWGAudioOutputSettings() { + device_name = nullptr; + m_device_name_isSet = false; + volume = 0.0f; + m_volume_isSet = false; + iq_mapping = 0; + m_iq_mapping_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = nullptr; + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; +} + +SWGAudioOutputSettings::~SWGAudioOutputSettings() { + this->cleanup(); +} + +void +SWGAudioOutputSettings::init() { + device_name = new QString(""); + m_device_name_isSet = false; + volume = 0.0f; + m_volume_isSet = false; + iq_mapping = 0; + m_iq_mapping_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = new QString(""); + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; +} + +void +SWGAudioOutputSettings::cleanup() { + if(device_name != nullptr) { + delete device_name; + } + + + + if(reverse_api_address != nullptr) { + delete reverse_api_address; + } + + +} + +SWGAudioOutputSettings* +SWGAudioOutputSettings::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGAudioOutputSettings::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&device_name, pJson["deviceName"], "QString", "QString"); + + ::SWGSDRangel::setValue(&volume, pJson["volume"], "float", ""); + + ::SWGSDRangel::setValue(&iq_mapping, pJson["iqMapping"], "qint32", ""); + + ::SWGSDRangel::setValue(&use_reverse_api, pJson["useReverseAPI"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_address, pJson["reverseAPIAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&reverse_api_port, pJson["reverseAPIPort"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_device_index, pJson["reverseAPIDeviceIndex"], "qint32", ""); + +} + +QString +SWGAudioOutputSettings::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGAudioOutputSettings::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(device_name != nullptr && *device_name != QString("")){ + toJsonValue(QString("deviceName"), device_name, obj, QString("QString")); + } + if(m_volume_isSet){ + obj->insert("volume", QJsonValue(volume)); + } + if(m_iq_mapping_isSet){ + obj->insert("iqMapping", QJsonValue(iq_mapping)); + } + if(m_use_reverse_api_isSet){ + obj->insert("useReverseAPI", QJsonValue(use_reverse_api)); + } + if(reverse_api_address != nullptr && *reverse_api_address != QString("")){ + toJsonValue(QString("reverseAPIAddress"), reverse_api_address, obj, QString("QString")); + } + if(m_reverse_api_port_isSet){ + obj->insert("reverseAPIPort", QJsonValue(reverse_api_port)); + } + if(m_reverse_api_device_index_isSet){ + obj->insert("reverseAPIDeviceIndex", QJsonValue(reverse_api_device_index)); + } + + return obj; +} + +QString* +SWGAudioOutputSettings::getDeviceName() { + return device_name; +} +void +SWGAudioOutputSettings::setDeviceName(QString* device_name) { + this->device_name = device_name; + this->m_device_name_isSet = true; +} + +float +SWGAudioOutputSettings::getVolume() { + return volume; +} +void +SWGAudioOutputSettings::setVolume(float volume) { + this->volume = volume; + this->m_volume_isSet = true; +} + +qint32 +SWGAudioOutputSettings::getIqMapping() { + return iq_mapping; +} +void +SWGAudioOutputSettings::setIqMapping(qint32 iq_mapping) { + this->iq_mapping = iq_mapping; + this->m_iq_mapping_isSet = true; +} + +qint32 +SWGAudioOutputSettings::getUseReverseApi() { + return use_reverse_api; +} +void +SWGAudioOutputSettings::setUseReverseApi(qint32 use_reverse_api) { + this->use_reverse_api = use_reverse_api; + this->m_use_reverse_api_isSet = true; +} + +QString* +SWGAudioOutputSettings::getReverseApiAddress() { + return reverse_api_address; +} +void +SWGAudioOutputSettings::setReverseApiAddress(QString* reverse_api_address) { + this->reverse_api_address = reverse_api_address; + this->m_reverse_api_address_isSet = true; +} + +qint32 +SWGAudioOutputSettings::getReverseApiPort() { + return reverse_api_port; +} +void +SWGAudioOutputSettings::setReverseApiPort(qint32 reverse_api_port) { + this->reverse_api_port = reverse_api_port; + this->m_reverse_api_port_isSet = true; +} + +qint32 +SWGAudioOutputSettings::getReverseApiDeviceIndex() { + return reverse_api_device_index; +} +void +SWGAudioOutputSettings::setReverseApiDeviceIndex(qint32 reverse_api_device_index) { + this->reverse_api_device_index = reverse_api_device_index; + this->m_reverse_api_device_index_isSet = true; +} + + +bool +SWGAudioOutputSettings::isSet(){ + bool isObjectUpdated = false; + do{ + if(device_name && *device_name != QString("")){ + isObjectUpdated = true; break; + } + if(m_volume_isSet){ + isObjectUpdated = true; break; + } + if(m_iq_mapping_isSet){ + isObjectUpdated = true; break; + } + if(m_use_reverse_api_isSet){ + isObjectUpdated = true; break; + } + if(reverse_api_address && *reverse_api_address != QString("")){ + isObjectUpdated = true; break; + } + if(m_reverse_api_port_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_device_index_isSet){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGAudioOutputSettings.h b/swagger/sdrangel/code/qt5/client/SWGAudioOutputSettings.h new file mode 100644 index 000000000..c594d47b1 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAudioOutputSettings.h @@ -0,0 +1,95 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGAudioOutputSettings.h + * + * AudioOutput + */ + +#ifndef SWGAudioOutputSettings_H_ +#define SWGAudioOutputSettings_H_ + +#include + + +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGAudioOutputSettings: public SWGObject { +public: + SWGAudioOutputSettings(); + SWGAudioOutputSettings(QString* json); + virtual ~SWGAudioOutputSettings(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGAudioOutputSettings* fromJson(QString &jsonString) override; + + QString* getDeviceName(); + void setDeviceName(QString* device_name); + + float getVolume(); + void setVolume(float volume); + + qint32 getIqMapping(); + void setIqMapping(qint32 iq_mapping); + + qint32 getUseReverseApi(); + void setUseReverseApi(qint32 use_reverse_api); + + QString* getReverseApiAddress(); + void setReverseApiAddress(QString* reverse_api_address); + + qint32 getReverseApiPort(); + void setReverseApiPort(qint32 reverse_api_port); + + qint32 getReverseApiDeviceIndex(); + void setReverseApiDeviceIndex(qint32 reverse_api_device_index); + + + virtual bool isSet() override; + +private: + QString* device_name; + bool m_device_name_isSet; + + float volume; + bool m_volume_isSet; + + qint32 iq_mapping; + bool m_iq_mapping_isSet; + + qint32 use_reverse_api; + bool m_use_reverse_api_isSet; + + QString* reverse_api_address; + bool m_reverse_api_address_isSet; + + qint32 reverse_api_port; + bool m_reverse_api_port_isSet; + + qint32 reverse_api_device_index; + bool m_reverse_api_device_index_isSet; + +}; + +} + +#endif /* SWGAudioOutputSettings_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.cpp index 02795a32c..a9d12fc7e 100644 --- a/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.cpp @@ -40,6 +40,8 @@ SWGDeviceSettings::SWGDeviceSettings() { m_airspy_hf_settings_isSet = false; audio_input_settings = nullptr; m_audio_input_settings_isSet = false; + audio_output_settings = nullptr; + m_audio_output_settings_isSet = false; blade_rf1_input_settings = nullptr; m_blade_rf1_input_settings_isSet = false; blade_rf2_input_settings = nullptr; @@ -130,6 +132,8 @@ SWGDeviceSettings::init() { m_airspy_hf_settings_isSet = false; audio_input_settings = new SWGAudioInputSettings(); m_audio_input_settings_isSet = false; + audio_output_settings = new SWGAudioOutputSettings(); + m_audio_output_settings_isSet = false; blade_rf1_input_settings = new SWGBladeRF1InputSettings(); m_blade_rf1_input_settings_isSet = false; blade_rf2_input_settings = new SWGBladeRF2InputSettings(); @@ -218,6 +222,9 @@ SWGDeviceSettings::cleanup() { if(audio_input_settings != nullptr) { delete audio_input_settings; } + if(audio_output_settings != nullptr) { + delete audio_output_settings; + } if(blade_rf1_input_settings != nullptr) { delete blade_rf1_input_settings; } @@ -348,6 +355,8 @@ SWGDeviceSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&audio_input_settings, pJson["audioInputSettings"], "SWGAudioInputSettings", "SWGAudioInputSettings"); + ::SWGSDRangel::setValue(&audio_output_settings, pJson["audioOutputSettings"], "SWGAudioOutputSettings", "SWGAudioOutputSettings"); + ::SWGSDRangel::setValue(&blade_rf1_input_settings, pJson["bladeRF1InputSettings"], "SWGBladeRF1InputSettings", "SWGBladeRF1InputSettings"); ::SWGSDRangel::setValue(&blade_rf2_input_settings, pJson["bladeRF2InputSettings"], "SWGBladeRF2InputSettings", "SWGBladeRF2InputSettings"); @@ -452,6 +461,9 @@ SWGDeviceSettings::asJsonObject() { if((audio_input_settings != nullptr) && (audio_input_settings->isSet())){ toJsonValue(QString("audioInputSettings"), audio_input_settings, obj, QString("SWGAudioInputSettings")); } + if((audio_output_settings != nullptr) && (audio_output_settings->isSet())){ + toJsonValue(QString("audioOutputSettings"), audio_output_settings, obj, QString("SWGAudioOutputSettings")); + } if((blade_rf1_input_settings != nullptr) && (blade_rf1_input_settings->isSet())){ toJsonValue(QString("bladeRF1InputSettings"), blade_rf1_input_settings, obj, QString("SWGBladeRF1InputSettings")); } @@ -621,6 +633,16 @@ SWGDeviceSettings::setAudioInputSettings(SWGAudioInputSettings* audio_input_sett this->m_audio_input_settings_isSet = true; } +SWGAudioOutputSettings* +SWGDeviceSettings::getAudioOutputSettings() { + return audio_output_settings; +} +void +SWGDeviceSettings::setAudioOutputSettings(SWGAudioOutputSettings* audio_output_settings) { + this->audio_output_settings = audio_output_settings; + this->m_audio_output_settings_isSet = true; +} + SWGBladeRF1InputSettings* SWGDeviceSettings::getBladeRf1InputSettings() { return blade_rf1_input_settings; @@ -994,6 +1016,9 @@ SWGDeviceSettings::isSet(){ if(audio_input_settings && audio_input_settings->isSet()){ isObjectUpdated = true; break; } + if(audio_output_settings && audio_output_settings->isSet()){ + isObjectUpdated = true; break; + } if(blade_rf1_input_settings && blade_rf1_input_settings->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.h b/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.h index b77b7cbf1..ef74f5bc6 100644 --- a/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGDeviceSettings.h @@ -25,6 +25,7 @@ #include "SWGAirspyHFSettings.h" #include "SWGAirspySettings.h" #include "SWGAudioInputSettings.h" +#include "SWGAudioOutputSettings.h" #include "SWGBladeRF1InputSettings.h" #include "SWGBladeRF1OutputSettings.h" #include "SWGBladeRF2InputSettings.h" @@ -98,6 +99,9 @@ public: SWGAudioInputSettings* getAudioInputSettings(); void setAudioInputSettings(SWGAudioInputSettings* audio_input_settings); + SWGAudioOutputSettings* getAudioOutputSettings(); + void setAudioOutputSettings(SWGAudioOutputSettings* audio_output_settings); + SWGBladeRF1InputSettings* getBladeRf1InputSettings(); void setBladeRf1InputSettings(SWGBladeRF1InputSettings* blade_rf1_input_settings); @@ -225,6 +229,9 @@ private: SWGAudioInputSettings* audio_input_settings; bool m_audio_input_settings_isSet; + SWGAudioOutputSettings* audio_output_settings; + bool m_audio_output_settings_isSet; + SWGBladeRF1InputSettings* blade_rf1_input_settings; bool m_blade_rf1_input_settings_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h index f4f960a9d..8c1faa313 100644 --- a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h +++ b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h @@ -38,6 +38,7 @@ #include "SWGAudioInputDevice.h" #include "SWGAudioInputSettings.h" #include "SWGAudioOutputDevice.h" +#include "SWGAudioOutputSettings.h" #include "SWGBFMDemodReport.h" #include "SWGBFMDemodSettings.h" #include "SWGBandwidth.h" @@ -306,6 +307,9 @@ namespace SWGSDRangel { if(QString("SWGAudioOutputDevice").compare(type) == 0) { return new SWGAudioOutputDevice(); } + if(QString("SWGAudioOutputSettings").compare(type) == 0) { + return new SWGAudioOutputSettings(); + } if(QString("SWGBFMDemodReport").compare(type) == 0) { return new SWGBFMDemodReport(); }