Compare commits
4 Commits
518ba285b6
...
9a63041a5e
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a63041a5e | |||
| ec52422955 | |||
| 5d18b8636c | |||
| d65b9c1fd4 |
@ -8,6 +8,7 @@ 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/*)
|
||||
|
||||
|
||||
13
scripts/raw-to-wav.sh
Executable file
13
scripts/raw-to-wav.sh
Executable file
@ -0,0 +1,13 @@
|
||||
#! /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}
|
||||
@ -1,6 +1,6 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
from tones import SQUARE_WAVE
|
||||
from tones import SQUARE_WAVE, SINE_WAVE
|
||||
from tones.mixer import Mixer
|
||||
import csv
|
||||
import math
|
||||
@ -14,7 +14,8 @@ 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 round(note)
|
||||
return note
|
||||
|
||||
tones = {}
|
||||
with open('tones.csv', newline='') as csvfile:
|
||||
@ -26,6 +27,27 @@ for tone in tones:
|
||||
print(f"{tone} is MIDI #{frequency_to_midi(tones[tone])}")
|
||||
|
||||
mixer = Mixer(44100, 1)
|
||||
mixer.create_track(0, SQUARE_WAVE)
|
||||
mixer.create_track(0, SINE_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')
|
||||
124
scripts/selcal-detect.py
Executable file
124
scripts/selcal-detect.py
Executable file
@ -0,0 +1,124 @@
|
||||
#! /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_()
|
||||
BIN
soundfonts/basic.sf2
Normal file
BIN
soundfonts/basic.sf2
Normal file
Binary file not shown.
Binary file not shown.
43
src/main.cpp
43
src/main.cpp
@ -7,10 +7,12 @@
|
||||
#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;
|
||||
@ -24,24 +26,27 @@ 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. This method generates a tuning in the format
|
||||
* that fluidsynth accepts which _only_ contains the SELCAL tones.
|
||||
* but we can make those frequencies anything we want.
|
||||
*/
|
||||
Tuning getSelcalTunings() {
|
||||
Tuning tunings;
|
||||
tunings.fill(0);
|
||||
TuningFrequencies getSelcalFrequencies() {
|
||||
TuningFrequencies tunings;
|
||||
tunings.fill(0.0);
|
||||
|
||||
// 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) {
|
||||
// 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.
|
||||
constexpr double MIDI_CENT_SCALE = 100.0;
|
||||
const int keyIndex = static_cast<int>(pair.first);
|
||||
tunings[keyIndex] = frequencyToMidi(pair.second) * MIDI_CENT_SCALE;
|
||||
auto value = static_cast<int>(pair.first);
|
||||
tunings[value] = pair.second;
|
||||
ss << value << ":" << pair.second << ", ";
|
||||
}
|
||||
|
||||
LOG4CXX_DEBUG(logger, "SELCAL freqs {" << ss.str() << "}");
|
||||
|
||||
return tunings;
|
||||
}
|
||||
} // anonymous
|
||||
|
||||
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
@ -89,11 +94,13 @@ 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 tunings = getSelcalTunings();
|
||||
if (!synth.setTuning(tunings)) {
|
||||
|
||||
auto tuning = getSelcalFrequencies();
|
||||
if (!synth.setTuning(tuning)) {
|
||||
LOG4CXX_ERROR(logger, "Failed to set SELCAL tuning on synth");
|
||||
return 1;
|
||||
}
|
||||
@ -127,9 +134,11 @@ int main(int argc, char** argv) {
|
||||
|
||||
SELCAL::Code code{parser.get<std::string>("code")};
|
||||
|
||||
|
||||
for (auto& group : code.getGroups()) {
|
||||
fluid_synth_noteon(raw_synth, 0, static_cast<int>(group[0]), 127);
|
||||
fluid_synth_noteon(raw_synth, 1, static_cast<int>(group[1]), 127);
|
||||
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);
|
||||
std::this_thread::sleep_for(std::chrono::duration<double>(toneDuration));
|
||||
|
||||
// Silence between tone groups
|
||||
@ -139,5 +148,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);
|
||||
}
|
||||
|
||||
23
src/midi.h
23
src/midi.h
@ -1,11 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
double frequencyToMidi(double f) {
|
||||
return ((12 * std::log(f / 220.0) / std::log(2.0)) + 57.01);
|
||||
// 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);
|
||||
}
|
||||
|
||||
int frequencyToNearestMidi(double f) {
|
||||
return (int) frequencyToMidi(f);
|
||||
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);
|
||||
}
|
||||
|
||||
106
src/synth.cpp
106
src/synth.cpp
@ -1,6 +1,18 @@
|
||||
#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 {
|
||||
@ -14,6 +26,15 @@ 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");
|
||||
@ -69,33 +90,35 @@ bool Synth::loadProgram(const Program& program) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
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;
|
||||
}
|
||||
);
|
||||
|
||||
if (result == FLUID_OK) {
|
||||
result = fluid_synth_activate_tuning(synth, 0, 0, 0, 1);
|
||||
result = fluid_synth_activate_tuning(synth, 1, 0, 0, 1);
|
||||
std::stringstream ss;
|
||||
for (auto p : pitchCents) {
|
||||
ss << p << ", ";
|
||||
}
|
||||
LOG4CXX_DEBUG(logger, "Tuning pitches [" << ss.str() << "]");
|
||||
|
||||
return result == FLUID_OK;
|
||||
auto result = fluid_synth_activate_key_tuning(synth, 0, 0, "default", pitchCents.data(), FALSE);
|
||||
return (result == FLUID_OK) ? activateTuning() : false;
|
||||
}
|
||||
|
||||
bool Synth::resetTuning() {
|
||||
int result = fluid_synth_activate_key_tuning(
|
||||
synth,
|
||||
0, 0,
|
||||
"default",
|
||||
nullptr,
|
||||
1
|
||||
);
|
||||
|
||||
return result == FLUID_OK;
|
||||
auto result = fluid_synth_activate_key_tuning(synth, 0, 0, "default", nullptr, 1);
|
||||
return (result == FLUID_OK) ? activateTuning() : false;
|
||||
}
|
||||
|
||||
const std::vector<Synth::Program>& Synth::getPrograms() const {
|
||||
@ -105,3 +128,46 @@ 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);
|
||||
}
|
||||
17
src/synth.h
17
src/synth.h
@ -5,9 +5,13 @@
|
||||
#include <string>
|
||||
#include <array>
|
||||
|
||||
constexpr int NUM_MIDI_NOTES = 128;
|
||||
#include "midi.h"
|
||||
|
||||
typedef std::array<double, NUM_MIDI_NOTES> Tuning;
|
||||
namespace {
|
||||
constexpr int NUM_MIDI_CHANNELS = 2;
|
||||
}
|
||||
|
||||
typedef std::array<double, NUM_MIDI_KEYS> TuningFrequencies;
|
||||
|
||||
/**
|
||||
* C++ wrapper around the fluidsynth API.
|
||||
@ -32,7 +36,7 @@ public:
|
||||
bool loadSoundfont(const std::string& filepath);
|
||||
bool loadProgram(const Program& program);
|
||||
|
||||
bool setTuning(const Tuning& pitches);
|
||||
bool setTuning(const TuningFrequencies& frequencies);
|
||||
bool resetTuning();
|
||||
|
||||
const std::vector<Program>& getPrograms() const;
|
||||
@ -41,7 +45,7 @@ public:
|
||||
fluid_synth_t* getSynth();
|
||||
|
||||
private:
|
||||
|
||||
bool activateTuning();
|
||||
|
||||
std::vector<Soundfont> soundfonts;
|
||||
std::vector<Program> programs;
|
||||
@ -50,3 +54,8 @@ 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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user