Skip to content

Commit

Permalink
Support KBM Files (surge-synthesizer#1462)
Browse files Browse the repository at this point in the history
Addresses surge-synthesizer#1041

This larger diff supports KBM files inside the surge engine
but doesn't include the workflow to load them in the UI or display
them at tuning time yet. Rather it sets up the ability to parse
KBM files and push them onto the storage and note architecture
and uses the Unit Test frameowrk to show that the synth is properly
retuned and remapped.

- Unit tests confirm we parse valid KBM files properly
- Add SurgeStorage hooks for a current mapping
- Confirm tuning and mapping for full-keyboard mappings works
- Allow users to pick a KBM file

Still work to do on persisting and showing.
  • Loading branch information
baconpaul authored Jan 9, 2020
1 parent fbf7301 commit 370a7ca
Show file tree
Hide file tree
Showing 11 changed files with 704 additions and 11 deletions.
113 changes: 106 additions & 7 deletions src/common/SurgeStorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1291,38 +1291,113 @@ bool SurgeStorage::retuneToScale(const Surge::Storage::Scale& s)
isStandardTuning = false;

float pitches[512];
int pos0 = 256 + scaleConstantNote();
int posPitch0 = 256 + scaleConstantNote();
int posScale0 = 256 + currentMapping.middleNote;
float pitchMod = log(scaleConstantPitch())/log(2) - 1;
pitches[pos0] = 1.0;

int scalePositionOfStartNote = 0;
int scalePositionOfTuningNote = currentMapping.keys[currentMapping.tuningConstantNote - currentMapping.middleNote];
float tuningCenterPitchOffset;
if( scalePositionOfTuningNote == 0 )
tuningCenterPitchOffset = 0;
else
tuningCenterPitchOffset = s.tones[scalePositionOfTuningNote-1].floatValue - 1.0;

pitches[posPitch0] = 1.0;
for (int i=0; i<512; ++i)
{
int distanceFromScale0 = i - pos0;
// TODO: ScaleCenter and PitchCenter are now two different notes.
int distanceFromPitch0 = i - posPitch0;
int distanceFromScale0 = i - posScale0;

if( distanceFromScale0 == 0 )
if( distanceFromPitch0 == 0 )
{
table_pitch[i] = pow( 2.0, pitches[i] + pitchMod );
#if DEBUG_SCALES
if( i > 296 && i < 340 )
std::cout << "PITCH: i=" << i << " n=" << i - 256
<< " p=" << pitches[i]
<< " tp=" << table_pitch[i]
<< " fr=" << table_pitch[i] * 8.175798915
<< std::endl;
#endif
}
else
{
/*
We used to have this which assumed 1-12
Now we have our note number, our distance from the
center note, and the key remapping
int rounds = (distanceFromScale0-1) / s.count;
int thisRound = (distanceFromScale0-1) % s.count;
*/

int rounds;
int thisRound;
int disable = false;
if( currentMapping.isStandardMapping )
{
rounds = (distanceFromScale0-1) / s.count;
thisRound = (distanceFromScale0-1) % s.count;
}
else
{
/*
** Now we have this situation. We are at note i so we
** are m away from the center note which is distanceFromScale0
**
** If we mod that by the mapping size we know which note we are on
*/
int mappingKey = distanceFromScale0 % currentMapping.count;
if( mappingKey < 0 )
mappingKey += currentMapping.count;
int cm = currentMapping.keys[mappingKey];
int push = 0;
if( cm < 0 )
{
disable = true;
}
else
{
push = mappingKey - cm;
}
rounds = (distanceFromScale0 - push - 1) / s.count;
thisRound = (distanceFromScale0 - push - 1) % s.count;
#ifdef DEBUG_SCALES
if( i > 296 && i < 340 )
std::cout << "MAPPING n=" << i - 256 << " pushes ds0=" << distanceFromScale0 << " cmc=" << currentMapping.count << " tr=" << thisRound << " r=" << rounds << " mk=" << mappingKey << " cm=" << cm << " push=" << push << " dis=" << disable << " mk-p-1=" << mappingKey - push - 1 << std::endl;
#endif


}

if( thisRound < 0 )
{
thisRound += s.count;
rounds -= 1;
}
float mul = pow( s.tones[s.count-1].floatValue, rounds);
pitches[i] = s.tones[thisRound].floatValue + rounds * (s.tones[s.count - 1].floatValue - 1.0);
if( disable )
pitches[i] = 0;
else
pitches[i] = s.tones[thisRound].floatValue + rounds * (s.tones[s.count - 1].floatValue - 1.0) - tuningCenterPitchOffset;

float otp = table_pitch[i];
table_pitch[i] = pow( 2.0, pitches[i] + pitchMod );

#if DEBUG_SCALES
if( i > 296 && i < 340 )
std::cout << "PITCH: i=" << i << " n=" << i - 256 << " r=" << rounds << " t=" << thisRound
std::cout << "PITCH: i=" << i << " n=" << i - 256
<< " ds0=" << distanceFromScale0
<< " dp0=" << distanceFromPitch0
<< " r=" << rounds << " t=" << thisRound
<< " p=" << pitches[i]
<< " t=" << s.tones[thisRound].floatValue
<< " t=" << s.tones[thisRound].floatValue << " " << s.tones[thisRound ]
<< " dis=" << disable
<< " tp=" << table_pitch[i]
<< " fr=" << table_pitch[i] * 8.175798915
<< " otp=" << otp
<< " tcpo=" << tuningCenterPitchOffset
<< " diff=" << table_pitch[i] - otp

//<< " l2p=" << log(otp)/log(2.0)
Expand All @@ -1345,6 +1420,30 @@ bool SurgeStorage::retuneToScale(const Surge::Storage::Scale& s)
return true;
}

bool SurgeStorage::remapToStandardKeyboard()
{
return remapToKeyboard(Surge::Storage::KeyboardMapping());
}

bool SurgeStorage::remapToKeyboard(const Surge::Storage::KeyboardMapping& k)
{
currentMapping = k;
isStandardMapping = k.isStandardMapping;
if( isStandardMapping )
{
tuningPitch = 32.0;
tuningPitchInv = 1.0 / 32.0;
}
else
{
tuningPitch = k.tuningFrequency / 8.175798915;
tuningPitchInv = 1.0 / tuningPitch;
}
// The mapping will change all the cached pitches
retuneToScale(currentScale);
return true;
}

#if TARGET_LV2
bool SurgeStorage::skipLoadWtAndPatch = false;
#endif
Expand Down
12 changes: 9 additions & 3 deletions src/common/SurgeStorage.h
Original file line number Diff line number Diff line change
Expand Up @@ -608,12 +608,18 @@ class alignas(16) SurgeStorage
void note_to_omega(float, float&, float&);

bool retuneToScale(const Surge::Storage::Scale& s);
inline int scaleConstantNote() { return 60; }
inline float scaleConstantPitch() { return 32.0; }
inline float scaleConstantPitchInv() { return 0.03125; } // Obviously that's the inverse of the above
bool remapToKeyboard(const Surge::Storage::KeyboardMapping &k);
bool remapToStandardKeyboard();
inline int scaleConstantNote() { return currentMapping.tuningConstantNote; }
inline float scaleConstantPitch() { return tuningPitch; }
inline float scaleConstantPitchInv() { return tuningPitchInv; } // Obviously that's the inverse of the above

Surge::Storage::Scale currentScale;
bool isStandardTuning;

Surge::Storage::KeyboardMapping currentMapping;
bool isStandardMapping = true;
float tuningPitch = 32.0f, tuningPitchInv = 0.03125f;

private:
TiXmlDocument snapshotloader;
Expand Down
91 changes: 91 additions & 0 deletions src/common/Tunings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,97 @@ Surge::Storage::Scale Surge::Storage::parseSCLData(const std::string &d)
return res;
}

Surge::Storage::KeyboardMapping keyboardMappingFromStream(std::istream &inf)
{
std::string line;
const int read_header = 0, read_count = 1, read_note = 2;

Surge::Storage::KeyboardMapping res;
std::ostringstream rawOSS;
res.isStandardMapping = false;
res.keys.clear();

enum parsePosition {
map_size = 0,
first_midi,
last_midi,
middle,
reference,
freq,
degree,
keys
};
parsePosition state = map_size;

while (std::getline(inf, line))
{
rawOSS << line << "\n";
if (line[0] == '!')
{
continue;
}

if( line == "x" ) line = "-1";

int i = std::atoi(line.c_str());
float v = std::atof(line.c_str());

switch (state)
{
case map_size:
res.count = i;
break;
case first_midi:
res.firstMidi = i;
break;
case last_midi:
res.lastMidi = i;
break;
case middle:
res.middleNote = i;
break;
case reference:
res.tuningConstantNote = i;
break;
case freq:
res.tuningFrequency = v;
break;
case degree:
res.octaveDegrees = i;
break;
case keys:
res.keys.push_back(i);
break;
}
if( state != keys ) state = (parsePosition)(state + 1);
}

res.rawText = rawOSS.str();
return res;
}

Surge::Storage::KeyboardMapping Surge::Storage::readKBMFile(std::string fname)
{
std::ifstream inf;
inf.open(fname);
if (!inf.is_open())
{
return KeyboardMapping();
}

auto res = keyboardMappingFromStream(inf);
res.name = fname;
return res;
}

Surge::Storage::KeyboardMapping Surge::Storage::parseKBMData(const std::string &d)
{
std::istringstream iss(d);
auto res = keyboardMappingFromStream(iss);
res.name = "Mapping from Patch";
return res;
}

std::ostream& Surge::Storage::operator<<(std::ostream& os, const Surge::Storage::Tone& t)
{
os << (t.type == Tone::kToneCents ? "cents" : "ratio") << " ";
Expand Down
41 changes: 40 additions & 1 deletion src/common/Tunings.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,49 @@ struct Scale
std::string toHtml(SurgeStorage *storage);
};

struct KeyboardMapping
{
bool isValid;
bool isStandardMapping;
int count;
int firstMidi, lastMidi;
int middleNote;
int tuningConstantNote;
float tuningFrequency;
int octaveDegrees;
std::vector<int> keys; // rather than an 'x' we use a '-1' for skipped keys

std::string rawText;
std::string name;

KeyboardMapping() : isValid(true),
isStandardMapping(true),
count(12),
firstMidi(0),
lastMidi(127),
middleNote(60),
tuningConstantNote(60),
tuningFrequency(8.175798915 * 32),
octaveDegrees(12),
rawText( "" ),
name( "" )
{
for( int i=0; i<12; ++i )
keys.push_back(i);
}

// TODO
// std::string toHtml();
};

std::ostream& operator<<(std::ostream& os, const Tone& sc);
std::ostream& operator<<(std::ostream& os, const Scale& sc);

//TODO
//std::ostream& operator<<(std::ostream& os, const KeyboardMapping& kbm);

Scale readSCLFile(std::string fname);
Scale parseSCLData(const std::string &sclContents);
KeyboardMapping readKBMFile(std::string fname);
KeyboardMapping parseKBMData(const std::string &kbmContents);
} // namespace Storage
} // namespace Surge
39 changes: 39 additions & 0 deletions src/common/gui/SurgeGUIEditor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3460,6 +3460,15 @@ VSTGUI::COptionMenu *SurgeGUIEditor::makeTuningMenu(VSTGUI::CRect &menuRect)
);
st->setEnabled(! this->synth->storage.isStandardTuning);
tid++;

