1 #include "metronomeeffect.h"
2 
3 #include <QtDebug>
4 
5 #include "metronomeclick.h"
6 #include "util/experiment.h"
7 #include "util/math.h"
8 #include "util/sample.h"
9 
10 
11 // static
getId()12 QString MetronomeEffect::getId() {
13     return "org.mixxx.effects.metronome";
14 }
15 
16 // static
getManifest()17 EffectManifestPointer MetronomeEffect::getManifest() {
18     EffectManifestPointer pManifest(new EffectManifest());
19     pManifest->setId(getId());
20     pManifest->setName(QObject::tr("Metronome"));
21     pManifest->setAuthor("The Mixxx Team");
22     pManifest->setVersion("1.0");
23     pManifest->setDescription(QObject::tr("Adds a metronome click sound to the stream"));
24 
25     // Period
26     // The maximum is at 128 + 1 allowing 128 as max value and
27     // enabling us to pause time when the parameter is above
28     EffectManifestParameterPointer period = pManifest->addParameter();
29     period->setId("bpm");
30     period->setName(QObject::tr("BPM"));
31     period->setDescription(QObject::tr("Set the beats per minute value of the click sound"));
32     period->setControlHint(EffectManifestParameter::ControlHint::KNOB_LOGARITHMIC);
33     period->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN);
34     period->setUnitsHint(EffectManifestParameter::UnitsHint::UNKNOWN);
35     period->setMinimum(40);
36     period->setDefault(120);
37     period->setMaximum(208);
38 
39 
40     // Period unit
41     EffectManifestParameterPointer periodUnit = pManifest->addParameter();
42     periodUnit->setId("sync");
43     periodUnit->setName(QObject::tr("Sync"));
44     periodUnit->setDescription(QObject::tr("Synchronizes the BPM with the track if it can be retrieved"));
45     periodUnit->setControlHint(EffectManifestParameter::ControlHint::TOGGLE_STEPPING);
46     periodUnit->setSemanticHint(EffectManifestParameter::SemanticHint::UNKNOWN);
47     periodUnit->setUnitsHint(EffectManifestParameter::UnitsHint::UNKNOWN);
48     periodUnit->setDefault(1);
49     periodUnit->setMinimum(0);
50     periodUnit->setMaximum(1);
51 
52     return pManifest;
53 }
54 
MetronomeEffect(EngineEffect * pEffect)55 MetronomeEffect::MetronomeEffect(EngineEffect* pEffect)
56     : m_pBpmParameter(pEffect->getParameterById("bpm")),
57       m_pSyncParameter(pEffect->getParameterById("sync")) {
58 }
59 
~MetronomeEffect()60 MetronomeEffect::~MetronomeEffect() {
61 }
62 
processChannel(const ChannelHandle & handle,MetronomeGroupState * pGroupState,const CSAMPLE * pInput,CSAMPLE * pOutput,const mixxx::EngineParameters & bufferParameters,const EffectEnableState enableState,const GroupFeatureState & groupFeatures)63 void MetronomeEffect::processChannel(
64         const ChannelHandle& handle,
65         MetronomeGroupState* pGroupState,
66         const CSAMPLE* pInput, CSAMPLE* pOutput,
67         const mixxx::EngineParameters& bufferParameters,
68         const EffectEnableState enableState,
69         const GroupFeatureState& groupFeatures) {
70     Q_UNUSED(handle);
71     Q_UNUSED(pInput);
72 
73     MetronomeGroupState* gs = pGroupState;
74 
75     if (enableState == EffectEnableState::Disabled) {
76         gs->m_framesSinceClickStart = 0;
77         return;
78     }
79 
80     SINT clickSize = kClickSize44100;
81     const CSAMPLE* click = kClick44100;
82     if (bufferParameters.sampleRate() >= 96000) {
83         clickSize = kClickSize96000;
84         click = kClick96000;
85     } else if (bufferParameters.sampleRate() >= 48000) {
86         clickSize = kClickSize48000;
87         click = kClick48000;
88     }
89 
90     SINT maxFrames;
91     if (m_pSyncParameter->toBool() && groupFeatures.has_beat_length_sec) {
92         maxFrames = static_cast<SINT>(
93                 bufferParameters.sampleRate() * groupFeatures.beat_length_sec);
94         if (groupFeatures.has_beat_fraction) {
95             const auto currentFrame = static_cast<SINT>(
96                     maxFrames * groupFeatures.beat_fraction);
97             if (maxFrames > clickSize &&
98                     currentFrame > clickSize &&
99                     currentFrame < maxFrames - clickSize &&
100                     gs->m_framesSinceClickStart > clickSize) {
101                 // plays a single click on low speed
102                 gs->m_framesSinceClickStart = currentFrame;
103             }
104         }
105     } else {
106         maxFrames = static_cast<SINT>(
107                 bufferParameters.sampleRate() * 60 / m_pBpmParameter->value());
108     }
109 
110     SampleUtil::copy(pOutput, pInput, bufferParameters.samplesPerBuffer());
111 
112     if (gs->m_framesSinceClickStart < clickSize) {
113         // still in click region, write remaining click frames.
114         const SINT copyFrames =
115                 math_min(bufferParameters.framesPerBuffer(),
116                         clickSize - gs->m_framesSinceClickStart);
117         SampleUtil::addMonoToStereo(pOutput, &click[gs->m_framesSinceClickStart],
118                 copyFrames);
119     }
120 
121     gs->m_framesSinceClickStart += bufferParameters.framesPerBuffer();
122 
123     if (gs->m_framesSinceClickStart > maxFrames) {
124         // overflow, all overflowed frames are the start of a new click sound
125         gs->m_framesSinceClickStart -= maxFrames;
126         while (gs->m_framesSinceClickStart > bufferParameters.framesPerBuffer()) {
127             // loop into a valid region, this happens if the maxFrames was lowered
128             gs->m_framesSinceClickStart -= bufferParameters.framesPerBuffer();
129         }
130 
131         // gs->m_framesSinceClickStart matches now already at the first frame
132         // of next pOutput.
133         const unsigned int outputOffset =
134                 bufferParameters.framesPerBuffer() - gs->m_framesSinceClickStart;
135         const unsigned int copyFrames =
136                 math_min(gs->m_framesSinceClickStart, clickSize);
137         SampleUtil::addMonoToStereo(&pOutput[outputOffset * 2], click,
138                 copyFrames);
139     }
140 }
141