diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ab24fc4 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 0a134fc..b587af8 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file diff --git a/const.h b/const.h new file mode 100644 index 0000000..43bddbd --- /dev/null +++ b/const.h @@ -0,0 +1,23 @@ +#ifndef const_HEADER_ +#define const_HEADER_ + +#include + +// 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 diff --git a/fx.c b/fx.c new file mode 100644 index 0000000..dbeabfd --- /dev/null +++ b/fx.c @@ -0,0 +1,381 @@ +#include +#include +#include +#include +#include +#include +#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; + } +} diff --git a/fx.h b/fx.h new file mode 100644 index 0000000..20d4f06 --- /dev/null +++ b/fx.h @@ -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 diff --git a/ini.c b/ini.c new file mode 100644 index 0000000..27ca85b --- /dev/null +++ b/ini.c @@ -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 +#include +#include + +#include "ini.h" + +#if !INI_USE_STACK +#include +#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; +} diff --git a/ini.h b/ini.h new file mode 100644 index 0000000..eaa554d --- /dev/null +++ b/ini.h @@ -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 + +/* 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__ */ diff --git a/init.c b/init.c new file mode 100644 index 0000000..de6b92d --- /dev/null +++ b/init.c @@ -0,0 +1,260 @@ +#include +#include +#include +#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); +} diff --git a/init.h b/init.h new file mode 100644 index 0000000..8c41c88 --- /dev/null +++ b/init.h @@ -0,0 +1,8 @@ +#ifndef init_HEADER_ +#define init_HEADER_ + +#include "sampler.h" + +void sampler_init(Sampler *sampler, char *conf_path); + +#endif diff --git a/jl_mem.c b/jl_mem.c new file mode 100644 index 0000000..924d30b --- /dev/null +++ b/jl_mem.c @@ -0,0 +1,145 @@ + +#include +#include +#include +#include +#include +#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"); +} diff --git a/jl_mem.h b/jl_mem.h new file mode 100644 index 0000000..3beed84 --- /dev/null +++ b/jl_mem.h @@ -0,0 +1,40 @@ +/***************************************************************************** + * VERSION: 1.05 + * 2015-08-28 + * + * Memory allocation functions. + *****************************************************************************/ + +#ifndef JLLIB_MEM_HEADER_ +#define JLLIB_MEM_HEADER_ + +#include + +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_ diff --git a/jl_ringbuf.c b/jl_ringbuf.c new file mode 100644 index 0000000..0a5a775 --- /dev/null +++ b/jl_ringbuf.c @@ -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; +} diff --git a/jl_ringbuf.h b/jl_ringbuf.h new file mode 100644 index 0000000..cab7dd1 --- /dev/null +++ b/jl_ringbuf.h @@ -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 +#include +#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_ diff --git a/jl_test.h b/jl_test.h new file mode 100644 index 0000000..ff7e55e --- /dev/null +++ b/jl_test.h @@ -0,0 +1,29 @@ +/***************************************************************************** + * VERSION: 1.00 + * 2015-08-25 + * + * Testing macros. + *****************************************************************************/ + +#ifndef JLLIB_TEST_HEADER_ +#define JLLIB_TEST_HEADER_ + +#include + +#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_ diff --git a/main.c b/main.c new file mode 100644 index 0000000..66be5e7 --- /dev/null +++ b/main.c @@ -0,0 +1,18 @@ +#include +#include +#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); +} diff --git a/parse.c b/parse.c new file mode 100644 index 0000000..7cb4ff4 --- /dev/null +++ b/parse.c @@ -0,0 +1,21 @@ +#include +#include +#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; +} diff --git a/parse.h b/parse.h new file mode 100644 index 0000000..9106472 --- /dev/null +++ b/parse.h @@ -0,0 +1,7 @@ +#ifndef parse_HEADER_ +#define parse_HEADER_ + +double parse_double(const char *value); +int parse_int(const char *value); + +#endif diff --git a/sample.c b/sample.c new file mode 100644 index 0000000..c70c89f --- /dev/null +++ b/sample.c @@ -0,0 +1,188 @@ +#include +#include +#include +#include +#include +#include +#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; +} diff --git a/sample.h b/sample.h new file mode 100644 index 0000000..2469732 --- /dev/null +++ b/sample.h @@ -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 diff --git a/sample_test.c b/sample_test.c new file mode 100644 index 0000000..e262da8 --- /dev/null +++ b/sample_test.c @@ -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; +} diff --git a/sampleinfo.h b/sampleinfo.h new file mode 100644 index 0000000..4f94068 --- /dev/null +++ b/sampleinfo.h @@ -0,0 +1,34 @@ +#ifndef sampleinfo_HEADER_ +#define sampleinfo_HEADER_ + +#include + + + +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 diff --git a/sampler.c b/sampler.c new file mode 100644 index 0000000..5542f0c --- /dev/null +++ b/sampler.c @@ -0,0 +1,288 @@ +#include +#include +#include +#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]); + } +} diff --git a/sampler.h b/sampler.h new file mode 100644 index 0000000..a94190a --- /dev/null +++ b/sampler.h @@ -0,0 +1,72 @@ +#ifndef sampler_HEADER_ +#define sampler_HEADER_ + +#include +#include +#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 diff --git a/scripts/keyboard.py b/scripts/keyboard.py new file mode 100755 index 0000000..980212a --- /dev/null +++ b/scripts/keyboard.py @@ -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()) diff --git a/sound.c b/sound.c new file mode 100644 index 0000000..6a41f37 --- /dev/null +++ b/sound.c @@ -0,0 +1,285 @@ +#include +#include +#include +#include +#include +#include +#include +#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]); + } + } +} diff --git a/sound.h b/sound.h new file mode 100644 index 0000000..29c6a61 --- /dev/null +++ b/sound.h @@ -0,0 +1,53 @@ +#ifndef config_HEADER_ +#define config_HEADER_ + +#include +#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 diff --git a/sound_test.c b/sound_test.c new file mode 100644 index 0000000..98e0266 --- /dev/null +++ b/sound_test.c @@ -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; +} diff --git a/soundconfig.c b/soundconfig.c new file mode 100644 index 0000000..41d9930 --- /dev/null +++ b/soundconfig.c @@ -0,0 +1,119 @@ +#include +#include +#include +#include +#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)); + } +} diff --git a/soundconfig.h b/soundconfig.h new file mode 100644 index 0000000..d2ee7d9 --- /dev/null +++ b/soundconfig.h @@ -0,0 +1,47 @@ +#ifndef soundconfig_HEADER_ +#define soundconfig_HEADER_ + +#include +#include // 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 diff --git a/soundfile.c b/soundfile.c new file mode 100644 index 0000000..eedd994 --- /dev/null +++ b/soundfile.c @@ -0,0 +1,48 @@ +#include +#include +#include +#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; +} diff --git a/soundfile.h b/soundfile.h new file mode 100644 index 0000000..3fbdb67 --- /dev/null +++ b/soundfile.h @@ -0,0 +1,6 @@ +#ifndef soundfile_HEADER_ +#define soundfile_HEADER_ + +int soundfile_load_stereo(const char *path, float **Lp, float **Rp); + +#endif diff --git a/test-files/sound.flac b/test-files/sound.flac new file mode 100644 index 0000000..7bfb506 Binary files /dev/null and b/test-files/sound.flac differ diff --git a/test-files/test.ini b/test-files/test.ini new file mode 100644 index 0000000..3e05c01 --- /dev/null +++ b/test-files/test.ini @@ -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 diff --git a/test-files/test1.ini b/test-files/test1.ini new file mode 100644 index 0000000..e16624d --- /dev/null +++ b/test-files/test1.ini @@ -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