diff --git a/user/NCO/Makefile b/user/NCO/Makefile new file mode 100644 index 000000000..6847634e1 --- /dev/null +++ b/user/NCO/Makefile @@ -0,0 +1,13 @@ +# Project Name +TARGET = PodNcoMain + +# Sources +CPP_SOURCES = nco.cpp harmonicNco.cpp PodNcoMain.cpp + +# Library Locations +LIBDAISY_DIR = ../../libdaisy +DAISYSP_DIR = ../../DaisySP + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/user/NCO/PodNcoMain.cpp b/user/NCO/PodNcoMain.cpp new file mode 100644 index 000000000..d2eb6be6a --- /dev/null +++ b/user/NCO/PodNcoMain.cpp @@ -0,0 +1,226 @@ +#include "daisysp.h" +#include "daisy_pod.h" +#include "nco.h" +#include "harmonicNco.h" + +#define MIDI_C4 60 +#define MIDI_A4 69 +#define FREQ_A4 440 +#define NUM_MIDI_NOTES_PER_OCTAVE 12 + +using namespace daisysp; +using namespace daisy; + +enum NoteInterval +{ + ROOT, + THIRD, + FIFTH, + NUM_INTERVALS +}; + +enum ChordSequence +{ + CHORD_I, + CHORD_V, + CHORD_VI_M, + CHORD_IV, + CHORD_SEQ_LEN +}; + +enum BasicChordType +{ + MAJOR, + MINOR +}; + +static void UpdateChord(uint16_t root_midi_note, BasicChordType chord_type); +static void AdvanceChord(uint16_t root_midi_note, int direction); +static int getButtonDir(void); + +static DaisyPod pod; +static HarmonicNCO harmonicNco[NUM_INTERVALS]; +static float amp = 0.5f; +static uint16_t s_key_root_note = MIDI_C4; + +static float harmonicAmps[] = +{ + 1.0, + 0.7, + 0.6, + 0.3, + 0.0, + 0.0, + 0.0, + 0.0 +}; + +static float harmonicPhases[] = +{ + 0.0, + 0.9, + 0.125, + 0.45, + 0.65, + 0.95, + 0.55, + 0.6 +}; + +static void AudioCallback(AudioHandle::InterleavingInputBuffer in, + AudioHandle::InterleavingOutputBuffer out, + size_t size) +{ + float sample = 0; + int encoder_dir = 0; + int button_dir = 0; + + pod.ProcessAllControls(); + + button_dir = getButtonDir(); + + if (button_dir) + { + AdvanceChord(s_key_root_note, button_dir); + } + + encoder_dir = pod.encoder.Increment(); + + if (encoder_dir) + { + s_key_root_note += encoder_dir; + AdvanceChord(s_key_root_note, 0); + } + + // Audio Loop + for (size_t ndx = 0; ndx < size; ndx += 2) + { + for (uint8_t note = ROOT; note < NUM_INTERVALS; note++) + { + sample += (amp / NUM_INTERVALS) * harmonicNco[note].NextSample(); + } + // left out + out[ndx] = sample; + // right out + out[ndx + 1] = sample; + + sample = 0; + } +} + +static void InitChordNcos(uint16_t root_midi_note, uint32_t sample_rate) +{ + for (uint8_t note = ROOT; note < NUM_INTERVALS; note++) + { + harmonicNco[note].SetSampleRate(sample_rate); + harmonicNco[note].SetAmplitudes(harmonicAmps); + harmonicNco[note].SetPhases(harmonicPhases); + } + + // Set the frequencies of each NCO + UpdateChord(root_midi_note, MAJOR); +} + +// Sets the NCO's to play a major chord +static void UpdateChord(uint16_t root_midi_note, BasicChordType chord_type) +{ + float freq = 0; + uint16_t midi_note = root_midi_note; + + for (uint8_t note = ROOT; note < NUM_INTERVALS; note++) + { + // Apply offset based on interval + midi_note = root_midi_note + (3 * note); + + if (chord_type == MAJOR) + { + midi_note += (note != ROOT) ? 1 : 0; + } + else + { + midi_note += (note == FIFTH) ? 1 : 0; + } + // Keep chord notes w/in an octave of the key root note + midi_note = ((midi_note - s_key_root_note) % NUM_MIDI_NOTES_PER_OCTAVE) + s_key_root_note; + + freq = mtof(midi_note); + harmonicNco[note].SetFrequency(freq); + } +} + +static void AdvanceChord(uint16_t root_midi_note, int direction) +{ + static uint8_t current_chord = CHORD_I; + + // Advance to next chord + if (direction > 0) + { + current_chord = (current_chord + 1) % CHORD_SEQ_LEN; + } + else if (direction < 0) + { + current_chord = (current_chord + CHORD_SEQ_LEN - 1) % CHORD_SEQ_LEN; + } + + switch (current_chord) + { + case CHORD_I: + UpdateChord(root_midi_note, MAJOR); + break; + + case CHORD_V: + UpdateChord((root_midi_note + 7), MAJOR); + break; + + case CHORD_VI_M: + UpdateChord((root_midi_note + 9), MINOR); + break; + + case CHORD_IV: + UpdateChord((root_midi_note + 5), MAJOR); + break; + + default: + break; + } +} + +static int getButtonDir(void) +{ + bool button_1_state = pod.button1.RisingEdge(); + bool button_2_state = pod.button2.RisingEdge(); + int button_dir = 0; + + if (button_1_state && !button_2_state) + { + button_dir = -1; + } + else if (!button_1_state && button_2_state) + { + button_dir = 1; + } + else + { + button_dir = 0; + } + + return button_dir; +} + +int main(void) +{ + // Initialize pod hardware and oscillator daisysp module + float sample_rate; + + pod.Init(); + sample_rate = pod.AudioSampleRate(); + + InitChordNcos(s_key_root_note, (uint32_t)sample_rate); + + // Start audio callback + pod.StartAudio(AudioCallback); + + + while(1) + ; // Infinite Loop +} diff --git a/user/NCO/harmonicNco.cpp b/user/NCO/harmonicNco.cpp new file mode 100644 index 000000000..76e6933f1 --- /dev/null +++ b/user/NCO/harmonicNco.cpp @@ -0,0 +1,82 @@ +#include "harmonicNco.h" + +// HarmonicNCO default constructor +HarmonicNCO::HarmonicNCO() +{ + uint8_t nHarmonic; + + for (nHarmonic = 0; nHarmonic < NUM_HARMONICS; nHarmonic++) + { + nco[nHarmonic].SetSampleRate(NCO::DEFAULT_FS); + amp[nHarmonic] = 1.0; + } +} + +HarmonicNCO::~HarmonicNCO() { + // TODO +} + +void HarmonicNCO::SetSampleRate(uint32_t sample_freq) +{ + uint8_t nHarmonic; + + for (nHarmonic = 0; nHarmonic < NUM_HARMONICS; nHarmonic++) + { + nco[nHarmonic].SetSampleRate(sample_freq); + } +} + +void HarmonicNCO::SetFrequency(float freq) +{ + uint8_t nHarmonic; + float harmonicFreq = 0.0; + + for (nHarmonic = 0; nHarmonic < NUM_HARMONICS; nHarmonic++) + { + harmonicFreq = (nHarmonic + 1) * freq; + nco[nHarmonic].SetFrequency(harmonicFreq); + } +} + +void HarmonicNCO::SetAmplitudes(float *amplitudes) +{ + uint8_t nHarmonic; + + if (amplitudes) + { + for (nHarmonic = 0; nHarmonic < NUM_HARMONICS; nHarmonic++) + { + amp[nHarmonic] = amplitudes[nHarmonic]; + } + } +} + +void HarmonicNCO::SetPhases(float *phases) +{ + uint8_t nHarmonic; + + if (phases) + { + for (nHarmonic = 0; nHarmonic < NUM_HARMONICS; nHarmonic++) + { + nco[nHarmonic].SetPhase(phases[nHarmonic]); + } + } +} + +float HarmonicNCO::NextSample() +{ + uint8_t nHarmonic; + float sample = 0.0; + float ampTotal = 0.0; + + for (nHarmonic = 0; nHarmonic < NUM_HARMONICS; nHarmonic++) + { + sample += (amp[nHarmonic] * nco[nHarmonic].NextSample()); + ampTotal += amp[nHarmonic]; + } + // Normalize amplitude of sample + sample /= ampTotal; + + return sample; +} diff --git a/user/NCO/harmonicNco.h b/user/NCO/harmonicNco.h new file mode 100644 index 000000000..94e64f2c3 --- /dev/null +++ b/user/NCO/harmonicNco.h @@ -0,0 +1,33 @@ +// harmonicNco.h +#pragma once +#ifndef HARMONIC_NCO_H +#define HARMONIC_NCO_H + +#include +#include +#include +#include "daisysp.h" +#include "nco.h" + +using namespace std; + +class HarmonicNCO +{ + public: + static const uint8_t NUM_HARMONICS = 8; + + HarmonicNCO(); + ~HarmonicNCO(); + + void SetSampleRate(uint32_t sample_freq); + void SetFrequency(float freq); + void SetAmplitudes(float *amplitudes); + void SetPhases(float *phases); + float NextSample(); + + private: + NCO nco[NUM_HARMONICS]; // The NCO for each harmonic + float amp[NUM_HARMONICS]; // The amplitude for each harmonic +}; + +#endif diff --git a/user/NCO/nco.cpp b/user/NCO/nco.cpp new file mode 100644 index 000000000..f4f9ad6c2 --- /dev/null +++ b/user/NCO/nco.cpp @@ -0,0 +1,82 @@ +#include "nco.h" + +// Define/initialize NCO's static members +float NCO::sine_table[NCO_LUT_SIZE]; +bool NCO::sine_table_init = false; + +// NCO Default Constructor +NCO::NCO() +{ + // Set sampling frequency to default + SetSampleRate(DEFAULT_FS); + + phase_accum = 0; + k_val = 0; + + if (!sine_table_init) + { + InitSineTable(); + } +} + +NCO::NCO(uint32_t sample_freq) +{ + // Set sampling frequency + SetSampleRate(sample_freq); + + phase_accum = 0; + k_val = 0; + + if (!sine_table_init) + { + InitSineTable(); + } +} + +NCO::~NCO() +{ + // TODO +} + +void NCO::SetSampleRate(uint32_t sample_freq) +{ + sample_rate = (sample_freq > 0) ? sample_freq : DEFAULT_FS; +} + +void NCO::SetFrequency(float freq) +{ + k_val = (uint16_t)((freq / sample_rate) * (1 << N_PHASE_ACCUM_BITS)); +} + +void NCO::SetPhase(float phase) +{ + // Limit phase param between 0.0 - 1.0 + if (phase >= 0.0 && phase < 1.0) + { + phase_accum = (uint16_t)(phase * (1 << N_PHASE_ACCUM_BITS)); + } +} + +float NCO::NextSample() +{ + uint16_t lut_ndx = 0; + + // Increment phase accumulator + phase_accum += k_val; + // Use top 10 bits of phase_accum as LUT index + lut_ndx = phase_accum >> (N_PHASE_ACCUM_BITS - N_PHASE_BITS); + lut_ndx &= NCO_LUT_MASK; + // Return the new LUT sample + return sine_table[lut_ndx]; +} + +void NCO::InitSineTable() +{ + int ndx; + + for (ndx = 0; ndx < NCO_LUT_SIZE; ndx++) + { + sine_table[ndx] = sin(2 * M_PI * ndx / (float)NCO_LUT_SIZE); + } + sine_table_init = true; +} diff --git a/user/NCO/nco.h b/user/NCO/nco.h new file mode 100644 index 000000000..20d94b292 --- /dev/null +++ b/user/NCO/nco.h @@ -0,0 +1,42 @@ +// nco.h +#pragma once +#ifndef NCO_H +#define NCO_H + +#include +#include +#include +#include "daisysp.h" + +using namespace std; + +class NCO +{ + public: + static const uint8_t N_PHASE_BITS = 10; + static const uint8_t N_PHASE_ACCUM_BITS = 16; + static const uint16_t NCO_LUT_SIZE = (1 << N_PHASE_BITS); + static const uint16_t NCO_LUT_MASK = (NCO_LUT_SIZE - 1); + static const uint32_t DEFAULT_FS = 10000; + + NCO(); + NCO(uint32_t sample_freq); + ~NCO(); + + void SetSampleRate(uint32_t sample_freq); + void SetFrequency(float freq); + void SetPhase(float phase); + float NextSample(); + + private: + // Actual LUT for sampled sine wave - shared between all instances + static float sine_table[]; + static bool sine_table_init; + uint32_t sample_rate; + uint16_t phase_accum; + uint16_t k_val; + + void InitSineTable(); +}; + +#endif