Initial commit.

master
johnl 2017-07-22 11:04:10 +02:00
parent 2bc375a5cb
commit b94eb57717
34 changed files with 2774 additions and 0 deletions

46
Makefile Normal file
View File

@ -0,0 +1,46 @@
#CC = gcc -O0 -march=native -Wall -pedantic -std=gnu11 -DJL_LEAK_CHECK \
-Warray-bounds
CC = gcc -Wall -std=gnu11 -march=native -Ofast -flto -fopenmp -pthread
LIBS = sndfile jack alsa fftw3
PKGCONFIG = $(shell which pkg-config)
CFLAGS = $(shell $(PKGCONFIG) --cflags $(LIBS))
LDFLAGS = $(shell $(PKGCONFIG) --libs $(LIBS)) \
-Wall -march=native -Ofast -lm -lpthread
SRC = sampler.c sound.c soundconfig.c sample.c soundfile.c fx.c \
parse.c jl_ringbuf.c jl_mem.c ini.c init.c main.c
OBJS = $(SRC:.c=.o)
all: jlsampler
%.o: %.c
$(CC) -c -o $(@F) $(CFLAGS) $<
jlsampler: $(OBJS)
$(CC) -o $(@F) $(LDFLAGS) $(OBJS)
SAMPLE_TEST_SRC = sample_test.c sample.c jl_mem.c
SAMPLE_TEST_OBJS = $(SAMPLE_TEST_SRC:.c=.o)
sample_test: $(SAMPLE_TEST_SRC) $(SAMPLE_TEST_OBJS)
$(CC) -o $(@F) $(LDFLAGS) $(SAMPLE_TEST_OBJS)
./sample_test
SOUND_TEST_SRC = sound_test.c sound.c sample.c ini.c jl_mem.c
SOUND_TEST_OBJS = $(SOUND_TEST_SRC:.c=.o)
sound_test: $(SOUND_TEST_SRC) $(SOUND_TEST_OBJS)
$(CC) -o $(@F) $(LDFLAGS) $(SOUND_TEST_OBJS)
./sound_test
test: sample_test sound_test
clean:
rm -f jlsampler $(OBJS) \
$(SAMPLE_TEST_OBJS) sample_test \
$(SOUND_TEST_OBJS) sound_test

View File

@ -1,2 +1,8 @@
# jlsampler
TODO:
* In-memory storage of samples as int16 type.
* Tuning via tuning file
* Layer mixing
* Fake RC Layer for single layered sounds

23
const.h Normal file
View File

@ -0,0 +1,23 @@
#ifndef const_HEADER_
#define const_HEADER_
#include <stdbool.h>
// Global controls.
bool gl_sustain;
#define MIDI_EVT_NOTE 0
#define MIDI_EVT_CTRL 1
#define SAMPLE_RATE 48000
#define MAX_PLAYING 256
#define MAX_MIDI_CHANS 128
#define MAX_SOUNDS 128
#define MAX_LAYERS 128
#define MAX_VARS 128
#define MIN_AMP 1e-6
#define MAX_NAME 128
#define MAX_FX 8
#define MAX_FX_LINE 256
#endif

381
fx.c Normal file
View File

@ -0,0 +1,381 @@
#include <stdio.h>
#include <math.h>
#include <limits.h>
#include <complex.h>
#include <pthread.h>
#include <fftw3.h>
#include "jl_mem.h"
#include "soundfile.h"
#include "fx.h"
void fx_amp(Sample *s, double gain) {
for(int i = 0; i < s->len; ++i) {
s->L[i] *= gain;
s->R[i] *= gain;
}
}
void fx_balance(Sample *s, double balance) {
if(balance == 0) {
return;
}
if(balance < -1) {
balance = -1;
} else if(balance > 1) {
balance = 1;
}
if(balance < 0) {
for(int i = 0; i < s->len; ++i) {
s->L[i] *= 1 + balance;
}
} else {
for(int i = 0; i < s->len; ++i) {
s->R[i] *= 1 - balance;
}
}
}
void fx_divide_rms(Sample *s, double t_ms) {
int di = t_ms * SAMPLE_RATE / 1000.0;
if(di > s->len) {
di = s->len;
}
double rms = 0;
double x;
for(int i = 0; i < di; ++i) {
x = s->L[i];
rms += x*x;
x = s->R[i];
rms += x*x;
}
rms = sqrt(rms / (2.0 * di));
fx_amp(s, 1.0 / rms);
}
void fx_fade_in(Sample *s, double fade_ms) {
int n_fade = fade_ms * SAMPLE_RATE / 1000.0;
for(int i = 0; i < n_fade && i < s->len; ++i) {
double val = (double)i / (double)n_fade;
s->L[i] *= val;
s->R[i] *= val;
}
}
pthread_mutex_t fftw_mutex = PTHREAD_MUTEX_INITIALIZER;
static fftw_complex *fft(int fft_len, int in_len, float *in_data) {
double *in = jl_malloc_exit(fft_len * sizeof(double));
for(int i = 0; i < in_len; ++i) {
in[i] = in_data[i];
}
for(int i = in_len; i < fft_len; ++i) {
in[i] = 0;
}
fftw_complex *out = jl_malloc_exit(fft_len * sizeof(fftw_complex));
pthread_mutex_lock(&fftw_mutex);
fftw_plan p = fftw_plan_dft_r2c_1d(fft_len, in, out, FFTW_ESTIMATE);
pthread_mutex_unlock(&fftw_mutex);
fftw_execute(p);
pthread_mutex_lock(&fftw_mutex);
fftw_destroy_plan(p);
pthread_mutex_unlock(&fftw_mutex);
jl_free(in);
return out;
}
static float *rfft(int fft_len, fftw_complex *in, int out_len) {
double *out = jl_malloc_exit(fft_len * sizeof(double));
pthread_mutex_lock(&fftw_mutex);
fftw_plan p = fftw_plan_dft_c2r_1d(fft_len, in, out, FFTW_ESTIMATE);
pthread_mutex_unlock(&fftw_mutex);
fftw_execute(p);
pthread_mutex_lock(&fftw_mutex);
fftw_destroy_plan(p);
pthread_mutex_unlock(&fftw_mutex);
float *f_out = jl_malloc_exit(out_len * sizeof(float));
for(int i = 0; i < out_len; ++i) {
f_out[i] = out[i] / (float)fft_len;
}
jl_free(out);
return f_out;
}
void fx_ir(Sample *s, const char *path, double mix) {
if(mix <= 0) {
return;
}
Sample ir;
ir.len = soundfile_load_stereo(path, &ir.L, &ir.R);
int out_len = ir.len + s->len - 1;
// Using a power of 2 fft length makes the computations MUCH faster.
int fft_len = pow(2, ceil(log2(out_len)));
fftw_complex *ir_L = fft(fft_len, ir.len, ir.L);
fftw_complex *ir_R = fft(fft_len, ir.len, ir.R);
sample_free(&ir);
fftw_complex *L = fft(fft_len, s->len, s->L);
fftw_complex *R = fft(fft_len, s->len, s->R);
// Convolve.
for(int i = 0; i < fft_len; ++i) {
L[i] *= ir_L[i];
R[i] *= ir_R[i];
}
// Reverse fft.
float *L_wet = rfft(fft_len, L, out_len);
float *R_wet = rfft(fft_len, R, out_len);
if(mix < 1) {
for(int i = 0; i < out_len; ++i) {
L_wet[i] *= mix;
R_wet[i] *= mix;
}
for(int i = 0; i < s->len; ++i) {
L_wet[i] += (1 - mix) * s->L[i];
R_wet[i] += (1 - mix) * s->R[i];
}
}
jl_free(ir_L);
jl_free(ir_R);
jl_free(L);
jl_free(R);
sample_set_data(s, L_wet, R_wet, out_len);
}
void fx_mono(Sample *s) {
for(int i = 0; i < s->len; ++i) {
s->L[i] += s->R[i];
s->L[i] /= 2.0;
s->R[i] = s->L[i];
}
}
void fx_pad_multiple(Sample *s, int n) {
if(s->len % n == 0) {
return;
}
// Compute new length.
int len = s->len + n;
len -= len % n;
// Allocate arrays.
float *L = jl_malloc_exit(len * sizeof(float));
float *R = jl_malloc_exit(len * sizeof(float));
// Zero padding samples.
for(int i = s->len; i < len; ++i) {
L[i] = 0;
R[i] = 0;
}
// Copy data.
for(int i = 0; i < s->len; ++i) {
L[i] = s->L[i];
R[i] = s->R[i];
}
sample_set_data(s, L, R, len);
}
void fx_pan(Sample *s, double pan) {
if(pan < 0) {
for(int i = 0; i < s->len; ++i) {
s->L[i] += s->R[i] * -pan;
s->R[i] *= (1 + pan);
}
} else {
for(int i = 0; i < s->len; ++i) {
s->R[i] += s->L[i] * pan;
s->L[i] *= (1 - pan);
}
}
}
static float interp(float *v, double idx) {
int i = (int)idx;
double mu = idx - i;
return mu * v[i+1] + (1 - mu) * v[i];
}
void fx_playback_speed(Sample *s, double speed) {
int len = s->len / speed;
float *L = jl_malloc_exit(len * sizeof(float));
float *R = jl_malloc_exit(len * sizeof(float));
double idx = 0;
for(int i = 0; i < len; ++i) {
L[i] = interp(s->L, idx);
R[i] = interp(s->R, idx);
idx += speed;
}
sample_set_data(s, L, R, len);
}
void fx_pre_cut(Sample *s, double pct, double fade_ms) {
float th = pct / 100.0;
int i_cut = 0;
for(i_cut = 0; i_cut < s->len; ++i_cut) {
if(s->L[i_cut] > th ||
s->R[i_cut] > th ||
s->L[i_cut] < -th ||
s->R[i_cut] < -th) {
break;
}
}
if(i_cut == s->len) {
printf("Warning: pre-cut threshold not reached.\n");
fx_fade_in(s, fade_ms);
return;
}
i_cut -= fade_ms * SAMPLE_RATE / 1000.0;
if(i_cut <= 0) {
fx_fade_in(s, fade_ms);
return;
}
if(i_cut > SAMPLE_RATE * 5 / 1000) {
printf("Warning: cutting %i ms from sample.\n",
i_cut * 1000 / SAMPLE_RATE);
}
int len = s->len - i_cut;
// Allocate arrays.
float *L = jl_malloc_exit(len * sizeof(float));
float *R = jl_malloc_exit(len * sizeof(float));
for(int i = 0; i < len; ++i) {
L[i] = s->L[i_cut + i];
R[i] = s->R[i_cut + i];
}
sample_set_data(s, L, R, len);
fx_fade_in(s, fade_ms);
}
static float *delay_array(float *in, int in_len, int out_len, int delay) {
float *out = jl_malloc_exit(out_len * sizeof(float));
for(int i = 0; i < delay; ++i) {
out[i] = 0;
}
for(int i = 0; i < in_len; ++i) {
out[delay + i] = in[i];
}
for(int i = delay + in_len; i < out_len; ++i) {
out[i] = 0;
}
return out;
}
void fx_pre_delay(Sample *s, double left_ms, double right_ms) {
int di_L = left_ms * SAMPLE_RATE / 1000;
int di_R = right_ms * SAMPLE_RATE / 1000;
int out_len = s->len;
out_len += di_L > di_R ? di_L : di_R;
float *L = delay_array(s->L, s->len, out_len, di_L);
float *R = delay_array(s->R, s->len, out_len, di_R);
sample_set_data(s, L, R, out_len);
}
void fx_rc_highpass(Sample *s, double freq, int order) {
fx_rc_lowshelf(s, freq, order, -1);
}
void fx_rc_lowpass(Sample *s, double freq, int order) {
fx_rc_highshelf(s, freq, order, -1);
}
static double freq_3db(double freq, int order) {
return freq / sqrt(pow(2.0, 1.0 / ((double)order)) - 1.0);
}
static void rc_highshelf1(float *data, int len, double freq, double gain) {
double rc = 1.0 / (freq * 2.0 * M_PI);
double dt = 1.0 / SAMPLE_RATE;
double alpha = dt / (rc + dt);
double prev = 0;
for (int i = 0; i < len; ++i) {
prev *= (1 - alpha);
prev += alpha * data[i];
data[i] += gain * (data[i] - prev);
}
}
static void rc_lowshelf1(float *data, int len, double freq, double gain) {
double rc = 1.0 / (freq * 2.0 * M_PI);
double dt = 1.0 / SAMPLE_RATE;
double alpha = dt / (rc + dt);
double prev = 0;
for (int i = 0; i < len; ++i) {
prev *= (1 - alpha);
prev += alpha * data[i];
data[i] += gain * prev;
}
}
void fx_rc_highshelf(Sample *s, double freq, int order, double gain) {
freq = freq_3db(freq, order);
for(int i = 0; i < order; ++i) {
rc_highshelf1(s->L, s->len, freq, gain);
rc_highshelf1(s->R, s->len, freq, gain);
}
}
void fx_rc_lowshelf(Sample *s, double freq, int order, double gain) {
freq = freq_3db(freq, order);
for(int i = 0; i < order; ++i) {
rc_lowshelf1(s->L, s->len, freq, gain);
rc_lowshelf1(s->R, s->len, freq, gain);
}
}
// See http://musicdsp.org/showArchiveComment.php?ArchiveID=256
void fx_stereo_width(Sample *s, double width) {
float w2 = width * 0.5;
float m, p;
for(int i = 0; i < s->len; ++i) {
m = (s->L[i] + s->R[i]) * 0.5;
p = (s->R[i] - s->L[i]) * w2;
s->L[i] = m - p;
s->R[i] = m + p;
}
}

