Merge pull request #5 from probonopd/pull-4

Apply PR #4 without Synth_Dexed git submodule
pull/6/head
probonopd 2 years ago committed by GitHub
commit 46ef06c574
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .gitmodules
  2. 2
      Synth_Dexed
  3. 0
      src/Dexed.h
  4. 5
      src/Makefile
  5. 14
      src/kernel.cpp
  6. 4
      src/kernel.h
  7. 375
      src/miniorgan.cpp
  8. 102
      src/miniorgan.h
  9. 9
      src/tuning-library/LICENSE.md
  10. 278
      src/tuning-library/Tunings.h
  11. 538
      src/tuning-library/TuningsImpl.h

2
.gitmodules vendored

@ -3,4 +3,4 @@
url = https://github.com/smuehlst/circle-stdlib
[submodule "Synth_Dexed"]
path = Synth_Dexed
url = https://codeberg.org/dcoredump/Synth_Dexed.git
url = https://github.com/probonopd/Synth_Dexed

@ -1 +1 @@
Subproject commit 06d90dfcf5440023667e161b83a50209d2abf506
Subproject commit 69d1f58e65fb41de721b893a87a8951d2c4ef9e9

@ -5,10 +5,9 @@
CIRCLE_STDLIB_DIR = ../circle-stdlib
SYNTH_DEXED_DIR = ../Synth_Dexed/src
OBJS = main.o kernel.o miniorgan.o \
$(SYNTH_DEXED_DIR)/synth_dexed.o \
OBJS = main.o kernel.o $(SYNTH_DEXED_DIR)/synth_dexed.o
INCLUDE += -I $(SYNTH_DEXED_DIR) -I tuning-library
INCLUDE += -I $(SYNTH_DEXED_DIR)
EXTRACLEAN = $(SYNTH_DEXED_DIR)/*.o $(SYNTH_DEXED_DIR)/*.d

@ -3,11 +3,12 @@
//
#include "kernel.h"
#include <iostream>
#include <synth_dexed.h>
CKernel::CKernel (void)
: CStdlibAppStdio ("minidexed"),
m_I2CMaster (CMachineInfo::Get ()->GetDevice (DeviceI2CMaster), TRUE),
m_MiniOrgan (&mInterrupt, &m_I2CMaster)
m_Dexed(16,SAMPLE_RATE,&mInterrupt, &m_I2CMaster)
{
mActLED.Blink (5); // show we are alive
}
@ -23,10 +24,7 @@ bool CKernel::Initialize (void)
return FALSE;
}
if (!m_MiniOrgan.Initialize ())
{
return FALSE;
}
m_Dexed.activate();
return TRUE;
}
@ -35,13 +33,13 @@ CStdlibApp::TShutdownMode CKernel::Run (void)
{
std::cout << "Hello MiniDexed!\n";
m_MiniOrgan.Start ();
m_Dexed.Start ();
while (m_MiniOrgan.IsActive ())
while(42==42)
{
boolean bUpdated = mUSBHCI.UpdatePlugAndPlay ();
m_MiniOrgan.Process (bUpdated);
m_Dexed.Process(bUpdated);
}
return ShutdownHalt;

@ -16,7 +16,7 @@
#include <circle/types.h>
#include <circle/i2cmaster.h>
#include <circle/usb/usbhcidevice.h>
#include "miniorgan.h"
#include "synth_dexed.h"
enum TShutdownMode
{
@ -38,7 +38,7 @@ public:
private:
// do not change this order
CI2CMaster m_I2CMaster;
CMiniOrgan m_MiniOrgan;
AudioSynthDexed m_Dexed;
};
#endif

@ -1,375 +0,0 @@
//
// miniorgan.cpp
//
// Circle - A C++ bare metal environment for Raspberry Pi
// Copyright (C) 2017-2021 R. Stange <rsta2@o2online.de>
//
// This program is free software: you can redistribute it and/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/>.
//
#include "miniorgan.h"
#include <circle/devicenameservice.h>
#include <circle/logger.h>
#include <assert.h>
#define VOLUME_PERCENT 20
#define MIDI_NOTE_OFF 0b1000
#define MIDI_NOTE_ON 0b1001
#define KEY_NONE 255
static const char FromMiniOrgan[] = "organ";
// See: http://www.deimos.ca/notefreqs/
const float CMiniOrgan::s_KeyFrequency[/* MIDI key number */] =
{
8.17580, 8.66196, 9.17702, 9.72272, 10.3009, 10.9134, 11.5623, 12.2499, 12.9783, 13.7500,
14.5676, 15.4339, 16.3516, 17.3239, 18.3540, 19.4454, 20.6017, 21.8268, 23.1247, 24.4997,
25.9565, 27.5000, 29.1352, 30.8677, 32.7032, 34.6478, 36.7081, 38.8909, 41.2034, 43.6535,
46.2493, 48.9994, 51.9131, 55.0000, 58.2705, 61.7354, 65.4064, 69.2957, 73.4162, 77.7817,
82.4069, 87.3071, 92.4986, 97.9989, 103.826, 110.000, 116.541, 123.471, 130.813, 138.591,
146.832, 155.563, 164.814, 174.614, 184.997, 195.998, 207.652, 220.000, 233.082, 246.942,
261.626, 277.183, 293.665, 311.127, 329.628, 349.228, 369.994, 391.995, 415.305, 440.000,
466.164, 493.883, 523.251, 554.365, 587.330, 622.254, 659.255, 698.456, 739.989, 783.991,
830.609, 880.000, 932.328, 987.767, 1046.50, 1108.73, 1174.66, 1244.51, 1318.51, 1396.91,
1479.98, 1567.98, 1661.22, 1760.00, 1864.66, 1975.53, 2093.00, 2217.46, 2349.32, 2489.02,
2637.02, 2793.83, 2959.96, 3135.96, 3322.44, 3520.00, 3729.31, 3951.07, 4186.01, 4434.92,
4698.64, 4978.03, 5274.04, 5587.65, 5919.91, 6271.93, 6644.88, 7040.00, 7458.62, 7902.13,
8372.02, 8869.84, 9397.27, 9956.06, 10548.1, 11175.3, 11839.8, 12543.9
};
const TNoteInfo CMiniOrgan::s_Keys[] =
{
{',', 72}, // C4
{'M', 71}, // B4
{'J', 70}, // A#4
{'N', 69}, // A4
{'H', 68}, // G#3
{'B', 67}, // G3
{'G', 66}, // F#3
{'V', 65}, // F3
{'C', 64}, // E3
{'D', 63}, // D#3
{'X', 62}, // D3
{'S', 61}, // C#3
{'Z', 60} // C3
};
CMiniOrgan *CMiniOrgan::s_pThis = 0;
CMiniOrgan::CMiniOrgan (CInterruptSystem *pInterrupt, CI2CMaster *pI2CMaster)
: SOUND_CLASS (pInterrupt, SAMPLE_RATE, CHUNK_SIZE
#ifdef USE_I2S
, FALSE, pI2CMaster, DAC_I2C_ADDRESS
#endif
),
m_pMIDIDevice (0),
m_pKeyboard (0),
m_Serial (pInterrupt, TRUE, 5),
m_bUseSerial (FALSE),
m_nSerialState (0),
m_nSampleCount (0),
m_nFrequency (0),
m_nPrevFrequency (0),
m_ucKeyNumber (KEY_NONE)
{
s_pThis = this;
m_nLowLevel = GetRangeMin () * VOLUME_PERCENT / 100;
m_nHighLevel = GetRangeMax () * VOLUME_PERCENT / 100;
m_nNullLevel = (m_nHighLevel + m_nLowLevel) / 2;
m_nCurrentLevel = m_nNullLevel;
}
CMiniOrgan::~CMiniOrgan (void)
{
s_pThis = 0;
}
boolean CMiniOrgan::Initialize (void)
{
CLogger::Get ()->Write (FromMiniOrgan, LogNotice,
"Please attach an USB keyboard or use serial MIDI!");
if (m_Serial.Initialize (31250))
{
m_bUseSerial = TRUE;
return TRUE;
}
return FALSE;
}
void CMiniOrgan::Process (boolean bPlugAndPlayUpdated)
{
if (m_pMIDIDevice != 0)
{
return;
}
if (bPlugAndPlayUpdated)
{
m_pMIDIDevice =
(CUSBMIDIDevice *) CDeviceNameService::Get ()->GetDevice ("umidi1", FALSE);
if (m_pMIDIDevice != 0)
{
m_pMIDIDevice->RegisterRemovedHandler (USBDeviceRemovedHandler);
m_pMIDIDevice->RegisterPacketHandler (MIDIPacketHandler);
return;
}
}
if (m_pKeyboard != 0)
{
return;
}
if (bPlugAndPlayUpdated)
{
m_pKeyboard =
(CUSBKeyboardDevice *) CDeviceNameService::Get ()->GetDevice ("ukbd1", FALSE);
if (m_pKeyboard != 0)
{
m_pKeyboard->RegisterRemovedHandler (USBDeviceRemovedHandler);
m_pKeyboard->RegisterKeyStatusHandlerRaw (KeyStatusHandlerRaw);
return;
}
}
if (!m_bUseSerial)
{
return;
}
// Read serial MIDI data
u8 Buffer[20];
int nResult = m_Serial.Read (Buffer, sizeof Buffer);
if (nResult <= 0)
{
return;
}
// Process MIDI messages
// See: https://www.midi.org/specifications/item/table-1-summary-of-midi-message
for (int i = 0; i < nResult; i++)
{
u8 uchData = Buffer[i];
switch (m_nSerialState)
{
case 0:
MIDIRestart:
if ((uchData & 0xE0) == 0x80) // Note on or off, all channels
{
m_SerialMessage[m_nSerialState++] = uchData;
}
break;
case 1:
case 2:
if (uchData & 0x80) // got status when parameter expected
{
m_nSerialState = 0;
goto MIDIRestart;
}
m_SerialMessage[m_nSerialState++] = uchData;
if (m_nSerialState == 3) // message is complete
{
MIDIPacketHandler (0, m_SerialMessage, sizeof m_SerialMessage);
m_nSerialState = 0;
}
break;
default:
assert (0);
break;
}
}
}
unsigned CMiniOrgan::GetChunk (u32 *pBuffer, unsigned nChunkSize)
{
unsigned nResult = nChunkSize;
// reset sample counter if key has changed
if (m_nFrequency != m_nPrevFrequency)
{
m_nSampleCount = 0;
m_nPrevFrequency = m_nFrequency;
}
// output level has to be changed on every nSampleDelay'th sample (if key is pressed)
unsigned nSampleDelay = 0;
if (m_nFrequency != 0)
{
nSampleDelay = (SAMPLE_RATE/2 + m_nFrequency/2) / m_nFrequency;
}
#ifdef USE_HDMI
unsigned nFrame = 0;
#endif
for (; nChunkSize > 0; nChunkSize -= 2) // fill the whole buffer
{
u32 nSample = (u32) m_nNullLevel;
if (m_nFrequency != 0) // key pressed?
{
// change output level if required to generate a square wave
if (++m_nSampleCount >= nSampleDelay)
{
m_nSampleCount = 0;
if (m_nCurrentLevel < m_nHighLevel)
{
m_nCurrentLevel = m_nHighLevel;
}
else
{
m_nCurrentLevel = m_nLowLevel;
}
}
nSample = (u32) m_nCurrentLevel;
}
#ifdef USE_HDMI
nSample = ConvertIEC958Sample (nSample, nFrame);
if (++nFrame == IEC958_FRAMES_PER_BLOCK)
{
nFrame = 0;
}
#endif
*pBuffer++ = nSample; // 2 stereo channels
*pBuffer++ = nSample;
}
return nResult;
}
void CMiniOrgan::MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength)
{
assert (s_pThis != 0);
// The packet contents are just normal MIDI data - see
// https://www.midi.org/specifications/item/table-1-summary-of-midi-message
if (nLength < 3)
{
return;
}
u8 ucStatus = pPacket[0];
//u8 ucChannel = ucStatus & 0x0F;
u8 ucType = ucStatus >> 4;
u8 ucKeyNumber = pPacket[1];
u8 ucVelocity = pPacket[2];
if (ucType == MIDI_NOTE_ON)
{
if ( ucVelocity > 0
&& ucKeyNumber < sizeof s_KeyFrequency / sizeof s_KeyFrequency[0])
{
s_pThis->m_ucKeyNumber = ucKeyNumber;
s_pThis->m_nFrequency = (unsigned) (s_KeyFrequency[ucKeyNumber] + 0.5);
}
else
{
if (s_pThis->m_ucKeyNumber == ucKeyNumber)
{
s_pThis->m_ucKeyNumber = KEY_NONE;
s_pThis->m_nFrequency = 0;
}
}
}
else if (ucType == MIDI_NOTE_OFF)
{
if (s_pThis->m_ucKeyNumber == ucKeyNumber)
{
s_pThis->m_ucKeyNumber = KEY_NONE;
s_pThis->m_nFrequency = 0;
}
}
}
void CMiniOrgan::KeyStatusHandlerRaw (unsigned char ucModifiers, const unsigned char RawKeys[6])
{
assert (s_pThis != 0);
// find the key code of a pressed key
char chKey = '\0';
for (unsigned i = 0; i <= 5; i++)
{
u8 ucKeyCode = RawKeys[i];
if (ucKeyCode != 0)
{
if (0x04 <= ucKeyCode && ucKeyCode <= 0x1D)
{
chKey = RawKeys[i]-0x04+'A'; // key code of 'A' is 0x04
}
else if (ucKeyCode == 0x36)
{
chKey = ','; // key code of ',' is 0x36
}
break;
}
}
if (chKey != '\0')
{
// find the pressed key in the key table and set its frequency
for (unsigned i = 0; i < sizeof s_Keys / sizeof s_Keys[0]; i++)
{
if (s_Keys[i].Key == chKey)
{
u8 ucKeyNumber = s_Keys[i].KeyNumber;
assert (ucKeyNumber < sizeof s_KeyFrequency / sizeof s_KeyFrequency[0]);
s_pThis->m_nFrequency = (unsigned) (s_KeyFrequency[ucKeyNumber] + 0.5);
return;
}
}
}
s_pThis->m_nFrequency = 0;
}
void CMiniOrgan::USBDeviceRemovedHandler (CDevice *pDevice, void *pContext)
{
assert (s_pThis != 0);
if (s_pThis->m_pMIDIDevice == (CUSBMIDIDevice *) pDevice)
{
CLogger::Get ()->Write (FromMiniOrgan, LogDebug, "USB MIDI keyboard removed");
s_pThis->m_pMIDIDevice = 0;
}
else if (s_pThis->m_pKeyboard == (CUSBKeyboardDevice *) pDevice)
{
CLogger::Get ()->Write (FromMiniOrgan, LogDebug, "USB PC keyboard removed");
s_pThis->m_pKeyboard = 0;
}
}

