Skip to content

Commit

Permalink
initial publish
Browse files Browse the repository at this point in the history
  • Loading branch information
mrf-r committed Feb 25, 2024
1 parent e96e2c6 commit 9644a9c
Show file tree
Hide file tree
Showing 16 changed files with 722 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build*
*unit
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "logue-sdk"]
path = logue-sdk
url = https://github.com/korginc/logue-sdk.git
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,40 @@
# FunFM-logue
exploration oscillator for KORG Logue platform

![](blocks.png "structure")

Consists of 2 extended range alias supressed FM operators with variable waveshape.

Each operator is capable of generating different waveforms, from pulse to sine to sawtooth. Unlike all other oscillators, the wave transition algorithm is not a crossfade, but is a bit like low-pass filtering. A pulse (bw=-100) can be “filtered” to a sine (bw=0), and then “unfiltered” to a saw (bw=100).
The modulation depth is bipolar: positive is the normal depth, negative is the absolute input value, sometimes giving a different result with more odd harmonics.
the output is an x-fade between op1 and op2, the value is then used as feedback modulation for op2.

### Parameters:

- Shape - high resolution op1 pitch offset
- Alt - high resolution op2 pitch offset
- 1bw - op1 bandwidth: -100 - pulse, 0 - sine (parabolic approximation), +100 - saw
- 1pm - op1 by op2 phase modulation depth: -100 - absolute "odd", +100 - bipolar "even"
- 2bw - op2 bandwidth
- 2fb - op2 by feedback modulation depth
- mix - OP1

LFO shape is routed to 1pm

### History:

