25bd5d8adb
subrepo: subdir: "deps/juce" merged: "b13f9084e" upstream: origin: "https://github.com/essej/JUCE.git" branch: "sono6good" commit: "b13f9084e" git-subrepo: version: "0.4.3" origin: "https://github.com/ingydotnet/git-subrepo.git" commit: "2f68596"
346 lines
13 KiB
C++
346 lines
13 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE examples.
|
|
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: MIDILogger
|
|
version: 1.0.0
|
|
vendor: JUCE
|
|
website: http://juce.com
|
|
description: Logs incoming MIDI messages.
|
|
|
|
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
|
juce_audio_plugin_client, 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
|
|
|
|
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
|
|
|
type: AudioProcessor
|
|
mainClass: MidiLoggerPluginDemoProcessor
|
|
|
|
useLocalCopy: 1
|
|
|
|
pluginCharacteristics: pluginWantsMidiIn, pluginProducesMidiOut, pluginIsMidiEffectPlugin
|
|
|
|
END_JUCE_PIP_METADATA
|
|
|
|
*******************************************************************************/
|
|
|
|
#pragma once
|
|
|
|
#include <iterator>
|
|
|
|
class MidiQueue
|
|
{
|
|
public:
|
|
void push (const MidiBuffer& buffer)
|
|
{
|
|
for (const auto metadata : buffer)
|
|
fifo.write (1).forEach ([&] (int dest) { messages[(size_t) dest] = metadata.getMessage(); });
|
|
}
|
|
|
|
template <typename OutputIt>
|
|
void pop (OutputIt out)
|
|
{
|
|
fifo.read (fifo.getNumReady()).forEach ([&] (int source) { *out++ = messages[(size_t) source]; });
|
|
}
|
|
|
|
private:
|
|
static constexpr auto queueSize = 1 << 14;
|
|
AbstractFifo fifo { queueSize };
|
|
std::vector<MidiMessage> messages = std::vector<MidiMessage> (queueSize);
|
|
};
|
|
|
|
// Stores the last N messages. Safe to access from the message thread only.
|
|
class MidiListModel
|
|
{
|
|
public:
|
|
template <typename It>
|
|
void addMessages (It begin, It end)
|
|
{
|
|
const auto numNewMessages = (int) std::distance (begin, end);
|
|
const auto numToAdd = juce::jmin (numToStore, numNewMessages);
|
|
const auto numToRemove = jmax (0, (int) messages.size() + numToAdd - numToStore);
|
|
messages.erase (messages.begin(), std::next (messages.begin(), numToRemove));
|
|
messages.insert (messages.end(), std::prev (end, numToAdd), end);
|
|
|
|
if (onChange != nullptr)
|
|
onChange();
|
|
}
|
|
|
|
void clear()
|
|
{
|
|
messages.clear();
|
|
|
|
if (onChange != nullptr)
|
|
onChange();
|
|
}
|
|
|
|
const MidiMessage& operator[] (size_t ind) const { return messages[ind]; }
|
|
|
|
size_t size() const { return messages.size(); }
|
|
|
|
std::function<void()> onChange;
|
|
|
|
private:
|
|
static constexpr auto numToStore = 1000;
|
|
std::vector<MidiMessage> messages;
|
|
};
|
|
|
|
//==============================================================================
|
|
class MidiTable : public Component,
|
|
private TableListBoxModel
|
|
{
|
|
public:
|
|
MidiTable (MidiListModel& m)
|
|
: messages (m)
|
|
{
|
|
addAndMakeVisible (table);
|
|
|
|
table.setModel (this);
|
|
table.setClickingTogglesRowSelection (false);
|
|
table.setHeader ([&]
|
|
{
|
|
auto header = std::make_unique<TableHeaderComponent>();
|
|
header->addColumn ("Message", messageColumn, 200, 30, -1, TableHeaderComponent::notSortable);
|
|
header->addColumn ("Channel", channelColumn, 100, 30, -1, TableHeaderComponent::notSortable);
|
|
header->addColumn ("Data", dataColumn, 200, 30, -1, TableHeaderComponent::notSortable);
|
|
return header;
|
|
}());
|
|
|
|
messages.onChange = [&] { table.updateContent(); };
|
|
}
|
|
|
|
~MidiTable() override { messages.onChange = nullptr; }
|
|
|
|
void resized() override { table.setBounds (getLocalBounds()); }
|
|
|
|
private:
|
|
enum
|
|
{
|
|
messageColumn = 1,
|
|
channelColumn,
|
|
dataColumn
|
|
};
|
|
|
|
int getNumRows() override { return (int) messages.size(); }
|
|
|
|
void paintRowBackground (Graphics&, int, int, int, bool) override {}
|
|
void paintCell (Graphics&, int, int, int, int, bool) override {}
|
|
|
|
Component* refreshComponentForCell (int rowNumber,
|
|
int columnId,
|
|
bool,
|
|
Component* existingComponentToUpdate) override
|
|
{
|
|
delete existingComponentToUpdate;
|
|
|
|
const auto index = (int) messages.size() - 1 - rowNumber;
|
|
const auto message = messages[(size_t) index];
|
|
|
|
return new Label ({}, [&]
|
|
{
|
|
switch (columnId)
|
|
{
|
|
case messageColumn: return getEventString (message);
|
|
case channelColumn: return String (message.getChannel());
|
|
case dataColumn: return getDataString (message);
|
|
default: break;
|
|
}
|
|
|
|
jassertfalse;
|
|
return String();
|
|
}());
|
|
}
|
|
|
|
static String getEventString (const MidiMessage& m)
|
|
{
|
|
if (m.isNoteOn()) return "Note on";
|
|
if (m.isNoteOff()) return "Note off";
|
|
if (m.isProgramChange()) return "Program change";
|
|
if (m.isPitchWheel()) return "Pitch wheel";
|
|
if (m.isAftertouch()) return "Aftertouch";
|
|
if (m.isChannelPressure()) return "Channel pressure";
|
|
if (m.isAllNotesOff()) return "All notes off";
|
|
if (m.isAllSoundOff()) return "All sound off";
|
|
if (m.isMetaEvent()) return "Meta event";
|
|
|
|
if (m.isController())
|
|
{
|
|
const auto* name = MidiMessage::getControllerName (m.getControllerNumber());
|
|
return "Controller " + (name == nullptr ? String (m.getControllerNumber()) : String (name));
|
|
}
|
|
|
|
return String::toHexString (m.getRawData(), m.getRawDataSize());
|
|
}
|
|
|
|
static String getDataString (const MidiMessage& m)
|
|
{
|
|
if (m.isNoteOn()) return MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3) + " Velocity " + String (m.getVelocity());
|
|
if (m.isNoteOff()) return MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3) + " Velocity " + String (m.getVelocity());
|
|
if (m.isProgramChange()) return String (m.getProgramChangeNumber());
|
|
if (m.isPitchWheel()) return String (m.getPitchWheelValue());
|
|
if (m.isAftertouch()) return MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3) + ": " + String (m.getAfterTouchValue());
|
|
if (m.isChannelPressure()) return String (m.getChannelPressureValue());
|
|
if (m.isController()) return String (m.getControllerValue());
|
|
|
|
return {};
|
|
}
|
|
|
|
MidiListModel& messages;
|
|
TableListBox table;
|
|
};
|
|
|
|
//==============================================================================
|
|
class MidiLoggerPluginDemoProcessor : public AudioProcessor,
|
|
private Timer
|
|
{
|
|
public:
|
|
MidiLoggerPluginDemoProcessor()
|
|
: AudioProcessor (getBusesLayout())
|
|
{
|
|
state.addChild ({ "uiState", { { "width", 500 }, { "height", 300 } }, {} }, -1, nullptr);
|
|
startTimerHz (60);
|
|
}
|
|
|
|
~MidiLoggerPluginDemoProcessor() override { stopTimer(); }
|
|
|
|
void processBlock (AudioBuffer<float>& audio, MidiBuffer& midi) override { process (audio, midi); }
|
|
void processBlock (AudioBuffer<double>& audio, MidiBuffer& midi) override { process (audio, midi); }
|
|
|
|
bool isBusesLayoutSupported (const BusesLayout&) const override { return true; }
|
|
bool isMidiEffect() const override { return true; }
|
|
bool hasEditor() const override { return true; }
|
|
AudioProcessorEditor* createEditor() override { return new Editor (*this); }
|
|
|
|
const String getName() const override { return "MIDI Logger"; }
|
|
bool acceptsMidi() const override { return true; }
|
|
bool producesMidi() const override { return true; }
|
|
double getTailLengthSeconds() const override { return 0.0; }
|
|
|
|
int getNumPrograms() override { return 0; }
|
|
int getCurrentProgram() override { return 0; }
|
|
void setCurrentProgram (int) override {}
|
|
const String getProgramName (int) override { return "None"; }
|
|
void changeProgramName (int, const String&) override {}
|
|
|
|
void prepareToPlay (double, int) override {}
|
|
void releaseResources() override {}
|
|
|
|
void getStateInformation (MemoryBlock& destData) override
|
|
{
|
|
if (auto xmlState = state.createXml())
|
|
copyXmlToBinary (*xmlState, destData);
|
|
}
|
|
|
|
void setStateInformation (const void* data, int size) override
|
|
{
|
|
if (auto xmlState = getXmlFromBinary (data, size))
|
|
state = ValueTree::fromXml (*xmlState);
|
|
}
|
|
|
|
private:
|
|
class Editor : public AudioProcessorEditor,
|
|
private Value::Listener
|
|
{
|
|
public:
|
|
explicit Editor (MidiLoggerPluginDemoProcessor& ownerIn)
|
|
: AudioProcessorEditor (ownerIn),
|
|
owner (ownerIn),
|
|
table (owner.model)
|
|
{
|
|
addAndMakeVisible (table);
|
|
addAndMakeVisible (clearButton);
|
|
|
|
setResizable (true, true);
|
|
lastUIWidth .referTo (owner.state.getChildWithName ("uiState").getPropertyAsValue ("width", nullptr));
|
|
lastUIHeight.referTo (owner.state.getChildWithName ("uiState").getPropertyAsValue ("height", nullptr));
|
|
setSize (lastUIWidth.getValue(), lastUIHeight.getValue());
|
|
|
|
lastUIWidth. addListener (this);
|
|
lastUIHeight.addListener (this);
|
|
|
|
clearButton.onClick = [&] { owner.model.clear(); };
|
|
}
|
|
|
|
void paint (Graphics& g) override
|
|
{
|
|
g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
|
|
}
|
|
|
|
void resized() override
|
|
{
|
|
auto bounds = getLocalBounds();
|
|
|
|
clearButton.setBounds (bounds.removeFromBottom (30).withSizeKeepingCentre (50, 24));
|
|
table.setBounds (bounds);
|
|
|
|
lastUIWidth = getWidth();
|
|
lastUIHeight = getHeight();
|
|
}
|
|
|
|
private:
|
|
void valueChanged (Value&) override
|
|
{
|
|
setSize (lastUIWidth.getValue(), lastUIHeight.getValue());
|
|
}
|
|
|
|
MidiLoggerPluginDemoProcessor& owner;
|
|
|
|
MidiTable table;
|
|
TextButton clearButton { "Clear" };
|
|
|
|
Value lastUIWidth, lastUIHeight;
|
|
};
|
|
|
|
void timerCallback() override
|
|
{
|
|
std::vector<MidiMessage> messages;
|
|
queue.pop (std::back_inserter (messages));
|
|
model.addMessages (messages.begin(), messages.end());
|
|
}
|
|
|
|
template <typename Element>
|
|
void process (AudioBuffer<Element>& audio, MidiBuffer& midi)
|
|
{
|
|
audio.clear();
|
|
queue.push (midi);
|
|
}
|
|
|
|
static BusesProperties getBusesLayout()
|
|
{
|
|
// Live doesn't like to load midi-only plugins, so we add an audio output there.
|
|
return PluginHostType().isAbletonLive() ? BusesProperties().withOutput ("out", AudioChannelSet::stereo())
|
|
: BusesProperties();
|
|
}
|
|
|
|
ValueTree state { "state" };
|
|
MidiQueue queue;
|
|
MidiListModel model; // The data to show in the UI. We keep it around in the processor so that
|
|
// the view is persistent even when the plugin UI is closed and reopened.
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiLoggerPluginDemoProcessor)
|
|
};
|