Fixes to the UI

This commit is contained in:
Tim
2025-10-26 00:49:50 +01:00
parent 0785f6fedd
commit c5105693a2
76 changed files with 2280 additions and 32 deletions

View File

@@ -0,0 +1,536 @@
/*
==============================================================================
This file is part of the JUCE tutorials.
Copyright (c) 2020 - Raw Material Software Limited
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
/*******************************************************************************
The block below describes the properties of this PIP. A PIP is a short snippet
of code that can be read by the Projucer and used to generate a JUCE project.
BEGIN_JUCE_PIP_METADATA
name: MPEIntroductionTutorial
version: 1.0.0
vendor: JUCE
website: http://juce.com
description: Synthesiser using MPE specifications.
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
juce_audio_processors, juce_audio_utils, juce_core,
juce_data_structures, juce_events, juce_graphics,
juce_gui_basics, juce_gui_extra
exporters: xcode_mac, vs2019, linux_make
type: Component
mainClass: MainComponent
useLocalCopy: 1
END_JUCE_PIP_METADATA
*******************************************************************************/
#pragma once
//==============================================================================
class NoteComponent : public juce::Component
{
public:
NoteComponent (const juce::MPENote& n)
: note (n), colour (juce::Colours::white)
{}
//==============================================================================
void update (const juce::MPENote& newNote, juce::Point<float> newCentre)
{
note = newNote;
centre = newCentre;
setBounds (getSquareAroundCentre (juce::jmax (getNoteOnRadius(), getNoteOffRadius(), getPressureRadius()))
.getUnion (getTextRectangle())
.getSmallestIntegerContainer()
.expanded (3));
repaint();
}
//==============================================================================
void paint (juce::Graphics& g) override
{
if (note.keyState == juce::MPENote::keyDown || note.keyState == juce::MPENote::keyDownAndSustained)
drawPressedNoteCircle (g, colour);
else if (note.keyState == juce::MPENote::sustained)
drawSustainedNoteCircle (g, colour);
else
return;
drawNoteLabel (g, colour);
}
//==============================================================================
juce::MPENote note;
juce::Colour colour;
juce::Point<float> centre;
private:
//==============================================================================
void drawPressedNoteCircle (juce::Graphics& g, juce::Colour zoneColour)
{
g.setColour (zoneColour.withAlpha (0.3f));
g.fillEllipse (translateToLocalBounds (getSquareAroundCentre (getNoteOnRadius())));
g.setColour (zoneColour);
g.drawEllipse (translateToLocalBounds (getSquareAroundCentre (getPressureRadius())), 2.0f);
}
//==============================================================================
void drawSustainedNoteCircle (juce::Graphics& g, juce::Colour zoneColour)
{
g.setColour (zoneColour);
juce::Path circle, dashedCircle;
circle.addEllipse (translateToLocalBounds (getSquareAroundCentre (getNoteOffRadius())));
const float dashLengths[] = { 3.0f, 3.0f };
juce::PathStrokeType (2.0, juce::PathStrokeType::mitered).createDashedStroke (dashedCircle, circle, dashLengths, 2);
g.fillPath (dashedCircle);
}
//==============================================================================
void drawNoteLabel (juce::Graphics& g, juce::Colour)
{
auto textBounds = translateToLocalBounds (getTextRectangle()).getSmallestIntegerContainer();
g.drawText ("+", textBounds, juce::Justification::centred);
g.drawText (juce::MidiMessage::getMidiNoteName (note.initialNote, true, true, 3), textBounds, juce::Justification::centredBottom);
g.setFont (juce::Font (22.0f, juce::Font::bold));
g.drawText (juce::String (note.midiChannel), textBounds, juce::Justification::centredTop);
}
//==============================================================================
juce::Rectangle<float> getSquareAroundCentre (float radius) const noexcept
{
return juce::Rectangle<float> (radius * 2.0f, radius * 2.0f).withCentre (centre);
}
juce::Rectangle<float> translateToLocalBounds (juce::Rectangle<float> r) const noexcept
{
return r - getPosition().toFloat();
}
juce::Rectangle<float> getTextRectangle() const noexcept
{
return juce::Rectangle<float> (30.0f, 50.0f).withCentre (centre);
}
float getNoteOnRadius() const noexcept { return note.noteOnVelocity.asUnsignedFloat() * maxNoteRadius; }
float getNoteOffRadius() const noexcept { return note.noteOffVelocity.asUnsignedFloat() * maxNoteRadius; }
float getPressureRadius() const noexcept { return note.pressure.asUnsignedFloat() * maxNoteRadius; }
static constexpr auto maxNoteRadius = 100.0f;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NoteComponent)
};
//==============================================================================
class Visualiser : public juce::Component,
public juce::MPEInstrument::Listener,
private juce::AsyncUpdater
{
public:
//==============================================================================
Visualiser() {}
//==============================================================================
void paint (juce::Graphics& g) override
{
g.fillAll (juce::Colours::black);
auto noteDistance = float (getWidth()) / 128;
for (auto i = 0; i < 128; ++i)
{
auto x = noteDistance * (float) i;
auto noteHeight = int (juce::MidiMessage::isMidiNoteBlack (i) ? 0.7 * getHeight() : getHeight());
g.setColour (juce::MidiMessage::isMidiNoteBlack (i) ? juce::Colours::white : juce::Colours::grey);
g.drawLine (x, 0.0f, x, (float) noteHeight);
if (i > 0 && i % 12 == 0)
{
g.setColour (juce::Colours::grey);
auto octaveNumber = (i / 12) - 2;
g.drawText ("C" + juce::String (octaveNumber), (int) x - 15, getHeight() - 30, 30, 30, juce::Justification::centredBottom);
}
}
}
//==============================================================================
void noteAdded (juce::MPENote newNote) override
{
const juce::ScopedLock sl (lock);
activeNotes.add (newNote);
triggerAsyncUpdate();
}
void notePressureChanged (juce::MPENote note) override { noteChanged (note); }
void notePitchbendChanged (juce::MPENote note) override { noteChanged (note); }
void noteTimbreChanged (juce::MPENote note) override { noteChanged (note); }
void noteKeyStateChanged (juce::MPENote note) override { noteChanged (note); }
void noteChanged (juce::MPENote changedNote)
{
const juce::ScopedLock sl (lock);
for (auto& note : activeNotes)
if (note.noteID == changedNote.noteID)
note = changedNote;
triggerAsyncUpdate();
}
void noteReleased (juce::MPENote finishedNote) override
{
const juce::ScopedLock sl (lock);
for (auto i = activeNotes.size(); --i >= 0;)
if (activeNotes.getReference(i).noteID == finishedNote.noteID)
activeNotes.remove (i);
triggerAsyncUpdate();
}
private:
//==============================================================================
const juce::MPENote* findActiveNote (int noteID) const noexcept
{
for (auto& note : activeNotes)
if (note.noteID == noteID)
return &note;
return nullptr;
}
NoteComponent* findNoteComponent (int noteID) const noexcept
{
for (auto& noteComp : noteComponents)
if (noteComp->note.noteID == noteID)
return noteComp;
return nullptr;
}
//==============================================================================
void handleAsyncUpdate() override
{
const juce::ScopedLock sl (lock);
for (auto i = noteComponents.size(); --i >= 0;)
if (findActiveNote (noteComponents.getUnchecked(i)->note.noteID) == nullptr)
noteComponents.remove (i);
for (auto& note : activeNotes)
if (findNoteComponent (note.noteID) == nullptr)
addAndMakeVisible (noteComponents.add (new NoteComponent (note)));
for (auto& noteComp : noteComponents)
if (auto* noteInfo = findActiveNote (noteComp->note.noteID))
noteComp->update (*noteInfo, getCentrePositionForNote (*noteInfo));
}
//==============================================================================
juce::Point<float> getCentrePositionForNote (juce::MPENote note) const
{
auto n = float (note.initialNote) + float (note.totalPitchbendInSemitones);
auto x = (float) getWidth() * n / 128;
auto y = (float) getHeight() * (1 - note.timbre.asUnsignedFloat());
return { x, y };
}
//==============================================================================
juce::OwnedArray<NoteComponent> noteComponents;
juce::CriticalSection lock;
juce::Array<juce::MPENote> activeNotes;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Visualiser)
};
//==============================================================================
class MPEDemoSynthVoice : public juce::MPESynthesiserVoice
{
public:
//==============================================================================
MPEDemoSynthVoice() {}
//==============================================================================
void noteStarted() override
{
jassert (currentlyPlayingNote.isValid());
jassert (currentlyPlayingNote.keyState == juce::MPENote::keyDown
|| currentlyPlayingNote.keyState == juce::MPENote::keyDownAndSustained);
// get data from the current MPENote
level .setTargetValue (currentlyPlayingNote.pressure.asUnsignedFloat());
frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());
timbre .setTargetValue (currentlyPlayingNote.timbre.asUnsignedFloat());
phase = 0.0;
auto cyclesPerSample = frequency.getNextValue() / currentSampleRate;
phaseDelta = 2.0 * juce::MathConstants<double>::pi * cyclesPerSample;
tailOff = 0.0;
}
void noteStopped (bool allowTailOff) override
{
jassert (currentlyPlayingNote.keyState == juce::MPENote::off);
if (allowTailOff)
{
// start a tail-off by setting this flag. The render callback will pick up on
// this and do a fade out, calling clearCurrentNote() when it's finished.
if (tailOff == 0.0) // we only need to begin a tail-off if it's not already doing so - the
// stopNote method could be called more than once.
tailOff = 1.0;
}
else
{
// we're being told to stop playing immediately, so reset everything..
clearCurrentNote();
phaseDelta = 0.0;
}
}
void notePressureChanged() override
{
level.setTargetValue (currentlyPlayingNote.pressure.asUnsignedFloat());
}
void notePitchbendChanged() override
{
frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());
}
void noteTimbreChanged() override
{
timbre.setTargetValue (currentlyPlayingNote.timbre.asUnsignedFloat());
}
void noteKeyStateChanged() override {}
void setCurrentSampleRate (double newRate) override
{
if (! juce::approximatelyEqual (currentSampleRate, newRate))
{
noteStopped (false);
currentSampleRate = newRate;
level .reset (currentSampleRate, smoothingLengthInSeconds);
timbre .reset (currentSampleRate, smoothingLengthInSeconds);
frequency.reset (currentSampleRate, smoothingLengthInSeconds);
}
}
//==============================================================================
void renderNextBlock (juce::AudioBuffer<float>& outputBuffer,
int startSample,
int numSamples) override
{
if (phaseDelta != 0.0)
{
if (tailOff > 0.0)
{
while (--numSamples >= 0)
{
auto currentSample = getNextSample() * (float) tailOff;
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
outputBuffer.addSample (i, startSample, currentSample);
++startSample;
tailOff *= 0.99;
if (tailOff <= 0.005)
{
clearCurrentNote();
phaseDelta = 0.0;
break;
}
}
}
else
{
while (--numSamples >= 0)
{
auto currentSample = getNextSample();
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
outputBuffer.addSample (i, startSample, currentSample);
++startSample;
}
}
}
}
private:
//==============================================================================
float getNextSample() noexcept
{
auto levelDb = (level.getNextValue() - 1.0) * maxLevelDb;
auto amplitude = std::pow (10.0f, 0.05f * levelDb) * maxLevel;
// timbre is used to blend between a sine and a square.
auto f1 = std::sin (phase);
auto f2 = std::copysign (1.0, f1);
auto a2 = timbre.getNextValue();
auto a1 = 1.0 - a2;
auto nextSample = float (amplitude * ((a1 * f1) + (a2 * f2)));
auto cyclesPerSample = frequency.getNextValue() / currentSampleRate;
phaseDelta = 2.0 * juce::MathConstants<double>::pi * cyclesPerSample;
phase = std::fmod (phase + phaseDelta, 2.0 * juce::MathConstants<double>::pi);
return nextSample;
}
//==============================================================================
juce::SmoothedValue<double> level, timbre, frequency;
double phase = 0.0;
double phaseDelta = 0.0;
double tailOff = 0.0;
// some useful constants
static constexpr auto maxLevel = 0.05;
static constexpr auto maxLevelDb = 31.0;
static constexpr auto smoothingLengthInSeconds = 0.01;
};
//==============================================================================
class MainComponent : public juce::Component,
private juce::AudioIODeviceCallback, // [1]
private juce::MidiInputCallback // [2]
{
public:
//==============================================================================
MainComponent()
: audioSetupComp (audioDeviceManager, 0, 0, 0, 256,
true, // showMidiInputOptions must be true
true, true, false)
{
audioDeviceManager.initialise (0, 2, nullptr, true, {}, nullptr);
audioDeviceManager.addMidiInputDeviceCallback ({}, this); // [6]
audioDeviceManager.addAudioCallback (this);
addAndMakeVisible (audioSetupComp);
addAndMakeVisible (visualiserViewport);
visualiserViewport.setScrollBarsShown (false, true);
visualiserViewport.setViewedComponent (&visualiserComp, false);
visualiserViewport.setViewPositionProportionately (0.5, 0.0);
visualiserInstrument.addListener (&visualiserComp);
for (auto i = 0; i < 15; ++i)
synth.addVoice (new MPEDemoSynthVoice());
synth.enableLegacyMode (24);
synth.setVoiceStealingEnabled (false);
visualiserInstrument.enableLegacyMode (24);
setSize (650, 560);
}
~MainComponent() override
{
audioDeviceManager.removeMidiInputDeviceCallback ({}, this);
audioDeviceManager.removeAudioCallback (this);
}
//==============================================================================
void resized() override
{
auto visualiserCompWidth = 2800;
auto visualiserCompHeight = 300;
auto r = getLocalBounds();
visualiserViewport.setBounds (r.removeFromBottom (visualiserCompHeight));
visualiserComp.setBounds ({ visualiserCompWidth,
visualiserViewport.getHeight() - visualiserViewport.getScrollBarThickness() });
audioSetupComp.setBounds (r);
}
//==============================================================================
void audioDeviceIOCallbackWithContext (const float* const* /*inputChannelData*/,
int /*numInputChannels*/,
float* const* outputChannelData,
int numOutputChannels,
int numSamples,
const juce::AudioIODeviceCallbackContext& /*context*/) override
{
// make buffer
juce::AudioBuffer<float> buffer (outputChannelData, numOutputChannels, numSamples);
// clear it to silence
buffer.clear();
juce::MidiBuffer incomingMidi;
// get the MIDI messages for this audio block
midiCollector.removeNextBlockOfMessages (incomingMidi, numSamples);
// synthesise the block
synth.renderNextBlock (buffer, incomingMidi, 0, numSamples);
}
void audioDeviceAboutToStart (juce::AudioIODevice* device) override
{
auto sampleRate = device->getCurrentSampleRate();
midiCollector.reset (sampleRate);
synth.setCurrentPlaybackSampleRate (sampleRate);
}
void audioDeviceStopped() override {}
private:
//==============================================================================
void handleIncomingMidiMessage (juce::MidiInput* /*source*/,
const juce::MidiMessage& message) override
{
visualiserInstrument.processNextMidiEvent (message);
midiCollector.addMessageToQueue (message);
}
//==============================================================================
juce::AudioDeviceManager audioDeviceManager; // [3]
juce::AudioDeviceSelectorComponent audioSetupComp; // [4]
Visualiser visualiserComp;
juce::Viewport visualiserViewport;
juce::MPEInstrument visualiserInstrument;
juce::MPESynthesiser synth;
juce::MidiMessageCollector midiCollector; // [5]
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};