From 75129a6ed74a8a7e81d536797b18a71bbed6a2e3 Mon Sep 17 00:00:00 2001 From: SeeLook <945374+SeeLook@users.noreply.github.com> Date: Fri, 23 Aug 2019 21:35:12 +0200 Subject: [PATCH] Added audible and visible countdown before playing and listening. Added countdown item to displaying digits according to metronome beats. Rearranged Tempo Bar to quickly set/unset countdown and audible ticking. Fixed determining tempo and beat when melody is read from XML. --- changes | 1 + fonts/nootka.ttf | Bin 34796 -> 35252 bytes spare_parts/nootka.sfd | 116 ++++++++++- spare_parts/scalable/glyphs/countdown.svg | 134 +++++++++++++ src/libs/core/music/tmelody.cpp | 26 ++- src/libs/core/music/tmelody.h | 13 +- src/libs/core/music/tmeter.cpp | 6 +- src/libs/core/music/tmeter.h | 34 +++- src/libs/sound/tabstractplayer.cpp | 34 ++-- src/libs/sound/tabstractplayer.h | 16 +- src/libs/sound/trtaudioout.cpp | 31 +-- src/libs/sound/trtaudioout.h | 6 +- src/libs/sound/tsound.cpp | 69 +++++-- src/libs/sound/tsound.h | 37 +++- src/main/texamexecutor.cpp | 25 +-- src/main/texamexecutor.h | 4 +- src/main/tmainscoreobject.cpp | 4 +- src/nootka.qrc | 1 + src/qml/about/AboutPage.qml | 2 +- src/qml/exam/ExamExecutor.qml | 2 +- src/qml/sound/CountdownItem.qml | 54 ++++++ src/qml/sound/PitchView.qml | 5 +- src/qml/sound/TempoBar.qml | 222 +++++++++++++--------- src/qml/sound/TempoMenu.qml | 16 +- 24 files changed, 663 insertions(+), 195 deletions(-) create mode 100644 spare_parts/scalable/glyphs/countdown.svg create mode 100644 src/qml/sound/CountdownItem.qml diff --git a/changes b/changes index 7c0aad0c3..eab150adc 100644 --- a/changes +++ b/changes @@ -2,6 +2,7 @@ Nootka says: I speak Italian - added Italian translation - metronome can tick audibly, also during pitch detection + - audible and/or visible countdown before playing and listening - added support for scientific octave numbers in note names - read/save melody title, composer, tempo, beat - level can consist set of melodies loaded from files diff --git a/fonts/nootka.ttf b/fonts/nootka.ttf index caef7dd707d3bea8e20a5b03401803d132eb9b7b..65f5c3a604d35a43354d0b74767153f6cdc2e21d 100644 GIT binary patch delta 919 zcmaJ<OH31C5T1YAEp4}S*>>rJUAvFATLiT&ZK?P|Q30d)egrj<22oV7jSvMBQxnmO z8b$Y_F&g!z@nEb6qUhOpFhozriw9o^k7(3H>THRRgYn<Y{`1W@^Ucn`|ECMo1D(nP z0RSf)2NQI)wr=h?5x;g2VDvIZ#kLhITOk4l&LP&;ZHhK@9@xSFkY2=H-Tj%|h2poX z0Mth`d-vf%85C+5z&wS#d2ePQS1O@0j2ijYy?w)bMmoP@J$(+K<3?{!CcoQx{7Ea4 zpU@h6F=G8rcVmMP;!tn@;L!a?&Pl{i(d52tcV=uVzZ12;B7dnrGn9jI%SPnd5z7ZN z{XIgWX98f;9DwBKTy|ja;<JDq;56!ymh!MTaOc@`!@Jl;pFvu0!Y>DyTwdie_E#tO ze<^(^wb0j1UW8Vlj3_QFGWkj^u$_L5{15tiS!#xxdAFA%?fP0WiySTXk)^~;zlH_+ zU22q!>5Zl=Gha#tUItzS9tLg{@0rGltc)wqmA9%{4XH!wE%kkHPjDi%H8d5T3{Pn| zba8}!yew~l1x7>a4gRJSWZ@W`g)4Z3K`xnYOj{}}EQzEOL`fmSCzXt7LDf<bDWgP# z*W>k6^A+X1>O|P^s(*)qY9xgq(J)gN(dtRc0Ka4~Id2FVGVWw~Gqi%Lmi`YS%~?W- z+2OD+ZoIRD+@2mAsrC68#>LH&n&d@Zzh70OiLl1=9H+|jR=gu!iX3ZJ6t5>5UA47E z4QcX1pO0ZgAzquWjr(hCHjbOEiGn*R)zvj^h{ia<#aB9=mAp$h^2a?SxZF-$ZkIrj zm|K#3G{e~9%L~&_I!@LJqQ_e!v#jWE!^U>T?NX%K_2E!3Sih7$l9m+*RUt{Kx#5t9 z7k6<sTQ#qF*d%MUZn4=}ju)${Dk~jpJS?jy)7u0O!w3T7@CyI%ic8v7H<Nb#%&GGf kaqGT<XpY63VtTAFpx-RikT!jyAo8#FsXfN!`TO?$1e8j_8vp<R delta 535 zcmdlondwbGQ#}JC0|NseLjwadLxP)Ih;LIp=RyXC_6tB!8Fv>~HwJZvTp)i1kgw(+ ztZx*V9rBNXfiVNfPe{&9Ea+eVa|Qzg(-R=(N-ir=U{GYLWME+V0F-A*Pb@C@{~u^F zL;C_CpCdh|GHnODavK8!YYPK|_nVB=#QGGTEw}FiCEozCSq4yqJ&}bAXn_iluac2l zQnBU%-!vfq1Q5&Q<R>SliPQisWcdP=pOBkaQNXaBEeI&*0pu&>CFZ6|8>jAJU<fQ= zU@-YwkY8N#wBVH(14A3o!HgDQ&jLNXWzt;#cz&C&4DtcYFMt{uE;tJaf#{n-9ligb z|94_xWfli=IT)BgqCf!D#4PdOi6M%m87Tjkg%xZRL=Hl_gGJmY`!UKg=5EembYPsk zfvJ<Rf3g*GKCijFiQFZ*i*h^V<})x(E@ZLT{EoSfQAXj5BA23;qL<=4#S=<AN<B)C zlxtL^RKBT7seaqs!@AdT^Is1Sdq(%kZXJCR?4@~`1_l-eVA{~a7)rZM{?<|K$p{Q* oCa?>5fX)$PkOT7=8Qg$kEI<*EKR_`J!XN;qZ@er7)5yLD0D}{PYXATM diff --git a/spare_parts/nootka.sfd b/spare_parts/nootka.sfd index f01780544..f56e594ff 100644 --- a/spare_parts/nootka.sfd +++ b/spare_parts/nootka.sfd @@ -1,4 +1,4 @@ -SplineFontDB: 3.0 +SplineFontDB: 3.2 FontName: nootka FullName: nootka FamilyName: nootka @@ -21,7 +21,7 @@ OS2Version: 0 OS2_WeightWidthSlopeOnly: 0 OS2_UseTypoMetrics: 1 CreationTime: 1411211154 -ModificationTime: 1563219981 +ModificationTime: 1566554742 PfmFamily: 17 TTFWeight: 500 TTFWidth: 5 @@ -103,7 +103,7 @@ Grid 2000 300.199996948 l 1024 Named: "middle" EndSplineSet -BeginChars: 65539 71 +BeginChars: 65539 72 StartChar: .notdef Encoding: 65536 -1 0 @@ -6152,5 +6152,115 @@ SplineSet 442.142578125 443.8828125 l 1,73,-1 EndSplineSet EndChar + +StartChar: uni0190 +Encoding: 400 400 71 +Width: 1499 +VWidth: 0 +Flags: W +HStem: -2.27637 99.5225<1127.07 1190.37 1246.07 1309.37 1360.01 1423.31> -2.27637 88.4648<777.233 971.173> 201.645 93.625<514.765 744.034> 232.891 85.5156<828.785 967.168> 322.093 21G<96.2373 137.13 418.241 450.998 786.572 809.617> 448.892 89.9385<828.686 971.579> 504.086 20G<202.886 319.436> 642.491 92.8867<481.318 603.454> +VStem: 208.118 111.317<-2.27637 406.871> 618.71 110.58<513.241 628.411> 987.283 109.104<339.478 433.563> 995.392 116.479<109.645 206.638> 1113.38 91.4141<9.89172 85.8389> 1232.38 91.4141<9.89172 85.8389> 1346.32 91.4131<9.89172 85.8389> +LayerCount: 2 +Fore +SplineSet +319.435546875 -2.2763671875 m 1,0,-1 + 208.118164062 -2.2763671875 l 1,1,-1 + 208.118164062 302.188476562 l 2,2,3 + 208.118164062 321.35546875 208.118164062 321.35546875 208.854492188 352.318359375 c 0,4,5 + 210.330078125 383.280273438 210.330078125 383.280273438 211.067382812 406.87109375 c 1,6,7 + 207.380859375 402.448242188 207.380859375 402.448242188 194.84765625 390.65234375 c 0,8,9 + 183.052734375 379.59375 183.052734375 379.59375 172.732421875 370.748046875 c 2,10,-1 + 112.28125 322.092773438 l 1,11,-1 + 58.4658203125 389.177734375 l 1,12,-1 + 228.022460938 524.0859375 l 1,13,-1 + 319.435546875 524.0859375 l 1,14,-1 + 319.435546875 -2.2763671875 l 1,0,-1 +744.034179688 201.64453125 m 1,15,-1 + 376.170898438 201.64453125 l 1,16,-1 + 376.170898438 279.05078125 l 1,17,-1 + 508.129882812 412.483398438 l 2,18,19 + 547.939453125 453.767578125 547.939453125 453.767578125 572.266601562 481.04296875 c 0,20,21 + 596.594726562 509.056640625 596.594726562 509.056640625 607.65234375 531.173828125 c 0,22,23 + 618.709960938 554.026367188 618.709960938 554.026367188 618.709960938 579.828125 c 0,24,25 + 618.709960938 611.528320312 618.709960938 611.528320312 601.017578125 627.008789062 c 0,26,27 + 584.0625 642.491210938 584.0625 642.491210938 554.573242188 642.491210938 c 0,28,29 + 524.348632812 642.491210938 524.348632812 642.491210938 495.59765625 628.484375 c 128,-1,30 + 466.84765625 614.477539062 466.84765625 614.477539062 435.147460938 588.673828125 c 1,31,-1 + 374.697265625 660.18359375 l 1,32,33 + 397.549804688 680.087890625 397.549804688 680.087890625 422.614257812 697.04296875 c 0,34,35 + 448.416992188 713.999023438 448.416992188 713.999023438 481.590820312 724.3203125 c 0,36,37 + 515.501953125 735.377929688 515.501953125 735.377929688 562.68359375 735.377929688 c 0,38,39 + 614.287109375 735.377929688 614.287109375 735.377929688 651.1484375 716.209960938 c 0,40,41 + 688.745117188 697.780273438 688.745117188 697.780273438 708.6484375 665.34375 c 0,42,43 + 729.290039062 633.643554688 729.290039062 633.643554688 729.290039062 593.098632812 c 0,44,45 + 729.290039062 549.603515625 729.290039062 549.603515625 711.59765625 513.48046875 c 0,46,47 + 694.642578125 477.358398438 694.642578125 477.358398438 661.467773438 441.97265625 c 0,48,49 + 629.03125 406.586914062 629.03125 406.586914062 582.587890625 363.829101562 c 2,50,-1 + 514.764648438 300.4296875 l 1,51,-1 + 514.764648438 295.26953125 l 1,52,-1 + 744.034179688 295.26953125 l 1,53,-1 + 744.034179688 201.64453125 l 1,15,-1 +1096.38769531 413.505859375 m 0,54,55 + 1096.38769531 358.953125 1096.38769531 358.953125 1063.21386719 326.515625 c 0,56,57 + 1030.77734375 294.080078125 1030.77734375 294.080078125 982.858398438 282.284179688 c 1,58,-1 + 982.858398438 280.071289062 l 1,59,60 + 1046.25878906 272.701171875 1046.25878906 272.701171875 1078.6953125 241.73828125 c 0,61,62 + 1111.87011719 210.775390625 1111.87011719 210.775390625 1111.87011719 158.43359375 c 0,63,64 + 1111.87011719 112.7265625 1111.87011719 112.7265625 1089.015625 75.8671875 c 0,65,66 + 1066.90039062 39.744140625 1066.90039062 39.744140625 1019.71972656 18.365234375 c 0,67,68 + 973.275390625 -2.2763671875 973.275390625 -2.2763671875 899.555664062 -2.2763671875 c 0,69,70 + 814.040039062 -2.2763671875 814.040039062 -2.2763671875 747.692382812 26.474609375 c 1,71,-1 + 747.692382812 120.836914062 l 1,72,73 + 781.603515625 103.880859375 781.603515625 103.880859375 818.462890625 95.03515625 c 0,74,75 + 856.060546875 86.1884765625 856.060546875 86.1884765625 887.760742188 86.1884765625 c 0,76,77 + 947.47265625 86.1884765625 947.47265625 86.1884765625 971.063476562 106.830078125 c 0,78,79 + 995.391601562 127.471679688 995.391601562 127.471679688 995.391601562 165.068359375 c 0,80,81 + 995.391601562 187.185546875 995.391601562 187.185546875 984.333984375 201.9296875 c 0,82,83 + 973.275390625 217.41015625 973.275390625 217.41015625 945.26171875 224.782226562 c 0,84,85 + 917.985351562 232.890625 917.985351562 232.890625 868.59375 232.890625 c 2,86,-1 + 828.78515625 232.890625 l 1,87,-1 + 828.78515625 318.40625 l 1,88,-1 + 869.331054688 318.40625 l 2,89,90 + 917.985351562 318.40625 917.985351562 318.40625 943.049804688 327.251953125 c 0,91,92 + 968.8515625 336.8359375 968.8515625 336.8359375 977.69921875 352.318359375 c 0,93,94 + 987.283203125 368.536132812 987.283203125 368.536132812 987.283203125 389.177734375 c 0,95,96 + 987.283203125 417.19140625 987.283203125 417.19140625 969.58984375 432.672851562 c 0,97,98 + 952.633789062 448.891601562 952.633789062 448.891601562 912.087890625 448.891601562 c 0,99,100 + 874.490234375 448.891601562 874.490234375 448.891601562 846.4765625 435.622070312 c 0,101,102 + 819.201171875 423.08984375 819.201171875 423.08984375 800.033203125 410.556640625 c 1,103,-1 + 748.4296875 487.2265625 l 1,104,105 + 779.392578125 509.341796875 779.392578125 509.341796875 820.674804688 524.0859375 c 0,106,107 + 862.6953125 538.830078125 862.6953125 538.830078125 920.197265625 538.830078125 c 0,108,109 + 1001.29003906 538.830078125 1001.29003906 538.830078125 1048.47070312 505.65625 c 0,110,111 + 1096.38769531 473.219726562 1096.38769531 473.219726562 1096.38769531 413.505859375 c 0,54,55 +1113.3828125 47.853515625 m 0,112,113 + 1113.3828125 75.1298828125 1113.3828125 75.1298828125 1126.65332031 86.1884765625 c 128,-1,114 + 1139.92285156 97.24609375 1139.92285156 97.24609375 1158.35253906 97.24609375 c 0,115,116 + 1177.51953125 97.24609375 1177.51953125 97.24609375 1190.79003906 86.1884765625 c 0,117,118 + 1204.796875 75.1298828125 1204.796875 75.1298828125 1204.796875 47.853515625 c 0,119,120 + 1204.796875 21.314453125 1204.796875 21.314453125 1190.79003906 9.51953125 c 0,121,122 + 1177.51953125 -2.2763671875 1177.51953125 -2.2763671875 1158.35253906 -2.2763671875 c 0,123,124 + 1139.92285156 -2.2763671875 1139.92285156 -2.2763671875 1126.65332031 9.51953125 c 128,-1,125 + 1113.3828125 21.314453125 1113.3828125 21.314453125 1113.3828125 47.853515625 c 0,112,113 +1232.37890625 47.853515625 m 0,126,127 + 1232.37890625 75.1298828125 1232.37890625 75.1298828125 1245.64941406 86.1884765625 c 128,-1,128 + 1258.91796875 97.24609375 1258.91796875 97.24609375 1277.34765625 97.24609375 c 0,129,130 + 1296.515625 97.24609375 1296.515625 97.24609375 1309.78613281 86.1884765625 c 0,131,132 + 1323.79296875 75.1298828125 1323.79296875 75.1298828125 1323.79296875 47.853515625 c 0,133,134 + 1323.79296875 21.314453125 1323.79296875 21.314453125 1309.78613281 9.51953125 c 0,135,136 + 1296.515625 -2.2763671875 1296.515625 -2.2763671875 1277.34765625 -2.2763671875 c 0,137,138 + 1258.91796875 -2.2763671875 1258.91796875 -2.2763671875 1245.64941406 9.51953125 c 128,-1,139 + 1232.37890625 21.314453125 1232.37890625 21.314453125 1232.37890625 47.853515625 c 0,126,127 +1346.3203125 47.853515625 m 0,140,141 + 1346.3203125 75.1298828125 1346.3203125 75.1298828125 1359.58984375 86.1884765625 c 128,-1,142 + 1372.86035156 97.24609375 1372.86035156 97.24609375 1391.29003906 97.24609375 c 0,143,144 + 1410.45703125 97.24609375 1410.45703125 97.24609375 1423.7265625 86.1884765625 c 0,145,146 + 1437.73339844 75.1298828125 1437.73339844 75.1298828125 1437.73339844 47.853515625 c 0,147,148 + 1437.73339844 21.314453125 1437.73339844 21.314453125 1423.7265625 9.51953125 c 0,149,150 + 1410.45703125 -2.2763671875 1410.45703125 -2.2763671875 1391.29003906 -2.2763671875 c 0,151,152 + 1372.86035156 -2.2763671875 1372.86035156 -2.2763671875 1359.58984375 9.51953125 c 128,-1,153 + 1346.3203125 21.314453125 1346.3203125 21.314453125 1346.3203125 47.853515625 c 0,140,141 +EndSplineSet +EndChar EndChars EndSplineFont diff --git a/spare_parts/scalable/glyphs/countdown.svg b/spare_parts/scalable/glyphs/countdown.svg new file mode 100644 index 000000000..2e95f4312 --- /dev/null +++ b/spare_parts/scalable/glyphs/countdown.svg @@ -0,0 +1,134 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + width="1100" + height="1000" + id="svg2" + inkscape:version="0.92.4 5da689c313, 2019-01-14" + sodipodi:docname="countdown.svg" + inkscape:export-filename="/home/tom/DEVELOP/QT4/nootka/unused-picts/proto/note.png" + inkscape:export-xdpi="300.09186" + inkscape:export-ydpi="300.09186"> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1600" + inkscape:window-height="1163" + id="namedview8" + showgrid="false" + inkscape:zoom="0.67396115" + inkscape:cx="714.56967" + inkscape:cy="507.66913" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" + showguides="true" + inkscape:guide-bbox="true"> + <sodipodi:guide + orientation="1,0" + position="348.68479,854.64867" + id="guide5187" + inkscape:locked="false" /> + <sodipodi:guide + orientation="0,1" + position="-83.090843,500.02882" + id="guide2996" + inkscape:locked="false" /> + </sodipodi:namedview> + <defs + id="defs4" /> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + aria-label="1" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.33857802" + id="text848" + transform="matrix(2.953529,0,0,2.953529,-600.69664,-518.44485)"> + <path + d="m 276.63674,418.3493 h -28.992 v -79.296 q 0,-4.992 0.192,-13.056 0.384,-8.064 0.576,-14.208 -0.96,1.152 -4.224,4.224 -3.072,2.88 -5.76,5.184 l -15.744,12.672 -14.016,-17.472 44.16,-35.136 h 23.808 z" + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:192px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Bold';stroke-width:0.33857802" + id="path910" + inkscape:connector-curvature="0" /> + </g> + <g + aria-label="2" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.33857802" + id="text848-0" + transform="matrix(2.953529,0,0,2.953529,-571.73401,-518.44485)"> + <path + d="m 377.41522,365.23938 h -95.808 v -20.16 l 34.368,-34.752 q 10.368,-10.752 16.704,-17.856 6.336,-7.296 9.216,-13.056 2.88,-5.952 2.88,-12.672 0,-8.256 -4.608,-12.288 -4.416,-4.032 -12.096,-4.032 -7.872,0 -15.36,3.648 -7.488,3.648 -15.744,10.368 l -15.744,-18.624 q 5.952,-5.184 12.48,-9.6 6.72,-4.416 15.36,-7.104 8.832,-2.88 21.12,-2.88 13.44,0 23.04,4.992 9.792,4.8 14.976,13.248 5.376,8.256 5.376,18.816 0,11.328 -4.608,20.736 -4.416,9.408 -13.056,18.624 -8.448,9.216 -20.544,20.352 l -17.664,16.512 v 1.344 h 59.712 z" + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:192px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Bold';stroke-width:0.33857802" + id="path913" + inkscape:connector-curvature="0" /> + </g> + <g + aria-label="3" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.33857802" + id="text848-06" + transform="matrix(2.953529,0,0,2.953529,-548.44231,-518.44485)"> + <path + d="m 461.29773,310.06129 q 0,14.208 -8.64,22.656 -8.448,8.448 -20.928,11.52 v 0.576 q 16.512,1.92 24.96,9.984 8.64,8.064 8.64,21.696 0,11.904 -5.952,21.504 -5.76,9.408 -18.048,14.976 -12.096,5.376 -31.296,5.376 -22.272,0 -39.552,-7.488 v -24.576 q 8.832,4.416 18.432,6.72 9.792,2.304 18.048,2.304 15.552,0 21.696,-5.376 6.336,-5.376 6.336,-15.168 0,-5.76 -2.88,-9.6 -2.88,-4.032 -10.176,-5.952 -7.104,-2.112 -19.968,-2.112 h -10.368 v -22.272 h 10.56 q 12.672,0 19.2,-2.304 6.72,-2.496 9.024,-6.528 2.496,-4.224 2.496,-9.6 0,-7.296 -4.608,-11.328 -4.416,-4.224 -14.976,-4.224 -9.792,0 -17.088,3.456 -7.104,3.264 -12.096,6.528 l -13.44,-19.968 q 8.064,-5.76 18.816,-9.6 10.944,-3.84 25.92,-3.84 21.12,0 33.408,8.64 12.48,8.448 12.48,24 z" + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:192px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Bold';stroke-width:0.33857802" + id="path916" + inkscape:connector-curvature="0" /> + </g> + <g + aria-label="." + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.33857802" + id="text848-2" + transform="matrix(2.953529,0,0,2.953529,-560.3183,-518.44485)"> + <path + d="m 469.74505,405.29329 q 0,-7.104 3.456,-9.984 3.456,-2.88 8.256,-2.88 4.992,0 8.448,2.88 3.648,2.88 3.648,9.984 0,6.912 -3.648,9.984 -3.456,3.072 -8.448,3.072 -4.8,0 -8.256,-3.072 -3.456,-3.072 -3.456,-9.984 z" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';stroke-width:0.33857802" + id="path919" + inkscape:connector-curvature="0" /> + </g> + <g + aria-label="." + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.33857802" + id="text848-2-8" + transform="matrix(2.953529,0,0,2.953529,-560.3183,-518.44485)"> + <path + d="m 500.73684,405.29329 q 0,-7.104 3.456,-9.984 3.456,-2.88 8.256,-2.88 4.992,0 8.448,2.88 3.648,2.88 3.648,9.984 0,6.912 -3.648,9.984 -3.456,3.072 -8.448,3.072 -4.8,0 -8.256,-3.072 -3.456,-3.072 -3.456,-9.984 z" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';stroke-width:0.33857802" + id="path922" + inkscape:connector-curvature="0" /> + </g> + <g + aria-label="." + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16px;line-height:1.25;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.33857802" + id="text848-2-2" + transform="matrix(2.953529,0,0,2.953529,-560.3183,-518.44485)"> + <path + d="m 530.41213,405.29329 q 0,-7.104 3.456,-9.984 3.456,-2.88 8.256,-2.88 4.992,0 8.448,2.88 3.648,2.88 3.648,9.984 0,6.912 -3.648,9.984 -3.456,3.072 -8.448,3.072 -4.8,0 -8.256,-3.072 -3.456,-3.072 -3.456,-9.984 z" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:192px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';stroke-width:0.33857802" + id="path925" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/src/libs/core/music/tmelody.cpp b/src/libs/core/music/tmelody.cpp index f1ab5d45c..0bce9a2f5 100644 --- a/src/libs/core/music/tmelody.cpp +++ b/src/libs/core/music/tmelody.cpp @@ -151,15 +151,21 @@ bool Tmelody::fromXml(QXmlStreamReader& xml) { m_measures.clear(); m_meter->setMeter(Tmeter::NoMeter); setTempo(0); // reset tempo, try to read from XML + int barNr = 0; while (xml.readNextStartElement()) { /** [measure] */ if (xml.name() == QLatin1String("measure")) { - int nr = xml.attributes().value(QStringLiteral("number")).toInt(); - m_measures << Tmeasure(nr); + int tmpBarNr = xml.attributes().value(QStringLiteral("number")).toInt(); + barNr++; + if (tmpBarNr != barNr) { + qDebug() << "[Tmelody] Something wrong with measure numbers!" << barNr << "was expected, but" << tmpBarNr << "was read.\n" + << "Better check integrity of this music XML file!"; + } + m_measures << Tmeasure(barNr); while (xml.readNextStartElement()) { /** [attributes] */ if (xml.name() == QLatin1String("attributes")) { - if (nr == 1) { + if (barNr == 1) { Tclef::EclefType clef1 = Tclef::NoClef, clef2 = Tclef::NoClef; int staffCnt = 1; while (xml.readNextStartElement()) { @@ -283,8 +289,8 @@ bool Tmelody::fromXml(QXmlStreamReader& xml) { else qDebug() << "[Tmelody] Metronome beat with dot only supports quarter. Ignore dot then!"; } - int quarterTempo = tempoWillBe / Tmeter::beatTempoFactor(beatWillBe); - if (nr == 1 || tempo() == 0) { // read metronome tempo but only for 1st bar or if not yet set + int quarterTempo = Tmeter::quarterTempo(tempoWillBe, beatWillBe); + if (barNr == 1 || tempo() == 0) { // read metronome tempo but only for 1st bar or if not yet set if (quarterTempo >= 40 && quarterTempo <= 180) { setTempo(tempoWillBe); setBeat(beatWillBe); @@ -310,15 +316,15 @@ bool Tmelody::fromXml(QXmlStreamReader& xml) { else xml.skipCurrentElement(); } - if (lastMeasure().number() != m_measures.size()) { - qDebug() << "[Tmelody] Wrong measure number" << lastMeasure().number() << m_measures.size(); - } } else xml.skipCurrentElement(); } - if (tempo() == 0) - setTempo(120); + if (tempo() == 0) { + setBeat(m_meter->optimalBeat()); + setTempo(qRound(60.0 * Tmeter::beatTempoFactor(m_beat))); + qDebug() << "[Tmelody] Tempo was not read from this melody file. Set it to" << m_tempo << "with beat" << m_beat; + } return ok; } diff --git a/src/libs/core/music/tmelody.h b/src/libs/core/music/tmelody.h index 19df2ece5..080d26668 100644 --- a/src/libs/core/music/tmelody.h +++ b/src/libs/core/music/tmelody.h @@ -34,8 +34,7 @@ class TnoteStruct; /** * Class describing a musical melody - sequence of notes (Tchunk) - * Also it is able to save/load a melody into/from MusicXML structure - * Default tempo of a melody is 120 bpm. + * Also it is able to save/load a melody into/from MusicXML structure. */ class NOOTKACORE_EXPORT Tmelody { @@ -71,6 +70,16 @@ public: int tempo() const { return m_tempo; } void setTempo(int tmp) { m_tempo = tmp; } + /** + * Tempo of quarter notes (per minute), independent on beat unit + */ + int quarterTempo() const { return Tmeter::quarterTempo(m_tempo, m_beat); } + + /** + * Set both tempo and beat at once + */ + void setMetronome(int mTempo, Tmeter::EbeatUnit beatUnit) { setTempo(mTempo); setBeat(beatUnit); } + Tmeter::EbeatUnit beat() const { return m_beat; } void setBeat(Tmeter::EbeatUnit bu) { m_beat = bu; } diff --git a/src/libs/core/music/tmeter.cpp b/src/libs/core/music/tmeter.cpp index fc2767863..da99067c9 100644 --- a/src/libs/core/music/tmeter.cpp +++ b/src/libs/core/music/tmeter.cpp @@ -89,7 +89,7 @@ int Tmeter::countTo() const { case Meter_7_8: return 7; case Meter_9_8: case Meter_12_8: return 3; - default: return 2; + default: return 4; } } @@ -191,8 +191,10 @@ void Tmeter::fillMeterGroups(QList<quint8>& durationList) { Tmeter::EbeatUnit Tmeter::optimalBeat(Tmeter::Emeter m) { - if (m <= Meter_7_4) // all time signatures with quarter + if (m <= Meter_7_4) // all time signatures with quarter, also when no meter NoMeter return BeatQuarter; + if (m == Meter_6_8 || m == Meter_9_8 || m == Meter_12_8) + return BeatQuarterDot; return BeatEighth; } diff --git a/src/libs/core/music/tmeter.h b/src/libs/core/music/tmeter.h index f98817bb5..482533315 100644 --- a/src/libs/core/music/tmeter.h +++ b/src/libs/core/music/tmeter.h @@ -49,13 +49,13 @@ public: Q_INVOKABLE void setMeter(Tmeter::Emeter m) { m_meter = m; } /** - * Returns upper digit of time signature - */ + * Returns upper digit of time signature + */ Q_INVOKABLE int upper() const; /** - * Returns lower digit of time signature - */ + * Returns lower digit of time signature + */ Q_INVOKABLE int lower() const; /** @@ -65,10 +65,10 @@ public: Q_INVOKABLE QString symbol() const; /** - * Returns numeric value representing duration of single measure, - * which is based on Trhythm calculation (RVALUE) - * 3/4 is 72, 4/4 is 96 (RVALUE), etc. - */ + * Returns numeric value representing duration of single measure, + * which is based on Trhythm calculation (RVALUE) + * 3/4 is 72, 4/4 is 96 (RVALUE), etc. + */ Q_INVOKABLE int duration() const; /** @@ -80,8 +80,8 @@ public: bool fromXml(QXmlStreamReader& xml); /** - * Prints current meter to std out with given text - */ + * Prints current meter to std out with given text + */ Q_INVOKABLE void debug(const QString& text = QString()); bool operator==(const Tmeter& m) const { return meter() == m.meter(); } @@ -114,6 +114,20 @@ public: */ Q_INVOKABLE static qreal beatTempoFactor(Tmeter::EbeatUnit bu); + /** + * Tempo of quarter notes (per minute), independent on beat unit + */ + static int quarterTempo(int beatTempo, Tmeter::EbeatUnit beatUnit) { + return qRound(static_cast<qreal>(beatTempo) / beatTempoFactor(beatUnit)); + } + + /** + * Tempo of quarter notes (per minute), independent on beat unit + */ + static int quarterTempo(int beatTempo, int beatUnit) { + return Tmeter::quarterTempo(beatTempo, static_cast<EbeatUnit>(beatUnit)); + } + private: Emeter m_meter; }; diff --git a/src/libs/sound/tabstractplayer.cpp b/src/libs/sound/tabstractplayer.cpp index 2be8e896a..90f379108 100644 --- a/src/libs/sound/tabstractplayer.cpp +++ b/src/libs/sound/tabstractplayer.cpp @@ -63,17 +63,22 @@ void TplayerThread::run() { GLOB->transposition(), static_cast<int>(m_player->p_audioParams->a440diff)); m_listToPlay = nullptr; } else if (m_melodyToPlay) { // melody (from Tmelody) - for (int n = 0; n < m_melodyToPlay->length(); ++n) { // TODO: !! Where is beat unit of melody !!!!!! + qreal rateFactor = m_player->p_oggScale->sampleRate() / 1000.0; + if (m_player->p_countdownDur > 0) + playList() << TsingleSound(CNTDWN_ID, REST_NR, qRound(((m_player->p_countdownDur / 24.0) * (60000.0 / m_melodyToPlay->quarterTempo())) * rateFactor)); + for (int n = 0; n < m_melodyToPlay->length(); ++n) { const Tnote& tmpN = m_melodyToPlay->note(n)->p(); - int samplesDuration = qRound(((tmpN.duration() > 0 ? tmpN.duration() / 24.0 : 1.0) * (60000.0 / m_melodyToPlay->tempo())) * (m_player->p_oggScale->sampleRate() / 1000.0)); + int samplesDur = + qRound(((tmpN.duration() > 0 ? tmpN.duration() / 24.0 : 1.0) * (60000.0 / m_melodyToPlay->quarterTempo())) * rateFactor); if (tmpN.rtm.tie() > Trhythm::e_tieStart) { // append duration if tie is continued or at end if (playList().isEmpty()) continue; // do not start playing in the middle of tied notes - playList().last().samplesCount += samplesDuration; + playList().last().samplesCount += samplesDur; } else playList() << TsingleSound(n, - tmpN.isValid() ? tmpN.chromatic() + GLOB->transposition() + m_transposition + m_player->p_audioParams->a440diff : REST_NR, - samplesDuration); + tmpN.isValid() ? tmpN.chromatic() + GLOB->transposition() + m_transposition + m_player->p_audioParams->a440diff : REST_NR, + samplesDur + ); } m_melodyToPlay = nullptr; } @@ -93,11 +98,11 @@ void TplayerThread::preparePlayList(QList<Tnote>* notes, int tempo, int firstNot const Tnote& tmpN = notes->at(n); int samplesDuration = qRound(((tmpN.duration() > 0 ? tmpN.duration() / 24.0 : 1.0) * (60000.0 / tempo)) * (sampleRate / 1000.0)); if (tmpN.rtm.tie() > Trhythm::e_tieStart) { // append duration if tie is continued or at end - if (playList().isEmpty()) - continue; // do not start playing in the middle of tied notes + if (playList().isEmpty()) + continue; // do not start playing in the middle of tied notes playList().last().samplesCount += samplesDuration; } else - playList() << TsingleSound(n, tmpN.isValid() ? tmpN.chromatic() + transposition + a440diff : REST_NR, samplesDuration); + playList() << TsingleSound(n, tmpN.isValid() ? tmpN.chromatic() + transposition + a440diff : REST_NR, samplesDuration); } } @@ -120,6 +125,7 @@ unsigned int TabstractPlayer::p_beatPeriod = 0; unsigned int TabstractPlayer::p_beatBytes = 7984; // beat file frames number (initial, finally obtained from file) unsigned int TabstractPlayer::p_beatOffset = 0; bool TabstractPlayer::p_lastNotePlayed = false; +int TabstractPlayer::p_ticksCountBefore = 0; TabstractPlayer::TabstractPlayer(QObject* parent) : @@ -164,12 +170,13 @@ bool TabstractPlayer::playNotes(QList<Tnote>* notes, int tempo, int firstNote, i } -bool TabstractPlayer::playMelody(Tmelody* melody, int transposition) { +bool TabstractPlayer::playMelody(Tmelody* melody, int transposition, int countdownDur) { if (!p_playable) return false; m_playThreaad->wait(); m_playThreaad->setMelodyToPlay(melody); m_playThreaad->setTransposition(transposition); + p_countdownDur = countdownDur; m_playThreaad->start(); return true; } @@ -181,7 +188,7 @@ bool TabstractPlayer::playMelody(Tmelody* melody, int transposition) { * But of course, read it once, and store decoded data in array. * TODO: consider to save decoded data somewhere into cache. */ -void TabstractPlayer::runMetronome(unsigned int beatTempo) { +void TabstractPlayer::setMetronome(unsigned int beatTempo) { if (!m_beatArray) { auto stdFile = fopen(Tpath::sound("beat").toStdString().c_str(), "r"); if(!stdFile) { @@ -215,7 +222,7 @@ void TabstractPlayer::runMetronome(unsigned int beatTempo) { ov_clear(&oggFile); } p_beatOffset = 0; - p_beatPeriod = beatTempo ? (44100 * 60) / beatTempo : 0; + p_beatPeriod = beatTempo ? (44100 * 60) / beatTempo : 0; //FIXME what if sample rate is 48000Hz? } @@ -238,3 +245,8 @@ void TabstractPlayer::setTickDuringPlay(bool tdp) { // TODO wait for busy callback to avoid cracks p_audioParams->audibleMetro = tdp; } + + +bool TabstractPlayer::doTicking() const { + return p_audioParams && (p_audioParams->audibleMetro || p_audioParams->countBefore); +} diff --git a/src/libs/sound/tabstractplayer.h b/src/libs/sound/tabstractplayer.h index dbf143667..03ca7888f 100644 --- a/src/libs/sound/tabstractplayer.h +++ b/src/libs/sound/tabstractplayer.h @@ -133,7 +133,7 @@ public: bool playNote(int noteNr); bool playNotes(QList<Tnote>* notes, int tempo, int firstNote = 0, int countdownDur = 0); - bool playMelody(Tmelody* melody, int transposition = 0); + bool playMelody(Tmelody* melody, int transposition = 0, int countdownDur = 0); enum EplayerType { e_audio, e_midi }; @@ -149,7 +149,7 @@ public: */ int playingNoteId() const { return p_playingNoteId; } - void runMetronome(unsigned int beatTempo); + void setMetronome(unsigned int beatTempo); void stopMetronome(); qint16 getBeatsample(unsigned int sampleNr) const { return m_beatArray[sampleNr]; } @@ -159,6 +159,17 @@ public: bool tickDuringPlay() const; void setTickDuringPlay(bool tdp); + /** + * Number of metronome ticks before melody will be played. + * Also before audio input will be processed (pitch detection started). + * It counts down, so becomes null when done, + * so has to be set every time it is needed. + */ + static int ticksCountBefore() { return p_ticksCountBefore; } + static void setTicksCountBefore(int tcb) { p_ticksCountBefore = tcb; } + + bool doTicking() const; + signals: void playingStarted(); @@ -202,6 +213,7 @@ protected: static unsigned int p_beatBytes; /**< Number of bytes in single beat sample */ static unsigned int p_beatOffset; /**< Callback position in beat period */ static bool p_lastNotePlayed; /**< @p TRUE set in callback only when last note just has been played */ + static int p_ticksCountBefore; /**< Number of metronome ticks before playing melody or sniffing */ private: TplayerThread *m_playThreaad; diff --git a/src/libs/sound/trtaudioout.cpp b/src/libs/sound/trtaudioout.cpp index 054d6e9ad..70ce70153 100755 --- a/src/libs/sound/trtaudioout.cpp +++ b/src/libs/sound/trtaudioout.cpp @@ -55,6 +55,7 @@ TaudioOUT* TaudioOUT::instance = nullptr; #define CROSS_SMP (2200) // 50ms +#define INVALID_NOTE_NR (-100) /** @@ -79,16 +80,16 @@ bool TaudioOUT::outCallBack(void* outBuff, unsigned int nBufferFrames, const RtA // qDebug() << "[trtaudioout] Stream underflow detected!"; bool endState = true; - if (!instance->playList().isEmpty() && p_playingNoteNr < instance->playList().size()) { + if (!instance->playList().isEmpty() && p_playingNoteNr < instance->playList().size() && p_ticksCountBefore == 0) { TsingleSound& playingSound = instance->playList()[p_playingNoteNr]; auto out = static_cast<qint16*>(outBuff); bool unfinished = true; qint16 sample = 0; for (int i = 0; i < nBufferFrames / instance->ratioOfRate; i++) { if (p_posInNote >= playingSound.samplesCount) { - p_prevNote = playingSound.number == REST_NR ? -100 : playingSound.number; + p_prevNote = playingSound.number == REST_NR || p_posInOgg > 61740 ? INVALID_NOTE_NR : playingSound.number; p_shiftOfPrev = 0; - p_lastPosOfPrev = p_posInNote; + p_lastPosOfPrev = p_posInOgg; // < 61740 ? p_posInNote : 0; p_playingNoteNr++; if (p_playingNoteNr < instance->playList().size()) { p_posInOgg = 0; @@ -122,13 +123,13 @@ bool TaudioOUT::outCallBack(void* outBuff, unsigned int nBufferFrames, const RtA } p_posInOgg++; } - if (unfinished && p_prevNote > -100 && p_shiftOfPrev < CROSS_SMP) { // fade out previous note, and mix it with the current one + if (unfinished && p_prevNote > INVALID_NOTE_NR && p_shiftOfPrev < CROSS_SMP) { // fade out previous note, and mix it with the current one qint16 sample2 = instance->oggScale->getNoteSample(p_prevNote, p_lastPosOfPrev + p_shiftOfPrev); sample2 = static_cast<qint16>(static_cast<qreal>(sample2) * (static_cast<qreal>(CROSS_SMP - p_shiftOfPrev) / 2200.0)); sample = mix(sample, sample2); p_shiftOfPrev++; if (p_shiftOfPrev == CROSS_SMP) - p_prevNote = -100; + p_prevNote = INVALID_NOTE_NR; } qint16 beatSample = 0; if (instance->tickDuringPlay() && p_beatPeriod) { @@ -149,13 +150,12 @@ bool TaudioOUT::outCallBack(void* outBuff, unsigned int nBufferFrames, const RtA } instance->m_callBackIsBussy = false; endState = p_playingNoteNr >= instance->playList().size(); - } else { // flush buffer with zeros if no sound will be played -// auto out = static_cast<qint32*>(outBuff); // 4 bytes for both channels at once + } else { // if no sound will be played: flush buffer with zeros or play a tick auto out = static_cast<qint16*>(outBuff); qint16 beatSample = 0; for (int i = 0; i < nBufferFrames / instance->ratioOfRate; i++) { beatSample = 0; - if (instance->tickDuringPlay() && p_beatPeriod) { + if (p_beatPeriod && ((instance->tickBeforePlay() && p_ticksCountBefore > 0) || instance->tickDuringPlay())) { if (p_beatOffset < p_beatBytes) beatSample = instance->getBeatsample(p_beatOffset); p_beatOffset++; @@ -165,16 +165,19 @@ bool TaudioOUT::outCallBack(void* outBuff, unsigned int nBufferFrames, const RtA p_lastNotePlayed = false; p_beatPeriod = 0; } + if (p_ticksCountBefore > 0) + p_ticksCountBefore--; + else if (!instance->tickDuringPlay()) + p_beatPeriod = 0; } } for (int r = 0; r < instance->ratioOfRate; r++) { -// *out++ = 0; // both channels at once channel *out++ = beatSample; // left channel *out++ = beatSample; // right channel } } instance->m_callBackIsBussy = false; - endState = true; + endState = p_ticksCountBefore == 0 && (instance->playList().isEmpty() || p_playingNoteNr >= instance->playList().size()); } if (instance->p_doEmit && !areStreamsSplit() && endState) { ao()->emitPlayingFinished(); // emit in duplex mode @@ -273,7 +276,7 @@ void TaudioOUT::startPlaying() { // if (loops) qDebug() << "latency:" << loops << "ms"; } - if (p_prevNote > -100) { + if (p_prevNote > INVALID_NOTE_NR) { p_shiftOfPrev = 0; p_lastPosOfPrev = p_posInNote; } @@ -303,6 +306,7 @@ void TaudioOUT::playingFinishedSlot() { if (areStreamsSplit() && state() == e_playing) closeStream(); + p_lastNotePlayed = false; p_isPlaying = false; setPlayCallbackInvolved(false); emit playingFinished(); @@ -312,7 +316,7 @@ void TaudioOUT::playingFinishedSlot() { void TaudioOUT::stop() { if (m_callBackIsBussy) { qDebug() << "[TrtAudioOUT] Stopping when outCallBack is running. Wait 2ms!"; - QTimer::singleShot(2, [=]{ this->stop(); }); + QTimer::singleShot(2, this, [=]{ this->stop(); }); } /** @@ -330,10 +334,11 @@ void TaudioOUT::stop() { QTimer::singleShot(50, [=]{ this->stop(); }); return; } - p_prevNote = -100; + p_prevNote = INVALID_NOTE_NR; p_shiftOfPrev = 0; p_lastPosOfPrev = 0; p_isPlaying = false; + p_ticksCountBefore = 0; if (areStreamsSplit() /*|| getCurrentApi() == RtAudio::LINUX_PULSE*/) closeStream(); else diff --git a/src/libs/sound/trtaudioout.h b/src/libs/sound/trtaudioout.h index b22d87c39..c2564bc44 100755 --- a/src/libs/sound/trtaudioout.h +++ b/src/libs/sound/trtaudioout.h @@ -63,7 +63,7 @@ protected: #if defined(Q_OS_WIN) void ASIORestartSlot(); #endif - + protected: static TaudioOUT *instance; /**< Static pointer of this class instance. */ @@ -71,8 +71,8 @@ protected: int ratioOfRate; /**< ratio of current sample rate to 44100 */ private: - bool m_callBackIsBussy; - bool m_singleNotePlayed = false; + bool m_callBackIsBussy; + bool m_singleNotePlayed = false; }; diff --git a/src/libs/sound/tsound.cpp b/src/libs/sound/tsound.cpp index cfb42c388..6730fb49c 100644 --- a/src/libs/sound/tsound.cpp +++ b/src/libs/sound/tsound.cpp @@ -94,7 +94,7 @@ void Tsound::play(const Tnote& note) { bool playing = true; if (player && note.isValid()) { m_stopSniffOnce = true; - player->runMetronome(0); // reset metronome + stopMetronome(); playing = player->playNote(note.chromatic()); } #if defined (Q_OS_ANDROID) @@ -119,14 +119,14 @@ void Tsound::play(const Tnote& note) { } -void Tsound::playMelody(Tmelody* mel, int transposition) { +void Tsound::playMelody(Tmelody* mel, int transposition, int countdownDuration) { if (player && player->isPlayable()) { if (player->isPlaying()) { stopPlaying(); } else { if (mel->length()) { m_stopSniffOnce = true; - player->playMelody(mel, transposition); + player->playMelody(mel, transposition, 0); } } } @@ -137,10 +137,10 @@ void Tsound::playNoteList(QList<Tnote>& notes, int firstNote, int countdownDurat if (player) { if (!player->isPlaying()) { if (!notes.isEmpty()) { - player->runMetronome(m_tempo); + runMetronome(firstNote == 0 && tickBeforePlay() ? Tmeter(static_cast<Tmeter::Emeter>(m_currentMeter)).countTo() : 0); m_stopSniffOnce = true; player->playNotes(std::addressof(notes), // beat unit has to be converted to quarter here - qRound(static_cast<qreal>(m_tempo) / Tmeter::beatTempoFactor(static_cast<Tmeter::EbeatUnit>(m_beatUnit))), + Tmeter::quarterTempo(m_tempo, m_beatUnit), firstNote, countdownDuration); } @@ -260,7 +260,6 @@ float Tsound::pitch() { void Tsound::setTempo(int t) { if (t != m_tempo && t > 39 && t < 181) { - //m_tempo = qRound(15000.0 / (qRound(15000.0 / static_cast<qreal>(t) / sniffer->chunkTime()) * sniffer->chunkTime())); m_tempo = t; emit tempoChanged(); } @@ -268,17 +267,27 @@ void Tsound::setTempo(int t) { void Tsound::setBeatUnit(int bu) { - if (bu != m_beatUnit) { - m_beatUnit = bu; - m_tempo *= Tmeter::beatTempoFactor(static_cast<Tmeter::EbeatUnit>(bu)); - emit tempoChanged(); + if (bu < 0 || bu > 3) + qDebug() << "[Tsound] FIXME! trying to set unsupported beat unit" << bu; + else { + if (bu != m_beatUnit) { + m_beatUnit = bu; + m_tempo *= Tmeter::beatTempoFactor(static_cast<Tmeter::EbeatUnit>(bu)); + emit tempoChanged(); + } } } +void Tsound::setCurrentMeter(int curMet) { + m_currentMeter = curMet; +} + + + void Tsound::setMetronome(int t, int beat) { if (beat != m_beatUnit || t != m_tempo) { - int quarterTempo = t / Tmeter::beatTempoFactor(static_cast<Tmeter::EbeatUnit>(beat)); + int quarterTempo = Tmeter::quarterTempo(t, beat); if (quarterTempo >= 40 && quarterTempo <= 180) { m_tempo = t; m_beatUnit = beat; @@ -288,6 +297,24 @@ void Tsound::setMetronome(int t, int beat) { } +void Tsound::runMetronome(int preTicksNr) { + if (player && !m_metronomeIsRun && player->doTicking()) { + player->setMetronome(m_tempo); + if (player->tickBeforePlay() && preTicksNr) { + qreal preTicksSeconds = static_cast<qreal>(preTicksNr) * (60.0 / static_cast<qreal>(Tmeter::quarterTempo(m_tempo, m_beatUnit))); + while (preTicksSeconds < 2.0) { // Multiple number of pre-ticks if it is to short (less than 2 sec) - to give user time to catch up + preTicksNr += preTicksNr; + preTicksSeconds += preTicksSeconds; + } + player->setTicksCountBefore(preTicksNr /** Tmeter::beatTempoFactor(static_cast<Tmeter::EbeatUnit>(m_beatUnit))*/); + emit countdownPrepare(preTicksNr); + } + m_metronomeIsRun = true; + emit metroRunningChanged(); + } +} + + /** * @p m_quantVal is expressed in @p Trhythm duration of: Sixteenth triplet -> 4 or just Sixteenth -> 6 or Eighth -> 12 */ @@ -324,15 +351,14 @@ bool Tsound::playing() const { void Tsound::stopListen() { if (sniffer) sniffer->stopListening(); - if (player) - player->stopMetronome(); + stopMetronome(); } void Tsound::startListen() { if (sniffer) { - if (player && player->tickDuringPlay()) - player->runMetronome(qRound(static_cast<qreal>(m_tempo) / Tmeter::beatTempoFactor(static_cast<Tmeter::EbeatUnit>(m_beatUnit)))); + if (!sniffer->stoppedByUser()) + runMetronome(Tmeter(static_cast<Tmeter::Emeter>(m_currentMeter)).countTo()); sniffer->startListening(); } } @@ -524,6 +550,7 @@ void Tsound::playingFinishedSlot() { } emit plaingFinished(); emit playingChanged(); + stopMetronome(); } @@ -541,7 +568,7 @@ void Tsound::noteFinishedSlot(const TnoteStruct& note) { if (note.pitch.isValid()) m_detectedNote = note.pitch; if (GLOB->rhythmsEnabled()) { - qreal rFactor = 2500.0 / (m_tempo / Tmeter::beatTempoFactor(static_cast<Tmeter::EbeatUnit>(m_beatUnit))); + qreal rFactor = 2500.0 / Tmeter::quarterTempo(m_tempo, m_beatUnit); qreal dur = (note.duration * 1000.0) / rFactor; int quant = dur > 20.0 ? 12 : 6; // avoid sixteenth dots int normDur = qRound(dur / static_cast<qreal>(quant)) * quant; @@ -595,3 +622,13 @@ void Tsound::selectNextNote() { NOO->selectPlayingNote(player->playingNoteId()); emit playingNoteIdChanged(); } + + +void Tsound::stopMetronome() { + if (m_metronomeIsRun) { + if (player) + player->stopMetronome(); + m_metronomeIsRun = false; + emit metroRunningChanged(); + } +} diff --git a/src/libs/sound/tsound.h b/src/libs/sound/tsound.h index 90aebcddd..77633fc94 100644 --- a/src/libs/sound/tsound.h +++ b/src/libs/sound/tsound.h @@ -40,10 +40,10 @@ class TaudioIN; * @class Tsound is a wrapper of @p TaudioIN & @p TaudioOUT classes * to manage them. It enables/disables them depends on @p Tglobals, * pauses sniffing when playback is proceeding. - * Also it has got @p TpitchView to show volume meter & pitch detection state. * * It has single instance available through @p instance() - * defined also as a macro @p SOUND + * defined also as a macro @p SOUND, also available from QML under this name. + * It exposes needed properties and methods to QML */ class NOOTKASOUND_EXPORT Tsound : public QObject { @@ -61,6 +61,7 @@ class NOOTKASOUND_EXPORT Tsound : public QObject Q_PROPERTY(bool tickBeforePlay READ tickBeforePlay WRITE setTickBeforePlay NOTIFY tickStateChanged) Q_PROPERTY(bool tickDuringPlay READ tickDuringPlay WRITE setTickDuringPlay NOTIFY tickStateChanged) Q_PROPERTY(int playingNoteId READ playingNoteId NOTIFY playingNoteIdChanged) + Q_PROPERTY(bool metroRunning READ metroRunning NOTIFY metroRunningChanged) friend class TtunerDialogItem; @@ -81,7 +82,7 @@ public: Q_INVOKABLE void play(const Tnote& note); - void playMelody(Tmelody* mel, int transposition = 0); + void playMelody(Tmelody* mel, int transposition = 0, int countdownDuration = 0); void playNoteList(QList<Tnote>& notes, int firstNote, int countdownDuration = 0); @@ -136,8 +137,24 @@ public: int beatUnit() const { return m_beatUnit; } void setBeatUnit(int bu); + /** + * Currently set meter in main score. + * Due to main score lays above sound in libraries hierarchy, + * main score controls it when its meter changes + */ + Q_INVOKABLE int currentMeter() const { return m_currentMeter; } + Q_INVOKABLE void setCurrentMeter(int curMet); + Q_INVOKABLE void setMetronome(int t, int beat); + /** + * Runs metronome routines placed in @p player but only when @p tickBeforePlay or @p tickDuringPlay are set. + * If @p preTicksNr is set and @p tickBeforePlay is enabled, + * calculates tick number to countdown before playing or listening. + * Emits @p countdownPrepare(int preTicksNr) to initialize QML part of pre-ticking + */ + void runMetronome(int preTicksNr = 0); + /** * Quantization value determines accuracy of detecting rhythm of played note by its duration. */ @@ -164,6 +181,13 @@ public: int playingNoteId() const; + /** + * Property about state of a metronome. + * It is automatically initialized when @p runMetronome() is invoked + * and disabled when playing and listening are not performed + */ + bool metroRunning() { return m_metronomeIsRun; } + /** * Prepares sound to exam. * Given notes in params are level range notes and are put to sniffer ambitus. @@ -183,7 +207,6 @@ public: bool tunerMode() const { return m_tunerMode; } void setTunerMode(bool isTuner); - #if !defined (Q_OS_ANDROID) void setDumpFileName(const QString& fName); #endif @@ -202,6 +225,8 @@ signals: void tunerModeChanged(); void tickStateChanged(); void playingNoteIdChanged(); + void metroRunningChanged(); + void countdownPrepare(int tickCount); /** * When sound got initialized at the very beginning @@ -215,6 +240,8 @@ private: void deleteSniffer(); void restoreSniffer(); /**< Brings back sniffer & pitch view state as such as before settings dialog */ + void stopMetronome(); + Tnote m_detectedNote; /**< detected note */ bool m_examMode = false; bool m_tunerMode = false; @@ -223,7 +250,9 @@ private: Tmelody *m_playedMelody; int m_tempo; int m_beatUnit = 0; /**< corresponds with Tmeter::EbeatUnit enum. Quarter by default */ + int m_currentMeter = 0; int m_quantVal; + bool m_metronomeIsRun = false; static Tsound *m_instance; diff --git a/src/main/texamexecutor.cpp b/src/main/texamexecutor.cpp index 7495b1c87..b8d34fd8a 100644 --- a/src/main/texamexecutor.cpp +++ b/src/main/texamexecutor.cpp @@ -323,7 +323,9 @@ void TexamExecutor::askQuestion(bool isAttempt) { if (!m_level.isMelodySet() && m_level.useRhythms()) { mergeRhythmAndMelody(rhythms, curQ->melody()); } - curQ->melody()->setTempo(SOUND->tempo()); + // So far we don't force tempo, just adjust it to user currently set + // We can change melody tempo due to this melody is never write back + curQ->melody()->setTempo(Tmeter::quarterTempo(SOUND->tempo(), SOUND->beatUnit()) * Tmeter::beatTempoFactor(curQ->melody()->beat())); } m_melody->newMelody(curQ->answerAsSound() ? curQ->melody()->length() : 0); // prepare list to store notes played by user or clear it m_exam->newAttempt(); @@ -548,7 +550,7 @@ void TexamExecutor::askQuestion(bool isAttempt) { void TexamExecutor::checkAnswer(bool showResults) { - TQAunit* curQ = m_exam->curQ(); + auto curQ = m_exam->curQ(); m_penalty->stopQuestionTime(); m_checkQuestAct->setEnabled(false); if (m_playAgainAct) @@ -1064,10 +1066,6 @@ void TexamExecutor::prepareToExam() { m_glStore->prepareGlobalsToExam(m_level); GLOB->setRhythmsEnabled(m_level.useRhythms()); - -// #if !defined (Q_OS_ANDROID) // Do not show it user Android - it sucks there -// SOUND->pitchView()->setVisible(GLOB->L->soundViewEnabled); -// #endif // INSTRUMENT->setVisible(GLOB->L->guitarEnabled); if (m_level.canBeSound()) { SOUND->acceptSettings(); @@ -1526,10 +1524,13 @@ QString TexamExecutor::saveExamToFile() { void TexamExecutor::repeatSound() { if (m_exam->curQ()->melody()) { + int nrTicksBefore = SOUND->tickBeforePlay() ? m_exam->curQ()->melody()->meter()->countTo() : 0; + SOUND->runMetronome(nrTicksBefore); SOUND->playMelody(m_exam->curQ()->melody(), - m_exam->curQ()->melody()->key() != m_exam->curQ()->key ? m_exam->curQ()->melody()->key().difference(m_exam->curQ()->key) : 0); - if (SOUND->melodyIsPlaying()) // the same methods stops a melody - m_exam->curQ()->lastAttempt()->melodyWasPlayed(); // increase only when playing was started + m_exam->curQ()->melody()->key() != m_exam->curQ()->key ? m_exam->curQ()->melody()->key().difference(m_exam->curQ()->key) : 0 + ); + if (SOUND->melodyIsPlaying()) // the same method can stop a melody + m_exam->curQ()->lastAttempt()->melodyWasPlayed(); // so increase only when playing was started } else SOUND->play(m_exam->curQ()->qa.note); connectPlayingFinished(); @@ -1857,10 +1858,10 @@ TtipHandler* TexamExecutor::tipHandler() { return m_tipHandler; } bool TexamExecutor::showPitchView() const { - return m_exam == nullptr || (m_exam->count() && m_exam->curQ()->answerAsSound()); + return m_exam && m_exam->count() && m_exam->curQ()->answerAsSound(); } -bool TexamExecutor::showPlayView() const { - return false; +bool TexamExecutor::showRtmView() const { + return m_exam && m_exam->count() && (m_exam->curQ()->answerAsSound() || m_exam->curQ()->questionAsSound()) && m_level.useRhythms(); } diff --git a/src/main/texamexecutor.h b/src/main/texamexecutor.h index 77a370da7..350720deb 100644 --- a/src/main/texamexecutor.h +++ b/src/main/texamexecutor.h @@ -57,7 +57,7 @@ class TexamExecutor : public QQuickItem Q_PROPERTY(TtipHandler* tipHandler READ tipHandler NOTIFY tipHandlerCreated) Q_PROPERTY(bool isExercise READ isExercise) Q_PROPERTY(bool showPitchView READ showPitchView NOTIFY questionChanged) - Q_PROPERTY(bool showPlayView READ showPlayView NOTIFY questionChanged) + Q_PROPERTY(bool showRtmView READ showRtmView NOTIFY questionChanged) friend class TexamSummary; friend class TnootkaCertificate; @@ -75,7 +75,7 @@ public: TtipHandler* tipHandler(); bool showPitchView() const; - bool showPlayView() const; + bool showRtmView() const; /** * Describes reason of starting executor diff --git a/src/main/tmainscoreobject.cpp b/src/main/tmainscoreobject.cpp index ba488a02c..4ef0f4fee 100644 --- a/src/main/tmainscoreobject.cpp +++ b/src/main/tmainscoreobject.cpp @@ -166,6 +166,8 @@ void TmainScoreObject::setScoreObject(TscoreObject* scoreObj) { } }); connect(m_scoreObj, &TscoreObject::stavesHeightChanged, this, &TmainScoreObject::checkExtraStaves); + connect(m_scoreObj, &TscoreObject::meterChanged, this, [=]{ SOUND->setCurrentMeter(m_scoreObj->meterToInt()); }); + SOUND->setCurrentMeter(m_scoreObj->meterToInt()); } @@ -547,8 +549,6 @@ void TmainScoreObject::playScoreSlot() { if (m_scoreObj->selectedItem() && m_scoreObj->selectedItem()->index() > 0) countDownDur = m_scoreObj->selectedItem()->measure()->durationBefore(m_scoreObj->selectedItem()); } - if (SOUND->tickBeforePlay() && countDownDur == 0) // do not count before when playing starts in the middle of a bar - countDownDur += m_scoreObj->meter()->duration(); SOUND->playNoteList(m_scoreObj->noteList(), m_scoreObj->selectedItem() ? m_scoreObj->selectedItem()->index() : 0, countDownDur); } diff --git a/src/nootka.qrc b/src/nootka.qrc index 09c49dfcb..1eaee0530 100644 --- a/src/nootka.qrc +++ b/src/nootka.qrc @@ -45,6 +45,7 @@ <file alias="sound/TempoMenu.qml">qml/sound/TempoMenu.qml</file> <file alias="sound/IntonationBar.qml">qml/sound/IntonationBar.qml</file> <file alias="sound/TunerDialog.qml">qml/sound/TunerDialog.qml</file> + <file alias="sound/CountdownItem.qml">qml/sound/CountdownItem.qml</file> <file alias="score/Score.qml">qml/score/Score.qml</file> <file alias="score/Staff.qml">qml/score/Staff.qml</file> diff --git a/src/qml/about/AboutPage.qml b/src/qml/about/AboutPage.qml index ec6e09599..f018bb5cd 100644 --- a/src/qml/about/AboutPage.qml +++ b/src/qml/about/AboutPage.qml @@ -55,7 +55,7 @@ Tflickable { Also there is a new look and a layout and there are new instruments (piano, saxophone and bandoneon) along with guitars.<br> <br> For more details about news in this release take a look at 'Changes' page.<br> - Also <a href=\"https://sourceforge.net/p/nootka/hg/ci/default/tree/TODO\">here is a link to TODO list</a> to explain what is planed next.<br> + Also <a href=\"https://sourceforge.net/p/nootka/hg/ci/default/tree/TODO.md\">here is a link to TODO list</a> to explain what is planed next.<br> <br> Anyway, main purpose of this release is to check all of that and give some feedback.<br> So happy testing,<br> diff --git a/src/qml/exam/ExamExecutor.qml b/src/qml/exam/ExamExecutor.qml index 75fd5bb83..876ad8a41 100644 --- a/src/qml/exam/ExamExecutor.qml +++ b/src/qml/exam/ExamExecutor.qml @@ -24,7 +24,7 @@ Texecutor { onShowSettings: { if (!examSettDialog) { var e = Qt.createComponent("qrc:/exam/ExamSettingsDialog.qml") - examSettDialog = e.createObject(executor, { "mode": isExercise ? 2 : 1 } ) + examSettDialog = e.createObject(executor, { "mode": isExercise ? 2 : 1 } ) examSettDialog.accepted.connect(settingsAccepted) examSettDialog.closed.connect(function() { examSettDialog.destroy() }) } diff --git a/src/qml/sound/CountdownItem.qml b/src/qml/sound/CountdownItem.qml new file mode 100644 index 000000000..485d07310 --- /dev/null +++ b/src/qml/sound/CountdownItem.qml @@ -0,0 +1,54 @@ +/** This file is part of Nootka (http://nootka.sf.net) * + * Copyright (C) 2019 by Tomasz Bojczuk (seelook@gmail.com) * + * on the terms of GNU GPLv3 license (http://www.gnu.org/licenses) */ + +import QtQuick 2.9 + +import "../" + +TipRect { + id: cntDwnItem + + property int tickCount: 0 + + width: row.width + Noo.fontSize() * 4; height: nootkaWindow.height / 10 + x: height * 2; y: Noo.fontSize() / 2 + z: 100 + color: Qt.tint(activPal.text, Noo.alpha("#00a0a0", 100)) + visible: counter <= tickCount && (SOUND.listening || SOUND.playing) + + // private + property int counter: 1 + + Connections { + target: tempoBar + onCntChanged: counter++ + } + +// Connections { +// target: SOUND +// onMetroRunningChanged: { +// if (SOUND.metroRunning) +// counter = 1 +// } +// } + + Row { + id: row + anchors.centerIn: parent + spacing: Noo.fontSize() * 2 + Repeater { + id: cntRep + model: tickCount + Text { + y: height * 0.07 + color: index + 1 === counter ? activPal.highlight : activPal.base + text: index % tempoBar.countTo + 1 + font { pixelSize: cntDwnItem.height * 0.8; family: "Scorek" } + } + } + } + +} + + diff --git a/src/qml/sound/PitchView.qml b/src/qml/sound/PitchView.qml index edba15446..af59e0c2c 100644 --- a/src/qml/sound/PitchView.qml +++ b/src/qml/sound/PitchView.qml @@ -19,15 +19,13 @@ Item { height: parent.height * 0.9 width: parent.width * 0.4 - visible: !executor || executor.showPitchView - // protected property real tickWidth: Screen.pixelDensity * 0.5 property real tickGap: tickWidth * 1.4 TempoBar { id: tempoBar - visible: !GLOB.singleNoteMode && GLOB.rhythmsEnabled + visible: !GLOB.singleNoteMode && GLOB.rhythmsEnabled && (!executor || executor.showRtmView) y: parent.height * 0.05 width: parent.width height: parent.height * 0.45 @@ -35,6 +33,7 @@ Item { VolumeBar { id: volBar + visible: !executor || executor.showPitchView y: parent.height * (tempoBar.visible ? 0.55 : 0.27) width: parent.width height: parent.height * 0.45 diff --git a/src/qml/sound/TempoBar.qml b/src/qml/sound/TempoBar.qml index 43676cd37..9289a6366 100644 --- a/src/qml/sound/TempoBar.qml +++ b/src/qml/sound/TempoBar.qml @@ -18,114 +18,154 @@ Item { property var hArray: [ 0.6, 0, 0.3, 0, 0.6] property var gArray: [ "\ue1d5", "\ue1d9", "\ue1d7", "\ue1d9", "\ue1d5" ] property int countTo: Noo.meter(score.meter).countTo() + property var preCountItem: null // protected property var beatModel: [ "\ue1d5", "\ue1d7", "\ue1d5 \ue1e7", "\ue1d3" ] + property real contW: metroText.width + cntBeforeBut.width + metroRow.width + loudTickButt.width + tunerButt.width - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: Noo.setStatusTip(qsTr("Metronome"), Item.TopLeft) - onExited: Noo.setStatusTip("", Item.TopLeft) - } - - RectButton { - id: metroText - forcedHeight: parent.height; yOffset: parent.height * -0.7 - statusTip: qsTr("Tempo") - font { family: "Scorek"; pixelSize: parent.height * 0.7 } - text: beatModel[SOUND.beatUnit] + "=" + SOUND.tempo - onClicked: { - if (!tMenu) { - var c = Qt.createComponent("qrc:/sound/TempoMenu.qml") - tMenu = c.createObject(tempoBar) + Row { + width: parent.width; height: parent.height + spacing: (width - contW) / 4 - 1 + RectButton { + id: metroText + forcedHeight: parent.height; yOffset: parent.height * -0.7 + statusTip: qsTr("Tempo") + font { family: "Scorek"; pixelSize: parent.height * 0.7 } + text: beatModel[SOUND.beatUnit] + "=" + SOUND.tempo + onClicked: { + if (!tMenu) { + var c = Qt.createComponent("qrc:/sound/TempoMenu.qml") + tMenu = c.createObject(tempoBar) + } + tMenu.open() } - tMenu.open() } - } - RectButton { - x: metroText.width + Noo.fontSize() - font { family: "Nootka"; pixelSize: parent.height } - text: "\u018f" - statusTip: qsTr("Audible metronome.<br>Use earphones! Otherwise ticking will disturb proper pitch detection!") - checked: SOUND.tickDuringPlay - onClicked: SOUND.tickDuringPlay = !SOUND.tickDuringPlay - } + RectButton { + id: cntBeforeBut + enabled: !SOUND.metroRunning + font { family: "Nootka"; pixelSize: parent.height } + text: "\u0190" + statusTip: qsTr("Countdown before playing or listening.") + checked: SOUND.tickBeforePlay + textColor: checked ? "#00a0a0" : activPal.text + onClicked: SOUND.tickBeforePlay = !SOUND.tickBeforePlay + } - Repeater { - id: rep - model: 5 - Rectangle { - readonly property color bgColor: Qt.tint(activPal.window, Noo.alpha(activPal.base, 100)) - x: tempoBar.width / 3 + index * (parent.height) - width / 2 - y: parent.height * (Math.abs(0.6 - hArray[index]) / 5) - width: parent.height * (0.9 - (Math.abs(0.6 - hArray[index]) / 3)); height: parent.height * 0.8 + hArray[index] * parent.height - radius: width / 2 - color: index === hiTick && timer.running ? (index === 0 || index === 4 ? activPal.highlight : activPal.text) : bgColor - rotation: (index - 2) * 30 - visible: SOUND.tempo < 110 || index % 2 !== 1 - Text { // rhythm - visible: index !== hiTick || !SOUND.listening - font { pixelSize: parent.height * 0.8; family: "Scorek" } - color: disdPal.text - text: gArray[index] - y: parent.height * -0.85 - x: (parent.width - width) / 2 - } - Text { // count - visible: pitchView.active && index === hiTick && (!tMenu || (tMenu.count && tMenu.tickEnable)) - font { pixelSize: parent.height; family: "Scorek" } - color: activPal.base - text: cnt - y: parent.height * -1.2 - x: (parent.width - width) / 2 + Row { + id: metroRow + enabled: !tMenu || tMenu.enableMetronome + height: tempoBar.height + spacing: height / (SOUND.tempo < 110 ? 5 : 2) + Repeater { + id: rep + model: 5 + Rectangle { + readonly property color bgColor: Qt.tint(activPal.window, Noo.alpha(activPal.base, 100)) + y: parent.height * (Math.abs(0.6 - hArray[index]) / 5) + width: parent.height * (0.9 - (Math.abs(0.6 - hArray[index]) / 3)); height: parent.height * 0.8 + hArray[index] * parent.height + radius: width / 2 + color: index === hiTick && timer.running && enabled ? (index === 0 || index === 4 ? "#00a0a0" : activPal.text) : bgColor + rotation: (index - 2) * 30 + visible: SOUND.tempo < 110 || index % 2 !== 1 + Text { // rhythm + visible: !enabled || (index !== hiTick || !SOUND.metroRunning) + font { pixelSize: parent.height * 0.8; family: "Scorek" } + color: enabled ? activPal.text : disdPal.text + text: gArray[index] + y: parent.height * -0.85 + x: (parent.width - width) / 2 + } + Text { // count + visible: timer.running && index === hiTick && (!tMenu || (tMenu.count && tMenu.enableMetronome)) + font { pixelSize: parent.height; family: "Scorek" } + color: activPal.base + text: cnt + y: parent.height * -1.2 + x: (parent.width - width) / 2 + } + } } } - } - Timer { - id: timer - running: visible && pitchView.active && (!tMenu || tMenu.tickEnable) - repeat: true - interval: (SOUND.tempo < 110 ? 15000 : 30000) / SOUND.tempo - property real elap: 0 - property real lag: 0 - property int phase: 0 - onRunningChanged: { - if (running) { - cnt = 1; elap = 0; lag = 0; phase = 0; hiTick = 4 - } + RectButton { + id: loudTickButt + enabled: !SOUND.metroRunning + font { family: "Nootka"; pixelSize: parent.height } + text: "\u018f" + statusTip: qsTr("Audible metronome.<br>Use earphones! Otherwise ticking will disturb proper pitch detection!") + checked: SOUND.tickDuringPlay + textColor: checked ? "#00a0a0" : activPal.text + onClicked: SOUND.tickDuringPlay = !SOUND.tickDuringPlay } - onTriggered: { - var currTime = new Date().getTime() - if (elap > 0) { - elap = currTime - elap - lag += elap - interval + + Timer { + id: timer + running: visible && SOUND.metroRunning //&& (!tMenu || tMenu.tickEnable) + repeat: true + interval: (SOUND.tempo < 110 ? 15000 : 30000) / SOUND.tempo + property real elap: 0 + property real lag: 0 + property int phase: 0 + onRunningChanged: { + if (running) { + cnt = 1; elap = 0; lag = 0; phase = 0; hiTick = 4 + } + } + onTriggered: { + var currTime = new Date().getTime() + if (elap > 0) { + elap = currTime - elap + lag += elap - interval + } + elap = currTime + interval = Math.max(((SOUND.tempo < 110 ? 15000 : 30000) / SOUND.tempo) - lag, 1) + lag = 0 + if ((phase + (SOUND.tempo < 110 ? 1 : 2)) % 4 === 0) { + if (cnt < countTo) + cnt++ + else + cnt = 1 + } + phase += SOUND.tempo < 110 ? 1 : 2 + if (phase > 7) phase = 0 + hiTick = Math.abs(phase - 4) } - elap = currTime - interval = Math.max(((SOUND.tempo < 110 ? 15000 : 30000) / SOUND.tempo) - lag, 1) - lag = 0 - if ((phase + (SOUND.tempo < 110 ? 1 : 2)) % 4 === 0) { - if (cnt < countTo) - cnt++ - else - cnt = 1 + } + + RectButton { + id: tunerButt + enabled: !executor + textColor: enabled ? activPal.text : disdPal.text + text: "440Hz" + font { pixelSize: parent.height * 0.8; bold: true } + statusTip: qsTr("Tuner") + onClicked: { + nootkaWindow.showDialog(Nootka.Tuner) + SOUND.startListen() } - phase += SOUND.tempo < 110 ? 1 : 2 - if (phase > 7) phase = 0 - hiTick = Math.abs(phase - 4) } } - RectButton { - x: parent.width - width * 1.1 - text: "440Hz" - font { pixelSize: parent.height * 0.8; bold: true } - statusTip: qsTr("Tuner") - onClicked: { - nootkaWindow.showDialog(Nootka.Tuner) - SOUND.startListen() + MouseArea { + x: metroRow.x; y: metroRow.y; width: metroRow.width; height: metroRow.height + hoverEnabled: true + onEntered: Noo.setStatusTip(qsTr("Metronome"), Item.TopLeft) + onExited: Noo.setStatusTip("", Item.TopLeft) + } + + Connections { + target: SOUND + onCountdownPrepare: { + if (SOUND.tickBeforePlay) { + if (!preCountItem) { + var d = Qt.createComponent("qrc:/sound/CountdownItem.qml") + preCountItem = d.createObject(score) + } + preCountItem.tickCount = tickCount + preCountItem.counter = 1 + } } } } diff --git a/src/qml/sound/TempoMenu.qml b/src/qml/sound/TempoMenu.qml index f1cfdd8eb..73ec108e7 100644 --- a/src/qml/sound/TempoMenu.qml +++ b/src/qml/sound/TempoMenu.qml @@ -9,7 +9,7 @@ import ".." Popup { - property alias tickEnable: metroVisibleChB.checked + property alias enableMetronome: metroVisibleChB.checked property alias count: countChB.checked margins: Noo.fontSize() @@ -100,16 +100,18 @@ Popup { } TcheckBox { + x: Noo.fontSize() id: countChB + enabled: metroVisibleChB.checked text: qsTr("Count up") checked: true } - TcheckBox { - id: beforeTickChB - text: qsTr("Tick before") - checked: SOUND.tickBeforePlay - } +// TcheckBox { +// id: beforeTickChB +// text: qsTr("Tick before") +// checked: SOUND.tickBeforePlay +// } ButtonGroup { buttons: radioRow.children } Item { @@ -147,7 +149,7 @@ Popup { onClicked: { SOUND.setMetronome(tempoSpin.value, beatUnitTumb.currentIndex) SOUND.quantization = radio16.checked ? 6 : 12 // See Tsound doc for values explanation - SOUND.tickBeforePlay = beforeTickChB.checked +// SOUND.tickBeforePlay = beforeTickChB.checked tempoSpin.value = SOUND.tempo accepted() close() -- GitLab