23
fx.h Normal file
View File

@ -0,0 +1,23 @@
#ifndef fx_HEADER_
#define fx_HEADER_
#include "sample.h"
void fx_amp(Sample *s, double gain);
void fx_balance(Sample *s, double balance);
void fx_divide_rms(Sample *s, double t_ms);
void fx_fade_in(Sample *s, double t_ms);
void fx_ir(Sample *s, const char *path, double mix);
void fx_mono(Sample *s);
void fx_pad_multiple(Sample *s, int n); // INTERNAL ONLY.
void fx_pan(Sample *s, double pan);
void fx_playback_speed(Sample *s, double speed);
void fx_pre_cut(Sample *s, double pct, double fade_ms);
void fx_pre_delay(Sample *s, double left_ms, double right_ms);
void fx_rc_highpass(Sample *s, double freq, int order);
void fx_rc_lowpass(Sample *s, double freq, int order);
void fx_rc_highshelf(Sample *s, double freq, int order, double gain);
void fx_rc_lowshelf(Sample *s, double freq, int order, double gain);
void fx_stereo_width(Sample *s, double width);
#endif

194
ini.c Normal file
View File

@ -0,0 +1,194 @@
/* inih -- simple .INI file parser
inih is released under the New BSD license (see LICENSE.txt). Go to the project
home page for more info:
https://github.com/benhoyt/inih
*/
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include "ini.h"
#if !INI_USE_STACK
#include <stdlib.h>
#endif
#define MAX_SECTION 50
#define MAX_NAME 50
/* Strip whitespace chars off end of given string, in place. Return s. */
static char* rstrip(char* s)
{
char* p = s + strlen(s);
while (p > s && isspace((unsigned char)(*--p)))
*p = '\0';
return s;
}
/* Return pointer to first non-whitespace char in given string. */
static char* lskip(const char* s)
{
while (*s && isspace((unsigned char)(*s)))
s++;
return (char*)s;
}
/* Return pointer to first char (of chars) or inline comment in given string,
or pointer to null at end of string if neither found. Inline comment must
be prefixed by a whitespace character to register as a comment. */
static char* find_chars_or_comment(const char* s, const char* chars)
{
#if INI_ALLOW_INLINE_COMMENTS
int was_space = 0;
while (*s && (!chars || !strchr(chars, *s)) &&
!(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
was_space = isspace((unsigned char)(*s));
s++;
}
#else
while (*s && (!chars || !strchr(chars, *s))) {
s++;
}
#endif
return (char*)s;
}
/* Version of strncpy that ensures dest (size bytes) is null-terminated. */
static char* strncpy0(char* dest, const char* src, size_t size)
{
strncpy(dest, src, size);
dest[size - 1] = '\0';
return dest;
}
/* See documentation in header file. */
int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
void* user)
{
/* Uses a fair bit of stack (use heap instead if you need to) */
#if INI_USE_STACK
char line[INI_MAX_LINE];
#else
char* line;
#endif
char section[MAX_SECTION] = "";
char prev_name[MAX_NAME] = "";
char* start;
char* end;
char* name;
char* value;
int lineno = 0;
int error = 0;
#if !INI_USE_STACK
line = (char*)malloc(INI_MAX_LINE);
if (!line) {
return -2;
}
#endif
/* Scan through stream line by line */
while (reader(line, INI_MAX_LINE, stream) != NULL) {
lineno++;
start = line;
#if INI_ALLOW_BOM
if (lineno == 1 && (unsigned char)start[0] == 0xEF &&
(unsigned char)start[1] == 0xBB &&
(unsigned char)start[2] == 0xBF) {
start += 3;
}
#endif
start = lskip(rstrip(start));
if (*start == ';' || *start == '#') {
/* Per Python configparser, allow both ; and # comments at the
start of a line */
}
#if INI_ALLOW_MULTILINE
else if (*prev_name && *start && start > line) {
/* Non-blank line with leading whitespace, treat as continuation
of previous name's value (as per Python configparser). */
if (!handler(user, section, prev_name, start) && !error)
error = lineno;
}
#endif
else if (*start == '[') {
/* A "[section]" line */
end = find_chars_or_comment(start + 1, "]");
if (*end == ']') {
*end = '\0';
strncpy0(section, start + 1, sizeof(section));
*prev_name = '\0';
}
else if (!error) {
/* No ']' found on section line */
error = lineno;
}
}
else if (*start) {
/* Not a comment, must be a name[=:]value pair */
end = find_chars_or_comment(start, "=:");
if (*end == '=' || *end == ':') {
*end = '\0';
name = rstrip(start);
value = lskip(end + 1);
#if INI_ALLOW_INLINE_COMMENTS
end = find_chars_or_comment(value, NULL);
if (*end)
*end = '\0';
#endif
rstrip(value);
/* Valid name[=:]value pair found, call handler */
strncpy0(prev_name, name, sizeof(prev_name));
if (!handler(user, section, name, value) && !error)
error = lineno;
}
else if (!error) {
/* No '=' or ':' found on name[=:]value line */
error = lineno;
}
}
#if INI_STOP_ON_FIRST_ERROR
if (error)
break;
#endif
}
#if !INI_USE_STACK
free(line);
#endif
return error;
}
/* See documentation in header file. */
int ini_parse_file(FILE* file, ini_handler handler, void* user)
{
return ini_parse_stream((ini_reader)fgets, file, handler, user);
}
/* See documentation in header file. */
int ini_parse(const char* filename, ini_handler handler, void* user)
{
FILE* file;
int error;
file = fopen(filename, "r");
if (!file)
return -1;
error = ini_parse_file(file, handler, user);
fclose(file);
return error;
}