The idea was born after meeting KORG Opsix. Despite the relatively convenient control, the synthesizer was disappointing with its low sampling rate and aliasing noises when deviating from the classic sinusoidal fm. The first proof-of-concept was created in 2021 with the support of the [Cultural Transit Foundation Yekaterinburg](http://www.cultt.ru/) as part of the laboratory of electronics in art. Thanks to all participants and artists for the inspiration!

### Build and load example

git submodule update --init --recursive
cd funfm-src
make install PLATFORMDIR=../logue-sdk/platform/minilogue-xd/
./../logue-sdk/tools/logue-cli/get_logue_cli_msys64.sh
./../logue-sdk/tools/logue-cli/logue-cli-win64-0.07-2b/logue-cli.exe load -i 0 -o 0 -u FunFM.mnlgxdunit

### Note

The NTS-1 initializes the parameters to -100, so the first sound you hear will be the harsh noise resulting from maximum PM and feedback. I recommend starting by setting the depths and pitches to the middle. The pitch range is intentionally wide, and the extremes are quite random. Please use your ears to adjust the settings. In general, the oscillator is capable of producing classic VA tones (detuned SAW, PWM, hardsync), but the main goal is to search for something new. Stay open to new things.

Peace!
Binary file added blocks.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions funfm-src/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

ifndef PLATFORMDIR
$(warning PLATFORMDIR not defined, building for nts-1)
PLATFORMDIR := $(abspath ../logue-sdk/platform/nutekt-digital)
endif

GCC_BIN_PATH ?= /bin
LDDIR := $(PLATFORMDIR)/dummy-osc/ld
include $(PLATFORMDIR)/dummy-osc/Makefile
59 changes: 59 additions & 0 deletions funfm-src/basic_osc.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// TEST WAVE
// Copyright (C) Eugene Chernyh man125403@gmail.com

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#ifndef _BASIC_OSC_H
#define _BASIC_OSC_H

#include <stdint.h>

// low accuracy for testing only
static inline int32_t boscInc(const int32_t freq, const uint32_t sample_rate)
{
return (0x100000000LL / sample_rate) * freq;
}

static inline int32_t boscSaw(int32_t* const acc, const int32_t inc)
{
*acc += inc;
return *acc;
}

static inline int32_t boscTriangle(const int32_t saw)
{
int32_t tri = saw;
if (tri < 0)
tri = ~tri;
tri = tri * 2;
tri -= 0x80000000;
return tri;
}

static inline int32_t boscParabolicSine(const int32_t tri)
{
volatile int32_t ps = tri;
if (ps < 0) {
ps = ps + 0x80000000;
ps = (ps / 65536) * (ps / 32768);
ps = ps - 0x80000000;
} else {
ps = 0x7FFFFFFF - ps;
ps = (ps / 65536) * (ps / 32768);
ps = 0x7FFFFFFF - ps;
}
return ps;
}

#endif // _BASIC_OSC_H
180 changes: 180 additions & 0 deletions funfm-src/ffm.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// FunFM oscillator port for KORG Logue platform
// Copyright (C) 2024 Eugene Chernyh man125403@gmail.com

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#include "userosc.h"
#include "osc_api.h"
#include "operator.h"
#include "scale12.h"

uint8_t osc_notecounter; // wtf, i don't have keyboard to test this!
float note;
float patch_glide;
float patch_op1pitch;
float patch_op1bw;
float patch_op1pm;
float patch_op2pitch;
float patch_op2bw;
float patch_op2pm;
float patch_mix;

#define RANGE_O1PITCH_HIGH 48.1f
#define RANGE_O1PITCH_LOW -47.4f
#define RANGE_O2PITCH_HIGH (39.3f)
#define RANGE_O2PITCH_LOW (-52.7f)
#define RANGE_BW 1.f
#define RANGE_O1PM 5.f
#define RANGE_O1PM_MAX_FBK 0.5f
#define RANGE_O2PM 1.1f
#define RANGE_MIX 1.f
#define RANGE_GLIDE 1.f

void OSC_INIT(uint32_t platform, uint32_t api)
{
(void)platform;
(void)api;
// FPU->FPDSCR |= FPU_FPDSCR_FZ_Msk;
// FPU->FPDSCR |= FPU_FPDSCR_DN_Msk;
// FPU->FPDSCR &= ~(1UL << 12);
// FPU->FPDSCR &= ~(1UL << 10);
// FPU->FPDSCR &= ~(1UL << 8);
osc_notecounter = 0;
}

// instance
static int32_t in_o1acc;
static int32_t in_o1pm_prev;
static int32_t in_o2acc;
static int32_t in_o2pm_prev;
static float in_feedback;

// blocksize 64
void OSC_CYCLE(const user_osc_param_t* const params, int32_t* yn, const uint32_t frames)
{
float pitch = (float)(params->pitch) * (1.f / 256.f);
float lfo = q31_to_f32(params->shape_lfo);
// pitch filter coefficient for 750Hz CR
// const float glide_cf = osc_notecounter > 1 ? 1.f : 0.f;
//
float op1speed = scale12EdoGetFreqHz(pitch + patch_op1pitch) * k_samplerate_recipf;
float op2speed = scale12EdoGetFreqHz(pitch + patch_op2pitch) * k_samplerate_recipf;
int32_t op1inc = (int32_t)(op1speed * 2147483648.f);
int32_t op2inc = (int32_t)(op2speed * 2147483648.f);
float op2pm = patch_op2pm * patch_op2pm;

// limitation of phasemod on high feedback
float op2pm_normalized = fabsf(patch_op2pm) / RANGE_O2PM;
float op1pm_mul = 1.f - ((RANGE_O1PM - RANGE_O1PM_MAX_FBK) / RANGE_O1PM) * op2pm_normalized;
float op1pm = (patch_op1pm + RANGE_O1PM * lfo) * op1pm_mul;
op1pm = op1pm * op1pm;

float op1bw;
float op1amp_comp1;
float op1amp_comp2;
float op2bw;
float op2amp_comp1;
float op2amp_comp2;
if (patch_op1bw < 0) {
op1bw = -patch_op1bw;
op1amp_comp1 = bwAmpComp(op1bw) / 2;
op1amp_comp2 = -op1amp_comp1;
} else {
op1bw = patch_op1bw;
op1amp_comp1 = bwAmpComp(op1bw);
op1amp_comp2 = 0;
}
if (patch_op2bw < 0) {
op2bw = -patch_op2bw;
op2amp_comp1 = bwAmpComp(op2bw) / 2;
op2amp_comp2 = -op2amp_comp1;
} else {
op2bw = patch_op2bw;
op2amp_comp1 = bwAmpComp(op2bw);
op2amp_comp2 = 0;
}

float op1amp_mix = patch_mix;
float op2amp_mix = 1.f - op1amp_mix;

float fb = in_feedback;

q31_t* __restrict y = (q31_t*)yn;
const q31_t* y_e = y + frames;
for (; y != y_e;) {
// comparison on each sample is not required, but we still have cycles and let's keep this code simple.
float o2pm = (patch_op2pm < 0 ? fabsf(fb) : fb) * op2pm;
float o2 = operatorDoubleSmpl(&in_o2acc, &in_o2pm_prev, o2pm, op2inc, op2speed, op2bw, 0x80000000, op2amp_comp1, op2amp_comp2);
float o1pm = (patch_op1pm < 0 ? fabsf(o2) : o2) * op1pm;
float o1 = operatorDoubleSmpl(&in_o1acc, &in_o1pm_prev, o1pm, op1inc, op1speed, op1bw, 0x80000000, op1amp_comp1, op1amp_comp2);
float out = o1 * op1amp_mix + o2 * op2amp_mix;
fb = out;
*(y++) = f32_to_q31(out);
}

in_feedback = fb;
}

void OSC_NOTEON(const user_osc_param_t* const params)
{
(void)params;
osc_notecounter++;
}
void OSC_NOTEOFF(const user_osc_param_t* const params)
{
(void)params;
if (osc_notecounter)
osc_notecounter--;
}

void OSC_PARAM(uint16_t index, uint16_t value)
{
switch (index) {
case k_user_osc_param_shape:
// o1pitch
patch_op1pitch = (float)value * ((RANGE_O1PITCH_HIGH - RANGE_O1PITCH_LOW) / 1024.f) - RANGE_O1PITCH_HIGH;
break;
case k_user_osc_param_shiftshape:
// o2pitch
patch_op2pitch = (float)value * ((RANGE_O2PITCH_HIGH - RANGE_O2PITCH_LOW) / 1024.f) - RANGE_O2PITCH_HIGH;
break;
case k_user_osc_param_id1:
// 1bw
patch_op1bw = (float)value * (RANGE_BW * 2.f / 200.f) - RANGE_BW;
break;
case k_user_osc_param_id2:
// 1pm
patch_op1pm = (float)value * (RANGE_O1PM * 2.f / 200.f) - RANGE_O1PM;
break;
case k_user_osc_param_id3:
// 2bw
patch_op2bw = (float)value * (RANGE_BW * 2.f / 200.f) - RANGE_BW;
break;
case k_user_osc_param_id4:
// 2pm
patch_op2pm = (float)value * (RANGE_O2PM * 2.f / 200.f) - RANGE_O2PM;
break;
case k_user_osc_param_id5:
// mix
patch_mix = (float)value * (RANGE_MIX / 200.f);
break;
case k_user_osc_param_id6:
// glide
patch_glide = (float)value * (RANGE_GLIDE / 100.f);
break;
default:
break;
}
(void)value;
}
1 change: 1 addition & 0 deletions funfm-src/load.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
./../logue-sdk/tools/logue-cli/logue-cli-win64-0.07-2b/logue-cli.exe load -i 0 -o 1 -u FunFM.ntkdigunit -s $1
20 changes: 20 additions & 0 deletions funfm-src/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"header" :
{
"platform" : "nutekt-digital",
"module" : "osc",
"api" : "1.1-0",
"dev_id" : 1836213874,
"prg_id" : 2,
"version" : "1.0-0",
"name" : "ffm",
"num_param" : 5,
"params" : [
["1bw", -100, 100, "%"],
["1pm", -100, 100, "%"],
["2bw", -100, 100, "%"],
["2fb", -100, 100, "%"],
["mix", -100, 100, "%"]
]
}
}
Loading

0 comments on commit 9644a9c

Please sign in to comment.