diff --git a/Makefile.dev b/Makefile.dev index e4e438a..f5cf687 100644 --- a/Makefile.dev +++ b/Makefile.dev @@ -1,6 +1,6 @@ TARGET ?= forge INSTALL_DIR ?= $(HOME)/.local/bin -TEST_ARGS ?= --frag=./shaders --config=./config/forge.cfg --video-in=/dev/video0 --video-in=/dev/video1 --video-in=/dev/video2 --video-in=/dev/video3 --video-in=/dev/video4 --video-in=/dev/video5 --video-in=/dev/video6 --video-in=/dev/video7 --video-in=/dev/video8 --video-in=/dev/video9 --tempo=30 +TEST_ARGS ?= --frag=./shaders --config=./config/forge.cfg --video-in=/dev/video0 --video-in=/dev/video1 --video-in=/dev/video2 --video-in=/dev/video3 --video-in=/dev/video4 --video-in=/dev/video5 --video-in=/dev/video6 --video-in=/dev/video7 --video-in=/dev/video8 --video-in=/dev/video9 SHELL := /bin/bash .PHONY: build diff --git a/README.md b/README.md index e2af5e9..f730505 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ make -f Makefile.dev release-arch - [x] Send midi data to shaders - [ ] Save midi state - [x] State machine with A/B switch - - [ ] Tap-tempo feature + - [x] Tap-tempo feature - [ ] Clean code and fix things - [x] Video input - [x] Fixed camera video @@ -151,5 +151,6 @@ make -f Makefile.dev release-arch - [x] Clean code and fix things - [x] Share openGL state between monitor and screen - [ ] Other + - [ ] Update readme with usage documentation - [ ] Clone "shaders" and config in system path at setup - [ ] Find and fix opengl errors 0500 ? \ No newline at end of file diff --git a/src/config.h b/src/config.h index f15fbe9..660aed4 100644 --- a/src/config.h +++ b/src/config.h @@ -1,6 +1,8 @@ #ifndef CONFIG_H #define CONFIG_H +/* PACKAGE */ + #ifndef PACKAGE #define PACKAGE "forge" #endif /* PACKAGE */ @@ -9,6 +11,8 @@ #define VERSION "(dev)" #endif /* VERSION */ +/* TYPES */ + #ifndef MAX_VIDEO #define MAX_VIDEO 16 #endif @@ -17,6 +21,8 @@ #define MAX_FRAG 64 #endif +/* MIDI */ + #ifndef UNSET_MIDI_CODE #define UNSET_MIDI_CODE 300 #endif @@ -25,6 +31,8 @@ #define MIDI_MAX 127 #endif +/* ARRAY */ + #ifndef ARRAY_SIZE #define ARRAY_SIZE 1024 #endif @@ -33,4 +41,36 @@ #define ARRAY_NOT_FOUND ARRAY_SIZE + 1 #endif +/* TEMPO */ + +#ifndef MAX_BEAT_LENGTH +// 30.0 bpm +#define MAX_BEAT_LENGTH 2000 +#endif + +#ifndef MIN_BEAT_LENGTH +// 240.0 bpm +#define MIN_BEAT_LENGTH 250 +#endif + +#ifndef BEATS_UNTIL_CHAIN_RESET +#define BEATS_UNTIL_CHAIN_RESET 3 +#endif + +#ifndef TOTAL_TAP_VALUES +#define TOTAL_TAP_VALUES 8 +#endif + +#ifndef SKIPPED_TAP_THRESHOLD_LOW +#define SKIPPED_TAP_THRESHOLD_LOW 1.75 +#endif + +#ifndef SKIPPED_TAP_THRESHOLD_HIGH +#define SKIPPED_TAP_THRESHOLD_HIGH 2.75 +#endif + +#ifndef MAX_TAP_VALUES +#define MAX_TAP_VALUES 10 +#endif + #endif /* CONFIG_H */ \ No newline at end of file diff --git a/src/forge.c b/src/forge.c index 1e4a07e..c8cb8c1 100644 --- a/src/forge.c +++ b/src/forge.c @@ -1,10 +1,11 @@ #include #include #include +#include #include +#include #include -#include "arr.h" #include "config.h" #include "config_file.h" #include "file.h" @@ -14,6 +15,7 @@ #include "shaders.h" #include "shared.h" #include "state.h" +#include "tempo.h" #include "timer.h" #include "types.h" #include "video.h" @@ -58,7 +60,8 @@ static void init_context(Parameters params, unsigned int in_count, unsigned int frag_count) { unsigned int i; - context->tempo = params.base_tempo; + context->tempo = tempo_init(); + tempo_set(&context->tempo, params.base_tempo); context->demo = params.demo; context->monitor = params.monitor; diff --git a/src/shaders.c b/src/shaders.c index 60d32b2..2002a4b 100644 --- a/src/shaders.c +++ b/src/shaders.c @@ -514,7 +514,7 @@ static void use_program(ShaderProgram program, int i, bool output, } // set fragment uniforms write_uniform_1f(program.itime_locations[i], context->time); - write_uniform_1f(program.itempo_locations[i], context->tempo); + write_uniform_1f(program.itempo_locations[i], context->tempo.tempo); write_uniform_1i(program.ifps_locations[i], context->fps); write_uniform_1i(program.idemo_locations[i], context->demo ? 1 : 0); write_uniform_1i(program.ipage_locations[i], context->page); diff --git a/src/state.c b/src/state.c index a90799f..6d2bba8 100644 --- a/src/state.c +++ b/src/state.c @@ -6,6 +6,7 @@ #include "midi.h" #include "rand.h" #include "state.h" +#include "tempo.h" #include "types.h" StateConfig state_parse_config(ConfigFile config) { @@ -250,6 +251,14 @@ void state_apply_event(SharedContext *context, StateConfig state_config, } } + if (code == state_config.tap_tempo_code) { + found = true; + midi_write(midi, code, value); + if (value > 0) { + tempo_tap(&context->tempo); + } + } + if (!found) { log_trace("unknown midi: %d %d", code, value); midi_write(midi, code, value); @@ -261,6 +270,7 @@ void state_apply_event(SharedContext *context, StateConfig state_config, bool state_background_midi_write(SharedContext *context, StateConfig state_config, MidiDevice midi) { pid_t pid; + bool beat_active, last_active; pid = fork(); if (pid < 0) { @@ -276,8 +286,21 @@ bool state_background_midi_write(SharedContext *context, update_active(context, state_config, midi); update_values(context, state_config, midi); + last_active = false; + while (!context->stop) { - // TODO tap tempo and more + beat_active = tempo_progress(context->tempo) < 0.25; + + if (beat_active != last_active) { + safe_midi_write(midi, state_config.tap_tempo_code, + beat_active ? MIDI_MAX : 0); + + safe_midi_write(midi, + state_config.select_frag_codes.values[context->selected], + beat_active ? MIDI_MAX : 0); + + last_active = beat_active; + } } log_info("(state) background writing stopped by main thread (pid: %d)", pid); diff --git a/src/tempo.c b/src/tempo.c index 8298b33..a23f6dc 100644 --- a/src/tempo.c +++ b/src/tempo.c @@ -1 +1,122 @@ -#include "tempo.h" \ No newline at end of file +#include +#include +#include + +#include "config.h" +#include "log.h" +#include "tempo.h" + +static long now() { + struct timeval now; + + gettimeofday(&now, NULL); + + return now.tv_sec * 1000 + now.tv_usec / 1000; +} + +static void reset_tap_chain(Tempo *tempo, long t) { + tempo->last_reset = t; + tempo->last_tap = 0; + tempo->taps_in_chain = 0; + tempo->tap_duration_index = 0; + tempo->last_tap_skipped = false; + + memset(tempo->tap_durations, 0, sizeof(tempo->tap_durations)); +} + +Tempo tempo_init() { + Tempo tempo; + long t; + + t = now(); + + reset_tap_chain(&tempo, t); + + return tempo; +} + +static bool is_chain_active(Tempo tempo, long t) { + return tempo.last_tap + MAX_BEAT_LENGTH > t && + tempo.last_tap + (tempo.beat_length * BEATS_UNTIL_CHAIN_RESET) > t; +} + +static unsigned long get_average_tap_duration(Tempo tempo) { + unsigned int amount, i; + unsigned long running_total, average_tap_duration; + + amount = tempo.taps_in_chain - 1; + if (amount > TOTAL_TAP_VALUES) { + amount = TOTAL_TAP_VALUES; + } + + running_total = 0; + for (i = 0; i < amount; i++) { + running_total += tempo.tap_durations[i]; + } + + average_tap_duration = running_total / amount; + if (average_tap_duration < MIN_BEAT_LENGTH) { + log_debug("%ld", average_tap_duration); + return MIN_BEAT_LENGTH; + } + + return average_tap_duration; +} + +static void add_tap_to_chain(Tempo *tempo, long t) { + long duration; + + duration = t - tempo->last_tap; + + tempo->last_tap = t; + + tempo->taps_in_chain++; + + if (tempo->taps_in_chain == 1) { + return; + } + + if (tempo->taps_in_chain > 2 && !tempo->last_tap_skipped && + duration > tempo->beat_length * SKIPPED_TAP_THRESHOLD_LOW && + duration < tempo->beat_length * SKIPPED_TAP_THRESHOLD_HIGH) { + duration = duration >> 1; + tempo->last_tap_skipped = true; + } else { + tempo->last_tap_skipped = false; + } + + tempo->tap_durations[tempo->tap_duration_index++] = duration; + + if (tempo->tap_duration_index == TOTAL_TAP_VALUES) { + tempo->tap_duration_index = 0; + } + + tempo->beat_length = get_average_tap_duration(*tempo); + + tempo->tempo = 60000.0 / tempo->beat_length; +} + +void tempo_set(Tempo *tempo, float value) { + tempo->tempo = value; + tempo->beat_length = 60000.0 / value; +} + +void tempo_tap(Tempo *tempo) { + long t; + + t = now(); + + if (!is_chain_active(*tempo, t)) { + reset_tap_chain(tempo, t); + } + + add_tap_to_chain(tempo, t); +} + +double tempo_progress(Tempo tempo) { + long t; + + t = now(); + + return fmod((double)(t - tempo.last_reset) / (double)tempo.beat_length, 1.0); +} \ No newline at end of file diff --git a/src/tempo.h b/src/tempo.h index 396eda4..7cd0af5 100644 --- a/src/tempo.h +++ b/src/tempo.h @@ -3,4 +3,12 @@ #ifndef TEMPO_H #define TEMPO_H +Tempo tempo_init(); + +void tempo_tap(Tempo *tempo); + +void tempo_set(Tempo *tempo, float value); + +double tempo_progress(Tempo tempo); + #endif /* TEMPO_H */ \ No newline at end of file diff --git a/src/types.h b/src/types.h index c412c2b..763a087 100644 --- a/src/types.h +++ b/src/types.h @@ -135,6 +135,17 @@ typedef ARRAY(VideoCaptureArray, VideoCapture); typedef GLFWwindow Window; +typedef struct Tempo { + long last_reset; + long last_tap; + unsigned int taps_in_chain; + unsigned int tap_duration_index; + unsigned int tap_durations[MAX_TAP_VALUES]; + bool last_tap_skipped; + unsigned long beat_length; + float tempo; +} Tempo; + typedef struct SharedContext { int fd; @@ -145,7 +156,7 @@ typedef struct SharedContext { unsigned int internal_height; double time; unsigned int fps; - float tempo; + Tempo tempo; // TODO use array unsigned int state[MAX_FRAG]; unsigned int page; diff --git a/src/video.c b/src/video.c index 7eed535..5e6e318 100644 --- a/src/video.c +++ b/src/video.c @@ -4,14 +4,13 @@ #include #include #include +#include #include #include -#include "shared.h" #include "timer.h" #include "types.h" #include "video.h" -#include "window.h" static void ioctl_error(VideoCapture *video_capture, const char *operation, const char *default_msg) {