Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new algo pitch2midi #1423

Open
wants to merge 42 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
dfda4d5
Add pitch2midi algorithm.
xaviliz Jun 19, 2024
15eb2c1
Add pitch2midi unitests.
xaviliz Jun 19, 2024
cb8edc4
Comment algorithm reset in the unittest pipeline.
xaviliz Jun 19, 2024
11617d7
Replace output values by vectors
xaviliz Jun 21, 2024
2a27876
Addapt tests to output vectors
xaviliz Jun 21, 2024
0d532ab
Small clean
xaviliz Jun 21, 2024
6b4438e
Remove E_INFO messages
xaviliz Jun 26, 2024
f5cc0b0
Small clean
xaviliz Jun 26, 2024
0243b4e
Add test for a chromatic continuous sequence
xaviliz Jun 26, 2024
8da44b4
Remove redundant parameter definition.
xaviliz Jun 28, 2024
a37ed2d
Remove redundant parameter definition in applyCompensation.
xaviliz Jun 28, 2024
100108e
Remove redundant Out postfix
xaviliz Jun 28, 2024
2bbf995
Clear documentation for sampleRate parameter.
xaviliz Jun 28, 2024
4dba710
Clear documentation for hopSize parameter.
xaviliz Jun 28, 2024
7577043
Remove Note class
xaviliz Jun 28, 2024
0a8b564
Add comment in Pitch2Midi::getMaxVoted()
xaviliz Jun 28, 2024
5594eb6
Remove checking functions
xaviliz Jul 2, 2024
0d28456
Replace containers by outputs
xaviliz Jul 2, 2024
24c9b60
First desing to unitest with real recordings.
xaviliz Jul 10, 2024
9c4c445
Merge branch 'master' into add-new-algo-pitch2midi
xaviliz Jul 11, 2024
7219791
Fix some issues and add some test design
xaviliz Jul 17, 2024
7b0b572
Complete tuning frequency parametere name
xaviliz Jul 18, 2024
62afb83
Fix issues in non-coherent cases and ensure note shifting in offset-o…
xaviliz Jul 18, 2024
bbba3a7
Design of testARealCaseWithEMajorScale() with a Freesound file of e m…
xaviliz Jul 18, 2024
d8a3343
Add testARealCaseWithEMajorScale() unitest
xaviliz Jul 19, 2024
99eec45
Add manual annotations for testARealCaseWithEMajorScale() unitest
xaviliz Jul 19, 2024
51f399c
Add testARealCaseWithDMinorScale() unittest and reference file
xaviliz Jul 19, 2024
382ad4f
Design for last details in pitch2midi unittests
xaviliz Jul 19, 2024
044aa9a
Small refactoring and removing testARealCase() unittest
xaviliz Jul 19, 2024
afdd104
Add reference files for synthetic unittests
xaviliz Jul 22, 2024
cb34b79
Refactor with reference files
xaviliz Jul 22, 2024
3aac019
Include offset error in unittest asssessment for real cases.
xaviliz Jul 24, 2024
ee2d9ae
Erase inconsistent code for pitch<=0
xaviliz Jul 26, 2024
b68bf9d
Estimate midi buffer capacity in compute() and remove capacity()
xaviliz Jul 26, 2024
a53ce08
Fix exception message
xaviliz Jul 26, 2024
874b30b
Remove getMidiNoteNumber()
xaviliz Jul 26, 2024
7155066
Remove empty space
xaviliz Jul 26, 2024
230d28d
Specify voiced output values
xaviliz Jul 26, 2024
076a1d9
Define parameter unit in checker period definitions
xaviliz Jul 26, 2024
c0d9554
Rephrase the description of the transpositionAmount parameter
xaviliz Jul 26, 2024
269860c
Add a real case with separated notes
xaviliz Aug 29, 2024
5d69a81
Clean up
xaviliz Aug 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 297 additions & 0 deletions src/algorithms/tonal/pitch2midi.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
#include "pitch2midi.h"
#include "essentiamath.h"

using namespace std;
using namespace essentia;
using namespace standard;