93
ini.h Normal file
View File

@ -0,0 +1,93 @@
/* inih -- simple .INI file parser
inih is released under the New BSD license (see LICENSE.txt). Go to the project
home page for more info:
https://github.com/benhoyt/inih
*/
#ifndef __INI_H__
#define __INI_H__
/* Make this header file easier to include in C++ code */
#ifdef __cplusplus
extern "C" {
#endif
#include <stdio.h>
/* Typedef for prototype of handler function. */
typedef int (*ini_handler)(void* user, const char* section,
const char* name, const char* value);
/* Typedef for prototype of fgets-style reader function. */
typedef char* (*ini_reader)(char* str, int num, void* stream);
/* Parse given INI-style file. May have [section]s, name=value pairs
(whitespace stripped), and comments starting with ';' (semicolon). Section
is "" if name=value pair parsed before any section heading. name:value
pairs are also supported as a concession to Python's configparser.
For each name=value pair parsed, call handler function with given user
pointer as well as section, name, and value (data only valid for duration
of handler call). Handler should return nonzero on success, zero on error.
Returns 0 on success, line number of first error on parse error (doesn't
stop on first error), -1 on file open error, or -2 on memory allocation
error (only when INI_USE_STACK is zero).
*/
int ini_parse(const char* filename, ini_handler handler, void* user);
/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't
close the file when it's finished -- the caller must do that. */
int ini_parse_file(FILE* file, ini_handler handler, void* user);
/* Same as ini_parse(), but takes an ini_reader function pointer instead of
filename. Used for implementing custom or string-based I/O. */
int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
void* user);
/* Nonzero to allow multi-line value parsing, in the style of Python's
configparser. If allowed, ini_parse() will call the handler with the same
name for each subsequent line parsed. */
#ifndef INI_ALLOW_MULTILINE
#define INI_ALLOW_MULTILINE 1
#endif
/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of
the file. See http://code.google.com/p/inih/issues/detail?id=21 */
#ifndef INI_ALLOW_BOM
#define INI_ALLOW_BOM 1
#endif
/* Nonzero to allow inline comments (with valid inline comment characters
specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match
Python 3.2+ configparser behaviour. */
#ifndef INI_ALLOW_INLINE_COMMENTS
#define INI_ALLOW_INLINE_COMMENTS 1
#endif
#ifndef INI_INLINE_COMMENT_PREFIXES
#define INI_INLINE_COMMENT_PREFIXES ";"
#endif
/* Nonzero to use stack, zero to use heap (malloc/free). */
#ifndef INI_USE_STACK
#define INI_USE_STACK 1
#endif
/* Stop parsing on first error (default is to keep parsing). */
#ifndef INI_STOP_ON_FIRST_ERROR
#define INI_STOP_ON_FIRST_ERROR 0
#endif
/* Maximum line length for any line in INI file. */
#ifndef INI_MAX_LINE
#define INI_MAX_LINE 200
#endif
#ifdef __cplusplus
}
#endif
#endif /* __INI_H__ */

260
init.c Normal file
View File

@ -0,0 +1,260 @@
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "ini.h"
#include "parse.h"
#include "init.h"
static int sound_index(Sampler *s, const char *name) {
for(int i = 0; i < s->num_sounds; ++i) {
if(strncmp(s->index[i], name, MAX_NAME) == 0) {
return i;
}
}
return -1;
}
static int sound_index_add(Sampler *s, const char *name) {
if(s->num_sounds >= MAX_SOUNDS) {
printf("Maximum number of sounds exceeded: %s\n", name);
exit(1);
}
strncpy(s->index[s->num_sounds], name, MAX_NAME);
s->num_sounds += 1;
return s->num_sounds - 1;
}
static int sampler_handler(void *user,
const char *section,
const char *name,
const char *value) {
Sampler *s = (Sampler*)user;
if(strcmp(section, "SAMPLER") != 0) {
return 1;
}
printf("%s: %s = %s\n", section, name, value);
if(strcmp(name, "base_dir") == 0) {
strncpy(s->base_dir, value, PATH_MAX);
}
else if(strcmp(name, "midi_min") == 0) {
s->midi_min_vel = parse_int(value);
}
else if(strcmp(name, "midi_max") == 0) {
s->midi_max_vel = parse_int(value);
} else {
printf("Unknown setting.\n");
exit(1);
}
return 1;
}
static int trigger_handler(void *user,
const char *section,
const char *name,
const char *value) {
Sampler *s = (Sampler*)user;
if(strcmp(section, "TRIGGERS") != 0) {
return 1;
}
printf("%s: %s = %s\n", section, name, value);
int from = parse_int(name);
if(from < 0 || from > 127) {
printf("Illegal trigger: %i\n", from);
exit(1);
}
int to = sound_index(s, value);
if(to == -1) {
to = sound_index_add(s, value);
}
s->trigger_map[from][s->num_triggers[from]] = to;
s->num_triggers[from]++;
return 1;
}
static int cut_handler(void *user,
const char *section,
const char *name,
const char *value) {
Sampler *s = (Sampler*)user;
if(strcmp(section, "CUTS") != 0) {
return 1;
}
printf("%s: %s = %s\n", section, name, value);
int from = sound_index(s, name);
if(from == -1) {
printf("Sound is not triggered: %s\n", name);
return 1;
}
int to = sound_index(s, value);
if(to == -1) {
printf("Sound is not triggered: %s\n", value);
return 1;
}
s->cut_map[from][s->num_cuts[from]] = to;
s->num_cuts[from]++;
return 1;
}
static void value_handler(SoundConfig *conf,
const char *name,
const char *value) {
if(strcmp(name, "path") == 0) {
strncpy((char*)&conf->path, value, PATH_MAX);
}
else if(strcmp(name, "gamma_amp") == 0) {
conf->gamma_amp = parse_double(value);
if(conf->gamma_amp <= 0) {
printf("Illegal gamma_amp value: %s\n", value);
exit(1);
}
}
else if(strcmp(name, "gamma_layer") == 0) {
conf->gamma_layer = parse_double(value);
if(conf->gamma_layer <= 0) {
printf("Illegal gamma_layer value: %s\n", value);
exit(1);
}
}
else if(strcmp(name, "tau_off") == 0) {
conf->tau_off = parse_double(value);
conf->tau_off = proc_raw_tau_value(conf->tau_off);
}
else if(strcmp(name, "tau_cut") == 0) {
conf->tau_cut = parse_double(value);
conf->tau_cut = proc_raw_tau_value(conf->tau_cut);
}
else if(strcmp(name, "fx_pre") == 0 || strcmp(name, "fx") == 0) {
soundconfig_proc_fx(conf, value, true);
}
else if(strcmp(name, "fx_post") == 0) {
soundconfig_proc_fx(conf, value, false);
}
else {
printf("Unknown value.\n");
exit(1);
}
}
static int sound_handler(void *user,
const char *section,
const char *name,
const char *value) {
Sampler *s = (Sampler*)user;
// Is this a new section? If so, record the name.
bool new_section = strcmp(s->section, section) != 0;
if(new_section) {
strncpy(s->section, section, MAX_NAME);
}
// Skip non-sound sections.
if(strcmp(section, "TRIGGERS") == 0 ||
strcmp(section, "CUTS") == 0 ||
strcmp(section, "SAMPLER") == 0) {
return 1;
}
printf("%s: %s = %s\n", section, name, value);
// Process a global section.
if(strcmp(section, "GLOBAL") == 0) {
if(new_section) {
soundconfig_load_defaults(&s->global_conf);
}
value_handler(&s->global_conf, name, value);
return 1;
}
// Otherwise this is a sound.
int i = sound_index(s, section);
if(i == -1) {
printf("Sound is not triggered: %s\n", section);
return 1;
}
Sound *sound = &s->sounds[i];
// If it's a new sound, apply the globals.
if(new_section) {
soundconfig_copy_globals(&sound->conf, &s->global_conf);
}
value_handler(&s->sounds[i].conf, name, value);
return 1;
}
void sampler_init(Sampler *sampler, char *conf_path) {
sampler->section[0] = '\0';
sampler->num_sounds = 0;
sampler->midi_min_vel = 0;
sampler->midi_max_vel = 127;
sampler->midi_free = jlrb_new(MAX_PLAYING);
sampler->midi_new = jlrb_new(MAX_PLAYING);
sampler->sustainControl = 64;
for(int i = 0; i < MAX_PLAYING; ++i) {
jlrb_put(sampler->midi_free, jl_malloc_exit(sizeof(MidiEvt)));
}
for(int i = 0; i < MAX_SOUNDS; ++i) {
sampler->index[i][0] = '\0';
sound_init(&sampler->sounds[i]);
sampler->num_triggers[i] = 0;
sampler->num_cuts[i] = 0;
}
// 1) Load sampler settings.
if(ini_parse(conf_path, sampler_handler, sampler) < 0) {
printf("Failed to parse config file %s (sampler).\n", conf_path);
exit(1);
}
// 2) Load the triggers.
if(ini_parse(conf_path, trigger_handler, sampler) < 0) {
printf("Failed to parse config file %s (triggers).\n", conf_path);
exit(1);
}
// 3) Load cuts.
if(ini_parse(conf_path, cut_handler, sampler) < 0) {
printf("Failed to parse config file %s (cuts).\n", conf_path);
exit(1);
}
// 4) Load configuration defaults before parsing sound section. We do this
// in case the user hasn't provided a GLOBAL section.
soundconfig_load_defaults(&sampler->global_conf);
// 5) Load sounds from the config file.
if(ini_parse(conf_path, sound_handler, sampler) < 0) {
printf("Failed to parse config file %s (sounds).\n", conf_path);
exit(1);
}
// Change into base directory.
if(chdir(sampler->base_dir) != 0) {
printf("Failed to change directory: %s\n", sampler->base_dir);
exit(1);
}
printf("Loaded %i sounds.\n", sampler->num_sounds);
}

