diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d99efa9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app \ No newline at end of file diff --git a/src/Dexed.h b/src/Dexed.h new file mode 100644 index 0000000..e69de29 diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..7edecbe --- /dev/null +++ b/src/Makefile @@ -0,0 +1,24 @@ +# +# Makefile +# + +CIRCLE_STDLIB_DIR = ../circle-stdlib +MSFA_DIR = msfa + +OBJS = main.o kernel.o \ + $(MSFA_DIR)/dx7note.o $(MSFA_DIR)/env.o $(MSFA_DIR)/exp2.o $(MSFA_DIR)/fm_core.o \ + $(MSFA_DIR)/fm_op_kernel.o $(MSFA_DIR)/freqlut.o $(MSFA_DIR)/lfo.o \ + $(MSFA_DIR)/pitchenv.o $(MSFA_DIR)/sin.o $(MSFA_DIR)/tuning.o + +INCLUDE += -I $(MSFA_DIR) -I tuning-library + +EXTRACLEAN = $(MSFA_DIR)/*.o $(MSFA_DIR)/*.d + +include ./Rules.mk + +%.o: %.cc + @echo " CPP $@" + @$(CPP) $(CPPFLAGS) -c -o $@ $< + +%.d: %.cc + @$(CPP) $(CPPFLAGS) -M -MG -MT $*.o -MT $@ -MF $@ $< diff --git a/src/Rules.mk b/src/Rules.mk new file mode 100644 index 0000000..575b3aa --- /dev/null +++ b/src/Rules.mk @@ -0,0 +1,28 @@ +# +# Rules.mk +# + +-include $(CIRCLE_STDLIB_DIR)/Config.mk + +NEWLIBDIR ?= $(CIRCLE_STDLIB_DIR)/install/$(NEWLIB_ARCH) +CIRCLEHOME ?= $(CIRCLE_STDLIB_DIR)/libs/circle + +include $(CIRCLEHOME)/Rules.mk + +INCLUDE += \ + -I $(CIRCLE_STDLIB_DIR)/include \ + -I $(NEWLIBDIR)/include + +LIBS += \ + $(NEWLIBDIR)/lib/libm.a \ + $(NEWLIBDIR)/lib/libc.a \ + $(NEWLIBDIR)/lib/libcirclenewlib.a \ + $(CIRCLEHOME)/addon/SDCard/libsdcard.a \ + $(CIRCLEHOME)/lib/usb/libusb.a \ + $(CIRCLEHOME)/lib/input/libinput.a \ + $(CIRCLEHOME)/addon/fatfs/libfatfs.a \ + $(CIRCLEHOME)/lib/fs/libfs.a \ + $(CIRCLEHOME)/lib/sched/libsched.a \ + $(CIRCLEHOME)/lib/libcircle.a + +-include $(DEPS) diff --git a/src/kernel.cpp b/src/kernel.cpp new file mode 100644 index 0000000..0282575 --- /dev/null +++ b/src/kernel.cpp @@ -0,0 +1,23 @@ +// +// kernel.cpp +// +#include "kernel.h" +#include + +CKernel::CKernel (void) +: CStdlibAppStdio ("minidexed") +{ + mActLED.Blink (5); // show we are alive +} + +bool CKernel::Initialize (void) +{ + return CStdlibAppStdio::Initialize (); +} + +CStdlibApp::TShutdownMode CKernel::Run (void) +{ + std::cout << "Hello MiniDexed!\n"; + + return ShutdownHalt; +} diff --git a/src/kernel.h b/src/kernel.h new file mode 100644 index 0000000..7c01378 --- /dev/null +++ b/src/kernel.h @@ -0,0 +1,19 @@ +// +// kernel.h +// +#ifndef _kernel_h +#define _kernel_h + +#include + +class CKernel : public CStdlibAppStdio +{ +public: + CKernel (void); + + bool Initialize (void); + + TShutdownMode Run (void); +}; + +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..14c8bfa --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,46 @@ +// +// main.cpp +// +// 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 . +// +#include "kernel.h" +#include + +int main (void) +{ + // cannot return here because some destructors used in CKernel are not implemented + + CKernel Kernel; + if (!Kernel.Initialize ()) + { + halt (); + return EXIT_HALT; + } + + CStdlibApp::TShutdownMode ShutdownMode = Kernel.Run (); + + Kernel.Cleanup (); + + switch (ShutdownMode) + { + case CStdlibApp::ShutdownReboot: + reboot (); + return EXIT_REBOOT; + + case CStdlibApp::ShutdownHalt: + default: + halt (); + return EXIT_HALT; + } +} diff --git a/src/msfa/aligned_buf.h b/src/msfa/aligned_buf.h new file mode 100644 index 0000000..8f7c5b6 --- /dev/null +++ b/src/msfa/aligned_buf.h @@ -0,0 +1,36 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// A convenient wrapper for buffers with alignment constraints + +// Note that if we were on C++11, we'd use aligned_storage or somesuch. + +#ifndef __ALIGNED_BUF_H +#define __ALIGNED_BUF_H + +#include + +template +class AlignedBuf { + public: + T *get() { + return (T *)((((intptr_t)storage_) + alignment - 1) & -alignment); + } + private: + unsigned char storage_[size * sizeof(T) + alignment]; +}; + +#endif // __ALIGNED_BUF_H diff --git a/src/msfa/controllers.h b/src/msfa/controllers.h new file mode 100644 index 0000000..99de232 --- /dev/null +++ b/src/msfa/controllers.h @@ -0,0 +1,132 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __CONTROLLERS_H +#define __CONTROLLERS_H + +#include "synth.h" +#include "../Dexed.h" +#include +#include + +#ifdef _WIN32 +#define snprintf _snprintf +#endif + +// State of MIDI controllers +const int kControllerPitch = 128; +const int kControllerPitchRangeUp = 129; +const int kControllerPitchStep = 130; +const int kControllerPitchRangeDn = 131; + +class FmCore; + +struct FmMod { + int range; + bool pitch; + bool amp; + bool eg; + + FmMod() { + range = 0; + pitch = false; + amp = false; + eg = false; + } + + void parseConfig(const char *cfg) { + int r = 0, p = 0, a = 0, e = 0; + sscanf(cfg, "%d %d %d %d", &r, &p, &a, &e); + + range = r < 0 || r > 127 ? 0 : r; + pitch = p != 0; + amp = a != 0; + eg = e != 0; + } + + void setConfig(char *cfg) { + snprintf(cfg, 13, "%d %d %d %d", range, pitch, amp, eg); + } +}; + +class Controllers { + void applyMod(int cc, FmMod &mod) { + float range = 0.01 * mod.range; + int total = cc * range; + if ( mod.amp ) + amp_mod = max(amp_mod, total); + + if ( mod.pitch ) + pitch_mod = max(pitch_mod, total); + + if ( mod.eg ) + eg_mod = max(eg_mod, total); + } + +public: + int values_[132]; + + char opSwitch[7]; + + int amp_mod; + int pitch_mod; + int eg_mod; + + int aftertouch_cc; + int breath_cc; + int foot_cc; + int modwheel_cc; + + int masterTune; + + bool transpose12AsScale = true; + + // MPE configuration. FIXME - make this switchable + bool mpeEnabled = true; + int mpePitchBendRange = 24; + + FmMod wheel; + FmMod foot; + FmMod breath; + FmMod at; + + Controllers() { + amp_mod = 0; + pitch_mod = 0; + eg_mod = 0; + strcpy(opSwitch, "111111"); + } + + void refresh() { + amp_mod = 0; + pitch_mod = 0; + eg_mod = 0; + + applyMod(modwheel_cc, wheel); + applyMod(breath_cc, breath); + applyMod(foot_cc, foot); + applyMod(aftertouch_cc, at); + + if ( ! ((wheel.eg || foot.eg) || (breath.eg || at.eg)) ) + eg_mod = 127; + + } + + FmCore *core; +}; + +#endif // __CONTROLLERS_H + diff --git a/src/msfa/dx7note.cc b/src/msfa/dx7note.cc new file mode 100644 index 0000000..4b68633 --- /dev/null +++ b/src/msfa/dx7note.cc @@ -0,0 +1,358 @@ +/* + * Copyright 2016-2017 Pascal Gauthier. + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "synth.h" +#include "freqlut.h" +#include "exp2.h" +#include "controllers.h" +#include "dx7note.h" +#include +#include + +const int FEEDBACK_BITDEPTH = 8; + +const int32_t coarsemul[] = { + -16777216, 0, 16777216, 26591258, 33554432, 38955489, 43368474, 47099600, + 50331648, 53182516, 55732705, 58039632, 60145690, 62083076, 63876816, + 65546747, 67108864, 68576247, 69959732, 71268397, 72509921, 73690858, + 74816848, 75892776, 76922906, 77910978, 78860292, 79773775, 80654032, + 81503396, 82323963, 83117622 +}; + +int32_t Dx7Note::osc_freq(int midinote, int mode, int coarse, int fine, int detune) { + // TODO: pitch randomization + int32_t logfreq; + if (mode == 0) { + logfreq = tuning_state_->midinote_to_logfreq(midinote); + + // could use more precision, closer enough for now. those numbers comes from my DX7 + double detuneRatio = 0.0209 * exp(-0.396 * (((float)logfreq)/(1<<24))) / 7; + logfreq += detuneRatio * logfreq * (detune - 7); + + logfreq += coarsemul[coarse & 31]; + if (fine) { + // (1 << 24) / log(2) + logfreq += (int32_t)floor(24204406.323123 * log(1 + 0.01 * fine) + 0.5); + } + + // // This was measured at 7.213Hz per count at 9600Hz, but the exact + // // value is somewhat dependent on midinote. Close enough for now. + // //logfreq += 12606 * (detune -7); + } else { + // ((1 << 24) * log(10) / log(2) * .01) << 3 + logfreq = (4458616 * ((coarse & 3) * 100 + fine)) >> 3; + logfreq += detune > 7 ? 13457 * (detune - 7) : 0; + } + return logfreq; +} + +const uint8_t velocity_data[64] = { + 0, 70, 86, 97, 106, 114, 121, 126, 132, 138, 142, 148, 152, 156, 160, 163, + 166, 170, 173, 174, 178, 181, 184, 186, 189, 190, 194, 196, 198, 200, 202, + 205, 206, 209, 211, 214, 216, 218, 220, 222, 224, 225, 227, 229, 230, 232, + 233, 235, 237, 238, 240, 241, 242, 243, 244, 246, 246, 248, 249, 250, 251, + 252, 253, 254 +}; + +// See "velocity" section of notes. Returns velocity delta in microsteps. +int ScaleVelocity(int velocity, int sensitivity) { + int clamped_vel = max(0, min(127, velocity)); + int vel_value = velocity_data[clamped_vel >> 1] - 239; + int scaled_vel = ((sensitivity * vel_value + 7) >> 3) << 4; + return scaled_vel; +} + +int ScaleRate(int midinote, int sensitivity) { + int x = min(31, max(0, midinote / 3 - 7)); + int qratedelta = (sensitivity * x) >> 3; +#ifdef SUPER_PRECISE + int rem = x & 7; + if (sensitivity == 3 && rem == 3) { + qratedelta -= 1; + } else if (sensitivity == 7 && rem > 0 && rem < 4) { + qratedelta += 1; + } +#endif + return qratedelta; +} + +const uint8_t exp_scale_data[] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 14, 16, 19, 23, 27, 33, 39, 47, 56, 66, + 80, 94, 110, 126, 142, 158, 174, 190, 206, 222, 238, 250 +}; + +int ScaleCurve(int group, int depth, int curve) { + int scale; + if (curve == 0 || curve == 3) { + // linear + scale = (group * depth * 329) >> 12; + } else { + // exponential + int n_scale_data = sizeof(exp_scale_data); + int raw_exp = exp_scale_data[min(group, n_scale_data - 1)]; + scale = (raw_exp * depth * 329) >> 15; + } + if (curve < 2) { + scale = -scale; + } + return scale; +} + +int ScaleLevel(int midinote, int break_pt, int left_depth, int right_depth, + int left_curve, int right_curve) { + int offset = midinote - break_pt - 17; + if (offset >= 0) { + return ScaleCurve((offset+1) / 3, right_depth, right_curve); + } else { + return ScaleCurve(-(offset-1) / 3, left_depth, left_curve); + } +} + +static const uint8_t pitchmodsenstab[] = { + 0, 10, 20, 33, 55, 92, 153, 255 +}; + +// 0, 66, 109, 255 +static const uint32_t ampmodsenstab[] = { + 0, 4342338, 7171437, 16777216 +}; + +Dx7Note::Dx7Note(std::shared_ptr ts) : tuning_state_(ts) { + for(int op=0;op<6;op++) { + params_[op].phase = 0; + params_[op].gain_out = 0; + } +} + +void Dx7Note::init(const uint8_t patch[156], int midinote, int velocity) { + int rates[4]; + int levels[4]; + playingMidiNote = midinote; + for (int op = 0; op < 6; op++) { + int off = op * 21; + for (int i = 0; i < 4; i++) { + rates[i] = patch[off + i]; + levels[i] = patch[off + 4 + i]; + } + int outlevel = patch[off + 16]; + outlevel = Env::scaleoutlevel(outlevel); + int level_scaling = ScaleLevel(midinote, patch[off + 8], patch[off + 9], + patch[off + 10], patch[off + 11], patch[off + 12]); + outlevel += level_scaling; + outlevel = min(127, outlevel); + outlevel = outlevel << 5; + outlevel += ScaleVelocity(velocity, patch[off + 15]); + outlevel = max(0, outlevel); + int rate_scaling = ScaleRate(midinote, patch[off + 13]); + env_[op].init(rates, levels, outlevel, rate_scaling); + + int mode = patch[off + 17]; + int coarse = patch[off + 18]; + int fine = patch[off + 19]; + int detune = patch[off + 20]; + int32_t freq = osc_freq(midinote, mode, coarse, fine, detune); + opMode[op] = mode; + basepitch_[op] = freq; + ampmodsens_[op] = ampmodsenstab[patch[off + 14] & 3]; + } + for (int i = 0; i < 4; i++) { + rates[i] = patch[126 + i]; + levels[i] = patch[130 + i]; + } + pitchenv_.set(rates, levels); + algorithm_ = patch[134]; + int feedback = patch[135]; + fb_shift_ = feedback != 0 ? FEEDBACK_BITDEPTH - feedback : 16; + pitchmoddepth_ = (patch[139] * 165) >> 6; + pitchmodsens_ = pitchmodsenstab[patch[143] & 7]; + ampmoddepth_ = (patch[140] * 165) >> 6; + + // MPE default valeus + mpePitchBend = 8192; + mpeTimbre = 0; + mpePressure = 0; +} + +void Dx7Note::compute(int32_t *buf, int32_t lfo_val, int32_t lfo_delay, const Controllers *ctrls) { + // ==== PITCH ==== + uint32_t pmd = pitchmoddepth_ * lfo_delay; // Q32 + int32_t senslfo = pitchmodsens_ * (lfo_val - (1 << 23)); + int32_t pmod_1 = (((int64_t) pmd) * (int64_t) senslfo) >> 39; + pmod_1 = abs(pmod_1); + int32_t pmod_2 = (int32_t)(((int64_t)ctrls->pitch_mod * (int64_t)senslfo) >> 14); + pmod_2 = abs(pmod_2); + int32_t pitch_mod = max(pmod_1, pmod_2); + pitch_mod = pitchenv_.getsample() + (pitch_mod * (senslfo < 0 ? -1 : 1)); + + // ---- PITCH BEND ---- + int pitchbend = ctrls->values_[kControllerPitch]; + int32_t pb = (pitchbend - 0x2000); + if (pb != 0) { + if (ctrls->values_[kControllerPitchStep] == 0) { + if( pb >= 0 ) + pb = ((float) (pb << 11)) * ((float) ctrls->values_[kControllerPitchRangeUp]) / 12.0; + else + pb = ((float) (pb << 11)) * ((float) ctrls->values_[kControllerPitchRangeDn]) / 12.0; + } else { + int stp = 12 / ctrls->values_[kControllerPitchStep]; + pb = pb * stp / 8191; + pb = (pb * (8191 / stp)) << 11; + } + } + + if( ctrls->mpeEnabled ) + { + int d = ((float)( (mpePitchBend-0x2000) << 11 )) * ctrls->mpePitchBendRange / 12.0; + // std::cout << mpePitchBend << " " << 0x2000 << " " << d << std::endl; + pb += d; + } + + if( ! tuning_state_->is_standard_tuning() && pb != 0 ) + { + // If we have a scale we want PB to be in scale space so we sort of need to + // unwind the combinations above and re-interpolate + + float notesTuned = ( pb >> 11 ) * 12.0 / 8192; // How many steps you tuned + int floorNote = std::floor(notesTuned); + float frac = notesTuned - floorNote; + float targetLog = tuning_state_->midinote_to_logfreq(playingMidiNote + floorNote) * ( 1.0 - frac ) + + tuning_state_->midinote_to_logfreq(playingMidiNote + floorNote + 1) * frac; // the interpolated log freq + float newpb = targetLog - tuning_state_->midinote_to_logfreq(playingMidiNote); // and the resulting bend + pb = newpb; + } + + int32_t pitch_base = pb + ctrls->masterTune; + pitch_mod += pitch_base; + + // ==== AMP MOD ==== + lfo_val = (1<<24) - lfo_val; + uint32_t amod_1 = (uint32_t)(((int64_t) ampmoddepth_ * (int64_t) lfo_delay) >> 8); // Q24 :D + amod_1 = (uint32_t)(((int64_t) amod_1 * (int64_t) lfo_val) >> 24); + uint32_t amod_2 = (uint32_t)(((int64_t) ctrls->amp_mod * (int64_t) lfo_val) >> 7); // Q?? :| + uint32_t amd_mod = max(amod_1, amod_2); + + // ==== EG AMP MOD ==== + uint32_t amod_3 = (ctrls->eg_mod+1) << 17; + amd_mod = max((1<<24) - amod_3, amd_mod); + + // ==== OP RENDER ==== + for (int op = 0; op < 6; op++) { + if ( ctrls->opSwitch[op] == '0' ) { + env_[op].getsample(); // advance the envelop even if it is not playing + params_[op].level_in = 0; + } else { + //int32_t gain = pow(2, 10 + level * (1.0 / (1 << 24))); + + if ( opMode[op] ) + params_[op].freq = Freqlut::lookup(basepitch_[op] + pitch_base); + else + params_[op].freq = Freqlut::lookup(basepitch_[op] + pitch_mod); + + int32_t level = env_[op].getsample(); + if (ampmodsens_[op] != 0) { + uint32_t sensamp = (uint32_t)(((uint64_t) amd_mod) * ((uint64_t) ampmodsens_[op]) >> 24); + + // TODO: mehhh.. this needs some real tuning. + uint32_t pt = exp(((float)sensamp)/262144 * 0.07 + 12.2); + uint32_t ldiff = (uint32_t)(((uint64_t)level) * (((uint64_t)pt<<4)) >> 28); + level -= ldiff; + } + params_[op].level_in = level; + } + } + ctrls->core->render(buf, params_, algorithm_, fb_buf_, fb_shift_); +} + +void Dx7Note::keyup() { + for (int op = 0; op < 6; op++) { + env_[op].keydown(false); + } + pitchenv_.keydown(false); +} + +void Dx7Note::update(const uint8_t patch[156], int midinote, int velocity) { + int rates[4]; + int levels[4]; + playingMidiNote = midinote; + for (int op = 0; op < 6; op++) { + int off = op * 21; + int mode = patch[off + 17]; + int coarse = patch[off + 18]; + int fine = patch[off + 19]; + int detune = patch[off + 20]; + basepitch_[op] = osc_freq(midinote, mode, coarse, fine, detune); + ampmodsens_[op] = ampmodsenstab[patch[off + 14] & 3]; + opMode[op] = mode; + + for (int i = 0; i < 4; i++) { + rates[i] = patch[off + i]; + levels[i] = patch[off + 4 + i]; + } + int outlevel = patch[off + 16]; + outlevel = Env::scaleoutlevel(outlevel); + int level_scaling = ScaleLevel(midinote, patch[off + 8], patch[off + 9], + patch[off + 10], patch[off + 11], patch[off + 12]); + outlevel += level_scaling; + outlevel = min(127, outlevel); + outlevel = outlevel << 5; + outlevel += ScaleVelocity(velocity, patch[off + 15]); + outlevel = max(0, outlevel); + int rate_scaling = ScaleRate(midinote, patch[off + 13]); + env_[op].update(rates, levels, outlevel, rate_scaling); + } + algorithm_ = patch[134]; + int feedback = patch[135]; + fb_shift_ = feedback != 0 ? FEEDBACK_BITDEPTH - feedback : 16; + pitchmoddepth_ = (patch[139] * 165) >> 6; + pitchmodsens_ = pitchmodsenstab[patch[143] & 7]; + ampmoddepth_ = (patch[140] * 165) >> 6; +} + +void Dx7Note::peekVoiceStatus(VoiceStatus &status) { + for(int i=0;i<6;i++) { + status.amp[i] = Exp2::lookup(params_[i].level_in - (14 * (1 << 24))); + env_[i].getPosition(&status.ampStep[i]); + } + pitchenv_.getPosition(&status.pitchStep); +} + +/** + * Used in monophonic mode to transfer voice state from different notes + */ +void Dx7Note::transferState(Dx7Note &src) { + for (int i=0;i<6;i++) { + env_[i].transfer(src.env_[i]); + params_[i].gain_out = src.params_[i].gain_out; + params_[i].phase = src.params_[i].phase; + } +} + +void Dx7Note::transferSignal(Dx7Note &src) { + for (int i=0;i<6;i++) { + params_[i].gain_out = src.params_[i].gain_out; + params_[i].phase = src.params_[i].phase; + } +} + +void Dx7Note::oscSync() { + for (int i=0;i<6;i++) { + params_[i].gain_out = 0; + params_[i].phase = 0; + } +} diff --git a/src/msfa/dx7note.h b/src/msfa/dx7note.h new file mode 100644 index 0000000..6125781 --- /dev/null +++ b/src/msfa/dx7note.h @@ -0,0 +1,90 @@ +/* + * Copyright 2016-2017 Pascal Gauthier. + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SYNTH_DX7NOTE_H_ +#define SYNTH_DX7NOTE_H_ + +// This is the logic to put together a note from the MIDI description +// and run the low-level modules. + +// It will continue to evolve a bit, as note-stealing logic, scaling, +// and real-time control of parameters live here. + +#include "env.h" +#include "pitchenv.h" +#include "fm_core.h" +#include "tuning.h" +#include + +struct VoiceStatus { + uint32_t amp[6]; + char ampStep[6]; + char pitchStep; +}; + +class Dx7Note { +public: + Dx7Note(std::shared_ptr ts); + void init(const uint8_t patch[156], int midinote, int velocity); + + // Note: this _adds_ to the buffer. Interesting question whether it's + // worth it... + void compute(int32_t *buf, int32_t lfo_val, int32_t lfo_delay, + const Controllers *ctrls); + + void keyup(); + + // TODO: some way of indicating end-of-note. Maybe should be a return + // value from the compute method? (Having a count return from keyup + // is also tempting, but if there's a dynamic parameter change after + // keyup, that won't work. + + // PG:add the update + void update(const uint8_t patch[156], int midinote, int velocity); + void peekVoiceStatus(VoiceStatus &status); + void transferState(Dx7Note& src); + void transferSignal(Dx7Note &src); + void oscSync(); + + int32_t osc_freq(int midinote, int mode, int coarse, int fine, int detune); + + std::shared_ptr tuning_state_; + + int mpePitchBend = 8192; + int mpePressure = 0; + int mpeTimbre = 0; + +private: + Env env_[6]; + FmOpParams params_[6]; + PitchEnv pitchenv_; + int32_t basepitch_[6]; + int32_t fb_buf_[2]; + int32_t fb_shift_; + int32_t ampmodsens_[6]; + int32_t opMode[6]; + + uint8_t playingMidiNote; // We need this for scale aware pitch bend + + int ampmoddepth_; + int algorithm_; + int pitchmoddepth_; + int pitchmodsens_; + +}; + +#endif // SYNTH_DX7NOTE_H_ diff --git a/src/msfa/env.cc b/src/msfa/env.cc new file mode 100644 index 0000000..dfc3958 --- /dev/null +++ b/src/msfa/env.cc @@ -0,0 +1,192 @@ +/* + * Copyright 2017 Pascal Gauthier. + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "synth.h" +#include "env.h" + +#include "../Dexed.h" +//using namespace std; + +uint32_t Env::sr_multiplier = (1<<24); + +const int levellut[] = { + 0, 5, 9, 13, 17, 20, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 42, 43, 45, 46 +}; + +#ifdef ACCURATE_ENVELOPE +const int statics[] = { + 1764000, 1764000, 1411200, 1411200, 1190700, 1014300, 992250, + 882000, 705600, 705600, 584325, 507150, 502740, 441000, 418950, + 352800, 308700, 286650, 253575, 220500, 220500, 176400, 145530, + 145530, 125685, 110250, 110250, 88200, 88200, 74970, 61740, + 61740, 55125, 48510, 44100, 37485, 31311, 30870, 27562, 27562, + 22050, 18522, 17640, 15435, 14112, 13230, 11025, 9261, 9261, 7717, + 6615, 6615, 5512, 5512, 4410, 3969, 3969, 3439, 2866, 2690, 2249, + 1984, 1896, 1808, 1411, 1367, 1234, 1146, 926, 837, 837, 705, + 573, 573, 529, 441, 441 + // and so on, I stopped measuring after R=76 (needs to be double-checked anyway) +}; +#endif + +void Env::init_sr(double sampleRate) { + sr_multiplier = (44100.0 / sampleRate) * (1<<24); +} + +void Env::init(const int r[4], const int l[4], int ol, int rate_scaling) { + for (int i = 0; i < 4; i++) { + rates_[i] = r[i]; + levels_[i] = l[i]; + } + outlevel_ = ol; + rate_scaling_ = rate_scaling; + level_ = 0; + down_ = true; + advance(0); +} + +int32_t Env::getsample() { +#ifdef ACCURATE_ENVELOPE + if (staticcount_) { + staticcount_ -= N; + if (staticcount_ <= 0) { + staticcount_ = 0; + advance(ix_ + 1); + } + } +#endif + + if (ix_ < 3 || ((ix_ < 4) && !down_)) { + if (staticcount_) { + ; + } + else if (rising_) { + const int jumptarget = 1716; + if (level_ < (jumptarget << 16)) { + level_ = jumptarget << 16; + } + level_ += (((17 << 24) - level_) >> 24) * inc_; + // TODO: should probably be more accurate when inc is large + if (level_ >= targetlevel_) { + level_ = targetlevel_; + advance(ix_ + 1); + } + } + else { // !rising + level_ -= inc_; + if (level_ <= targetlevel_) { + level_ = targetlevel_; + advance(ix_ + 1); + } + } + } + // TODO: this would be a good place to set level to 0 when under threshold + return level_; +} + +void Env::keydown(bool d) { + if (down_ != d) { + down_ = d; + advance(d ? 0 : 3); + } +} + +int Env::scaleoutlevel(int outlevel) { + return outlevel >= 20 ? 28 + outlevel : levellut[outlevel]; +} + +void Env::advance(int newix) { + ix_ = newix; + if (ix_ < 4) { + int newlevel = levels_[ix_]; + int actuallevel = scaleoutlevel(newlevel) >> 1; + actuallevel = (actuallevel << 6) + outlevel_ - 4256; + actuallevel = actuallevel < 16 ? 16 : actuallevel; + // level here is same as Java impl + targetlevel_ = actuallevel << 16; + rising_ = (targetlevel_ > level_); + + // rate + int qrate = (rates_[ix_] * 41) >> 6; + qrate += rate_scaling_; + qrate = min(qrate, 63); + +#ifdef ACCURATE_ENVELOPE + if (targetlevel_ == level_ || (ix_ == 0 && newlevel == 0)) { + // approximate number of samples at 44.100 kHz to achieve the time + // empirically gathered using 2 TF1s, could probably use some double-checking + // and cleanup, but it's pretty close for now. + int staticrate = rates_[ix_]; + staticrate += rate_scaling_; // needs to be checked, as well, but seems correct + staticrate = min(staticrate, 99); + staticcount_ = staticrate < 77 ? statics[staticrate] : 20 * (99 - staticrate); + if (staticrate < 77 && (ix_ == 0 && newlevel == 0)) { + staticcount_ /= 20; // attack is scaled faster + } + staticcount_ = (int)(((int64_t)staticcount_ * (int64_t)sr_multiplier) >> 24); + } + else { + staticcount_ = 0; + } +#endif + inc_ = (4 + (qrate & 3)) << (2 + LG_N + (qrate >> 2)); + // meh, this should be fixed elsewhere + inc_ = (int)(((int64_t)inc_ * (int64_t)sr_multiplier) >> 24); + } +} + +void Env::update(const int r[4], const int l[4], int ol, int rate_scaling) { + for (int i = 0; i < 4; i++) { + rates_[i] = r[i]; + levels_[i] = l[i]; + } + outlevel_ = ol; + rate_scaling_ = rate_scaling; + if ( down_ ) { + // for now we simply reset ourselves at level 3 + int newlevel = levels_[2]; + int actuallevel = scaleoutlevel(newlevel) >> 1; + actuallevel = (actuallevel << 6) - 4256; + actuallevel = actuallevel < 16 ? 16 : actuallevel; + targetlevel_ = actuallevel << 16; + advance(2); + } +} + +void Env::getPosition(char *step) { + *step = ix_; +} + +void Env::transfer(Env &src) { + for(int i=0;i<4;i++) { + rates_[i] = src.rates_[i]; + levels_[i] = src.levels_[i]; + } + outlevel_ = src.outlevel_; + rate_scaling_ = src.rate_scaling_; + level_ = src.level_; + targetlevel_ = src.targetlevel_; + rising_= src.rising_; + ix_ = src.ix_; + down_ = src.down_; +#ifdef ACCURATE_ENVELOPE + staticcount_ = src.staticcount_; +#endif + inc_ = src.inc_; +} + diff --git a/src/msfa/env.h b/src/msfa/env.h new file mode 100644 index 0000000..9aaf7ec --- /dev/null +++ b/src/msfa/env.h @@ -0,0 +1,82 @@ +/* + * Copyright 2017 Pascal Gauthier. + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __ENV_H +#define __ENV_H + +#include "synth.h" + +// DX7 envelope generation + +#define ACCURATE_ENVELOPE + +class Env { + public: + + // The rates and levels arrays are calibrated to match the Dx7 parameters + // (ie, value 0..99). The outlevel parameter is calibrated in microsteps + // (ie units of approx .023 dB), with 99 * 32 = nominal full scale. The + // rate_scaling parameter is in qRate units (ie 0..63). + void init(const int rates[4], const int levels[4], int outlevel, + int rate_scaling); + + void update(const int rates[4], const int levels[4], int outlevel, + int rate_scaling); + // Result is in Q24/doubling log format. Also, result is subsampled + // for every N samples. + // A couple more things need to happen for this to be used as a gain + // value. First, the # of outputs scaling needs to be applied. Also, + // modulation. + // Then, of course, log to linear. + int32_t getsample(); + + void keydown(bool down); + static int scaleoutlevel(int outlevel); + void getPosition(char *step); + + static void init_sr(double sample_rate); + void transfer(Env &src); + + private: + + // PG: This code is normalized to 44100, need to put a multiplier + // if we are not using 44100. + static uint32_t sr_multiplier; + + int rates_[4]; + int levels_[4]; + int outlevel_; + int rate_scaling_; + // Level is stored so that 2^24 is one doubling, ie 16 more bits than + // the DX7 itself (fraction is stored in level rather than separate + // counter) + int32_t level_; + int targetlevel_; + bool rising_; + int ix_; + int inc_; +#ifdef ACCURATE_ENVELOPE + int staticcount_; +#endif + + bool down_; + + void advance(int newix); +}; + +#endif // __ENV_H + diff --git a/src/msfa/exp2.cc b/src/msfa/exp2.cc new file mode 100644 index 0000000..84feb20 --- /dev/null +++ b/src/msfa/exp2.cc @@ -0,0 +1,72 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define _USE_MATH_DEFINES +#include + +#include "synth.h" +#include "exp2.h" + +#include + +#ifdef _MSC_VER +#define exp2(arg) pow(2.0, arg) +#endif + + + +int32_t exp2tab[EXP2_N_SAMPLES << 1]; + +void Exp2::init() { + double inc = exp2(1.0 / EXP2_N_SAMPLES); + double y = 1 << 30; + for (int i = 0; i < EXP2_N_SAMPLES; i++) { + exp2tab[(i << 1) + 1] = (int32_t)floor(y + 0.5); + y *= inc; + } + for (int i = 0; i < EXP2_N_SAMPLES - 1; i++) { + exp2tab[i << 1] = exp2tab[(i << 1) + 3] - exp2tab[(i << 1) + 1]; + } + exp2tab[(EXP2_N_SAMPLES << 1) - 2] = (1U << 31) - exp2tab[(EXP2_N_SAMPLES << 1) - 1]; +} + +int32_t tanhtab[TANH_N_SAMPLES << 1]; + +static double dtanh(double y) { + return 1 - y * y; +} + +void Tanh::init() { + double step = 4.0 / TANH_N_SAMPLES; + double y = 0; + for (int i = 0; i < TANH_N_SAMPLES; i++) { + tanhtab[(i << 1) + 1] = (1 << 24) * y + 0.5; + //printf("%d\n", tanhtab[(i << 1) + 1]); + // Use a basic 4th order Runge-Kutte to compute tanh from its + // differential equation. + double k1 = dtanh(y); + double k2 = dtanh(y + 0.5 * step * k1); + double k3 = dtanh(y + 0.5 * step * k2); + double k4 = dtanh(y + step * k3); + double dy = (step / 6) * (k1 + k4 + 2 * (k2 + k3)); + y += dy; + } + for (int i = 0; i < TANH_N_SAMPLES - 1; i++) { + tanhtab[i << 1] = tanhtab[(i << 1) + 3] - tanhtab[(i << 1) + 1]; + } + int32_t lasty = (1 << 24) * y + 0.5; + tanhtab[(TANH_N_SAMPLES << 1) - 2] = lasty - tanhtab[(TANH_N_SAMPLES << 1) - 1]; +} diff --git a/src/msfa/exp2.h b/src/msfa/exp2.h new file mode 100644 index 0000000..016ca33 --- /dev/null +++ b/src/msfa/exp2.h @@ -0,0 +1,80 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +class Exp2 { + public: + Exp2(); + + static void init(); + + // Q24 in, Q24 out + static int32_t lookup(int32_t x); +}; + +#define EXP2_LG_N_SAMPLES 10 +#define EXP2_N_SAMPLES (1 << EXP2_LG_N_SAMPLES) + +#define EXP2_INLINE + +extern int32_t exp2tab[EXP2_N_SAMPLES << 1]; + +#ifdef EXP2_INLINE +inline +int32_t Exp2::lookup(int32_t x) { + const int SHIFT = 24 - EXP2_LG_N_SAMPLES; + int lowbits = x & ((1 << SHIFT) - 1); + int x_int = (x >> (SHIFT - 1)) & ((EXP2_N_SAMPLES - 1) << 1); + int dy = exp2tab[x_int]; + int y0 = exp2tab[x_int + 1]; + + int y = y0 + (((int64_t)dy * (int64_t)lowbits) >> SHIFT); + return y >> (6 - (x >> 24)); +} +#endif + +class Tanh { + public: + static void init(); + + // Q24 in, Q24 out + static int32_t lookup(int32_t x); +}; + +#define TANH_LG_N_SAMPLES 10 +#define TANH_N_SAMPLES (1 << TANH_LG_N_SAMPLES) + +extern int32_t tanhtab[TANH_N_SAMPLES << 1]; + +inline +int32_t Tanh::lookup(int32_t x) { + int32_t signum = x >> 31; + x ^= signum; + if (x >= (4 << 24)) { + if (x >= (17 << 23)) { + return signum ^ (1 << 24); + } + int32_t sx = ((int64_t)-48408812 * (int64_t)x) >> 24; + return signum ^ ((1 << 24) - 2 * Exp2::lookup(sx)); + } else { + const int SHIFT = 26 - TANH_LG_N_SAMPLES; + int lowbits = x & ((1 << SHIFT) - 1); + int x_int = (x >> (SHIFT - 1)) & ((TANH_N_SAMPLES - 1) << 1); + int dy = tanhtab[x_int]; + int y0 = tanhtab[x_int + 1]; + int y = y0 + (((int64_t)dy * (int64_t)lowbits) >> SHIFT); + return y ^ signum; + } +} diff --git a/src/msfa/fm_core.cc b/src/msfa/fm_core.cc new file mode 100644 index 0000000..8bfd9eb --- /dev/null +++ b/src/msfa/fm_core.cc @@ -0,0 +1,135 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifdef VERBOSE +#include +#endif + +#include "synth.h" +#include "exp2.h" +#include "fm_op_kernel.h" +#include "fm_core.h" + + +//using namespace std; + +const FmAlgorithm FmCore::algorithms[32] = { + { { 0xc1, 0x11, 0x11, 0x14, 0x01, 0x14 } }, // 1 + { { 0x01, 0x11, 0x11, 0x14, 0xc1, 0x14 } }, // 2 + { { 0xc1, 0x11, 0x14, 0x01, 0x11, 0x14 } }, // 3 + { { 0xc1, 0x11, 0x94, 0x01, 0x11, 0x14 } }, // 4 + { { 0xc1, 0x14, 0x01, 0x14, 0x01, 0x14 } }, // 5 + { { 0xc1, 0x94, 0x01, 0x14, 0x01, 0x14 } }, // 6 + { { 0xc1, 0x11, 0x05, 0x14, 0x01, 0x14 } }, // 7 + { { 0x01, 0x11, 0xc5, 0x14, 0x01, 0x14 } }, // 8 + { { 0x01, 0x11, 0x05, 0x14, 0xc1, 0x14 } }, // 9 + { { 0x01, 0x05, 0x14, 0xc1, 0x11, 0x14 } }, // 10 + { { 0xc1, 0x05, 0x14, 0x01, 0x11, 0x14 } }, // 11 + { { 0x01, 0x05, 0x05, 0x14, 0xc1, 0x14 } }, // 12 + { { 0xc1, 0x05, 0x05, 0x14, 0x01, 0x14 } }, // 13 + { { 0xc1, 0x05, 0x11, 0x14, 0x01, 0x14 } }, // 14 + { { 0x01, 0x05, 0x11, 0x14, 0xc1, 0x14 } }, // 15 + { { 0xc1, 0x11, 0x02, 0x25, 0x05, 0x14 } }, // 16 + { { 0x01, 0x11, 0x02, 0x25, 0xc5, 0x14 } }, // 17 + { { 0x01, 0x11, 0x11, 0xc5, 0x05, 0x14 } }, // 18 + { { 0xc1, 0x14, 0x14, 0x01, 0x11, 0x14 } }, // 19 + { { 0x01, 0x05, 0x14, 0xc1, 0x14, 0x14 } }, // 20 + { { 0x01, 0x14, 0x14, 0xc1, 0x14, 0x14 } }, // 21 + { { 0xc1, 0x14, 0x14, 0x14, 0x01, 0x14 } }, // 22 + { { 0xc1, 0x14, 0x14, 0x01, 0x14, 0x04 } }, // 23 + { { 0xc1, 0x14, 0x14, 0x14, 0x04, 0x04 } }, // 24 + { { 0xc1, 0x14, 0x14, 0x04, 0x04, 0x04 } }, // 25 + { { 0xc1, 0x05, 0x14, 0x01, 0x14, 0x04 } }, // 26 + { { 0x01, 0x05, 0x14, 0xc1, 0x14, 0x04 } }, // 27 + { { 0x04, 0xc1, 0x11, 0x14, 0x01, 0x14 } }, // 28 + { { 0xc1, 0x14, 0x01, 0x14, 0x04, 0x04 } }, // 29 + { { 0x04, 0xc1, 0x11, 0x14, 0x04, 0x04 } }, // 30 + { { 0xc1, 0x14, 0x04, 0x04, 0x04, 0x04 } }, // 31 + { { 0xc4, 0x04, 0x04, 0x04, 0x04, 0x04 } }, // 32 +}; + +int n_out(const FmAlgorithm &alg) { + int count = 0; + for (int i = 0; i < 6; i++) { + if ((alg.ops[i] & 7) == OUT_BUS_ADD) count++; + } + return count; +} + +void FmCore::dump() { +#ifdef VERBOSE + for (int i = 0; i < 32; i++) { + cout << (i + 1) << ":"; + const FmAlgorithm &alg = algorithms[i]; + for (int j = 0; j < 6; j++) { + int flags = alg.ops[j]; + cout << " "; + if (flags & FB_IN) cout << "["; + cout << (flags & IN_BUS_ONE ? "1" : flags & IN_BUS_TWO ? "2" : "0") << "->"; + cout << (flags & OUT_BUS_ONE ? "1" : flags & OUT_BUS_TWO ? "2" : "0"); + if (flags & OUT_BUS_ADD) cout << "+"; + //cout << alg.ops[j].in << "->" << alg.ops[j].out; + if (flags & FB_OUT) cout << "]"; + } + cout << " " << n_out(alg); + cout << endl; + } +#endif +} + +void FmCore::render(int32_t *output, FmOpParams *params, int algorithm, int32_t *fb_buf, int32_t feedback_shift) { + const int kLevelThresh = 1120; + const FmAlgorithm alg = algorithms[algorithm]; + bool has_contents[3] = { true, false, false }; + for (int op = 0; op < 6; op++) { + int flags = alg.ops[op]; + bool add = (flags & OUT_BUS_ADD) != 0; + FmOpParams ¶m = params[op]; + int inbus = (flags >> 4) & 3; + int outbus = flags & 3; + int32_t *outptr = (outbus == 0) ? output : buf_[outbus - 1].get(); + int32_t gain1 = param.gain_out; + int32_t gain2 = Exp2::lookup(param.level_in - (14 * (1 << 24))); + param.gain_out = gain2; + + if (gain1 >= kLevelThresh || gain2 >= kLevelThresh) { + if (!has_contents[outbus]) { + add = false; + } + if (inbus == 0 || !has_contents[inbus]) { + // todo: more than one op in a feedback loop + if ((flags & 0xc0) == 0xc0 && feedback_shift < 16) { + // cout << op << " fb " << inbus << outbus << add << endl; + FmOpKernel::compute_fb(outptr, param.phase, param.freq, + gain1, gain2, + fb_buf, feedback_shift, add); + } else { + // cout << op << " pure " << inbus << outbus << add << endl; + FmOpKernel::compute_pure(outptr, param.phase, param.freq, + gain1, gain2, add); + } + } else { + // cout << op << " normal " << inbus << outbus << " " << param.freq << add << endl; + FmOpKernel::compute(outptr, buf_[inbus - 1].get(), + param.phase, param.freq, gain1, gain2, add); + } + has_contents[outbus] = true; + } else if (!add) { + has_contents[outbus] = false; + } + param.phase += param.freq << LG_N; + } +} diff --git a/src/msfa/fm_core.h b/src/msfa/fm_core.h new file mode 100644 index 0000000..b1ec263 --- /dev/null +++ b/src/msfa/fm_core.h @@ -0,0 +1,57 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __FM_CORE_H +#define __FM_CORE_H + +#include "aligned_buf.h" +#include "fm_op_kernel.h" +#include "synth.h" +#include "controllers.h" + + +class FmOperatorInfo { +public: + int in; + int out; +}; + +enum FmOperatorFlags { + OUT_BUS_ONE = 1 << 0, + OUT_BUS_TWO = 1 << 1, + OUT_BUS_ADD = 1 << 2, + IN_BUS_ONE = 1 << 4, + IN_BUS_TWO = 1 << 5, + FB_IN = 1 << 6, + FB_OUT = 1 << 7 +}; + +class FmAlgorithm { +public: + int ops[6]; +}; + +class FmCore { +public: + virtual ~FmCore() {}; + static void dump(); + virtual void render(int32_t *output, FmOpParams *params, int algorithm, int32_t *fb_buf, int32_t feedback_gain); +protected: + AlignedBufbuf_[2]; + const static FmAlgorithm algorithms[32]; +}; + +#endif // __FM_CORE_H diff --git a/src/msfa/fm_op_kernel.cc b/src/msfa/fm_op_kernel.cc new file mode 100644 index 0000000..a14ff6a --- /dev/null +++ b/src/msfa/fm_op_kernel.cc @@ -0,0 +1,283 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#ifdef HAVE_NEON +#include +#endif + +#include "synth.h" +#include "sin.h" +#include "fm_op_kernel.h" + +#ifdef HAVE_NEONx +static bool hasNeon() { + return true; + return (android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0; +} + +extern "C" +void neon_fm_kernel(const int *in, const int *busin, int *out, int count, + int32_t phase0, int32_t freq, int32_t gain1, int32_t dgain); + +const int32_t __attribute__ ((aligned(16))) zeros[N] = {0}; + +#else +static bool hasNeon() { + return false; +} +#endif + +void FmOpKernel::compute(int32_t *output, const int32_t *input, + int32_t phase0, int32_t freq, + int32_t gain1, int32_t gain2, bool add) { + int32_t dgain = (gain2 - gain1 + (N >> 1)) >> LG_N; + int32_t gain = gain1; + int32_t phase = phase0; + if (hasNeon()) { +#ifdef HAVE_NEON + neon_fm_kernel(input, add ? output : zeros, output, N, + phase0, freq, gain, dgain); +#endif + } else { + if (add) { + for (int i = 0; i < N; i++) { + gain += dgain; + int32_t y = Sin::lookup(phase + input[i]); + int32_t y1 = ((int64_t)y * (int64_t)gain) >> 24; + output[i] += y1; + phase += freq; + } + } else { + for (int i = 0; i < N; i++) { + gain += dgain; + int32_t y = Sin::lookup(phase + input[i]); + int32_t y1 = ((int64_t)y * (int64_t)gain) >> 24; + output[i] = y1; + phase += freq; + } + } + } +} + +void FmOpKernel::compute_pure(int32_t *output, int32_t phase0, int32_t freq, + int32_t gain1, int32_t gain2, bool add) { + int32_t dgain = (gain2 - gain1 + (N >> 1)) >> LG_N; + int32_t gain = gain1; + int32_t phase = phase0; + if (hasNeon()) { +#ifdef HAVE_NEON + neon_fm_kernel(zeros, add ? output : zeros, output, N, + phase0, freq, gain, dgain); +#endif + } else { + if (add) { + for (int i = 0; i < N; i++) { + gain += dgain; + int32_t y = Sin::lookup(phase); + int32_t y1 = ((int64_t)y * (int64_t)gain) >> 24; + output[i] += y1; + phase += freq; + } + } else { + for (int i = 0; i < N; i++) { + gain += dgain; + int32_t y = Sin::lookup(phase); + int32_t y1 = ((int64_t)y * (int64_t)gain) >> 24; + output[i] = y1; + phase += freq; + } + } + } +} + +#define noDOUBLE_ACCURACY +#define HIGH_ACCURACY + +void FmOpKernel::compute_fb(int32_t *output, int32_t phase0, int32_t freq, + int32_t gain1, int32_t gain2, + int32_t *fb_buf, int fb_shift, bool add) { + int32_t dgain = (gain2 - gain1 + (N >> 1)) >> LG_N; + int32_t gain = gain1; + int32_t phase = phase0; + int32_t y0 = fb_buf[0]; + int32_t y = fb_buf[1]; + if (add) { + for (int i = 0; i < N; i++) { + gain += dgain; + int32_t scaled_fb = (y0 + y) >> (fb_shift + 1); + y0 = y; + y = Sin::lookup(phase + scaled_fb); + y = ((int64_t)y * (int64_t)gain) >> 24; + output[i] += y; + phase += freq; + } + } else { + for (int i = 0; i < N; i++) { + gain += dgain; + int32_t scaled_fb = (y0 + y) >> (fb_shift + 1); + y0 = y; + y = Sin::lookup(phase + scaled_fb); + y = ((int64_t)y * (int64_t)gain) >> 24; + output[i] = y; + phase += freq; + } + } + fb_buf[0] = y0; + fb_buf[1] = y; +} + +//////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////// + +// Experimental sine wave generators below +#if 0 +// Results: accuracy 64.3 mean, 170 worst case +// high accuracy: 5.0 mean, 49 worst case +void FmOpKernel::compute_pure(int32_t *output, int32_t phase0, int32_t freq, + int32_t gain1, int32_t gain2, bool add) { + int32_t dgain = (gain2 - gain1 + (N >> 1)) >> LG_N; + int32_t gain = gain1; + int32_t phase = phase0; +#ifdef HIGH_ACCURACY + int32_t u = Sin::compute10(phase << 6); + u = ((int64_t)u * gain) >> 30; + int32_t v = Sin::compute10((phase << 6) + (1 << 28)); // quarter cycle + v = ((int64_t)v * gain) >> 30; + int32_t s = Sin::compute10(freq << 6); + int32_t c = Sin::compute10((freq << 6) + (1 << 28)); +#else + int32_t u = Sin::compute(phase); + u = ((int64_t)u * gain) >> 24; + int32_t v = Sin::compute(phase + (1 << 22)); // quarter cycle + v = ((int64_t)v * gain) >> 24; + int32_t s = Sin::compute(freq) << 6; + int32_t c = Sin::compute(freq + (1 << 22)) << 6; +#endif + for (int i = 0; i < N; i++) { + output[i] = u; + int32_t t = ((int64_t)v * (int64_t)c - (int64_t)u * (int64_t)s) >> 30; + u = ((int64_t)u * (int64_t)c + (int64_t)v * (int64_t)s) >> 30; + v = t; + } +} +#endif + +#if 0 +// Results: accuracy 392.3 mean, 15190 worst case (near freq = 0.5) +// for freq < 0.25, 275.2 mean, 716 worst +// high accuracy: 57.4 mean, 7559 worst +// freq < 0.25: 17.9 mean, 78 worst +void FmOpKernel::compute_pure(int32_t *output, int32_t phase0, int32_t freq, + int32_t gain1, int32_t gain2, bool add) { + int32_t dgain = (gain2 - gain1 + (N >> 1)) >> LG_N; + int32_t gain = gain1; + int32_t phase = phase0; +#ifdef HIGH_ACCURACY + int32_t u = floor(gain * sin(phase * (M_PI / (1 << 23))) + 0.5); + int32_t v = floor(gain * cos((phase - freq * 0.5) * (M_PI / (1 << 23))) + 0.5); + int32_t a = floor((1 << 25) * sin(freq * (M_PI / (1 << 24))) + 0.5); +#else + int32_t u = Sin::compute(phase); + u = ((int64_t)u * gain) >> 24; + int32_t v = Sin::compute(phase + (1 << 22) - (freq >> 1)); + v = ((int64_t)v * gain) >> 24; + int32_t a = Sin::compute(freq >> 1) << 1; +#endif + for (int i = 0; i < N; i++) { + output[i] = u; + v -= ((int64_t)a * (int64_t)u) >> 24; + u += ((int64_t)a * (int64_t)v) >> 24; + } +} +#endif + +#if 0 +// Results: accuracy 370.0 mean, 15480 worst case (near freq = 0.5) +// with double accuracy initialization: mean 1.55, worst 58 (near freq = 0) +// with high accuracy: mean 4.2, worst 292 (near freq = 0.5) +void FmOpKernel::compute_pure(int32_t *output, int32_t phase0, int32_t freq, + int32_t gain1, int32_t gain2, bool add) { + int32_t dgain = (gain2 - gain1 + (N >> 1)) >> LG_N; + int32_t gain = gain1; + int32_t phase = phase0; +#ifdef DOUBLE_ACCURACY + int32_t u = floor((1 << 30) * sin(phase * (M_PI / (1 << 23))) + 0.5); + double a_d = sin(freq * (M_PI / (1 << 24))); + int32_t v = floor((1LL << 31) * a_d * cos((phase - freq * 0.5) * + (M_PI / (1 << 23))) + 0.5); + int32_t aa = floor((1LL << 31) * a_d * a_d + 0.5); +#else +#ifdef HIGH_ACCURACY + int32_t u = Sin::compute10(phase << 6); + int32_t v = Sin::compute10((phase << 6) + (1 << 28) - (freq << 5)); + int32_t a = Sin::compute10(freq << 5); + v = ((int64_t)v * (int64_t)a) >> 29; + int32_t aa = ((int64_t)a * (int64_t)a) >> 29; +#else + int32_t u = Sin::compute(phase) << 6; + int32_t v = Sin::compute(phase + (1 << 22) - (freq >> 1)); + int32_t a = Sin::compute(freq >> 1); + v = ((int64_t)v * (int64_t)a) >> 17; + int32_t aa = ((int64_t)a * (int64_t)a) >> 17; +#endif +#endif + + if (aa < 0) aa = (1 << 31) - 1; + for (int i = 0; i < N; i++) { + gain += dgain; + output[i] = ((int64_t)u * (int64_t)gain) >> 30; + v -= ((int64_t)aa * (int64_t)u) >> 29; + u += v; + } +} +#endif + +#if 0 +// Results:: accuracy 112.3 mean, 4262 worst (near freq = 0.5) +// high accuracy 2.9 mean, 143 worst +void FmOpKernel::compute_pure(int32_t *output, int32_t phase0, int32_t freq, + int32_t gain1, int32_t gain2, bool add) { + int32_t dgain = (gain2 - gain1 + (N >> 1)) >> LG_N; + int32_t gain = gain1; + int32_t phase = phase0; +#ifdef HIGH_ACCURACY + int32_t u = Sin::compute10(phase << 6); + int32_t lastu = Sin::compute10((phase - freq) << 6); + int32_t a = Sin::compute10((freq << 6) + (1 << 28)) << 1; +#else + int32_t u = Sin::compute(phase) << 6; + int32_t lastu = Sin::compute(phase - freq) << 6; + int32_t a = Sin::compute(freq + (1 << 22)) << 7; +#endif + if (a < 0 && freq < 256) a = (1 << 31) - 1; + if (a > 0 && freq > 0x7fff00) a = -(1 << 31); + for (int i = 0; i < N; i++) { + gain += dgain; + output[i] = ((int64_t)u * (int64_t)gain) >> 30; + //output[i] = u; + int32_t newu = (((int64_t)u * (int64_t)a) >> 30) - lastu; + lastu = u; + u = newu; + } +} +#endif + diff --git a/src/msfa/fm_op_kernel.h b/src/msfa/fm_op_kernel.h new file mode 100644 index 0000000..c9739e5 --- /dev/null +++ b/src/msfa/fm_op_kernel.h @@ -0,0 +1,47 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __FM_OP_KERNEL_H +#define __FM_OP_KERNEL_H + +struct FmOpParams { + int32_t level_in; // value to be computed (from level to gain[0]) + int32_t gain_out; // computed value (gain[1] to gain[0]) + int32_t freq; + int32_t phase; +}; + +class FmOpKernel { + public: + // gain1 and gain2 represent linear step: gain for sample i is + // gain1 + (1 + i) / 64 * (gain2 - gain1) + + // This is the basic FM operator. No feedback. + static void compute(int32_t *output, const int32_t *input, + int32_t phase0, int32_t freq, + int32_t gain1, int32_t gain2, bool add); + + // This is a sine generator, no feedback. + static void compute_pure(int32_t *output, int32_t phase0, int32_t freq, + int32_t gain1, int32_t gain2, bool add); + + // One op with feedback, no add. + static void compute_fb(int32_t *output, int32_t phase0, int32_t freq, + int32_t gain1, int32_t gain2, + int32_t *fb_buf, int fb_gain, bool add); +}; + +#endif diff --git a/src/msfa/freqlut.cc b/src/msfa/freqlut.cc new file mode 100644 index 0000000..931ef67 --- /dev/null +++ b/src/msfa/freqlut.cc @@ -0,0 +1,55 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Resolve frequency signal (1.0 in Q24 format = 1 octave) to phase delta. + +// The LUT is just a global, and we'll need the init function to be called before +// use. + +#include +#include + +#include "freqlut.h" + +#define LG_N_SAMPLES 10 +#define N_SAMPLES (1 << LG_N_SAMPLES) +#define SAMPLE_SHIFT (24 - LG_N_SAMPLES) + +#define MAX_LOGFREQ_INT 20 + +int32_t lut[N_SAMPLES + 1]; + +void Freqlut::init(double sample_rate) { + double y = (1LL << (24 + MAX_LOGFREQ_INT)) / sample_rate; + double inc = pow(2, 1.0 / N_SAMPLES); + for (int i = 0; i < N_SAMPLES + 1; i++) { + lut[i] = (int32_t)floor(y + 0.5); + y *= inc; + } +} + +// Note: if logfreq is more than 20.0, the results will be inaccurate. However, +// that will be many times the Nyquist rate. +int32_t Freqlut::lookup(int32_t logfreq) { + int ix = (logfreq & 0xffffff) >> SAMPLE_SHIFT; + + int32_t y0 = lut[ix]; + int32_t y1 = lut[ix + 1]; + int lowbits = logfreq & ((1 << SAMPLE_SHIFT) - 1); + int32_t y = y0 + ((((int64_t)(y1 - y0) * (int64_t)lowbits)) >> SAMPLE_SHIFT); + int hibits = logfreq >> 24; + return y >> (MAX_LOGFREQ_INT - hibits); +} diff --git a/src/msfa/freqlut.h b/src/msfa/freqlut.h new file mode 100644 index 0000000..5cba4af --- /dev/null +++ b/src/msfa/freqlut.h @@ -0,0 +1,21 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +class Freqlut { + public: + static void init(double sample_rate); + static int32_t lookup(int32_t logfreq); +}; diff --git a/src/msfa/lfo.cc b/src/msfa/lfo.cc new file mode 100644 index 0000000..b9b71de --- /dev/null +++ b/src/msfa/lfo.cc @@ -0,0 +1,97 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Low frequency oscillator, compatible with DX7 + +#include "synth.h" + +#include "sin.h" +#include "lfo.h" + +uint32_t Lfo::unit_; + +void Lfo::init(double sample_rate) { + // constant is 1 << 32 / 15.5s / 11 + Lfo::unit_ = (int32_t)(N * 25190424 / sample_rate + 0.5); +} + +void Lfo::reset(const uint8_t params[6]) { + int rate = params[0]; // 0..99 + int sr = rate == 0 ? 1 : (165 * rate) >> 6; + sr *= sr < 160 ? 11 : (11 + ((sr - 160) >> 4)); + delta_ = unit_ * sr; + int a = 99 - params[1]; // LFO delay + if (a == 99) { + delayinc_ = ~0u; + delayinc2_ = ~0u; + } else { + a = (16 + (a & 15)) << (1 + (a >> 4)); + delayinc_ = unit_ * a; + a &= 0xff80; + a = max(0x80, a); + delayinc2_ = unit_ * a; + } + waveform_ = params[5]; + sync_ = params[4] != 0; +} + +int32_t Lfo::getsample() { + phase_ += delta_; + int32_t x; + switch (waveform_) { + case 0: // triangle + x = phase_ >> 7; + x ^= -(phase_ >> 31); + x &= (1 << 24) - 1; + return x; + case 1: // sawtooth down + return (~phase_ ^ (1U << 31)) >> 8; + case 2: // sawtooth up + return (phase_ ^ (1U << 31)) >> 8; + case 3: // square + return ((~phase_) >> 7) & (1 << 24); + case 4: // sine + return (1 << 23) + (Sin::lookup(phase_ >> 8) >> 1); + case 5: // s&h + if (phase_ < delta_) { + randstate_ = (randstate_ * 179 + 17) & 0xff; + } + x = randstate_ ^ 0x80; + return (x + 1) << 16; + } + return 1 << 23; +} + +int32_t Lfo::getdelay() { + uint32_t delta = delaystate_ < (1U << 31) ? delayinc_ : delayinc2_; + uint64_t d = ((uint64_t)delaystate_) + delta; + if (d > ~0u) { + return 1 << 24; + } + delaystate_ = d; + if (d < (1U << 31)) { + return 0; + } else { + return (d >> 7) & ((1 << 24) - 1); + } +} + +void Lfo::keydown() { + if (sync_) { + phase_ = (1U << 31) - 1; + } + delaystate_ = 0; +} diff --git a/src/msfa/lfo.h b/src/msfa/lfo.h new file mode 100644 index 0000000..c7c0a9c --- /dev/null +++ b/src/msfa/lfo.h @@ -0,0 +1,43 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Low frequency oscillator, compatible with DX7 + +class Lfo { + public: + static void init(double sample_rate); + void reset(const uint8_t params[6]); + + // result is 0..1 in Q24 + int32_t getsample(); + + // result is 0..1 in Q24 + int32_t getdelay(); + + void keydown(); + private: + static uint32_t unit_; + + uint32_t phase_; // Q32 + uint32_t delta_; + uint8_t waveform_; + uint8_t randstate_; + bool sync_; + + uint32_t delaystate_; + uint32_t delayinc_; + uint32_t delayinc2_; +}; diff --git a/src/msfa/module.h b/src/msfa/module.h new file mode 100644 index 0000000..e165531 --- /dev/null +++ b/src/msfa/module.h @@ -0,0 +1,31 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SYNTH_MODULE_H +#define SYNTH_MODULE_H + +#include + +class Module { + public: + static const int lg_n = 6; + static const int n = 1 << lg_n; + virtual void process(const int32_t **inbufs, const int32_t *control_in, + const int32_t *control_last, int32_t **outbufs) = 0; +}; + +#endif // SYNTH_MODULE_H + diff --git a/src/msfa/pitchenv.cc b/src/msfa/pitchenv.cc new file mode 100644 index 0000000..a85b8d4 --- /dev/null +++ b/src/msfa/pitchenv.cc @@ -0,0 +1,95 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "synth.h" +#include "pitchenv.h" + +int PitchEnv::unit_; + +void PitchEnv::init(double sample_rate) { + unit_ = N * (1 << 24) / (21.3 * sample_rate) + 0.5; +} + +const uint8_t pitchenv_rate[] = { + 1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, + 12, 13, 13, 14, 14, 15, 16, 16, 17, 18, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 30, 31, 33, 34, 36, 37, 38, 39, 41, 42, 44, 46, 47, + 49, 51, 53, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 79, 82, + 85, 88, 91, 94, 98, 102, 106, 110, 115, 120, 125, 130, 135, 141, 147, + 153, 159, 165, 171, 178, 185, 193, 202, 211, 232, 243, 254, 255 +}; + +const int8_t pitchenv_tab[] = { + -128, -116, -104, -95, -85, -76, -68, -61, -56, -52, -49, -46, -43, + -41, -39, -37, -35, -33, -32, -31, -30, -29, -28, -27, -26, -25, -24, + -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, + -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + 28, 29, 30, 31, 32, 33, 34, 35, 38, 40, 43, 46, 49, 53, 58, 65, 73, + 82, 92, 103, 115, 127 +}; + +void PitchEnv::set(const int r[4], const int l[4]) { + for (int i = 0; i < 4; i++) { + rates_[i] = r[i]; + levels_[i] = l[i]; + } + level_ = pitchenv_tab[l[3]] << 19; + down_ = true; + advance(0); +} + +int32_t PitchEnv::getsample() { + if (ix_ < 3 || ((ix_ < 4) && !down_)) { + if (rising_) { + level_ += inc_; + if (level_ >= targetlevel_) { + level_ = targetlevel_; + advance(ix_ + 1); + } + } else { // !rising + level_ -= inc_; + if (level_ <= targetlevel_) { + level_ = targetlevel_; + advance(ix_ + 1); + } + } + } + return level_; +} + +void PitchEnv::keydown(bool d) { + if (down_ != d) { + down_ = d; + advance(d ? 0 : 3); + } +} + +void PitchEnv::advance(int newix) { + ix_ = newix; + if (ix_ < 4) { + int newlevel = levels_[ix_]; + targetlevel_ = pitchenv_tab[newlevel] << 19; + rising_ = (targetlevel_ > level_); + inc_ = pitchenv_rate[rates_[ix_]] * unit_; + } +} + +void PitchEnv::getPosition(char *step) { + *step = ix_; +} + + diff --git a/src/msfa/pitchenv.h b/src/msfa/pitchenv.h new file mode 100644 index 0000000..e106341 --- /dev/null +++ b/src/msfa/pitchenv.h @@ -0,0 +1,53 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __PITCHENV_H +#define __PITCHENV_H + +// Computation of the DX7 pitch envelope + +class PitchEnv { + public: + static void init(double sample_rate); + + // The rates and levels arrays are calibrated to match the Dx7 parameters + // (ie, value 0..99). + void set(const int rates[4], const int levels[4]); + + // Result is in Q24/octave + int32_t getsample(); + void keydown(bool down); + void getPosition(char *step); + private: + static int unit_; + int rates_[4]; + int levels_[4]; + int32_t level_; + int targetlevel_; + bool rising_; + int ix_; + int inc_; + + bool down_; + + void advance(int newix); +}; + +extern const uint8_t pitchenv_rate[]; +extern const int8_t pitchenv_tab[]; + + #endif // __PITCHENV_H + diff --git a/src/msfa/sin.cc b/src/msfa/sin.cc new file mode 100644 index 0000000..8a8471f --- /dev/null +++ b/src/msfa/sin.cc @@ -0,0 +1,143 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define _USE_MATH_DEFINES +#include + +#include "synth.h" +#include "sin.h" + +#define R (1 << 29) +#define M_PI 3.1415926 + +#ifdef SIN_DELTA +int32_t sintab[SIN_N_SAMPLES << 1]; +#else +int32_t sintab[SIN_N_SAMPLES + 1]; +#endif + +void Sin::init() { + double dphase = 2 * M_PI / SIN_N_SAMPLES; + int32_t c = (int32_t)floor(cos(dphase) * (1 << 30) + 0.5); + int32_t s = (int32_t)floor(sin(dphase) * (1 << 30) + 0.5); + int32_t u = 1 << 30; + int32_t v = 0; + for (int i = 0; i < SIN_N_SAMPLES / 2; i++) { +#ifdef SIN_DELTA + sintab[(i << 1) + 1] = (v + 32) >> 6; + sintab[((i + SIN_N_SAMPLES / 2) << 1) + 1] = -((v + 32) >> 6); +#else + sintab[i] = (v + 32) >> 6; + sintab[i + SIN_N_SAMPLES / 2] = -((v + 32) >> 6); +#endif + int32_t t = ((int64_t)u * (int64_t)s + (int64_t)v * (int64_t)c + R) >> 30; + u = ((int64_t)u * (int64_t)c - (int64_t)v * (int64_t)s + R) >> 30; + v = t; + } +#ifdef SIN_DELTA + for (int i = 0; i < SIN_N_SAMPLES - 1; i++) { + sintab[i << 1] = sintab[(i << 1) + 3] - sintab[(i << 1) + 1]; + } + sintab[(SIN_N_SAMPLES << 1) - 2] = -sintab[(SIN_N_SAMPLES << 1) - 1]; +#else + sintab[SIN_N_SAMPLES] = 0; +#endif +} + +#ifndef SIN_INLINE +int32_t Sin::lookup(int32_t phase) { + const int SHIFT = 24 - SIN_LG_N_SAMPLES; + int lowbits = phase & ((1 << SHIFT) - 1); +#ifdef SIN_DELTA + int phase_int = (phase >> (SHIFT - 1)) & ((SIN_N_SAMPLES - 1) << 1); + int dy = sintab[phase_int]; + int y0 = sintab[phase_int + 1]; + + return y0 + (((int64_t)dy * (int64_t)lowbits) >> SHIFT); +#else + int phase_int = (phase >> SHIFT) & (SIN_N_SAMPLES - 1); + int y0 = sintab[phase_int]; + int y1 = sintab[phase_int + 1]; + + return y0 + (((int64_t)(y1 - y0) * (int64_t)lowbits) >> SHIFT); +#endif +} +#endif + + +#if 0 +// The following is an implementation designed not to use any lookup tables, +// based on the following implementation by Basile Graf: +// http://www.rossbencina.com/static/code/sinusoids/even_polynomial_sin_approximation.txt + +#define C0 (1 << 24) +#define C1 (331121857 >> 2) +#define C2 (1084885537 >> 4) +#define C3 (1310449902 >> 6) + +int32_t Sin::compute(int32_t phase) { + int32_t x = (phase & ((1 << 23) - 1)) - (1 << 22); + int32_t x2 = ((int64_t)x * (int64_t)x) >> 22; + int32_t x4 = ((int64_t)x2 * (int64_t)x2) >> 24; + int32_t x6 = ((int64_t)x2 * (int64_t)x4) >> 24; + int32_t y = C0 - + (((int64_t)C1 * (int64_t)x2) >> 24) + + (((int64_t)C2 * (int64_t)x4) >> 24) - + (((int64_t)C3 * (int64_t)x6) >> 24); + y ^= -((phase >> 23) & 1); + return y; +} +#endif + +#if 1 +// coefficients are Chebyshev polynomial, computed by compute_cos_poly.py +#define C8_0 16777216 +#define C8_2 -331168742 +#define C8_4 1089453524 +#define C8_6 -1430910663 +#define C8_8 950108533 + +int32_t Sin::compute(int32_t phase) { + int32_t x = (phase & ((1 << 23) - 1)) - (1 << 22); + int32_t x2 = ((int64_t)x * (int64_t)x) >> 16; + int32_t y = (((((((((((((int64_t)C8_8 + * (int64_t)x2) >> 32) + C8_6) + * (int64_t)x2) >> 32) + C8_4) + * (int64_t)x2) >> 32) + C8_2) + * (int64_t)x2) >> 32) + C8_0); + y ^= -((phase >> 23) & 1); + return y; +} +#endif + +#define C10_0 (1 << 30) +#define C10_2 -1324675874 // scaled * 4 +#define C10_4 1089501821 +#define C10_6 -1433689867 +#define C10_8 1009356886 +#define C10_10 -421101352 +int32_t Sin::compute10(int32_t phase) { + int32_t x = (phase & ((1 << 29) - 1)) - (1 << 28); + int32_t x2 = ((int64_t)x * (int64_t)x) >> 26; + int32_t y = ((((((((((((((((int64_t)C10_10 + * (int64_t)x2) >> 34) + C10_8) + * (int64_t)x2) >> 34) + C10_6) + * (int64_t)x2) >> 34) + C10_4) + * (int64_t)x2) >> 32) + C10_2) + * (int64_t)x2) >> 30) + C10_0); + y ^= -((phase >> 29) & 1); + return y; +} diff --git a/src/msfa/sin.h b/src/msfa/sin.h new file mode 100644 index 0000000..6605e99 --- /dev/null +++ b/src/msfa/sin.h @@ -0,0 +1,62 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +class Sin { + public: + Sin(); + + static void init(); + static int32_t lookup(int32_t phase); + static int32_t compute(int32_t phase); + + // A more accurate sine, both input and output Q30 + static int32_t compute10(int32_t phase); +}; + +#define SIN_LG_N_SAMPLES 10 +#define SIN_N_SAMPLES (1 << SIN_LG_N_SAMPLES) + +#define SIN_INLINE + +// Use twice as much RAM for the LUT but avoid a little computation +#define SIN_DELTA + +#ifdef SIN_DELTA +extern int32_t sintab[SIN_N_SAMPLES << 1]; +#else +extern int32_t sintab[SIN_N_SAMPLES + 1]; +#endif + +#ifdef SIN_INLINE +inline +int32_t Sin::lookup(int32_t phase) { + const int SHIFT = 24 - SIN_LG_N_SAMPLES; + int lowbits = phase & ((1 << SHIFT) - 1); +#ifdef SIN_DELTA + int phase_int = (phase >> (SHIFT - 1)) & ((SIN_N_SAMPLES - 1) << 1); + int dy = sintab[phase_int]; + int y0 = sintab[phase_int + 1]; + + return y0 + (((int64_t)dy * (int64_t)lowbits) >> SHIFT); +#else + int phase_int = (phase >> SHIFT) & (SIN_N_SAMPLES - 1); + int y0 = sintab[phase_int]; + int y1 = sintab[phase_int + 1]; + + return y0 + (((int64_t)(y1 - y0) * (int64_t)lowbits) >> SHIFT); +#endif +} +#endif diff --git a/src/msfa/synth.h b/src/msfa/synth.h new file mode 100644 index 0000000..c64005b --- /dev/null +++ b/src/msfa/synth.h @@ -0,0 +1,71 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __SYNTH_H +#define __SYNTH_H + +// This IS not be present on MSVC. +// See http://stackoverflow.com/questions/126279/c99-stdint-h-header-and-ms-visual-studio +#include +#ifdef _MSC_VER +typedef __int32 int32_t; +typedef unsigned __int32 uint32_t; +typedef __int16 SInt16; +#endif + +const static int LG_N = 6; +const static int N = (1 << LG_N); + +#if defined(__APPLE__) +#include +#define SynthMemoryBarrier() OSMemoryBarrier() +#elif defined(__GNUC__) +#if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 1) +#define SynthMemoryBarrier() __sync_synchronize() +#endif +#endif + + +// #undef SynthMemoryBarrier() + +#ifndef SynthMemoryBarrier +// need to understand why this must be defined +// #warning Memory barrier is not enabled +#define SynthMemoryBarrier() +#endif + +template +inline static T min(const T& a, const T& b) { + return a < b ? a : b; +} + +template +inline static T max(const T& a, const T& b) { + return a > b ? a : b; +} + +void dexed_trace(const char *source, const char *fmt, ...); + +#define QER(n,b) ( ((float)n)/(1< +#include +#include +#include +#include +#include +#include + + +struct StandardTuning : public TuningState { + StandardTuning() { + const int base = 50857777; // (1 << 24) * (log(440) / log(2) - 69/12) + const int step = (1 << 24) / 12; + for( int mn = 0; mn < 128; ++mn ) + { + auto res = base + step * mn; + current_logfreq_table_[mn] = res; + } + } + + virtual int32_t midinote_to_logfreq(int midinote) override { + return current_logfreq_table_[midinote]; + } + + int current_logfreq_table_[128]; +}; + + +struct SCLAndKBMTuningState : public TuningState { + virtual bool is_standard_tuning() override { + return false; + } + + virtual int32_t midinote_to_logfreq(int midinote) override { + const int base = 50857777; + const int step = ( 1 << 24 ); + return tuning.logScaledFrequencyForMidiNote( midinote ) * step + base; + } + + virtual int scale_length() { return tuning.scale.count; } + virtual std::string display_tuning_str() { return "SCL KBM Tuning"; } + + virtual Tunings::Tuning &getTuning() override { return tuning; } + + Tunings::Tuning tuning; + +}; + +std::shared_ptr createStandardTuning() +{ + return std::make_shared(); +} + +std::shared_ptr createTuningFromSCLData( const std::string &scl ) +{ + auto s = Tunings::parseSCLData(scl); + auto res = std::make_shared(); + res->tuning = Tunings::Tuning( s ); + return res; +} + +std::shared_ptr createTuningFromKBMData( const std::string &kbm ) +{ + auto k = Tunings::parseKBMData(kbm); + auto res = std::make_shared(); + res->tuning = Tunings::Tuning( k ); + return res; +} + +std::shared_ptr createTuningFromSCLAndKBMData( const std::string &sclData, const std::string &kbmData ) +{ + auto s = Tunings::parseSCLData(sclData); + auto k = Tunings::parseKBMData(kbmData); + auto res = std::make_shared(); + res->tuning = Tunings::Tuning( s, k ); + return res; +} diff --git a/src/msfa/tuning.h b/src/msfa/tuning.h new file mode 100644 index 0000000..cf7f287 --- /dev/null +++ b/src/msfa/tuning.h @@ -0,0 +1,30 @@ +#ifndef __SYNTH_TUNING_H +#define __SYNTH_TUNING_H + +#include "synth.h" +#include +#include +#include "Tunings.h" + +class TuningState { +public: + virtual ~TuningState() { } + + virtual int32_t midinote_to_logfreq(int midinote) = 0; + virtual bool is_standard_tuning() { return true; } + virtual int scale_length() { return 12; } + virtual std::string display_tuning_str() { return "Standard Tuning"; } + + virtual Tunings::Tuning &getTuning() { + static Tunings::Tuning t; + return t; + } +}; + +std::shared_ptr createStandardTuning(); + +std::shared_ptr createTuningFromSCLData( const std::string &sclData ); +std::shared_ptr createTuningFromKBMData( const std::string &kbmData ); +std::shared_ptr createTuningFromSCLAndKBMData( const std::string &sclData, const std::string &kbmData ); + +#endif diff --git a/src/tuning-library/LICENSE.md b/src/tuning-library/LICENSE.md new file mode 100644 index 0000000..64d1d07 --- /dev/null +++ b/src/tuning-library/LICENSE.md @@ -0,0 +1,9 @@ +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. + + diff --git a/src/tuning-library/Tunings.h b/src/tuning-library/Tunings.h new file mode 100644 index 0000000..fb0280b --- /dev/null +++ b/src/tuning-library/Tunings.h @@ -0,0 +1,278 @@ +// -*-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 +#include +#include +#include +#include + +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 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 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 ptable, lptable; + std::array scalepositiontable; + }; + +} // namespace Tunings + +#include "TuningsImpl.h" + +#endif diff --git a/src/tuning-library/TuningsImpl.h b/src/tuning-library/TuningsImpl.h new file mode 100644 index 0000000..adf787b --- /dev/null +++ b/src/tuning-library/TuningsImpl.h @@ -0,0 +1,538 @@ +// -*-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 +#include +#include +#include +#include +#include +#include + +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 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