/*
 * so-404.c
 *
 * Copyright (C) 2011 - Jeremy Salwen
 * Copyright (C) 2010 - 50m30n3
 * Copyright (C) 2020 - Google LLC
 *
 * 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 2 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 "so-404.h"

inline float midi_to_hz(int note) {
  return 440.0 * powf(2.0, (note - 69) / 12.0);
}

void runSO_404(LV2_Handle arg, uint32_t nframes) {
  so_404* so = (so_404*)arg;

  LV2_Atom_Event* iter = lv2_atom_sequence_begin(&so->midi_p->body);

  float* outbuffer = so->output;

  if (*so->controlmode_p > 0) {
    so->cutoff = *so->cutoff_p;
    so->portamento = *so->portamento_p;
    so->release = *so->release_p;
    so->volume = *so->volume_p;
    so->envmod = *so->envmod_p;
    so->resonance = *so->resonance_p;
  }

  int i;
  for (i = 0; i < nframes; i++) {
    while (!lv2_atom_sequence_is_end(&so->midi_p->body, so->midi_p->atom.size,
                                     iter)) {
      if (iter->time.frames > i) {
        break;
      }
      if (iter->body.type == so->uris.midi_MidiEvent) {
        const uint8_t* evt = (uint8_t*)(iter + 1);
        if ((evt[0] & MIDI_CHANNELMASK) == (int)(*so->channel_p)) {
          if ((evt[0] & MIDI_COMMANDMASK) == MIDI_NOTEON) {
            unsigned int note = evt[1];
            so->tfreq = midi_to_hz(note);
            if (so->noteson == 0) {
              so->freq = so->tfreq;
              so->amp = 1.0;
              so->vel = ((float)evt[2]);
              so->env = so->vel / 127.0;
              so->cdelay = 0;
            }
            so->noteson += 1;
          } else if ((evt[0] & MIDI_COMMANDMASK) == MIDI_NOTEOFF) {
            so->noteson -= 1;
            if (so->noteson < 0) {
              so->noteson = 0;
            }
          } else if ((*so->controlmode_p <= 0) &&
                     (evt[0] & MIDI_COMMANDMASK) == MIDI_CONTROL) {
            unsigned int command_val = evt[2];
            switch (evt[1]) {
              case 74:
                so->cutoff = command_val;
                break;
              case 65:
                so->portamento = command_val;
                break;
              case 72:
                so->release = command_val;
                break;
              case 7:
                so->volume = command_val;
                break;
              case 79:
                so->envmod = command_val;
                break;
              case 71:
                so->resonance = command_val;
                break;
            }
          }
        }
      }
      iter = lv2_atom_sequence_next(iter);
    }
    if (so->cdelay <= 0) {
      so->freq = ((so->portamento / 127.0) * 0.9) * so->freq +
                 (1.0 - ((so->portamento / 127.0) * 0.9)) * so->tfreq;
      if (so->noteson > 0) {
        so->amp *= 0.99;
      } else {
        so->amp *= 0.5;
      }
      so->env *= 0.8 + powf(so->release / 127.0, 0.25) / 5.1;

      so->fcutoff = powf(so->cutoff / 127.0, 2.0) +
                    powf(so->env, 2.0) * powf(so->envmod / 127.0, 2.0);
      so->fcutoff = tanh(so->fcutoff);
      so->freso = powf(so->resonance / 130.0, 0.25);
      so->cdelay = so->samplerate / 100;
    }
    so->cdelay--;

    float max = so->samplerate / so->freq;
    float sample = (so->phase / max) * (so->phase / max) - 0.25;
    so->phase++;
    if (so->phase >= max) {
      so->phase -= max;
    }

    if (so->vel > 100) {
      sample *= so->env;
    } else {
      sample *= so->amp;
    }

    so->fpos += so->fspeed;
    so->fspeed *= so->freso;
    so->fspeed += (sample - so->fpos) * so->fcutoff;
    sample = so->fpos;

    sample = sample * 0.5 + so->lastsample * 0.5;
    so->lastsample = sample;

    outbuffer[i] = sample * (so->volume / 127.0);
  }
}

LV2_Handle instantiateSO_404(const LV2_Descriptor* descriptor, double s_rate,
                             const char* path,
                             const LV2_Feature* const* features) {
  so_404* so = malloc(sizeof(so_404));
  for (int i = 0; features[i]; ++i) {
    if (!strcmp(features[i]->URI, LV2_URID__map)) {
      LV2_URID_Map* map = (LV2_URID_Map*)features[i]->data;
      so->uris.midi_MidiEvent = map->map(map->handle, LV2_MIDI__MidiEvent);
      break;
    }
  }

  puts("SO-404 v.1.2 by 50m30n3 2009-2011");

  so->phase = 0.0;
  so->freq = 440.0;
  so->tfreq = 440.0;
  so->amp = 0.0;
  so->env = 0.0;
  so->vel = 0;
  so->fcutoff = 0.0;
  so->fspeed = 0.0;
  so->fpos = 0.0;
  so->lastsample = 0.0;
  so->noteson = 0;
  so->cdelay = s_rate / 100;
  so->samplerate = s_rate;

  so->release = 100;
  so->cutoff = 50;
  so->envmod = 80;
  so->resonance = 100;
  so->volume = 100;
  so->portamento = 64;

  return so;
}
void cleanupSO_404(LV2_Handle instance) { free(instance); }

void connectPortSO_404(LV2_Handle instance, uint32_t port,
                       void* data_location) {
  so_404* so = (so_404*)instance;
  switch (port) {
    case PORT_404_OUTPUT:
      so->output = data_location;
      break;
    case PORT_404_MIDI:
      so->midi_p = data_location;
      break;
    case PORT_404_CONTROLMODE:
      so->controlmode_p = data_location;
      break;
    case PORT_404_VOLUME:
      so->volume_p = data_location;
      break;
    case PORT_404_CUTOFF:
      so->cutoff_p = data_location;
      break;
    case PORT_404_RESONANCE:
      so->resonance_p = data_location;
      break;
    case PORT_404_ENVELOPE:
      so->envmod_p = data_location;
      break;
    case PORT_404_PORTAMENTO:
      so->portamento_p = data_location;
      break;
    case PORT_404_RELEASE:
      so->release_p = data_location;
      break;
    case PORT_404_CHANNEL:
      so->channel_p = data_location;
      break;
    default:
      fputs("Warning, unconnected port!\n", stderr);
  }
}