8
init.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef init_HEADER_
#define init_HEADER_
#include "sampler.h"
void sampler_init(Sampler *sampler, char *conf_path);
#endif

145
jl_mem.c Normal file
View File

@ -0,0 +1,145 @@
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
#include "jl_mem.h"
#ifdef jl_malloc_exit
#undef jl_malloc_exit
#undef jl_calloc_exit
#undef jl_realloc_exit
#endif
// ALIGN_SIZE should be larger than sizeof(malloc_info), and properly aligned.
// On x86_64, memory is generally aligned on 16 byte boundaries.
#define ALIGN_SIZE 32
void *jl_malloc_exit(size_t size)
{
void *data = aligned_alloc(ALIGN_SIZE, size);
if(data == NULL) {
printf("Out of memory.\n");
exit(EXIT_FAILURE);
}
return data;
}
typedef struct malloc_info malloc_info;
struct malloc_info
{
char *file;
int line;
size_t size;
malloc_info *prev, *next;
};
static malloc_info *mi_head = NULL;
static pthread_mutex_t lock;
static size_t alloc_count = 0;
static size_t free_count = 0;
static size_t alloc_bytes = 0;
static size_t free_bytes = 0;
void jl_mem_leak_check_init()
{
if(pthread_mutex_init(&lock, NULL) != 0) {
printf("Failed to initialize mutex.\n");
exit(EXIT_FAILURE);
}
atexit(jl_mem_print_leaks);
}
void jl_free_chk(void *ptr)
{
if(ptr == NULL) {
return;
}
pthread_mutex_lock(&lock);
malloc_info *mi = ptr - ALIGN_SIZE;
if(mi->prev == NULL) {
mi_head = mi->next;
}
if(mi->prev) {
mi->prev->next = mi->next;
}
if(mi->next) {
mi->next->prev = mi->prev;
}
free_bytes += mi->size;
free_count++;
free(mi);
pthread_mutex_unlock(&lock);
}
void *jl_malloc_chk(size_t size, char *file, int line)
{
pthread_mutex_lock(&lock);
malloc_info *mi = aligned_alloc(ALIGN_SIZE, size + ALIGN_SIZE);
if(mi == NULL) {
printf("\nMALLOC FAILED:\n");
printf(" File: %s\n", file);
printf(" Line: %d\n", line);
printf(" Size: %u bytes\n", (unsigned int)size);
exit(EXIT_FAILURE);
}
mi->file = file;
mi->line = line;
mi->size = size;
alloc_bytes += size;
alloc_count++;
mi->prev = NULL;
mi->next = mi_head;
if(mi_head) {
mi_head->prev = mi;
}
mi_head = mi;
pthread_mutex_unlock(&lock);
return (void*)mi + ALIGN_SIZE;
}
void jl_mem_print_leaks(void)
{
int count = 0;
malloc_info *mi = mi_head;
while(mi) {
printf("MEMORY LEAK DETECTED\n");
printf(" File: %s\n", mi->file);
printf(" Line: %d\n", (int)mi->line);
printf(" Size: %d bytes\n", (int)mi->size);
mi = mi->next;
++count;
if(count > 10) {
printf("ADDITIONAL LEAKS NOT PRINTED.\n");
break;
}
}
printf("\n");
printf("MEMORY SUMMARY\n");
printf(" Num allocs: %lu\n", (unsigned long)alloc_count);
printf(" Num frees: %lu\n", (unsigned long)free_count);
printf("\n");
printf(" Bytes alloced: %lu\n", (unsigned long)alloc_bytes);
printf(" Bytes freed: %lu\n", (unsigned long)free_bytes);
printf(" Bytes leaked: %lu\n",
(unsigned long)(alloc_bytes - free_bytes));
printf("\n");
}

40
jl_mem.h Normal file
View File

@ -0,0 +1,40 @@
/*****************************************************************************
* VERSION: 1.05
* 2015-08-28
*
* Memory allocation functions.
*****************************************************************************/
#ifndef JLLIB_MEM_HEADER_
#define JLLIB_MEM_HEADER_
#include <stdlib.h>
void *jl_malloc_chk(size_t size, char *file, int line);
void jl_free_chk(void *ptr);
void *jl_malloc_exit(size_t size);
void jl_mem_print_leaks(void);
void jl_mem_leak_check_init();
#ifdef JL_LEAK_CHECK
#define jl_malloc_exit(s) jl_malloc_chk(s, __FILE__, __LINE__)
#define jl_malloc(s) jl_malloc_chk(s, __FILE__, __LINE__)
#define jl_free(p) jl_free_chk(p)
#define JL_LEAK_CHECK_INIT jl_mem_leak_check_init()
#else // JL_LEAK_CHECK
#define JL_LEAK_CHECK_INIT
#define jl_malloc(s) malloc(s)
#define jl_calloc(n, s) calloc(n, s)
#define jl_realloc(p, s) realloc(p, s)
#define jl_free(p) free(p)
#endif // JL_LEAK_CHECK
#endif // JLLIB_MEM_HEADER_

53
jl_ringbuf.c Normal file
View File

@ -0,0 +1,53 @@
#include "jl_ringbuf.h"
struct RingBuffer {
int size;
jack_ringbuffer_t *buf;
};
RingBuffer *jlrb_new(int size)
{
RingBuffer *rb = jl_malloc_exit(sizeof(RingBuffer));
rb->size = size;
// It appears that we need to add one extra byte to the allocation
// in order to get the capacity we desire -jdl 2015-08-29.
rb->buf = jack_ringbuffer_create(sizeof(void *) * size + 1);
return rb;
}
void jlrb_free(RingBuffer ** rb)
{
jack_ringbuffer_free((*rb)->buf);
jl_free(*rb);
*rb = NULL;
}
int jlrb_size(RingBuffer * rb)
{
return rb->size;
}
inline int jlrb_count(RingBuffer * rb)
{
return jack_ringbuffer_read_space(rb->buf) / sizeof(void *);
}
inline bool jlrb_put(RingBuffer * rb, void *data)
{
int writeSpace = jack_ringbuffer_write_space(rb->buf);
if (writeSpace < sizeof(void *)) {
return false;
}
jack_ringbuffer_write(rb->buf, (char *)(&data), sizeof(void *));
return true;
}
inline void *jlrb_get(RingBuffer * rb)
{
if (jlrb_count(rb) == 0) {
return NULL;
}
void *data;
jack_ringbuffer_read(rb->buf, (char *)(&data), sizeof(void *));
return data;
}

38
jl_ringbuf.h Normal file
View File

@ -0,0 +1,38 @@
/*****************************************************************************
* VERSION: 1.01
* 2015-08-29
*
* Single producer, single consumer ring buffer.
*****************************************************************************/
#ifndef JLLIB_RINGBUF_HEADER_
#define JLLIB_RINGBUF_HEADER_
#include <stdbool.h>
#include <jack/ringbuffer.h>
#include "jl_mem.h"
typedef struct RingBuffer RingBuffer;
// Create a new RingBuffer with the given capacity.
RingBuffer *jlrb_new(int size);
// Free the buffer. Note that the items in the buffer must be freed by the
// caller before the buffer is freed.
void jlrb_free(RingBuffer ** rb);
// Return the total capacity of the buffer.
int jlrb_size(RingBuffer * rb);
// Return the number of items currently in the buffer.
int jlrb_count(RingBuffer * rb);
// Put the given data into the buffer. The return value is true if the put was
// successful, and false if there wasn't room.
bool jlrb_put(RingBuffer * rb, void *data);
// Return the next item in the buffer. Returns NULL if there was no item
// available.
void *jlrb_get(RingBuffer * rb);
#endif // JLLIB_RINGBUF_HEADER_

29
jl_test.h Normal file
View File

@ -0,0 +1,29 @@
/*****************************************************************************
* VERSION: 1.00
* 2015-08-25
*
* Testing macros.
*****************************************************************************/
#ifndef JLLIB_TEST_HEADER_
#define JLLIB_TEST_HEADER_
#include <stdio.h>
#define JL_TEST_FUNC printf("Running: %s\n", __func__)
#define JL_FAIL_MSG(msg) \
do { \
printf(" FAILED: %s() line %d\n", __func__, __LINE__); \
printf(" %s\n", msg); \
exit(EXIT_FAILURE); \
} while (0)
#define JL_FAIL_IF(cond, msg) \
do { \
if(cond) { \
JL_FAIL_MSG(msg); \
} \
} while (0)
#endif // JLLIB_TEST_HEADER_

18
main.c Normal file
View File