@ -1,102 +0,0 @@
//
// miniorgan.h
//
// Circle - A C++ bare metal environment for Raspberry Pi
// Copyright (C) 2017-2021 R. Stange <rsta2@o2online.de>
//
// This program is free software: you can redistribute it and/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/>.
//
#ifndef _miniorgan_h
#define _miniorgan_h
// define only one
//#define USE_I2S
//#define USE_HDMI
#ifdef USE_I2S
#include <circle/i2ssoundbasedevice.h>
#define SOUND_CLASS CI2SSoundBaseDevice
#define SAMPLE_RATE 192000
#define CHUNK_SIZE 8192
#define DAC_I2C_ADDRESS 0 // I2C slave address of the DAC (0 for auto probing)
#elif defined (USE_HDMI)
#include <circle/hdmisoundbasedevice.h>
#define SOUND_CLASS CHDMISoundBaseDevice
#define SAMPLE_RATE 48000
#define CHUNK_SIZE (384 * 10)
#else
#include <circle/pwmsoundbasedevice.h>
#define SOUND_CLASS CPWMSoundBaseDevice
#define SAMPLE_RATE 48000
#define CHUNK_SIZE 2048
#endif
#include <circle/interrupt.h>
#include <circle/i2cmaster.h>
#include <circle/usb/usbmidi.h>
#include <circle/usb/usbkeyboard.h>
#include <circle/serial.h>
#include <circle/types.h>
struct TNoteInfo
{
char Key;
u8 KeyNumber; // MIDI number
};
class CMiniOrgan : public SOUND_CLASS
{
public:
CMiniOrgan (CInterruptSystem *pInterrupt, CI2CMaster *pI2CMaster);
~CMiniOrgan (void);
boolean Initialize (void);
void Process (boolean bPlugAndPlayUpdated);
unsigned GetChunk (u32 *pBuffer, unsigned nChunkSize);
private:
static void MIDIPacketHandler (unsigned nCable, u8 *pPacket, unsigned nLength);
static void KeyStatusHandlerRaw (unsigned char ucModifiers, const unsigned char RawKeys[6]);
static void USBDeviceRemovedHandler (CDevice *pDevice, void *pContext);
private:
CUSBMIDIDevice * volatile m_pMIDIDevice;
CUSBKeyboardDevice * volatile m_pKeyboard;
CSerialDevice m_Serial;
boolean m_bUseSerial;
unsigned m_nSerialState;
u8 m_SerialMessage[3];
int m_nLowLevel;
int m_nNullLevel;
int m_nHighLevel;
int m_nCurrentLevel;
unsigned m_nSampleCount;
unsigned m_nFrequency; // 0 if no key pressed
unsigned m_nPrevFrequency;
u8 m_ucKeyNumber;
static const float s_KeyFrequency[];
static const TNoteInfo s_Keys[];
static CMiniOrgan *s_pThis;
};
#endif

