/* ============================================================================== 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; } }