@ -0,0 +1,18 @@
#include <stdio.h>
#include <stdlib.h>
#include "jl_mem.h"
#include "sampler.h"
#include "init.h"
Sampler *sampler;
int main(int argc, char **argv) {
if(argc != 2) {
printf("Please provide a configuration file.\n");
exit(1);
}
sampler = jl_malloc_exit(sizeof(Sampler));
sampler_init(sampler, argv[1]);
sampler_main(sampler);
}

21
parse.c Normal file
View File

@ -0,0 +1,21 @@
#include <stdio.h>
#include <stdlib.h>
#include "parse.h"
double parse_double(const char *value) {
double dest;
if(sscanf(value, "%lf", &dest) != 1) {
printf("Failed to parse double value: %s\n", value);
exit(1);
}
return dest;
}
int parse_int(const char *value) {
int val;
if(sscanf(value, "%d", &val) != 1) {
printf("Failed to parse int value: %s\n", value);
exit(1);
}
return val;
}

7
parse.h Normal file
View File

@ -0,0 +1,7 @@
#ifndef parse_HEADER_
#define parse_HEADER_
double parse_double(const char *value);
int parse_int(const char *value);
#endif

188
sample.c Normal file
View File

@ -0,0 +1,188 @@
#include <math.h>
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <limits.h>
#include <string.h>
#include "jl_mem.h"
#include "soundfile.h"
#include "sample.h"
#include "fx.h"
static bool starts_with(char *s, char *prefix) {
int n = strlen(prefix);
if(n > strlen(s)) {
return false;
}
for(int i = 0; i < n; ++i) {
if(s[i] != prefix[i]) {
return false;
}
}
return true;
}
static void sample_apply_effect(Sample *s, char *txt) {
// Attempt to parse each effect until one works.
if(starts_with(txt, "amp(")) {
double gain;
if(sscanf(txt, "amp(%lf)", &gain) != 1) {
goto apply_fx_err;
}
fx_amp(s, gain);
}
else if(starts_with(txt, "balance(")) {
double balance;
if(sscanf(txt, "balance(%lf)", &balance) != 1) {
goto apply_fx_err;
}
fx_balance(s, balance);
}
else if(starts_with(txt, "divide_rms(")) {
double t_ms;
if(sscanf(txt, "divide_rms(%lf)", &t_ms) != 1) {
goto apply_fx_err;
}
fx_divide_rms(s, t_ms);
}
else if(starts_with(txt, "fade_in(")) {
double t_ms;
if(sscanf(txt, "fade_in(%lf)", &t_ms) != 1) {
goto apply_fx_err;
}
fx_fade_in(s, t_ms);
}
else if(starts_with(txt, "ir(")) {
char path[MAX_FX_LINE];
double mix;
if(sscanf(txt, "ir(%[^,], %lf)", path, &mix) != 2) {
goto apply_fx_err;
}
fx_ir(s, path, mix);
}
else if(starts_with(txt, "mono()")) {
fx_mono(s);
}
else if(starts_with(txt, "pan(")) {
double pan;
if(sscanf(txt, "pan(%lf)", &pan) != 1) {
goto apply_fx_err;
}
fx_pan(s, pan);
}
else if(starts_with(txt, "pre_cut(")) {
double pct, fade_ms;
if(sscanf(txt, "pre_cut(%lf, %lf)", &pct, &fade_ms) != 2) {
goto apply_fx_err;
}
fx_pre_cut(s, pct, fade_ms);
}
else if(starts_with(txt, "playback_speed(")) {
double speed;
if(sscanf(txt, "playback_speed(%lf)", &speed) != 1) {
goto apply_fx_err;
}
fx_playback_speed(s, speed);
}
else if(starts_with(txt, "pre_delay(")) {
double left_ms, right_ms;
if(sscanf(txt, "pre_delay(%lf, %lf)", &left_ms, &right_ms) != 2) {
goto apply_fx_err;
}
fx_pre_delay(s, left_ms, right_ms);
}
else if(starts_with(txt, "rc_highpass(")) {
double freq;
int order;
if(sscanf(txt, "rc_highpass(%lf, %d)", &freq, &order) != 2) {
goto apply_fx_err;
}
fx_rc_highpass(s, freq, order);
}
else if(starts_with(txt, "rc_lowpass(")) {
double freq;
int order;
if(sscanf(txt, "rc_lowpass(%lf, %d)", &freq, &order) != 2) {
goto apply_fx_err;
}
fx_rc_lowpass(s, freq, order);
}
else if(starts_with(txt, "rc_highshelf(")) {
double freq, gain;
int order;
if(sscanf(txt, "rc_highshelf(%lf, %d, %lf)", &freq, &order, &gain) != 3) {
goto apply_fx_err;
}
fx_rc_highshelf(s, freq, order, gain);
}
else if(starts_with(txt, "rc_lowshelf(")) {
double freq, gain;
int order;
if(sscanf(txt, "rc_lowshelf(%lf, %d, %lf)", &freq, &order, &gain) != 3) {
goto apply_fx_err;
}
fx_rc_lowshelf(s, freq, order, gain);
}
else if(starts_with(txt, "stereo_width(")) {
double width;
if(sscanf(txt, "stereo_width(%lf)", &width) != 1) {
goto apply_fx_err;
}
fx_stereo_width(s, width);
}
else {
printf("Unknown effect: %s\n", txt);
}
return;
apply_fx_err:
printf("Parsing error: %s\n", txt);
exit(1);
}
void sample_load(Sample *s, char *path, SoundConfig *conf, int buf_size)
{
s->len = soundfile_load_stereo(path, &s->L, &s->R);
// Apply global pre effects.
for(int i = 0; conf->global_fx_pre[i][0] != '\0'; ++i) {
sample_apply_effect(s, conf->global_fx_pre[i]);
}
// Apply pre effects.
for(int i = 0; conf->fx_pre[i][0] != '\0'; ++i) {
sample_apply_effect(s, conf->fx_pre[i]);
}
// Apply post effets.
for(int i = 0; conf->fx_post[i][0] != '\0'; ++i) {
sample_apply_effect(s, conf->fx_post[i]);
}
// Apply global post effects.
for(int i = 0; conf->global_fx_post[i][0] != '\0'; ++i) {
sample_apply_effect(s, conf->global_fx_post[i]);
}
// Pad to buffer size.
fx_pad_multiple(s, buf_size);
}
void sample_set_data(Sample *s, float *L, float *R, int len) {
jl_free(s->L);
jl_free(s->R);
s->L = L;
s->R = R;
s->len = len;
}
void sample_free(Sample *s)
{
jl_free(s->L);
jl_free(s->R);
s->R = s->L = NULL;
s->len = 0;
}

20
sample.h Normal file
View File

@ -0,0 +1,20 @@
#ifndef sample_HEADER_
#define sample_HEADER_
#include "soundconfig.h"
typedef struct {
int len; // The number of samples in each channel.
float *L; // The left channel.
float *R; // The right channel.
} Sample;
// Load a sample. Allocates.
void sample_load(Sample *s, char *path, SoundConfig *conf, int buf_size);
void sample_set_data(Sample *s, float *L, float *R, int len);
// Free a sample.
void sample_free(Sample *s);
#endif

57
sample_test.c Normal file
View File

@ -0,0 +1,57 @@
#include "jl_mem.h"
#include "jl_test.h"
#include "sample.h"
static void test_load_free()
{
JL_TEST_FUNC;
Sample s;
sample_load(&s, "test-files/sound.flac", 1.0, 8);
// Check length.
int len = 71750 + 8;
len -= len % 8;
JL_FAIL_IF(s.len != len, "Incorrect length.");
// Check that data is normalized.
float max = 0;
for(int i = 0; i < s.len; ++i) {
if(s.L[i] > max) {
max = s.L[i];
} else if(-s.L[i] > max) {
max = -s.L[i];
}
if(s.R[i] > max) {
max = s.R[i];
} else if(-s.R[i] > max) {
max = -s.R[i];
}
}
JL_FAIL_IF(max != 1.0, "Data not normalized.");
sample_free(&s);
}
static void test_playback_speed()
{
JL_TEST_FUNC;
Sample s1;
Sample s2;
sample_load(&s1, "test-files/sound.flac", 1.0, 0);
sample_load(&s2, "test-files/sound.flac", 0.5, 0);
JL_FAIL_IF(2 * s1.len != s2.len, "Incorrect lengths.");
for(int i = 0; i < s1.len; ++i) {
JL_FAIL_IF(s1.L[i] != s2.L[2*i], "Incorrect data.");
JL_FAIL_IF(s1.R[i] != s2.R[2*i], "Incorrect data.");
}
sample_free(&s1);
sample_free(&s2);
}
int main(int argc, char **argv)
{
JL_LEAK_CHECK_INIT;
test_load_free();
test_playback_speed();
return 0;
}

34
sampleinfo.h Normal file
View File

@ -0,0 +1,34 @@
#ifndef sampleinfo_HEADER_
#define sampleinfo_HEADER_
#include <stdint.h>
typedef struct {
char *name; // The sample name - used when opening files.
uint8_t numLayers; // Number velocity layers.
uint8_t numVariations[MAX_LAYERS]; // Number of variations per layer.
uint8_t rrIdx[MAX_LAYERS]; // Round-robin index per layer.
Sample *sample[MAX_LAYERS][MAX_VARS]; // Samples.
uint8_t numCut; // Number of samples cut by this one.
uint8_t cutInds[MAX_SAMPLES]; // Indices of samples cut by this one.
// Applied at load time.
double rms_time; // In ms.
double crop_thresh; // 0-1
double playback_speed; // 1 => no change.
double gain;
double pan;
double gammaAmp;
double gammaLayer;
double tauOff; // If non-zero, fade out time-constant when pad release.
double tauCut; // Fade out time constant when sample is cut.
double tauFadeIn;
} SampleInfo;
#endif