@ -1,9 +0,0 @@
Copyright 2019-2020, Paul Walker
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -1,278 +0,0 @@
// -*-c++-*-
/**
* Tunings.h
* Copyright Paul Walker, 2019-2020
* Released under the MIT License. See LICENSE.md
*
* Tunings.h contains the public API required to determine full keyboard frequency maps
* for a scala SCL and KBM file in standalone, tested, open licensed C++ header only library.
*
* An example of using the API is
*
* ```
* auto s = Tunings::readSCLFile( "./my-scale.scl" );
* auto k = Tunings::readKBMFile( "./my-mapping.kbm" );
*
* Tunings::Tuning t( s, k );
*
* std::cout << "The frequency of C4 and A4 are "
* << t.frequencyForMidiNote( 60 ) << " and "
* << t.frequencyForMidiNote( 69 ) << std::endl;
* ```
*
* The API provides several other points, such as access to the structure of the SCL and KBM,
* the ability to create several prototype SCL and KBM files wthout SCL or KBM content,
* a frequency measure which is normalized by the frequency of standard tuning midi note 0
* and the logarithmic frequency scale, with a doubling per frequency doubling.
*
* Documentation is in the class header below; tests are in `tests/all_tests.cpp` and
* a variety of command line tools accompany the header.
*/
#ifndef __INCLUDE_TUNINGS_H
#define __INCLUDE_TUNINGS_H
#include <string>
#include <vector>
#include <iostream>
#include <memory>
#include <array>
namespace Tunings
{
const double MIDI_0_FREQ=8.17579891564371; // or 440.0 * pow( 2.0, - (69.0/12.0 ) )
/**
* A Tone is a single entry in an SCL file. It is expressed either in cents or in
* a ratio, as described in the SCL documentation.
*
* In most normal use, you will not use this class, and it will be internal to a Scale
*/
struct Tone
{
typedef enum Type
{
kToneCents, // An SCL representation like "133.0"
kToneRatio // An SCL representation like "3/7"
} Type;
Type type;
double cents;
int ratio_d, ratio_n;
std::string stringRep;
double floatValue; // cents / 1200 + 1.
Tone() : type(kToneRatio),
cents(0),
ratio_d(1),
ratio_n(1),
stringRep("1/1"),
floatValue(1.0)
{
}
};
/**
* Given an SCL string like "100.231" or "3/7" set up a Tone
*/
inline Tone toneFromString(const std::string &t, int lineno=-1);
/**
* The Scale is the representation of the SCL file. It contains several key
* features. Most importantly it has a count and a vector of Tones.
*
* In most normal use, you will simply pass around instances of this class
* to a Tunings::Tuning instance, but in some cases you may want to create
* or inspect this class yourself. Especially if you are displaying this
* class to your end users, you may want to use the rawText or count methods.
*/
struct Scale
{
std::string name; // The name in the SCL file. Informational only
std::string description; // The description in the SCL file. Informational only
std::string rawText; // The raw text of the SCL file used to create this Scale
int count; // The number of tones.
std::vector<Tone> tones; // The tones
Scale() : name("empty scale"),
description(""),
rawText(""),
count(0)
{
}
};
/**
* The KeyboardMapping class represents a KBM file. In most cases, the salient
* features are the tuningConstantNote and tuningFrequency, which allow you to
* pick a fixed note in the midi keyboard when retuning. The KBM file can also
* remap individual keys to individual points in a scale, which kere is done with the
* keys vector.
*
* Just as with Scale, the rawText member contains the text of the KBM file used.
*/
struct KeyboardMapping
{
int count;
int firstMidi, lastMidi;
int middleNote;
int tuningConstantNote;
double tuningFrequency, tuningPitch; // pitch = frequency / MIDI_0_FREQ
int octaveDegrees;
std::vector<int> keys; // rather than an 'x' we use a '-1' for skipped keys
std::string rawText;
std::string name;
KeyboardMapping() : count(0),
firstMidi(0),
lastMidi(127),
middleNote(60),
tuningConstantNote(60),
tuningFrequency(MIDI_0_FREQ * 32.0),
tuningPitch(32.0),
octaveDegrees(12),
rawText( "" ),
name( "" )
{
}
};
/**
* In some failure states, the tuning library will throw an exception of
* type TuningError with a descriptive message.
*/
class TuningError : public std::exception {
public:
TuningError(std::string m) : whatv(m) { }
virtual const char* what() const noexcept override { return whatv.c_str(); }
private:
std::string whatv;
};
/**
* readSCLFile returns a Scale from the SCL File in fname
*/
Scale readSCLFile(std::string fname);
/**
* parseSCLData returns a scale from the SCL file contents in memory
*/
Scale parseSCLData(const std::string &sclContents);
/**
* evenTemperament12NoteScale provides a utility scale which is
* the "standard tuning" scale
*/
Scale evenTemperament12NoteScale();
/**
* evenDivisionOfSpanByM provides a scale referd to as "ED2-17" or
* "ED3-24" by dividing the Span into M points. eventDivisionOfSpanByM(2,12)
* should be the evenTemperament12NoteScale
*/
Scale evenDivisionOfSpanByM( int Span, int M );
/**
* readKBMFile returns a KeyboardMapping from a KBM file name
*/
KeyboardMapping readKBMFile(std::string fname);
/**
* parseKBMData returns a KeyboardMapping from a KBM data in memory
*/
KeyboardMapping parseKBMData(const std::string &kbmContents);
/**
* tuneA69To creates a KeyboardMapping which keeps the midi note 69 (A4) set
* to a constant frequency, given
*/
KeyboardMapping tuneA69To(double freq);
/**
* tuneNoteTo creates a KeyboardMapping which keeps the midi note given is set
* to a constant frequency, given
*/
KeyboardMapping tuneNoteTo(int midiNote, double freq);
/**
* startScaleOnAndTuneNoteTo generates a KBM where scaleStart is the note 0
* of the scale, where midiNote is the tuned note, and where feq is the frequency
*/
KeyboardMapping startScaleOnAndTuneNoteTo(int scaleStart, int midiNote, double freq);
/**
* The Tuning class is the primary place where you will interact with this library.
* It is constructed for a scale and mapping and then gives you the ability to
* determine frequencies across and beyond the midi keyboard. Since modulation
* can force key number well outside the [0,127] range in some of our synths we
* support a midi note range from -256 to + 256 spanning more than the entire frequency
* space reasonable.
*
* To use this class, you construct a fresh instance every time you want to use a
* different Scale and Keyboard. If you want to tune to a different scale or mapping,
* just construct a new instance.
*/
class Tuning {
public:
// The number of notes we pre-compute
constexpr static int N = 512;
// Construct a tuning with even temperament and standard mapping
Tuning();
/**
* Construct a tuning for a particular scale, mapping, or for both.
*/
Tuning( const Scale &s );
Tuning( const KeyboardMapping &k );
Tuning( const Scale &s, const KeyboardMapping &k );
/**
* These three related functions provide you the information you
* need to use this tuning.
*
* frequencyForMidiNote returns the Frequency in HZ for a given midi
* note. In standard tuning, FrequencyForMidiNote(69) will be 440
* and frequencyForMidiNote(60) will be 261.62 - the standard frequencies
* for A and middle C.
*
* frequencyForMidiNoteScaledByMidi0 returns the frequency but with the
* standard frequency of midi note 0 divided out. So in standard tuning
* frequencyForMidiNoteScaledByMidi0(0) = 1 and frequencyForMidiNoteScaledByMidi0(60) = 32
*
* Finally logScaledFrequencyForMidiNote returns the log base 2 of the scaled frequency.
* So logScaledFrequencyForMidiNote(0) = 0 and logScaledFrequencyForMidiNote(60) = 5.
*
* Both the frequency measures have the feature of doubling when frequency doubles
* (or when a standard octave is spanned), whereas the log one increase by 1 per frequency double.
*
* Depending on your internal pitch model, one of these three methods should allow you
* to calibrate your oscillators to the appropriate frequency based on the midi note
* at hand.
*
* The scalePositionForMidiNote returns the space in the logical scale. Note 0 is the root.
* It has a maxiumum value of count-1. Note that SCL files omit the root internally and so
* this logical scale position is off by 1 from the index in the tones array of the Scale data.
*/
double frequencyForMidiNote( int mn ) const;
double frequencyForMidiNoteScaledByMidi0( int mn ) const;
double logScaledFrequencyForMidiNote( int mn ) const;
int scalePositionForMidiNote( int mn ) const;
// For convenience, the scale and mapping used to construct this are kept as public copies
Scale scale;
KeyboardMapping keyboardMapping;
private:
std::array<double, N> ptable, lptable;
std::array<int, N> scalepositiontable;
};
} // namespace Tunings
#include "TuningsImpl.h"
#endif