auto *kst = addCallbackMenu(tuningSubMenu, "Set to Standard Keyboard Mapping",
[this]()
{
this->synth->storage.remapToStandardKeyboard();
}
);
kst->setEnabled(! this->synth->storage.currentMapping.isStandardMapping);
tid++;

addCallbackMenu(tuningSubMenu, "Apply .scl file tuning",
[this]()
Expand Down Expand Up @@ -3491,6 +3500,36 @@ VSTGUI::COptionMenu *SurgeGUIEditor::makeTuningMenu(VSTGUI::CRect &menuRect)
);
tid++;

addCallbackMenu(tuningSubMenu, "Apply .kbm keyboard mapping",
[this]()
{
auto cb = [this](std::string sf)
{
std::string sfx = ".kbm";
if( sf.length() >= sfx.length())
{
if( sf.compare(sf.length() - sfx.length(), sfx.length(), sfx) != 0 )
{
Surge::UserInteractions::promptError( "Please only select .kbm files", "Invalid Choice" );
std::cout << "FILE is [" << sf << "]" << std::endl;
return;
}
}
auto kb = Surge::Storage::readKBMFile(sf);

if (!this->synth->storage.remapToKeyboard(kb) )
{
Surge::UserInteractions::promptError( "This .kbm file is not valid", "File format error" );
return;
}
};
Surge::UserInteractions::promptFileOpenDialog(this->synth->storage.userDataPath,
".scl",
cb);
}
);
tid++;

auto *sct = addCallbackMenu(tuningSubMenu, "Show current tuning",
[this]()
{
Expand Down
Loading

0 comments on commit 370a7ca

Please sign in to comment.