288
sampler.c Normal file
View File

@ -0,0 +1,288 @@
#include <alsa/asoundlib.h>
#include <math.h>
#include <pthread.h>
#include "jl_mem.h"
#include "jl_ringbuf.h"
#include "sampler.h"
/****************
* sampler_main *
****************/
static void proc_midi_event(Sampler *s, MidiEvt *evt) {
if(evt->type == MIDI_EVT_NOTE) {
int num_triggers = s->num_triggers[evt->note];
for(int i = 0; i < num_triggers; ++i) {
int idx = s->trigger_map[evt->note][i];
for(int j = 0; j < s->num_cuts[idx]; ++j) {
sound_cut(&s->sounds[s->cut_map[idx][j]]);
}
sound_trigger(&s->sounds[idx], evt->velocity);
}
} else if(evt->type == MIDI_EVT_CTRL) {
if(evt->control == s->sustainControl) {
gl_sustain = evt->value > 0.5;
}
}
}
static void proc_midi(Sampler *s) {
int num_events = jlrb_count(s->midi_new);
for(int i = 0; i < num_events; ++i) {
MidiEvt *evt = jlrb_get(s->midi_new);
proc_midi_event(s, evt);
jlrb_put(s->midi_free, evt);
}
}
static int jack_process(jack_nframes_t nframes, void *data) {
Sampler *s = data;
if(nframes != s->buf_size) {
printf("Buffer size changed.\n");
exit(1);
}
// Processing incoming midi events.
proc_midi(s);
// Get output port arrays.
float *L = jack_port_get_buffer(s->jack_port_L, nframes);
float *R = jack_port_get_buffer(s->jack_port_R, nframes);
// Zero the output buffers.
memset(L, 0, nframes * sizeof(float));
memset(R, 0, nframes * sizeof(float));
// Write output from each sound.
int count = s->num_sounds;
for(int i = 0; i < count; ++i) {
Sound *sound = &s->sounds[i];
if(sound->num_playing == 0) {
continue;
}
sound_write_buffer(sound, L, R);
}
for(int i = 0; i < nframes; ++i) {
if(L[i] > s->L_max) {
s->L_max = L[i];
} else if (-L[i] > s->L_max) {
s->L_max = -L[i];
}
if(R[i] > s->R_max) {
s->R_max = R[i];
} else if (-R[i] > s->R_max) {
s->R_max = -R[i];
}
}
return 0;
}
static double calc_velocity(Sampler *s, int v_in) {
if(v_in <= s->midi_min_vel) {
return 0;
} else if(v_in >= s->midi_max_vel) {
return 1;
}
double vel = v_in;
vel -= s->midi_min_vel;
vel /= s->midi_max_vel - s->midi_min_vel;
return vel;
}
void midi_loop(Sampler *s) {
// We need to open the sequencer before doing anything else.
snd_seq_t *handle;
int status = snd_seq_open(&handle, "default", SND_SEQ_OPEN_INPUT, 0);
if (status != 0) {
printf("Failed to open sequencer.\n");
exit(1);
}
// Give the client a name.
status = snd_seq_set_client_name(handle, "JLSampler");
if (status != 0) {
printf("Failed to set sequencer client name.\n");
exit(1);
}
// Create a port with write capabilities.
int caps = SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE;
int portNum = snd_seq_create_simple_port(
handle, "MIDI In", caps, SND_SEQ_PORT_TYPE_MIDI_GM);
if (portNum < 0) {
printf("Failed to create sequencer port.\n");
exit(1);
}
// We need a midi event handle to read incoming midi events.
snd_seq_event_t *seq_event;
MidiEvt *event;
while(1) {
status = snd_seq_event_input(handle, &seq_event);
if (status < 0) {
printf("Sampler: Failed to read MIDI event. Status: %i\n", status);
continue;
}
// Skip events that we won't process.
if(seq_event->type != SND_SEQ_EVENT_NOTEON &&
seq_event->type != SND_SEQ_EVENT_NOTEOFF &&
seq_event->type != SND_SEQ_EVENT_CONTROLLER) {
continue;
}
event = jlrb_get(s->midi_free);
if(event == NULL) {
printf("No free midi events available.\n");
continue;
}
switch (seq_event->type) {
case SND_SEQ_EVENT_NOTEON:
event->type = MIDI_EVT_NOTE;
event->note = seq_event->data.note.note;
event->velocity = calc_velocity(s, seq_event->data.note.velocity);
break;
case SND_SEQ_EVENT_NOTEOFF:
event->type = MIDI_EVT_NOTE;
event->note = seq_event->data.note.note;
event->velocity = 0;
break;
case SND_SEQ_EVENT_CONTROLLER:
event->type = MIDI_EVT_CTRL;
event->control = seq_event->data.control.param;
event->value = (double)(seq_event->data.control.value) / 127.0;
break;
}
jlrb_put(s->midi_new, event);
}
}
static void print_meter(float val) {
val = (log10(val) + 3.0) / 3.0;
for(int j = 0; j < 64; ++j) {
if(val > (float)j/64.0) {
printf("-");
} else {
printf(" ");
}
}
printf("|");
for(int j = 0; j < 8; ++j) {
if(val > 1.0 + (float)j/8.0) {
printf("=");
} else {
printf(" ");
}
}
printf("|\n");
}
static void meter_loop(void *data) {
Sampler *s = data;
struct timespec sleep_time;
sleep_time.tv_sec = 0;
sleep_time.tv_nsec = 100000000;
while(1) {
nanosleep(&sleep_time, NULL);
printf("\n\n");
printf("L ");
print_meter(s->L_max);
s->L_max = 0;
printf("R ");
print_meter(s->R_max);
s->R_max = 0;
}
}
void sampler_main(Sampler *s) {
// Initialize JACK client.
printf("Creating JACK client...\n");
s->jack_client = jack_client_open("JLSampler", JackNullOption, NULL);
// Create jack output ports.
printf("Creating JACK output ports...\n");
s->jack_port_L = jack_port_register(s->jack_client, "Out_1",
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsOutput, 0);
s->jack_port_R = jack_port_register(s->jack_client, "Out_2",
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsOutput, 0);
// Get the jack buffer size.
printf("Getting JACK buffer size...\n");
s->buf_size = (int)jack_get_buffer_size(s->jack_client);
printf(" Got %i\n", s->buf_size);
if(s->buf_size % 8 != 0) {
printf("Buffer size must be a multiple of 8.\n");
exit(1);
}
// Load samples.
#pragma omp parallel for
for(int i = 0; i < s->num_sounds; ++i) {
sound_load_samples(&s->sounds[i], s->buf_size);
}
// Register the jack callback function.
printf("Setting JACK callback...\n");
jack_set_process_callback(s->jack_client, jack_process, (void *)s);
printf("Activating JACK client...\n");
jack_activate(s->jack_client);
// Start the VU meter loop in the background.
pthread_t th;
pthread_create(&th, NULL, (void *) &meter_loop, s);
// Collect midi events in the main thread.
printf("Starting midi loop...\n");
midi_loop(s);
}
/****************
* sampler_free *
****************/
void sampler_free(Sampler *sampler) {
int n = jlrb_count(sampler->midi_free);
for(int i = 0; i < n; ++i) {
MidiEvt *evt = jlrb_get(sampler->midi_free);
jl_free(evt);
}
jlrb_free(&sampler->midi_free);
n = jlrb_count(sampler->midi_new);
for(int i = 0; i < n; ++i) {
MidiEvt *evt = jlrb_get(sampler->midi_new);
jl_free(evt);
}
jlrb_free(&sampler->midi_new);
for(int i = 0; i < MAX_SOUNDS; ++i) {
sound_free(&sampler->sounds[i]);
}
}

72
sampler.h Normal file
View File

@ -0,0 +1,72 @@
#ifndef sampler_HEADER_
#define sampler_HEADER_
#include <limits.h>
#include <jack/jack.h>
#include "jl_ringbuf.h"
#include "sound.h"
// MidiEvt: A MIDI event. The event has a `type`, which is one of the constants
// MIDI_EVT_*. Depending on the event type, it will contain a (control, value)
// or a (node, velocity) pair.
typedef struct {
int type; // One of the MIDI_EVT_* constants.
union {
int control;
int note;
};
union {
double value;
double velocity;
};
} MidiEvt;
typedef struct {
// Global sound config.
char section[MAX_NAME];
SoundConfig global_conf;
// When loading the configuration file, this is used to map names to
// sound indices.
int32_t num_sounds;
char index[MAX_SOUNDS][MAX_NAME];
char base_dir[PATH_MAX];
// Min/max values output by controller.
int midi_min_vel;
int midi_max_vel;
// Ring buffers for midi events.
RingBuffer *midi_free; // Consumed by the midi thread.
RingBuffer *midi_new; // Consumed by the jack thread.
// Midi map.
int num_triggers[MAX_SOUNDS];
int trigger_map[MAX_SOUNDS][MAX_SOUNDS];
// Cut map.
int num_cuts[MAX_SOUNDS];
int cut_map[MAX_SOUNDS][MAX_SOUNDS];
// Jack client and ports.
jack_client_t *jack_client;
jack_port_t *jack_port_L, *jack_port_R;
// The sounds.
int buf_size;
Sound sounds[MAX_SOUNDS];
// Sustain pedal value.
int sustainControl;
// Output amplitudes.
float L_max;
float R_max;
} Sampler;
void sampler_main(Sampler *sampler);
void sampler_free(Sampler *sampler);
#endif