@ -1,538 +0,0 @@
// -*-c++-*-
/**
* TuningsImpl.h
* Copyright 2019-2020 Paul Walker
* Released under the MIT License. See LICENSE.md
*
* This contains the nasty nitty gritty implementation of the api in Tunings.h. You probably
* don't need to read it unless you have found and are fixing a bug, are curious, or want
* to add a feature to the API. For usages of this library, the documentation in Tunings.h and
* the usages in tests/all_tests.cpp should provide you more than enough guidance.
*/
#ifndef __INCLUDE_TUNINGS_IMPL_H
#define __INCLUDE_TUNINGS_IMPL_H
#include <iostream>
#include <iomanip>
#include <fstream>
#include <cstdlib>
#include <math.h>
#include <sstream>
#include <cctype>
namespace Tunings
{
inline double locale_atof(const char* s)
{
double result = 0;
std::istringstream istr(s);
istr.imbue(std::locale("C"));
istr >> result;
return result;
}
inline Tone toneFromString(const std::string &line, int lineno)
{
Tone t;
t.stringRep = line;
if (line.find(".") != std::string::npos)
{
t.type = Tone::kToneCents;
t.cents = locale_atof(line.c_str());
}
else
{
t.type = Tone::kToneRatio;
auto slashPos = line.find("/");
if (slashPos == std::string::npos)
{
t.ratio_n = atoi(line.c_str());
t.ratio_d = 1;
}
else
{
t.ratio_n = atoi(line.substr(0, slashPos).c_str());
t.ratio_d = atoi(line.substr(slashPos + 1).c_str());
}
if( t.ratio_n == 0 || t.ratio_d == 0 )
{
std::string s = "Invalid Tone in SCL file.";
if( lineno >= 0 )
s += "Line " + std::to_string(lineno) + ".";
s += " Line is '" + line + "'.";
throw TuningError(s);
}
// 2^(cents/1200) = n/d
// cents = 1200 * log(n/d) / log(2)
t.cents = 1200 * log(1.0 * t.ratio_n/t.ratio_d) / log(2.0);
}
t.floatValue = t.cents / 1200.0 + 1.0;
return t;
}
inline Scale scaleFromStream(std::istream &inf)
{
std::string line;
const int read_header = 0, read_count = 1, read_note = 2, trailing = 3;
int state = read_header;
Scale res;
std::ostringstream rawOSS;
int lineno = 0;
while (std::getline(inf, line))
{
rawOSS << line << "\n";
lineno ++;
if (line[0] == '!')
{
continue;
}
switch (state)
{
case read_header:
res.description = line;
state = read_count;
break;
case read_count:
res.count = atoi(line.c_str());
state = read_note;
break;
case read_note:
auto t = toneFromString(line, lineno);
res.tones.push_back(t);
if( (int)res.tones.size() == res.count )
state = trailing;
break;
}
}
if( ! ( state == read_note || state == trailing ) )
{
throw TuningError( "Incomplete SCL file. Found no notes section in the file" );
}
if( (int)res.tones.size() != res.count )
{
std::string s = "Read fewer notes than count in file. Count=" + std::to_string( res.count )
+ " notes array size=" + std::to_string( res.tones.size() );
throw TuningError(s);
}
res.rawText = rawOSS.str();
return res;
}
inline Scale readSCLFile(std::string fname)
{
std::ifstream inf;
inf.open(fname);
if (!inf.is_open())
{
std::string s = "Unable to open file '" + fname + "'";
throw TuningError(s);
}
auto res = scaleFromStream(inf);
res.name = fname;
return res;
}
inline Scale parseSCLData(const std::string &d)
{
std::istringstream iss(d);
auto res = scaleFromStream(iss);
res.name = "Scale from Patch";
return res;
}
inline Scale evenTemperament12NoteScale()
{
std::string data = R"SCL(! even.scl
!
12 note even temperament
12
!
100.0
200.0
300.0
400.0
500.0
600.0
700.0
800.0
900.0
1000.0
1100.0
2/1
)SCL";
return parseSCLData(data);
}
inline Scale evenDivisionOfSpanByM( int Span, int M )
{
std::ostringstream oss;
oss.imbue( std::locale( "C" ) );
oss << "! Automatically generated ED" << Span << "-" << M << " scale\n";
oss << "Automatically generated ED" << Span << "-" << M << " scale\n";
oss << M << "\n";
oss << "!\n";
double topCents = 1200.0 * log(1.0 * Span) / log(2.0);
double dCents = topCents / M;
for( int i=1; i<M; ++i )
oss << std::fixed << dCents * i << "\n";
oss << Span << "/1\n";
return parseSCLData( oss.str() );
}
inline KeyboardMapping keyboardMappingFromStream(std::istream &inf)
{
std::string line;
KeyboardMapping res;
std::ostringstream rawOSS;
res.keys.clear();
enum parsePosition {
map_size = 0,
first_midi,
last_midi,
middle,
reference,
freq,
degree,
keys,
trailing
};
parsePosition state = map_size;
int lineno = 0;
while (std::getline(inf, line))
{
rawOSS << line << "\n";
lineno ++;
if (line[0] == '!')
{
continue;
}
if( line == "x" ) line = "-1";
else if( state != trailing )
{
const char* lc = line.c_str();
bool validLine = line.length() > 0;
char badChar = '\0';
while( validLine && *lc != '\0' )
{
if( ! ( *lc == ' ' || std::isdigit( *lc ) || *lc == '.' || *lc == (char)13 || *lc == '\n' ) )
{
validLine = false;
badChar = *lc;
}
lc ++;
}
if( ! validLine )
{
throw TuningError( "Invalid line " + std::to_string( lineno ) + ". line='" + line + "'. Bad char is '" +
badChar + "/" + std::to_string( (int)badChar ) + "'" );
}
}
int i = std::atoi(line.c_str());
double v = locale_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;
res.tuningPitch = res.tuningFrequency / 8.17579891564371;
break;
case degree:
res.octaveDegrees = i;
break;
case keys:
res.keys.push_back(i);
if( (int)res.keys.size() == res.count ) state = trailing;
break;
case trailing:
break;
}
if( ! ( state == keys || state == trailing ) ) state = (parsePosition)(state + 1);
if( state == keys && res.count == 0 ) state = trailing;
}
if( ! ( state == keys || state == trailing ) )
{
throw TuningError( "Incomplete KBM file. Ubable to get to keys section of file" );
}
if( (int)res.keys.size() != res.count )
{
throw TuningError( "Different number of keys than mapping file indicates. Count is "
+ std::to_string( res.count ) + " and we parsed " + std::to_string( res.keys.size() ) + " keys." );
}
res.rawText = rawOSS.str();
return res;
}
inline KeyboardMapping readKBMFile(std::string fname)
{
std::ifstream inf;
inf.open(fname);
if (!inf.is_open())
{
std::string s = "Unable to open file '" + fname + "'";
throw TuningError(s);
}
auto res = keyboardMappingFromStream(inf);
res.name = fname;
return res;
}
inline KeyboardMapping parseKBMData(const std::string &d)
{
std::istringstream iss(d);
auto res = keyboardMappingFromStream(iss);
res.name = "Mapping from Patch";
return res;
}
inline Tuning::Tuning() : Tuning( evenTemperament12NoteScale(), KeyboardMapping() ) { }
inline Tuning::Tuning(const Scale &s ) : Tuning( s, KeyboardMapping() ) {}
inline Tuning::Tuning(const KeyboardMapping &k ) : Tuning( evenTemperament12NoteScale(), k ) {}
inline Tuning::Tuning(const Scale& s, const KeyboardMapping &k)
{
scale = s;
keyboardMapping = k;
if( s.count <= 0 )
throw TuningError( "Unable to tune to a scale with no notes. Your scale provided " + std::to_string( s.count ) + " notes." );
double pitches[N];
int posPitch0 = 256 + k.tuningConstantNote;
int posScale0 = 256 + k.middleNote;
double pitchMod = log(k.tuningPitch)/log(2) - 1;
int scalePositionOfTuningNote = k.tuningConstantNote - k.middleNote;
if( k.count > 0 )
scalePositionOfTuningNote = k.keys[scalePositionOfTuningNote];
double tuningCenterPitchOffset;
if( scalePositionOfTuningNote == 0 )
tuningCenterPitchOffset = 0;
else
{
double tshift = 0;
double dt = s.tones[s.count -1].floatValue - 1.0;
while( scalePositionOfTuningNote < 0 )
{
scalePositionOfTuningNote += s.count;
tshift += dt;;
}
while( scalePositionOfTuningNote > s.count )
{
scalePositionOfTuningNote -= s.count;
tshift -= dt;
}
if( scalePositionOfTuningNote == 0 )
tuningCenterPitchOffset = -tshift;
else
tuningCenterPitchOffset = s.tones[scalePositionOfTuningNote-1].floatValue - 1.0 - tshift;
}
for (int i=0; i<N; ++i)
{
// TODO: ScaleCenter and PitchCenter are now two different notes.
int distanceFromPitch0 = i - posPitch0;
int distanceFromScale0 = i - posScale0;
if( distanceFromPitch0 == 0 )
{
pitches[i] = 1;
lptable[i] = pitches[i] + pitchMod;
ptable[i] = pow( 2.0, lptable[i] );
scalepositiontable[i] = scalePositionOfTuningNote % s.count;
#if DEBUG_SCALES
std::cout << "PITCH: i=" << i << " n=" << i - 256
<< " p=" << pitches[i]
<< " lp=" << lptable[i]
<< " tp=" << ptable[i]
<< " fr=" << ptable[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( ( k.count == 0 ) )
{
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 % k.count;
if( mappingKey < 0 )
mappingKey += k.count;
int cm = k.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=" << k.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;
}
if( disable )
{
pitches[i] = 0;
scalepositiontable[i] = -1;
}
else
{
pitches[i] = s.tones[thisRound].floatValue + rounds * (s.tones[s.count - 1].floatValue - 1.0) - tuningCenterPitchOffset;
scalepositiontable[i] = ( thisRound + 1 ) % s.count;
}
lptable[i] = pitches[i] + pitchMod;
ptable[i] = pow( 2.0, pitches[i] + pitchMod );
#if DEBUG_SCALES
if( i > 296 && i < 340 )
std::cout << "PITCH: i=" << i << " n=" << i - 256
<< " ds0=" << distanceFromScale0
<< " dp0=" << distanceFromPitch0
<< " r=" << rounds << " t=" << thisRound
<< " p=" << pitches[i]
<< " t=" << s.tones[thisRound].floatValue << " " << s.tones[thisRound ].cents
<< " dis=" << disable
<< " tp=" << ptable[i]
<< " fr=" << ptable[i] * 8.175798915
<< " tcpo=" << tuningCenterPitchOffset
//<< " l2p=" << log(otp)/log(2.0)
//<< " l2p-p=" << log(otp)/log(2.0) - pitches[i] - rounds - 3
<< std::endl;
#endif
}
}
}
inline double Tuning::frequencyForMidiNote( int mn ) const {
auto mni = std::min( std::max( 0, mn + 256 ), N-1 );
return ptable[ mni ] * MIDI_0_FREQ;
}
inline double Tuning::frequencyForMidiNoteScaledByMidi0( int mn ) const {
auto mni = std::min( std::max( 0, mn + 256 ), N-1 );
return ptable[ mni ];
}
inline double Tuning::logScaledFrequencyForMidiNote( int mn ) const {
auto mni = std::min( std::max( 0, mn + 256 ), N-1 );
return lptable[ mni ];
}
inline int Tuning::scalePositionForMidiNote( int mn ) const {
auto mni = std::min( std::max( 0, mn + 256 ), N-1 );
return scalepositiontable[ mni ];
}
inline KeyboardMapping tuneA69To(double freq)
{
return tuneNoteTo( 69, freq );
}
inline KeyboardMapping tuneNoteTo( int midiNote, double freq )
{
return startScaleOnAndTuneNoteTo( 60, midiNote, freq );
}
inline KeyboardMapping startScaleOnAndTuneNoteTo( int scaleStart, int midiNote, double freq )
{
std::ostringstream oss;
oss.imbue( std::locale( "C" ) );
oss << "! Automatically generated mapping, tuning note " << midiNote << " to " << freq << " hz\n"
<< "!\n"
<< "! Size of Map\n"
<< 0 << "\n"
<< "! First and Last Midi Notes to map - map the entire keyboard\n"
<< 0 << "\n" << 127 << "\n"
<< "! Middle note where the first entry in the scale is mapped.\n"
<< scaleStart << "\n"
<< "! Reference not where frequency is fixed\n"
<< midiNote << "\n"
<< "! Frequency for midi note " << midiNote << "\n"
<< freq << "\n"
<< "! Scale degree for formal octave. This is am empty mapping so:\n"
<< 0 << "\n"
<< "! Mapping. This is an empty mapping so list no keys\n";
return parseKBMData( oss.str() );
}
}
#endif
Loading…
Cancel
Save