Compare commits

..

No commits in common. "9a63041a5e0ff716aab2bff8617b8b663e158d80" and "518ba285b66a61d2fd25092b4cb6bdd10c0431f6" have entirely different histories.

10 changed files with 49 additions and 308 deletions

View File

@ -8,7 +8,6 @@ add_subdirectory(external/magic_enum)
add_subdirectory(external/argparse)
add_subdirectory(external/log4cxx)
file(GLOB_RECURSE sources src/*.cpp src/*.h)
file(GLOB_RECURSE data resources/*)

View File

@ -1,13 +0,0 @@
#! /bin/sh
set -eux
input_file="$1"
output_file="$2"
tempfile=$(mktemp /tmp/stereoXXXXXX.wav)
# Two step process:
# 1. Convert s16 PCM to a stereo wav file
# 2. Convert stereo wav file to mono
sox -r 44100 -e signed-integer -b 16 -c 2 ${input_file} ${tempfile}
sox ${tempfile} -c 1 ${output_file}

View File

@ -1,6 +1,6 @@
#! /usr/bin/env python3
from tones import SQUARE_WAVE, SINE_WAVE
from tones import SQUARE_WAVE
from tones.mixer import Mixer
import csv
import math
@ -14,8 +14,7 @@ def frequency_to_midi(frequency):
f = 440 * 2 ^ (n - 69)/12
'''
note = 12 * (math.log(frequency/220)/math.log(2)) + 57
#return round(note)
return note
return round(note)
tones = {}
with open('tones.csv', newline='') as csvfile:
@ -27,27 +26,6 @@ for tone in tones:
print(f"{tone} is MIDI #{frequency_to_midi(tones[tone])}")
mixer = Mixer(44100, 1)
mixer.create_track(0, SINE_WAVE)
mixer.create_track(0, SQUARE_WAVE)
mixer.add_tone(0, frequency=tones[tone], duration=1.0)
mixer.write_wav(f'samples/{tone}.wav')
mixer = Mixer(44100, 1)
mixer.create_track(0, SQUARE_WAVE)
mixer.add_tone(0, frequency=440, duration=1.0)
mixer.write_wav(f'samples/MIDI-69-square.wav')
mixer = Mixer(44100, 1)
mixer.create_track(0, SINE_WAVE)
mixer.add_tone(0, frequency=440, duration=1.0)
mixer.write_wav(f'samples/MIDI-69-sine.wav')
mixer = Mixer(44100, 1)
mixer.create_track(0, SQUARE_WAVE)
mixer.add_tone(0, frequency=261.6256, duration=1.0)
mixer.write_wav(f'samples/MIDI-60-square.wav')
mixer = Mixer(44100, 1)
mixer.create_track(0, SINE_WAVE)
mixer.add_tone(0, frequency=261.6256, duration=1.0)
mixer.write_wav(f'samples/MIDI-60-sine.wav')

View File

@ -1,124 +0,0 @@
#! /usr/bin/env python3
import csv
import sys
import numpy as np
from scipy import signal
from scipy.io import wavfile
from scipy.signal import butter, lfilter
tones = {}
with open('tones.csv', newline='') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
tones[row['designator']] = float(row['frequency'])
'''
def freq_of_key(midi_key):
return 440.0 * (2 ** ((midi_key - 69)/12))
tones = {}
for c in range(65, 90):
tones[c] = freq_of_key(c)
'''
# Shamelessly lifted from
# https://scipy.github.io/old-wiki/pages/Cookbook/ButterworthBandpass
def butter_bandpass(lowcut, highcut, fs, order=5):
nyq = 0.5 * fs
low = lowcut / nyq
high = highcut / nyq
b, a = butter(order, [low, high], btype='band')
return b, a
def butter_bandpass_filter(data, lowcut, highcut, fs, order=5):
b, a = butter_bandpass(lowcut, highcut, fs, order=order)
y = lfilter(b, a, data)
return y
# tone synthesis
def note(freq, cycles, amp=32767.0, rate=44100):
len = cycles * (1.0/rate)
t = np.linspace(0, len, int(len * rate))
if freq == 0:
data = np.zeros(int(len * rate))
else:
data = np.sin(2 * np.pi * freq * t) * amp
return data.astype(int)
def decimate_from_sample_rate(sample_rate):
if sample_rate == 44100:
return 4 # rate = 11025, Fmax = 5512.5 Hz
elif sample_rate == 48000:
return 5 # rate = 9600, Fmax = 4800 Hz
elif sample_rate == 22050:
return 2 # rate = 11025, Fmax = 5512.5 Hz
elif sample_rate == 11025:
return 1 # rate = 11025, Fmax = 5512.5 Hz
else:
raise ValueError("Sample rate not supported")
if __name__ == '__main__':
# TODO JMT: What is this?
FLT_LEN = 2000
file_name = sys.argv[1]
sample_rate, data = wavfile.read(file_name)
print(f"{file_name}: {len(data)} samples @ {sample_rate} Hz")
decimate = decimate_from_sample_rate(sample_rate)
if decimate > 1:
data = signal.decimate(data, decimate)
sample_rate = sample_rate / decimate
print(f'Length after decimation: {len(data)} samples')
data = butter_bandpass_filter(data, 270, 1700, sample_rate, order=8)
pure_signals = {tone:note(freq, FLT_LEN, rate=sample_rate) for tone,freq in tones.items()}
correlations = {tone:np.abs(signal.correlate(data, pure, mode='same')) for tone,pure in pure_signals.items()}
N = FLT_LEN # Rolling average length
cumsum_convolution = np.ones(N)/N
massaged = {tone:np.convolve(correlation, cumsum_convolution, mode='valid') for tone,correlation in correlations.items()}
# Only import if we're actually plotting, these imports are pretty heavy.
import pyqtgraph as pg
from PyQt5 import QtWidgets
app = QtWidgets.QApplication([])
layout = pg.GraphicsLayoutWidget(show=True, title="SELCAL Tone Correlation")
layout.setGeometry(0, 0, 1600, 960)
plot = layout.addPlot(title=file_name)
legend_view = layout.addViewBox()
legend = pg.LegendItem(offset=(0, 0))
legend.setParentItem(legend_view)
color_map = pg.colormap.get('CET-C6s')
colors = color_map.getLookupTable(nPts=len(tones))
for (tone, correlation), color in zip(massaged.items(), colors):
line = plot.plot(correlation, pen=pg.mkPen(color=color), fillLevel=0.1, name=tone)
legend.addItem(line, tone)
y_max = max(line.getData()[1]) # Maximum y-value
x_max = line.getData()[0][np.argmax(line.getData()[1])] # Corresponding x-coordinate
label = pg.TextItem(html=f'<div style="text-align: center"><span style="color: #FFFFFF; font-size: 12pt;">{line.opts["name"]}</span></div>', anchor=(0.5, 0.5))
plot.addItem(label)
label.setPos(x_max, y_max)
label.setZValue(100) # Ensure label is above other items
plot.setLabel('left', 'Signal Correlation')
plot.setLabel('bottom', 'Time (samples)')
plot.showGrid(x=True, y=True)
legend_view.setFixedWidth(80)
layout.ci.layout.setColumnFixedWidth(1, 80)
app.exec_()

Binary file not shown.

Binary file not shown.

View File

@ -7,12 +7,10 @@
#include <argparse/argparse.hpp>
#include <log4cxx/logger.h>
#include <log4cxx/basicconfigurator.h>
#include <sstream>
#include <thread>
#include <chrono>
namespace {
// TODO JMT: Does fluidsynth define this itself?
constexpr int ALL_CHANNELS = -1;
constexpr double DEFAULT_TONE_DURATION = 1.0;
@ -26,27 +24,24 @@ static auto logger = log4cxx::Logger::getLogger("tone-generator");
* to exactly play the the SELCAL tones. Worse again, using the nearest standard MIDI note available
* results in collisions for SELCAL tones.
* The way around this is to define a custom MIDI tuning. We're still limited to 128 frequencies,
* but we can make those frequencies anything we want.
* but we can make those frequencies anything we want. This method generates a tuning in the format
* that fluidsynth accepts which _only_ contains the SELCAL tones.
*/
TuningFrequencies getSelcalFrequencies() {
TuningFrequencies tunings;
tunings.fill(0.0);
Tuning getSelcalTunings() {
Tuning tunings;
tunings.fill(0);
for (auto pair : SELCAL::KeyFrequencies) {
// fluidsynth wants tunings in MIDI cents, which effectively means MIDI key * 100.
// As we only care about the SELCAL 32 tones, only tune those specific keys. We don't
// need to tune the full 128-key keyboard.
std::stringstream ss;
for (auto pair : SELCAL::KeyFrequencies) {
auto value = static_cast<int>(pair.first);
tunings[value] = pair.second;
ss << value << ":" << pair.second << ", ";
constexpr double MIDI_CENT_SCALE = 100.0;
const int keyIndex = static_cast<int>(pair.first);
tunings[keyIndex] = frequencyToMidi(pair.second) * MIDI_CENT_SCALE;
}
LOG4CXX_DEBUG(logger, "SELCAL freqs {" << ss.str() << "}");
return tunings;
}
} // anonymous
int main(int argc, char** argv) {
@ -94,13 +89,11 @@ int main(int argc, char** argv) {
return 1;
}
// Handle fluidsynth log messages before touching anything else in the lib
redirect_fluid_logs();
Synth synth;
auto tuning = getSelcalFrequencies();
if (!synth.setTuning(tuning)) {
auto tunings = getSelcalTunings();
if (!synth.setTuning(tunings)) {
LOG4CXX_ERROR(logger, "Failed to set SELCAL tuning on synth");
return 1;
}
@ -134,11 +127,9 @@ int main(int argc, char** argv) {
SELCAL::Code code{parser.get<std::string>("code")};
for (auto& group : code.getGroups()) {
LOG4CXX_INFO(logger, "Playing MIDI " << static_cast<int>(group[0]) << " & " << static_cast<int>(group[1]));
fluid_synth_noteon(raw_synth, 0, static_cast<int>(group[0]), 80);
fluid_synth_noteon(raw_synth, 1, static_cast<int>(group[1]), 80);
fluid_synth_noteon(raw_synth, 0, static_cast<int>(group[0]), 127);
fluid_synth_noteon(raw_synth, 1, static_cast<int>(group[1]), 127);
std::this_thread::sleep_for(std::chrono::duration<double>(toneDuration));
// Silence between tone groups
@ -148,5 +139,5 @@ int main(int argc, char** argv) {
// Wait long enough for the full tone to play before tearing down.
using namespace std::chrono_literals;
//std::this_thread::sleep_for(500ms);
std::this_thread::sleep_for(500ms);
}

View File

@ -1,26 +1,11 @@
#pragma once
#include <cmath>
#include <algorithm>
// Applicable for standard (even temperament) MIDI tuning only.
// The formula connecting the MIDI note number and the base frequency assume equal tuning based
// on A4 = 440 Hz.
constexpr int NUM_MIDI_KEYS = 128; // Also inclusive of 0 & 128, apparently.
constexpr double A4_FREQUENCY = 440.0; // Hz
constexpr int A4_MIDI_KEY = 69; // Hehe
constexpr int KEYS_PER_OCTAVE = 12;
// Template parameter allows for (non-standard) fractional keys.
template <typename T>
constexpr double MidiKeyToFrequency(T key) {
return A4_FREQUENCY * std::pow(2, (key - A4_MIDI_KEY) / KEYS_PER_OCTAVE);
double frequencyToMidi(double f) {
return ((12 * std::log(f / 220.0) / std::log(2.0)) + 57.01);
}
constexpr double frequencyToMidiKey(double f) {
return ((KEYS_PER_OCTAVE * std::log2(f / A4_FREQUENCY)) + A4_MIDI_KEY);
}
constexpr int frequencyToNearestMidiKey(double f) {
return std::clamp(static_cast<int>(std::round(frequencyToMidiKey(f))), 0, NUM_MIDI_KEYS);
int frequencyToNearestMidi(double f) {
return (int) frequencyToMidi(f);
}

View File

@ -1,18 +1,6 @@
#include "synth.h"
#include <stdexcept>
#include <sstream>
#include <log4cxx/logger.h>
namespace {
// Helpers for the C API stuff
constexpr int TRUE = 1;
constexpr int FALSE = 0;
static auto logger = log4cxx::Logger::getLogger("synth");
static auto fluidLogger = log4cxx::Logger::getLogger("synth.fluid");
}
Synth::Synth() {
try {
@ -26,15 +14,6 @@ Synth::Synth() {
throw new std::runtime_error("Unable to create fluid synth");
}
fluid_settings_setstr(settings, "audio.driver", "file");
fluid_settings_setstr(settings, "audio.file.name", "output.raw");
fluid_settings_setstr(settings, "audio.file.format", "s16");
fluid_settings_setstr(settings, "audio.file.type", "raw");
fluid_settings_setnum(settings, "synth.sample-rate", 44100);
fluid_settings_setnum(settings, "synth.gain", 1.0);
//fluid_settings_setint(settings, "synth.chorus.active", 0);
//fluid_settings_setint(settings, "synth.reverb.active", 0);
adriver = new_fluid_audio_driver(settings, synth);
if (adriver == nullptr) {
throw new std::runtime_error("Unable to create audio driver");
@ -90,35 +69,33 @@ bool Synth::loadProgram(const Program& program) {
return true;
}
bool Synth::setTuning(const TuningFrequencies& frequencies) {
// fluidsynth wants tunings in MIDI cents, which effectively means MIDI key * 100.
// For example: normally note 0 is 0.0, 1 is 100.0, 60 is 6000.0, etc.
constexpr double MIDI_CENT_SCALE = 100.0;
std::array<double, NUM_MIDI_KEYS> pitchCents;
std::transform(
frequencies.begin(),
frequencies.end(),
pitchCents.begin(),
[] (double frequency) {
// Remap 0 Hz to MIDI 0, otherwise we get -inf which causes some interpolations between
// notes to result in zero sound.
return frequency == 0.0 ? 0.0 : frequencyToMidiKey(frequency) * MIDI_CENT_SCALE;
}
bool Synth::setTuning(const std::array<double, NUM_MIDI_NOTES>& pitches) {
int result = fluid_synth_activate_key_tuning(
synth,
0, 0,
"default",
pitches.data(),
1
);
std::stringstream ss;
for (auto p : pitchCents) {
ss << p << ", ";
if (result == FLUID_OK) {
result = fluid_synth_activate_tuning(synth, 0, 0, 0, 1);
result = fluid_synth_activate_tuning(synth, 1, 0, 0, 1);
}
LOG4CXX_DEBUG(logger, "Tuning pitches [" << ss.str() << "]");
auto result = fluid_synth_activate_key_tuning(synth, 0, 0, "default", pitchCents.data(), FALSE);
return (result == FLUID_OK) ? activateTuning() : false;
return result == FLUID_OK;
}
bool Synth::resetTuning() {
auto result = fluid_synth_activate_key_tuning(synth, 0, 0, "default", nullptr, 1);
return (result == FLUID_OK) ? activateTuning() : false;
int result = fluid_synth_activate_key_tuning(
synth,
0, 0,
"default",
nullptr,
1
);
return result == FLUID_OK;
}
const std::vector<Synth::Program>& Synth::getPrograms() const {
@ -128,46 +105,3 @@ const std::vector<Synth::Program>& Synth::getPrograms() const {
fluid_synth_t* Synth::getSynth() {
return synth;
}
bool Synth::activateTuning() {
int channelIndex = 0;
int result;
do {
result = fluid_synth_activate_tuning(synth, channelIndex++, 0, 0, TRUE);
} while (result == FLUID_OK && channelIndex < NUM_MIDI_CHANNELS);
LOG4CXX_DEBUG(logger, "Tuning activation result: " << result);
return result == FLUID_OK;
}
// Fluid logging redirector.
void fluid_log_function(int level, const char* message, void* data) {
switch (level) {
case FLUID_PANIC:
LOG4CXX_FATAL(fluidLogger, message);
break;
case FLUID_ERR:
LOG4CXX_ERROR(fluidLogger, message);
break;
case FLUID_WARN:
LOG4CXX_WARN(fluidLogger, message);
break;
case FLUID_INFO:
LOG4CXX_INFO(fluidLogger, message);
break;
case FLUID_DBG:
LOG4CXX_DEBUG(fluidLogger, message);
break;
}
}
void redirect_fluid_logs() {
fluid_set_log_function(FLUID_PANIC, fluid_log_function, NULL);
fluid_set_log_function(FLUID_ERR, fluid_log_function, NULL);
fluid_set_log_function(FLUID_WARN, fluid_log_function, NULL);
fluid_set_log_function(FLUID_INFO, fluid_log_function, NULL);
fluid_set_log_function(FLUID_DBG, fluid_log_function, NULL);
}

View File

@ -5,13 +5,9 @@
#include <string>
#include <array>
#include "midi.h"
constexpr int NUM_MIDI_NOTES = 128;
namespace {
constexpr int NUM_MIDI_CHANNELS = 2;
}
typedef std::array<double, NUM_MIDI_KEYS> TuningFrequencies;
typedef std::array<double, NUM_MIDI_NOTES> Tuning;
/**
* C++ wrapper around the fluidsynth API.
@ -36,7 +32,7 @@ public:
bool loadSoundfont(const std::string& filepath);
bool loadProgram(const Program& program);
bool setTuning(const TuningFrequencies& frequencies);
bool setTuning(const Tuning& pitches);
bool resetTuning();
const std::vector<Program>& getPrograms() const;
@ -45,7 +41,7 @@ public:
fluid_synth_t* getSynth();
private:
bool activateTuning();
std::vector<Soundfont> soundfonts;
std::vector<Program> programs;
@ -54,8 +50,3 @@ private:
fluid_synth_t* synth = NULL;
fluid_audio_driver_t* adriver = NULL;
};
// Fluid logging redirector.
void fluid_log_function(int level, const char* message, void* data);
void redirect_fluid_logs();