96
scripts/keyboard.py Executable file
View File

@ -0,0 +1,96 @@
#!/usr/bin/env python3
import sys
import configparser
from glob import glob
class Config:
def __init__(self, config_path):
cp = configparser.ConfigParser()
cp.read(config_path)
# Print non-keyboard keys.
for key in cp.keys():
if key in ('KEYBOARD', 'DEFAULT'):
continue
print('[' + key + ']')
conf = cp[key]
for k in conf.keys():
print('{0} = {1}'.format(k, conf[k]))
conf = cp['KEYBOARD']
self.midi_min = conf.getint('midi_min', 21)
self.midi_max = conf.getint('midi_max', 109)
self.amp_low = conf.getfloat('amp_low', 0.24)
self.amp_high = conf.getfloat('amp_high', 0.04)
self.rms_time_low = conf.getfloat('rms_time_low', 500)
self.rms_time_high = conf.getfloat('rms_time_high', 50)
class Tuning:
def __init__(self, path):
self.cp = configparser.ConfigParser()
self.cp.read(path)
self.conf = self.cp['TUNING']
def get_freq(self, note):
# TODO!
return None
class Note:
def __init__(self, idx, conf):
self.conf = conf
self.idx = idx
self.path = "samples/*{0:03}-*-*.flac".format(idx)
def name(self):
return "note-{0:03}".format(self.idx)
def _linear(self, low, high):
m = (high - low) / 87.0
return low + m * (self.idx - 21)
def amp(self):
return self._linear(conf.amp_low, conf.amp_high)
def rms_time(self):
return self._linear(conf.rms_time_low, conf.rms_time_high)
def conf_path(self):
return 'path = ' + self.path
def conf_fx(self):
return 'fx = divide_rms({0}) amp({1})'.format(
self.rms_time(), self.amp())
def noteFromFile(fn):
base = os.path.splitext(fn)[0]
return int(base.split('-')[-3])
if __name__ == '__main__':
conf = Config(sys.argv[1])
# Create the notes.
notes = []
for idx in range(conf.midi_min, conf.midi_max + 1):
notes.append(Note(idx, conf))
# Print triggers.
print('\n[TRIGGERS]')
for note in notes:
print('{0} = {1}'.format(note.idx, note.name()))
print('\n[GLOBAL]')
print('tau_off = 100')
# Print notes.
for note in notes:
print('')
print('[{0}]'.format(note.name()))
print(note.conf_path())
print(note.conf_fx())

285
sound.c Normal file
View File

@ -0,0 +1,285 @@
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <glob.h>
#include <x86intrin.h>
#include "jl_mem.h"
#include "sound.h"
/**************
* sound_init *
**************/
void sound_init(Sound *sound) {
sound->num_layers = 0;
for(int i = 0; i < MAX_LAYERS; ++i) {
sound->num_vars[i] = 0;
sound->rr_idx[i] = 0;
}
sound->key_down = false;
sound->buf_size = 0;
sound->num_free = 0;
sound->num_playing = 0;
for(int i = 0; i < MAX_PLAYING; ++i) {
sound->ps_free[i] = jl_malloc_exit(sizeof(PlayingSample));
sound->num_free++;
}
}
/**********************
* sound_load_samples *
**********************/
static void load_sound_sample(Sound *sound, char *path, int32_t buf_size) {
SoundConfig *conf = &(sound->conf);
char *path_end = &path[strlen(path) - 12];
int layer, var;
if(sscanf(path_end, "%d-%d.flac", &layer, &var) != 2) {
printf("Error parsing sample filename: %s\n", path);
exit(1);
}
if(layer >= MAX_LAYERS) {
printf("Layer is too large.");
exit(1);
}
if(var >= MAX_VARS) {
printf("Variation is too large.");
exit(1);
}
if(sound->num_layers < layer) {
sound->num_layers = layer;
}
sound->num_vars[layer-1] += 1;
Sample *sample = &sound->samples[layer-1][var-1];
sample_load(sample, path, conf, buf_size);
}
void sound_load_samples(Sound *sound, int32_t buf_size) {
sound->buf_size = buf_size;
sound->amp_buf = jl_malloc_exit(buf_size * sizeof(float));
char *path_glob = sound->conf.path;
if(strlen(path_glob) == 0) {
return;
}
glob_t results;
int ret = glob(path_glob, 0, NULL, &results);
if(ret == GLOB_NOMATCH) {
printf("No files found: %s\n", path_glob);
exit(1);
} else if(ret != 0) {
printf("Error matching files: %s\n", path_glob);
exit(1);
}
for(int i = 0; i < results.gl_pathc; i++) {
char *path = results.gl_pathv[i];
load_sound_sample(sound, path, buf_size);
}
globfree(&results);
}
/*****************
* sound_trigger *
*****************/
void sound_trigger(Sound *sound, double vel) {
if(vel == 0) {
sound->key_down = false;
return;
}
sound->key_down = true;
if(sound->num_layers == 0) {
return;
}
if(sound->num_playing == MAX_PLAYING) {
// TODO: Log here.
// No open slots left.
return;
}
PlayingSample *ps = sound->ps_free[sound->num_free - 1];
sound->num_free--;
// Playback from the start with no fade out.
ps->state = PS_ON;
ps->idx = 0;
ps->tau = 1;
// Compute the playback amplitude.
ps->amp = pow(vel, sound->conf.gamma_amp);
// Get the appropriate layer.
vel = pow(vel, sound->conf.gamma_layer);
int layer = (int)((double)sound->num_layers * vel);
if(layer == sound->num_layers) {
--layer;
}
// Update round-robin index.
sound->rr_idx[layer] = (sound->rr_idx[layer] + 1) % sound->num_vars[layer];
// Chose the appropriate variation.
ps->sample = &(sound->samples[layer][sound->rr_idx[layer]]);
// Put playing sample into ring buffer.
sound->ps_playing[sound->num_playing] = ps;
sound->num_playing++;
}
/*************
* sound_cut *
*************/
void sound_cut(Sound *sound) {
double tau = sound->conf.tau_cut;
if(tau == 1) {
return;
}
for(int i = 0; i < sound->num_playing; ++i) {
PlayingSample *ps = sound->ps_playing[i];
ps->state = PS_CUT;
ps->tau = tau;
}
}
/**********************
* sound_write_buffer *
**********************/
static inline bool write_smpl_fast(
Sound *sound, PlayingSample *ps,
float *L, float *R,
float *amp_buf, int count) {
// Update tau for the playing sample based on key state.
switch(ps->state) {
case PS_OFF:
if(sound->key_down || gl_sustain) {
ps->state = PS_ON;
ps->tau = 1;
}
break;
case PS_ON:
if(!sound->key_down && !gl_sustain) {
ps->state = PS_OFF;
ps->tau = sound->conf.tau_off;
}
break;
}
float amp = ps->amp;
float tau = ps->tau;
for(int i = 0; i < count; ++i) {
amp_buf[i] = amp;
amp *= tau;
}
ps->amp = amp;
__m256 *v_amp = (__m256*)amp_buf;
__m256 *L_out = (__m256*)L;
__m256 *R_out = (__m256*)R;
__m256 *L_smp = (__m256*)ps->sample->L;
__m256 *R_smp = (__m256*)ps->sample->R;
int v_count = count / 8;
int idx = ps->idx / 8;
for(int i = 0; i < v_count; ++i) {
L_out[i] = _mm256_add_ps(
L_out[i], _mm256_mul_ps(L_smp[idx], v_amp[i]));
R_out[i] = _mm256_add_ps(
R_out[i], _mm256_mul_ps(R_smp[idx], v_amp[i]));
idx += 1;
}
ps->idx += count;
return ps->idx < ps->sample->len && ps->amp > MIN_AMP;
}
static inline bool write_smpl(
PlayingSample *ps, float *L, float *R, int32_t count) {
// TODO: We could pre-compute the amplitude array and then use SIMD
// instructions to speed this up.
float *sL = ps->sample->L;
float *sR = ps->sample->R;
double amp = ps->amp;
double tau = ps->tau;
int idx = ps->idx;
// Note: We've ensured that each sample has a length that is a multiple of
// the buffer size and of 8, so this will always succeed.
for(int32_t i = 0; i < count; ++i) {
L[i] += sL[idx] * amp;
R[i] += sR[idx] * amp;
amp *= tau;
idx += 1;
}
ps->amp = amp;
ps->idx = idx;
return ps->idx < ps->sample->len && ps->amp > MIN_AMP;
}
void sound_write_buffer(Sound *sound, float *L, float *R) {
int count = sound->num_playing;
sound->num_playing = 0;
for(int i = 0; i < count; ++i) {
PlayingSample *ps = sound->ps_playing[i];
if(write_smpl_fast(sound, ps, L, R, sound->amp_buf, sound->buf_size)) {
sound->ps_playing[sound->num_playing] = ps;
sound->num_playing++;
} else {
sound->ps_free[sound->num_free] = ps;
sound->num_free++;
}
}
}
/**************
* sound_free *
**************/
void sound_free(Sound *sound) {
for(int i = 0; i < MAX_PLAYING; ++i) {
jl_free(sound->ps_free[i]);
}
for(int i = 0; i < MAX_PLAYING; ++i) {
jl_free(sound->ps_playing[i]);
}
for(int i = 0; i < MAX_LAYERS; ++i) {
for(int j = 0; j < sound->num_vars[i]; ++j) {
sample_free(&sound->samples[i][j]);
}
}
}

