Tchick/Source/Metronome.cpp
2026-02-04 11:51:22 +01:00

247 lines
7.7 KiB
C++

/*
==============================================================================
Copyright 2022 Nicolas Chambert
This program is free software : you can redistribute itand /or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either 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 for more details.
You should have received a copy of the GNU General Public License
along with this program.If not, see < http://www.gnu.org/licenses/>.
==============================================================================
*/
/*
==============================================================================
Moteur de l'application. Gère la lecture des samples Callé sur le bon tempo.
==============================================================================
*/
#include "Metronome.h"
Metronome::Metronome(Config& conf)
{
// on récupère les valeurs de la configuration
bpm.set(conf.bpm);
measures.set(conf.measures);
gain.set(conf.gain);
setFigure(conf.figure);
// on initialise le lecteur wav
formatManager.registerBasicFormats();
// On crée 2 voie sur le synthé
for (auto i = 0; i < 2; ++i)
synth.addVoice(new juce::SamplerVoice());
}
Metronome::~Metronome()
{
// libère les sons
synth.clearSounds();
}
void Metronome::loadSounds(std::string high, std::string low)
{
// chargement des sons high et low
auto mySamplesHigh = juce::File{high};
auto mySamplesLow = juce::File{ low };
jassert(mySamplesHigh.exists());
jassert(mySamplesLow.exists());
synth.clearSounds();
juce::BigInteger usedNotes;
usedNotes.setRange(0, 1, true);
auto formatReaderHigh = formatManager.createReaderFor(mySamplesHigh);
synth.addSound(new juce::SamplerSound("High", *formatReaderHigh, usedNotes, 0, 0.0f, 0.0f, 999.0));
delete formatReaderHigh;
juce::BigInteger usedNotes2;
usedNotes2.setRange(1, 1, true);
auto formatReaderLow = formatManager.createReaderFor(mySamplesLow);
synth.addSound(new juce::SamplerSound("Low", *formatReaderLow, usedNotes2, 1, 0.0f, 0.0f, 999.0));
delete formatReaderLow;
}
void Metronome::prepareToPlay (double sr)
{
// Appelé avant de démaré la lecture, on récupère la sr et on initialise le reste du moteur
sampleRate = sr;
updateInterval = (int)(60.0 / bpm.get() * sampleRate);
synth.setCurrentPlaybackSampleRate(sampleRate);
initMeasure();
}
void Metronome::getNextAudioBlock(juce::AudioSourceChannelInfo const& bufferToFill)
{
// Méthode invoqué par le moteur audio. Le but est de remplir bufferToFill avec la waveform que l'on souhaite jouer
int const numSamples = bufferToFill.numSamples;
juce::MidiBuffer midi;
int measCount = measures.get();
int measLength = updateInterval * measCount; // nombre de sample total dans une mesure
int start = lastPos; // Le premier index du buffer correspond au lastPos
int stop = lastPos + numSamples; // et la dernière position du buffer
for (auto cur : measStruct)
{
// On cherche Si dans notre mesure on a un click entre start et stop
int nextTick = (int)(cur.measPos * updateInterval); // le measPos est en relatif, on convertit en équivalent samples
if (stop > measLength)
{
// Dans le cas ou stop se situe sur la mesure suivante, on décale le nextTick d'une mesure
nextTick += measLength;
}
if (nextTick >= start && nextTick < stop)
{
// On à trouvé un tick
if (cur.type != NoteType::mute)
{
// qui est pas mute
// on sélectionne la bone note, la bonne vélocité et on génère un événement midi à nextTick - start
int note = cur.type == NoteType::high ? 0 : 1;
float vel = cur.type == NoteType::lowLow ? 0.2f : 1.0f;
midi.addEvent(juce::MidiMessage::noteOn(1, note, vel), nextTick - start);
}
// on signale l'interface (pour marquer le temps même s'il est mute)
signal.set(true);
}
}
// On incrémente lastPos
lastPos = stop;
if (lastPos > measLength)
{
// Et on le remet dans la mesure suivante
lastPos -= measLength;
}
// On demande au synthé de remplir le buffer
synth.renderNextBlock(*bufferToFill.buffer,
midi,
0,
bufferToFill.numSamples);
// On reprend la main sur le buffer pour appliquer le gain manuellement
float mult = gain.get();
for (auto channel = 0; channel < bufferToFill.buffer->getNumChannels(); ++channel)
{
auto* buffer = bufferToFill.buffer->getWritePointer(channel, bufferToFill.startSample);
for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
buffer[sample] = buffer[sample] * mult;
}
}
void Metronome::reset()
{
// raz
lastPos = 0;
signal.set(true);
}
void Metronome::setBPM(int value)
{
// contraint les BPM entre 10 et 400
int coerce = juce::jlimit(10, 400, value);
bpm.set(coerce);
int lastInterval = updateInterval;
updateInterval = (int)(60.0 / bpm.get() * sampleRate);
// En cas de changement de BPM brutal on constate un décalage dans la lecture
// Pour remédier à ca on recalibre le lastPos en proportion du nouvel intervale
lastPos = (int)(lastPos * (float)updateInterval / (float)lastInterval);
}
int Metronome::getCurrentMeasure() const
{
// permet à l'UI de savoir sur quel temps de la mesure on se trouve
int numTemps = (lastPos + updateInterval - 1) / updateInterval; // nombre de temps depuis le start
return numTemps;
}
void Metronome::initMeasure()
{
// construit la structure de la mesure
measStruct.clear();
int measCount = measures.get();
switch (figure.get())
{
case FigureType::blanche:
// On alterne click/Mute
for (size_t i = 0; i < measCount; ++i)
{
if (i % 2 == 0)
{
measStruct.push_back(MeasureTick((float)i, i == 0 ? NoteType::high : NoteType::low));
}
else
{
measStruct.push_back(MeasureTick((float)i, NoteType::mute));
}
}
break;
case FigureType::noire:
// Un click par temps
for (size_t i = 0; i < measCount; ++i)
{
measStruct.push_back(MeasureTick((float)i, i == 0 ? NoteType::high : NoteType::low));
}
break;
case FigureType::croche:
// On alterne click/lowlow
for (size_t i = 0; i < measCount; ++i)
{
measStruct.push_back(MeasureTick((float)i, i == 0 ? NoteType::high : NoteType::low));
measStruct.push_back(MeasureTick((float)i + 0.5f, NoteType::lowLow));
}
break;
case FigureType::triolet:
// On alterne click/lowlow/lowlow
for (size_t i = 0; i < measCount; ++i)
{
measStruct.push_back(MeasureTick((float)i, i == 0 ? NoteType::high : NoteType::low));
measStruct.push_back(MeasureTick((float)i + 1.f / 3.f, NoteType::lowLow));
measStruct.push_back(MeasureTick((float)i + 2.f / 3.f, NoteType::lowLow));
}
break;
case FigureType::doubleCroche:
// On alterne click/lowlow/lowlow/lowlow
for (size_t i = 0; i < measCount; ++i)
{
measStruct.push_back(MeasureTick((float)i, i == 0 ? NoteType::high : NoteType::low));
measStruct.push_back(MeasureTick((float)i + 0.25f, NoteType::lowLow));
measStruct.push_back(MeasureTick((float)i + 0.5f, NoteType::lowLow));
measStruct.push_back(MeasureTick((float)i + 0.75f, NoteType::lowLow));
}
break;
case FigureType::swing:
// On alterne click/lowlow sur les 2/3 du temps
for (size_t i = 0; i < measCount; ++i)
{
measStruct.push_back(MeasureTick((float)i, i == 0 ? NoteType::high : NoteType::low));
measStruct.push_back(MeasureTick((float)i + 2.f / 3.f, NoteType::lowLow));
}
break;
default:
break;
}
}