const char* Pitch2Midi::name = "Pitch2Midi";
const char* Pitch2Midi::category = "Pitch";
const char *Pitch2Midi::description = DOC("This algorithm estimates the midi note ON/OFF detection from raw pitch and voiced values, using midi buffer and uncertainty checkers.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be unclear what "uncertainty checkers" means for a reader.


void Pitch2Midi::configure()
{
_sampleRate = parameter("sampleRate").toReal();
_hopSize = parameter("hopSize").toInt();
_minFrequency = parameter("minFrequency").toReal();
_minOcurrenceRate = parameter("minOcurrenceRate").toReal();
_bufferDuration = parameter("midiBufferDuration").toReal();
_minOnsetCheckPeriod = parameter("minOnsetCheckPeriod").toReal();
_minOffsetCheckPeriod = parameter("minOffsetCheckPeriod").toReal();
_minNoteChangePeriod = parameter("minNoteChangePeriod").toReal();
_applyCompensation = parameter("applyTimeCompensation").toBool();
// former Pitch2Midi only parameters
_tuningFreq = parameter("tuningFreq").toReal();
_transposition = parameter("transpositionAmount").toInt();

_frameTime = _hopSize / _sampleRate;
_minOnsetCheckThreshold = _minOnsetCheckPeriod / _frameTime;
_minOffsetCheckThreshold = _minOffsetCheckPeriod / _frameTime;
_minNoteChangeThreshold = _minNoteChangePeriod / _frameTime;

_unvoicedFrameCounter = 0;
_offsetCheckCounter = 0;
_onsetCheckCounter = 0;

_minOcurrenceRatePeriod = _minOcurrenceRate * _bufferDuration;
_minOcurrenceRateThreshold = _minOcurrenceRatePeriod / _frameTime;

_capacity = capacity();
_framebuffer = AlgorithmFactory::create("FrameBuffer");
_framebuffer->configure("bufferSize", _capacity);

}

void Pitch2Midi::getMidiNoteNumber(Real pitch)
{
_detectedPitch = pitch;

if (pitch <= 0) { _detectedPitch = 1e-05; }
xaviliz marked this conversation as resolved.
Show resolved Hide resolved
int idx = hz2midi(pitch, _tuningFreq);
_midiNoteNumberTransposed = idx + _transposition;
}

int Pitch2Midi::capacity()
xaviliz marked this conversation as resolved.
Show resolved Hide resolved
{
float hopSizeFloat = _hopSize; // ensure no int/int division happens
float sampleRateFloat = _sampleRate;
xaviliz marked this conversation as resolved.
Show resolved Hide resolved
int c = static_cast<int>( round( sampleRateFloat / hopSizeFloat * _bufferDuration ) );
return max(_minCapacity, c);
}

// this should NOT be called until framebuffer.compute has been called
bool Pitch2Midi::hasCoherence()
{
Real sum = accumulate(_buffer.begin(), _buffer.end(), 0.0);
if (sum / _capacity == _buffer[0]) {
return true;
}
return false;
xaviliz marked this conversation as resolved.
Show resolved Hide resolved
}

// this should NOT be called until framebuffer.compute has been called and _capacity has been set on the configure
void Pitch2Midi::getMaxVoted()
xaviliz marked this conversation as resolved.
Show resolved Hide resolved
{
map<Real, Real> counts;
for (Real value : _buffer) {
counts[value]++;
}

Real maxCount = 0;
Real maxValue = 0;
for (auto& pair : counts) {
if (pair.second > maxCount) {
maxCount = pair.second;
maxValue = pair.first;
}
}

_maxVoted[0] = maxValue;
_maxVoted[1] = maxCount / _capacity;
}

bool Pitch2Midi::isMaxVotedZero() {
return _maxVoted[0] == 0.0;
}

bool Pitch2Midi::isCurrentMidiNoteEqualToMaxVoted() {
return note->midiNote == _maxVoted[0];
}

bool Pitch2Midi::isMaxVotedCountGreaterThanMinOcurrenceRate() {
return _maxVoted[1] > _minOcurrenceRate;
}

void Pitch2Midi::setOutputs(int midiNoteNumber, float onsetTimeCompensation, float offsetTimeCompensation) {
vector<string>& messageTypeOut = _messageTypeOut.get();
vector<Real>& midiNoteNumberOut = _midiNoteNumberOut.get();
vector<Real>& timeCompensationOut = _timeCompensationOut.get();

// reuse bins
_messageTypeBin.resize(0);
_midiNoteNumberBin.resize(0);
_timeCompensationBin.resize(0);
xaviliz marked this conversation as resolved.
Show resolved Hide resolved

// TODO: this is not clear because it might remove an note_off message which is defined by dnote.
//#! it would be better just to provide some code for midiNoteNumbre when this happens
if (midiNoteNumber <= 0 && midiNoteNumber >= 127) {
//E_INFO("SCAPE");
return;
}

// let's define first the message type
if (_noteOff) {
_messageTypeBin.push_back("note_off");
}

if (_noteOn) {
_messageTypeBin.push_back("note_on");
}

if (!_applyCompensation) {
onsetTimeCompensation = 0.f;
offsetTimeCompensation = 0.f;
}

_midiNoteNumberBin.push_back(static_cast<Real>(dnote_->midiNote));
_midiNoteNumberBin.push_back(static_cast<Real>(midiNoteNumber));
_timeCompensationBin.push_back(offsetTimeCompensation);
_timeCompensationBin.push_back(onsetTimeCompensation);

messageTypeOut = _messageTypeBin;
midiNoteNumberOut = _midiNoteNumberBin;
timeCompensationOut = _timeCompensationBin;
}

void Pitch2Midi::push(int midiNoteNumber) {
// push new MIDI note number in the MIDI buffer
_midiNoteNumberVector[0] = static_cast<Real>(midiNoteNumber);
_framebuffer->input("frame").set(_midiNoteNumberVector);
_framebuffer->output("frame").set(_buffer);
_framebuffer->compute();
}

void Pitch2Midi::compute()
{
// former MidiPool inputs are now Pitch2Midi internal vars
// all we need is to run the conversions:
const Real& pitch = _pitch.get();
const int& voiced = _voiced.get();

// do sanity checks
if (pitch < 0) {
throw EssentiaException("PitchContoursMultiMelody: specified duration of the input signal must be non-negative");
xaviliz marked this conversation as resolved.
Show resolved Hide resolved
}

getMidiNoteNumber(pitch);
xaviliz marked this conversation as resolved.
Show resolved Hide resolved

// refresh note_on and note_off timestamps
_noteOn = false;
_noteOff = false;

// unvoiced frame detection
if (!voiced) {
if ( _NOTED_ON ) {
_unvoicedFrameCounter++;
if (_unvoicedFrameCounter > _minNoteChangeThreshold) {
_NOTED_ON = false;
_noteOff = true;
updateDnote();
setOutputs(dnote_->midiNote, 0.f, _minNoteChangePeriod);
//E_INFO("offset(unvoiced frame)");
_unvoicedFrameCounter = 0;
_offsetCheckCounter = 0;
_onsetCheckCounter = 0;
}
} else {
_unvoicedFrameCounter = 0;
push(0); // push 0th MIDI note to remove the past
_offsetCheckCounter = 0;
_onsetCheckCounter = 0;
}
return;
}

_unvoicedFrameCounter = 0;

// push new MIDI note number in the MIDI buffer
push(_midiNoteNumberTransposed);

// update max_voting
getMaxVoted();

/*
E_INFO("onset thresholds: "<< _onsetCheckCounter <<" - " << _minOnsetCheckThreshold);
E_INFO("offset thresholds: "<< _offsetCheckCounter <<" - " << _minOffsetCheckThreshold);
*/

// analyze pitch buffer
if (hasCoherence() && _NOTED_ON) {
if (isCurrentMidiNoteEqualToMaxVoted()) {
xaviliz marked this conversation as resolved.
Show resolved Hide resolved
_offsetCheckCounter = 0;
_onsetCheckCounter = 0;
}
else {
// IMPORTANT: this hardly happens so if hasCoherence() current MIDI note is equals to max voted.
_offsetCheckCounter++;
if (_offsetCheckCounter > _minOffsetCheckThreshold) {
updateDnote();
delete note;
note = new Note(_buffer[0]);
_noteOff = true;
_noteOn = true;
_offsetCheckCounter = 0;
_onsetCheckCounter = 0;
//E_INFO("off-onset(" << _buffer[0] << ", coherent & NOTED)");
}
}
setOutputs(_midiNoteNumberTransposed, _minOffsetCheckPeriod, _minOffsetCheckPeriod);
return;
}

if (hasCoherence() && !_NOTED_ON) {

_onsetCheckCounter++;

if (_onsetCheckCounter > _minOnsetCheckThreshold){
delete note;
note = new Note(_buffer[0]);
_noteOn = true;
_NOTED_ON = true;
//E_INFO("onset(" << _buffer[0] << ", coherent & !NOTED): "<< _onsetCheckCounter <<" - " << _minOnsetCheckThreshold);
_onsetCheckCounter = 0;
_offsetCheckCounter = 0;
}
setOutputs(_midiNoteNumberTransposed, _minOnsetCheckPeriod, _minOffsetCheckPeriod);
return;
}

if (!hasCoherence() && _NOTED_ON) {
if (!isMaxVotedZero()) {
_onsetCheckCounter++;
// combines checker with minOcurrenceRate
if (_onsetCheckCounter > _minOcurrenceRateThreshold){
_noteOff = true;
_noteOn = true;
_NOTED_ON = true;
updateDnote();
delete note;
note = new Note(_maxVoted[0]);
//E_INFO("off-onset(" << _maxVoted[0] << ", uncoherent & NOTED): " << _onsetCheckCounter << " - " << _minOcurrenceRateThreshold);
_offsetCheckCounter = 0;
_onsetCheckCounter = 0;
}
}
setOutputs(_midiNoteNumberTransposed, _minOcurrenceRatePeriod, _minOcurrenceRatePeriod);
return;
}

if (!hasCoherence() && !_NOTED_ON) {
if (isMaxVotedCountGreaterThanMinOcurrenceRate()) {
_onsetCheckCounter++;

if (_onsetCheckCounter > _minOnsetCheckThreshold) {
if (!isMaxVotedZero()) {
delete note;
note = new Note(_maxVoted[0]);
_NOTED_ON = true;
_noteOn = true;
//E_INFO("onset(" << _maxVoted[0] << ", uncoherent & unNOTED)");
_onsetCheckCounter = 0;
_offsetCheckCounter = 0;
}
}
}
setOutputs(_midiNoteNumberTransposed, _minOnsetCheckPeriod, _minOffsetCheckPeriod);
return;
}
// E_INFO("Compute() -END");
}




void Pitch2Midi::updateDnote () {
xaviliz marked this conversation as resolved.
Show resolved Hide resolved
delete dnote_;
dnote_ = new Note(*note);
}
Loading
Loading