53
sound.h Normal file
View File

@ -0,0 +1,53 @@
#ifndef config_HEADER_
#define config_HEADER_
#include <stdint.h>
#include "const.h"
#include "sample.h"
#include "soundconfig.h"
// Playing sample states.
#define PS_ON 0 // Undamped.
#define PS_OFF 1 // Damped because key off and sustain off.
#define PS_CUT 2 // Damped because cut by other key.
typedef struct {
Sample *sample;
int state;
int idx;
double amp;
double tau;
} PlayingSample;
typedef struct {
// Configuration
SoundConfig conf;
// Whether or not the key is depressed.
bool key_down;
// Samples
uint8_t num_layers;
uint8_t num_vars[MAX_LAYERS];
uint8_t rr_idx[MAX_LAYERS];
Sample samples[MAX_LAYERS][MAX_VARS];
// Playing samples
int32_t buf_size;
int32_t num_free;
PlayingSample *ps_free[MAX_PLAYING];
float *amp_buf;
int32_t num_playing;
PlayingSample *ps_playing[MAX_PLAYING];
} Sound;
void sound_init(Sound *sound);
void sound_load_samples(Sound *sound, int32_t buf_size);
void sound_trigger(Sound *sound, double velocity);
void sound_cut(Sound *sound);
void sound_write_buffer(Sound *sound, float *L, float *R);
void sound_free(Sound *sound);
#endif

18
sound_test.c Normal file
View File

@ -0,0 +1,18 @@
#include "jl_mem.h"
#include "jl_test.h"
#include "sound.h"
static void test_load()
{
load_config_file("test-files/test.ini");
load_samples(64);
free_samples();
dump_config();
}
int main(int argc, char **argv)
{
JL_LEAK_CHECK_INIT;
test_load();
return 0;
}

119
soundconfig.c Normal file
View File

@ -0,0 +1,119 @@
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "soundconfig.h"
/*****************************
* soundconfig_load_defaults *
*****************************/
void soundconfig_load_defaults(SoundConfig *conf) {
conf->path[0] = '\0';
conf->gamma_amp = 2.2;
conf->gamma_layer = 1;
conf->tau_off = 1;
conf->tau_cut = proc_raw_tau_value(100);
for(int i = 0; i < MAX_FX; ++i) {
conf->fx_pre[i][0] = '\0';
conf->fx_post[i][0] = '\0';
conf->global_fx_pre[i][0] = '\0';
conf->global_fx_post[i][0] = '\0';
}
}
/********************
* soundconfig_copy *
********************/
void soundconfig_copy_globals(SoundConfig *to, SoundConfig *from) {
to->gamma_amp = from->gamma_amp;
to->gamma_layer = from->gamma_layer;
to->tau_off = from->tau_off;
to->tau_cut = from->tau_cut;
for(int i = 0; i < MAX_FX; ++i) {
strncpy(to->global_fx_pre[i], from->fx_pre[i], MAX_FX_LINE);
strncpy(to->global_fx_post[i], from->fx_post[i], MAX_FX_LINE);
}
}
/**********************
* proc_fx *
**********************/
// This function parses a config file `fx` value into discrete lines that
// can be parsed later.
void soundconfig_proc_fx(SoundConfig *conf, const char *line, bool pre) {
const char *src = line;
int i_src = 0;
int i_fx = 0;
char (*fx)[MAX_FX_LINE] = pre ? conf->fx_pre : conf->fx_post;
while(1) {
// Find the next empty fx slot.
for(; i_fx < MAX_FX; ++i_fx) {
if(fx[i_fx][0] == '\0') {
break;
}
}
// Make sure we're not beyond the end.
if(i_fx == MAX_FX) {
printf("Maximum number of effects reached.\n");
exit(1);
}
// Skip spaces.
while(src[i_src] == ' ') {
i_src++;
}
// Check for end of line.
if(src[i_src] == '\0') {
return;
}
char *dest = fx[i_fx];
int i_dest = 0;
// Copy until closing paren.
while(1) {
char c = src[i_src];
i_src++;
if(c == '\0') {
printf("Failed to parse effects line: %s\n", line);
exit(1);
}
dest[i_dest] = c;
i_dest++;
if(i_dest >= MAX_FX_LINE) {
printf("FX line was too long: %s\n", line);
exit(1);
}
if(c == ')') {
dest[i_dest] = '\0';
break;
}
}
}
}
/**********************
* proc_raw_tau_value *
**********************/
// Convert a millisecond time-constant value into a per-sample decay.
double proc_raw_tau_value(double value)
{
if (value <= 0) {
return 1;
} else {
return exp(-1000.0 / (SAMPLE_RATE * value));
}
}

47
soundconfig.h Normal file
View File

@ -0,0 +1,47 @@
#ifndef soundconfig_HEADER_
#define soundconfig_HEADER_
#include <stdbool.h>
#include <linux/limits.h> // Provides PATH_MAX.
#include "const.h"
// A SoundConfig struct represents the configuration for a single sampled
// sound. It contains the path to the files, the sound effects (pre and post),
// global sound effects, and standard configuration.
typedef struct {
// This is a glob spec for the files to use. All files should end in
// -XXX-YYY.flac where XXX is the velocity level and YYY is the variation.
char path[PATH_MAX];
char fx_pre[MAX_FX][MAX_FX_LINE];
char fx_post[MAX_FX][MAX_FX_LINE];
char global_fx_pre[MAX_FX][MAX_FX_LINE];
char global_fx_post[MAX_FX][MAX_FX_LINE];
// The amplitude of the sample as a function of velocity. For a normalized
// velocity between 0 and 1, the amplitude of the output will scale like
// velocity^gamma_amp.
double gamma_amp;
// Similar to gamma_amp, but used when choosing the velocity layer.
double gamma_layer;
// If non-zero, this is the fade out time-constant (e-folding time) when a
// sound is released (key-up event).
double tau_off;
// If non-zero, this is the fade-out time-constant (e-folding time) when a
// sound is cut by another sound. The cut time constant will override
// tau_off.
double tau_cut;
} SoundConfig;
void soundconfig_load_defaults(SoundConfig *conf);
void soundconfig_copy_globals(SoundConfig *to, SoundConfig *from);
void soundconfig_proc_fx(SoundConfig *conf, const char *line, bool pre);
double proc_raw_tau_value(double value);
#endif

48
soundfile.c Normal file
View File

@ -0,0 +1,48 @@
#include <stdlib.h>
#include <stdio.h>
#include <sndfile.h>
#include "jl_mem.h"
#include "const.h"
#include "soundfile.h"
int soundfile_load_stereo(const char *path, float **Lp, float **Rp) {
SF_INFO fileInfo;
fileInfo.format = 0;
fileInfo.channels = 2;
fileInfo.samplerate = 48000;
// Open the file.
SNDFILE *sndFile = sf_open(path, SFM_READ, &fileInfo);
if (sndFile == NULL) {
printf("Failed to open file: %s\n", path);
printf(" Error: %s\n", sf_strerror(sndFile));
exit(1);
}
// Check that the parameters are correct.
if (fileInfo.channels != 2 || fileInfo.samplerate != 48000) {
printf("File must be 2 channels and 48 kHz.\n");
exit(1);
}
int len = fileInfo.frames;
float *L = jl_malloc_exit(len * sizeof(float));
float *R = jl_malloc_exit(len * sizeof(float));
float buf[2];
for(int i = 0; i < len; ++i) {
if(sf_readf_float(sndFile, buf, 1) != 1) {
printf("Failed to read frame from file: %s\n", path);
exit(1);
}
L[i] = buf[0];
R[i] = buf[1];
}
sf_close(sndFile);
*Lp = L;
*Rp = R;
return len;
}

6
soundfile.h Normal file
View File

@ -0,0 +1,6 @@
#ifndef soundfile_HEADER_
#define soundfile_HEADER_
int soundfile_load_stereo(const char *path, float **Lp, float **Rp);
#endif

BIN
test-files/sound.flac Normal file

Binary file not shown.

34
test-files/test.ini Normal file
View File

@ -0,0 +1,34 @@
[triggers]
16 = 18
16 = 19
17 = 19
19 = 19
[cuts]
16 = 18
25 = 18
[global]
rms_time = 250
gamma_amp = 2.2
gamma_layer = 1.0
[0]
path = /home/johnl/samples/percussion/kits/natural-mf/samples/snare-*
gain = 0.88
pan = -0.10
[1]
gain = 0.25
pan = -0.50
[2]
gain = 0.25
pan = -0.50
[3]
gain = 0.25
pan = -0.50

24
test-files/test1.ini Normal file
View File

@ -0,0 +1,24 @@
[triggers]
36 = 0
39 = 0
[cuts]
37 = 0
38 = 0
[global]
rms_time = 250
gamma_amp = 2.2
gamma_layer = 1.0
[0]
path = /home/johnl/samples/percussion/kits/sm-drums/samples/hh-tight/hh-*
rms_time = 100
gain = 0.45
pan = 0
gamma_amp = 1
gamma_layer = 0.2
tau_cut = 1