Compare commits
59 Commits
11-no-hard
...
3646f6c47e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3646f6c47e | ||
|
|
10d0269a5a | ||
| b994911dab | |||
|
|
bb648d471b | ||
|
|
fa9dbf2185 | ||
|
|
51493d5cab | ||
|
|
ce2dd7be76 | ||
|
|
87d5e658c5 | ||
|
|
525516fe03 | ||
|
|
3e52142f62 | ||
|
|
a92b5c51e1 | ||
|
|
bb3dfa8b2a | ||
|
|
3721c0c9e1 | ||
|
|
c041645019 | ||
|
|
6344eaed47 | ||
|
|
f96d7d290d | ||
|
|
2d254c0503 | ||
|
|
4339fda529 | ||
|
|
04b59999c8 | ||
|
|
df1f4fa6bd | ||
|
|
7e5362259b | ||
|
|
b10d218749 | ||
|
|
cc50577444 | ||
|
|
346c15d1c3 | ||
|
|
7deea9266b | ||
|
|
7d842163a2 | ||
|
|
54fa307360 | ||
|
|
5430795510 | ||
|
|
5a2414b4c3 | ||
|
|
6b490ed739 | ||
|
|
d4a811e552 | ||
|
|
567799a2d3 | ||
|
|
755af275d8 | ||
|
|
74db4ed46c | ||
|
|
15be644af7 | ||
|
|
aaca25ebf1 | ||
|
|
e3b9321b1a | ||
|
|
015ad2c5a7 | ||
|
|
c8b9de8e81 | ||
|
|
1ba98fc768 | ||
|
|
4dfb7a87c1 | ||
|
|
8892acd3d2 | ||
|
|
7b00246443 | ||
|
|
44177f785f | ||
|
|
94d6bc25f1 | ||
|
|
86d9bc72f1 | ||
| 75f347c418 | |||
|
|
0be6cfb31d | ||
|
|
de8202a0d2 | ||
|
|
fe3fb7d873 | ||
|
|
ffe422d83f | ||
|
|
5b1969415f | ||
|
|
91d58a07f5 | ||
|
|
4e489b5e40 | ||
|
|
df5ecef580 | ||
|
|
df181b117e | ||
|
|
ff226a8ea6 | ||
|
|
85e828f461 | ||
| f11a18a203 |
90
docs/2-midi-looping.md
Normal file
90
docs/2-midi-looping.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Per‑Channel MIDI Looping
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Each looper channel can be either **audio** or **MIDI**. Audio channels record and loop audio samples (existing behaviour). MIDI channels record and loop MIDI event sequences, using separate JACK MIDI input/output ports. The state machine (`IDLE → RECORD → LOOPING → PAUSED`) operates identically for both types.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Source | Action |
|
||||||
|
|----------------------------|-----------------|------------------------------------------------------------|
|
||||||
|
| `CMD_ADD_MIDI_CHANNEL` | MIDI note 66 | Adds a new MIDI looping channel |
|
||||||
|
| `add_midi` | FIFO pipe | Same |
|
||||||
|
| `CMD_REMOVE_CHANNEL` | MIDI note 61 | Removes the last‑added channel (audio or MIDI) |
|
||||||
|
| `CMD_CYCLE` | any note binding| Toggles channel state (IDLE→RECORD→LOOPING→PAUSED) |
|
||||||
|
|
||||||
|
## Ports
|
||||||
|
|
||||||
|
When a MIDI channel is created, two JACK MIDI ports are registered:
|
||||||
|
|
||||||
|
- `looper:channel<N>_midi_in` (input)
|
||||||
|
- `looper:channel<N>_midi_out` (output)
|
||||||
|
|
||||||
|
The `<N>` is a global counter, independent of the index inside the internal channel array.
|
||||||
|
|
||||||
|
## Recording
|
||||||
|
|
||||||
|
During `STATE_RECORD`:
|
||||||
|
|
||||||
|
1. All incoming MIDI events on the `_midi_in` port are stored in the channel’s event buffer, along with their frame offset relative to the start of the recording.
|
||||||
|
2. The incoming events are also **forwarded** to the `_midi_out` port, providing a direct pass‑through during recording.
|
||||||
|
|
||||||
|
**Buffer limit:** A channel can hold up to `MAX_MIDI_EVENTS` (1024) events.
|
||||||
|
|
||||||
|
## Looping
|
||||||
|
|
||||||
|
During `STATE_LOOPING`:
|
||||||
|
|
||||||
|
- All recorded events are output at the **start** of every cycle (frame 0). This is a simplification; no per‑event timestamp scheduling is implemented. The loop length is determined by the total number of recorded events.
|
||||||
|
|
||||||
|
## Pass‑Through
|
||||||
|
|
||||||
|
During `STATE_IDLE` (and `STATE_PAUSED` for MIDI) incoming MIDI events are **copied** from `_midi_in` to `_midi_out` unchanged.
|
||||||
|
|
||||||
|
## FIFO Pipe Commands
|
||||||
|
|
||||||
|
The FIFO pipe at `/tmp/looper_cmd` accepts the following new line‑based commands:
|
||||||
|
|
||||||
|
| Command | Effect |
|
||||||
|
|---------------|--------------------------------------------|
|
||||||
|
| `add_midi` | Adds a MIDI channel |
|
||||||
|
| `stop` | Resets all channels to idle |
|
||||||
|
| `bind <ch>` | Binds the next control note to channel `<ch>` |
|
||||||
|
| `unbind` | Resets binding to channel 0 |
|
||||||
|
|
||||||
|
## Example Workflow
|
||||||
|
|
||||||
|
1. Start the looper.
|
||||||
|
2. Connect a MIDI keyboard to `looper:channel1_midi_in`.
|
||||||
|
3. Send MIDI note 66 on `looper:control` to create a MIDI channel.
|
||||||
|
4. Send a CYCLE command (e.g., MIDI note 62 under control key) to start recording.
|
||||||
|
5. Play notes on the keyboard – the events are captured.
|
||||||
|
6. Send CYCLE again to enter LOOPING mode – the captured sequence repeats.
|
||||||
|
7. Send CYCLE again to pause, or send STOP (note 65 under control key) to reset.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
- **Channel structure** (`struct channel_t` in `channel.h`):
|
||||||
|
- `type` field (`CHANNEL_AUDIO` or `CHANNEL_MIDI`)
|
||||||
|
- `loop` union containing `audio_buffer[MAX_BUFFER]` or `midi_events[MAX_MIDI_EVENTS]`
|
||||||
|
- **MIDI event type** (`midi_event_t`):
|
||||||
|
- `timestamp` (frame offset relative to loop start)
|
||||||
|
- `status`, `note`, `velocity`
|
||||||
|
- **Processing** (`process_callback` in `looper.c`):
|
||||||
|
- The callback checks `type` before routing to the appropriate handler block.
|
||||||
|
- MIDI handler reads from `midi_in` port, writes to `midi_out` port.
|
||||||
|
- **Port cleanup**: On channel removal, both MIDI ports are unregistered via `jack_port_unregister()` after a one‑RT‑cycle grace period.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Integration tests in `tests/integration.c` cover:
|
||||||
|
|
||||||
|
- `test_midi_channel_add` – verifies that sending `add_midi` via FIFO creates `looper:channel<N>_midi_in` ports.
|
||||||
|
- `test_fifo_stop_bind_unbind` – verifies that `stop`, `bind`, and `unbind` FIFO commands are processed correctly.
|
||||||
|
- Other existing tests continue to verify audio‑only functionality.
|
||||||
|
|
||||||
|
Run the test suite with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
```
|
||||||
91
docs/4-implement-scene-switching-engine.md
Normal file
91
docs/4-implement-scene-switching-engine.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Scene Switching Engine
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The scene switching engine allows a channel to have multiple independent recording/playback states (scenes).
|
||||||
|
Only one scene per channel is active at a time. The active scene's state (IDLE / RECORD / LOOPING / PAUSED) is
|
||||||
|
controlled independently of other scenes.
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
Each `channel_t` holds an array of up to `MAX_SCENES` (16) `scene_t` structures. Two atomic integers keep track
|
||||||
|
of the number of scenes and which scene is currently active:
|
||||||
|
|
||||||
|
```c
|
||||||
|
atomic_int scene_count; // number of scenes for this channel
|
||||||
|
atomic_int current_scene; // index of the active scene (0 ≤ current_scene < scene_count)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each `scene_t` contains the loop buffer (audio or MIDI events) and the per‑scene atomic state:
|
||||||
|
|
||||||
|
```c
|
||||||
|
union {
|
||||||
|
float audio_buffer[LOOP_BUF_SIZE];
|
||||||
|
midi_event_t midi_events[MAX_MIDI_EVENTS];
|
||||||
|
} loop;
|
||||||
|
|
||||||
|
atomic_int loop_count;
|
||||||
|
atomic_int record_pos;
|
||||||
|
atomic_int playback_pos;
|
||||||
|
atomic_int state; // STATE_IDLE / STATE_RECORD / STATE_LOOPING / STATE_PAUSED
|
||||||
|
atomic_int prev_state; // previous state (used by RT callback to detect transitions)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Trigger (MIDI) | Trigger (FIFO) | Effect |
|
||||||
|
|--------------------------|------------------------|-----------------------|---------------------------------------------------------|
|
||||||
|
| **CMD_NEXT_SCENE** | note 67 (control key) | `scene_next\n` | Increments `current_scene` (wraps around). |
|
||||||
|
| **CMD_PREV_SCENE** | note 68 (control key) | `scene_prev\n` | Decrements `current_scene` (wraps around). |
|
||||||
|
| **CMD_ADD_SCENE** | note 69 (control key) | `scene_add\n` | Appends a new empty scene, increments `scene_count`. |
|
||||||
|
| **CMD_REMOVE_SCENE** | note 70 (control key) | `scene_remove\n` | Removes the current scene (shifts remaining scenes). |
|
||||||
|
|
||||||
|
All scene commands are processed on the main loop (not in the RT callback). They are pushed to
|
||||||
|
`cmd_queue_main_midi` (for MIDI) or `cmd_queue_main_fifo` (for FIFO) and applied by
|
||||||
|
`looper_process_commands()`.
|
||||||
|
|
||||||
|
## Thread Safety
|
||||||
|
|
||||||
|
- `scene_count` and `current_scene` are `atomic_int`; all reads/writes use `atomic_load`/`atomic_store`.
|
||||||
|
- The per‑scene fields (`loop_count`, `record_pos`, `playback_pos`, `state`, `prev_state`) are also `atomic_int`,
|
||||||
|
so the RT callback and the main loop can safely read and write them concurrently.
|
||||||
|
- The audio loop buffer itself (a plain `float` array) is not atomic. During scene removal the buffer is copied
|
||||||
|
via `memcpy`. If a scene is actively looping, this copy may produce a temporarily inconsistent buffer.
|
||||||
|
**Known limitation:** scene removal should only be performed when the channel is idle (all scenes in
|
||||||
|
`STATE_IDLE`). The integration test `test_scene_add_remove` does exactly this.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
1. **`channel_add_scene`**
|
||||||
|
- Called from main loop.
|
||||||
|
- Checks `scene_count < MAX_SCENES` (atomically).
|
||||||
|
- Calls `init_scene()` to zero the new scene and set its state to `STATE_IDLE`.
|
||||||
|
- Atomically increments `scene_count`.
|
||||||
|
|
||||||
|
2. **`channel_remove_scene`**
|
||||||
|
- Called from main loop.
|
||||||
|
- Refuses if `scene_count <= 1` (at least one scene must always exist).
|
||||||
|
- Shifts all scenes after the current one down one position – each scene field is copied with
|
||||||
|
`atomic_store`/`atomic_load`.
|
||||||
|
- The audio buffer is copied with `memcpy` (see limitation above).
|
||||||
|
- Decrements `scene_count` and adjusts `current_scene` if it would become out of bounds.
|
||||||
|
|
||||||
|
3. **`channel_next_scene` / `channel_prev_scene`**
|
||||||
|
- Called from main loop.
|
||||||
|
- If `scene_count > 1`, atomically increments/decrements `current_scene` (wrapping using modulo).
|
||||||
|
|
||||||
|
4. **RT callback (`process_callback`)**
|
||||||
|
- At the start of each frame it reads `current_scene` atomically to obtain the scene index for that
|
||||||
|
channel.
|
||||||
|
- All per‑scene reads (state, loop_count, record_pos, playback_pos) use `atomic_load`.
|
||||||
|
- When the state changes, the callback atomically resets `record_pos`, `loop_count`, `playback_pos`
|
||||||
|
as appropriate.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- `test_scene_add_remove` (FIFO) – adds a scene, cycles next, removes the scene, exits.
|
||||||
|
- `test_scene_next_prev_midi` – sends control key + notes 67/68 to switch scenes.
|
||||||
|
- `test_scene_cycle_per_scene` – records a loop on scene 0, switches to scene 1, verifies scene 1 is idle.
|
||||||
|
- `test_scene_add_remove_midi` – sends control key + notes 69/70 to add/remove scenes.
|
||||||
|
|
||||||
|
All scene tests pass as part of `make test`.
|
||||||
45
docs/6-sampling-and-recording.md
Normal file
45
docs/6-sampling-and-recording.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Sampling and Recording (WAV Load/Save)
|
||||||
|
|
||||||
|
The looper supports loading a WAV file into channel 0 and saving the current loop of channel 0 as a WAV file. Both operations use the **libsndfile** library, ensuring correct handling of RIFF headers, chunk sizes, and sample format conversion.
|
||||||
|
|
||||||
|
## Load Command
|
||||||
|
|
||||||
|
- **MIDI note 70** with the control key (note 64) triggers loading.
|
||||||
|
- The file `loop.wav` (located in the working directory) is read by `wav_read()` in `src/wav.c`.
|
||||||
|
- The function calls `sf_open(path, SFM_READ, &info)`.
|
||||||
|
- It accepts only mono PCM WAV files. If the file is not mono or has an invalid sample rate, it returns `-1`.
|
||||||
|
- The number of frames read is capped at `LOOP_BUF_SIZE` (5 seconds at 48 kHz).
|
||||||
|
- The data is stored in `channels[0].loop_buffer` and `channels[0].loop_count` is set atomically.
|
||||||
|
- The state of channel 0 is set to `STATE_LOOPING` and `prev_state` is set to `-1` to trigger the loop start in the next audio cycle.
|
||||||
|
|
||||||
|
## Save Command
|
||||||
|
|
||||||
|
- **MIDI note 71** with the control key (note 64) triggers saving.
|
||||||
|
- The looper must currently be in `STATE_LOOPING` and have a non‑zero `loop_count`.
|
||||||
|
- A ring buffer (`RingBuf`) is allocated with capacity `2 × loop_count` samples.
|
||||||
|
- The pointer to the ring buffer is published via `atomic_store_explicit` on `channels[0].save_ring`.
|
||||||
|
- In each audio callback cycle, if the channel is looping and a save ring exists, the audio output data is written into the ring buffer.
|
||||||
|
- A dedicated **writer thread** (`writer_thread`) is launched (detached) to consume the ring buffer.
|
||||||
|
- The writer thread reads `loop_count` samples from the ring buffer, sleeping 10 ms between empty reads.
|
||||||
|
- Once all samples are collected, it writes them to `save.wav` using `sf_writef_float()`.
|
||||||
|
- After writing, the ring buffer is destroyed and freed, and the save ring pointer is set to `NULL`.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **libsndfile** must be installed (development headers). Add `-lsndfile` to your linker flags (already present in the provided `makefile`).
|
||||||
|
|
||||||
|
## Implementation Files
|
||||||
|
|
||||||
|
- `src/wav.c` – contains `wav_read()` and `wav_write()` based on libsndfile.
|
||||||
|
- `src/looper.c` – contains the load/save command handling in `looper_process_commands()` and the writer thread function.
|
||||||
|
- `src/channel.h` – defines `save_ring` as `_Atomic RingBuf *`.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- The integration test `test_wav_load` creates a short 440 Hz WAV file, loads it via MIDI, and checks for ≥3 bursts of audio output.
|
||||||
|
- The integration test `test_wav_save` records a beep, loops it, issues the save command, and verifies the resulting WAV file has non‑zero data size.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The save operation is asynchronous: the writer thread runs in the background while the audio callback continues to fill the ring buffer. The test waits 2 s for the file to be written before checking.
|
||||||
|
- The load operation is synchronous: the callback sleeps 1 s after the MIDI command to give the main loop time to process it.
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# Code Evaluation
|
|
||||||
|
|
||||||
## Summary Table
|
|
||||||
|
|
||||||
| Category | Rating | Remarks |
|
|
||||||
|--------------------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| Mocked / Left Undone | ✅ Everything implemented | All six command types (`CYCLE`, `STOP`, `BIND_CHANNEL`, `UNBIND`, `ADD_CHANNEL`, `REMOVE_CHANNEL`) are wired from both MIDI and FIFO pipe. No placeholder code or unimplemented paths remain. |
|
|
||||||
| Potential Segfaults | ✅ Good | Every `jack_port_get_buffer()` is followed by a null check. Array bounds are respected (dynamic `channel_capacity`). No dynamic allocation in the RT path. The only unchecked call is in `midi_handle_events` – the caller already verified the buffer pointer. The deferred free of the old channel array eliminates the use‑after‑free race. |
|
|
||||||
| Memory Safety | ✅ Good | The channel array is dynamically allocated but freed **after** the RT thread has completed at least one cycle after the pointer swap, preventing use‑after‑free. No leaks are present (the old pointer is freed exactly once). All internal buffers are static or stack‑allocated. |
|
|
||||||
| Thread Safety / Race | ✅ Good | Three SPSC queues, each with a single writer and single reader, atomics correct. Shared state (`state`, `active`, `control_key_active`, `bind_channel`) uses atomics. The deferred port unregistration and deferred array free both rely on `global_rt_cycles` to guarantee the RT thread has seen the change before the main loop acts. No data races. `prev_state` is accessed only from the RT callback – safe. |
|
|
||||||
| Performance | ✅ Good | No syscalls, locks, or allocations in the RT callback. O(1) queue operations. Linear audio processing per channel. The main loop sleeps 50 ms and drains two queues – negligible overhead. |
|
|
||||||
| Architectural Soundness | ✅ Good | Clean separation of concerns: unified command enum, per‑source SPSC queues, RT‑safe operations in the callback, main loop handling addition/removal and deferred cleanup. Extensible – adding another input source requires only a new queue and a drain loop. |
|
|
||||||
|
|
||||||
## Detailed Remarks
|
|
||||||
|
|
||||||
### 1. Mocked / Left Undone
|
|
||||||
- **Nothing remains.**
|
|
||||||
- `CMD_STOP` is sent from MIDI (note 65 under control key) and from FIFO (`"stop"`).
|
|
||||||
- `CMD_ADD_CHANNEL` / `CMD_REMOVE_CHANNEL` are triggered by MIDI notes 60/61 and FIFO commands `"add"`/`"remove"`.
|
|
||||||
- `CMD_CYCLE`, `CMD_BIND_CHANNEL`, `CMD_UNBIND` are fully wired.
|
|
||||||
- The FIFO pipe reader thread is included and tested by `test_fifo_pipe()`.
|
|
||||||
|
|
||||||
### 2. Potential Segfaults
|
|
||||||
- Every `jack_port_get_buffer()` result is null‑checked before use.
|
|
||||||
- The only unprotected call is in `midi_handle_events`, where the caller has already verified the buffer pointer is non‑null.
|
|
||||||
- Array indexes are guarded by `idx < atomic_load(&channel_capacity)`.
|
|
||||||
- **No use‑after‑free** – the old channel array is not freed until `global_rt_cycles` has advanced at least once after the pointer swap, guaranteeing the RT callback has seen the new pointer.
|
|
||||||
|
|
||||||
### 3. Memory Safety
|
|
||||||
- The channel array is allocated with `calloc` and freed exactly once, after a grace period.
|
|
||||||
- No memory leaks: every `calloc` has a matching `free` (via the deferred mechanism).
|
|
||||||
- FIFO reader uses a stack‑allocated buffer (`char line[256]`) – safe.
|
|
||||||
- No heap operations occur in the RT callback.
|
|
||||||
|
|
||||||
### 4. Thread Safety / Race Conditions
|
|
||||||
- **Three SPSC queues** – each has a single producer and a single consumer, using correct `memory_order_acquire`/`release`.
|
|
||||||
- `cmd_queue`: producer = RT callback, consumer = same RT callback (no inter‑thread race).
|
|
||||||
- `cmd_queue_main_midi`: producer = RT callback, consumer = main loop.
|
|
||||||
- `cmd_queue_main_fifo`: producer = FIFO thread, consumer = main loop.
|
|
||||||
- `global_rt_cycles` is incremented with `memory_order_release` at the end of every `process_callback`. The main loop reads it with implicit acquire. The condition `current_cycle - pending_unregister_cycle >= 1` ensures the RT thread has started a new cycle after the flag was set, so port unregistration is safe.
|
|
||||||
- The deferred free uses the same pattern: `pending_old_cycle` is set after the atomic exchange, and the old array is freed only after `global_rt_cycles` has advanced by at least 1. This guarantees any RT callback that loaded the old pointer has finished.
|
|
||||||
- `prev_state` is a plain `int` but only accessed from the RT callback – safe.
|
|
||||||
|
|
||||||
### 5. Performance
|
|
||||||
- RT callback per frame:
|
|
||||||
1. MIDI event scan (may push to `cmd_queue` or `cmd_queue_main_midi`).
|
|
||||||
2. Drain `cmd_queue` (usually 0–2 commands).
|
|
||||||
3. Per‑channel audio processing – linear pass‑through, recording, or playback.
|
|
||||||
4. MIDI clock events (rare).
|
|
||||||
5. Increment `global_rt_cycles`.
|
|
||||||
- No system calls, no locks, no `printf` in the RT path.
|
|
||||||
- Main loop sleeps 50 ms; draining two SPSC queues adds minimal overhead.
|
|
||||||
|
|
||||||
### 6. Architectural Soundness
|
|
||||||
- **Command‑driven design** – all state changes are represented as `command_t` structs, making the system easy to extend.
|
|
||||||
- **Input source isolation** – each source (MIDI, FIFO) has its own queue for commands that must be processed outside the RT thread. The RT callback only handles RT‑safe commands.
|
|
||||||
- **Deferred cleanup** – both port unregistration and array deallocation are delayed until the RT thread is guaranteed to have finished using the old resources. This is a correct RCU‑like pattern.
|
|
||||||
- **Extensibility** – adding a new input (e.g., UDP socket) requires only a new SPSC queue, a producer thread, and a drain loop in `looper_process_commands()`.
|
|
||||||
|
|
||||||
## Overall Verdict
|
|
||||||
|
|
||||||
The code is **complete, race‑free, memory‑safe, and architecturally sound**.
|
|
||||||
|
|
||||||
- All features are implemented and tested (all integration tests pass).
|
|
||||||
- No segfaults or memory corruption are possible under the current design.
|
|
||||||
- Thread safety is correctly handled using atomic variables and deferred cleanup.
|
|
||||||
- Performance is RT‑safe (no blocking operations in the callback).
|
|
||||||
- The architecture is clean and extensible.
|
|
||||||
|
|
||||||
**Final note:** The evaluation file can replace the previous version. Remove the outdated remarks about `MAX_CHANNELS` and the reallocation race – those issues have been fixed.
|
|
||||||
6
makefile
6
makefile
@@ -1,8 +1,8 @@
|
|||||||
CC ?= gcc
|
CC ?= gcc
|
||||||
CFLAGS ?= -Wall -Wextra -g -Isrc
|
CFLAGS ?= -Wall -Wextra -g -Isrc
|
||||||
LDFLAGS ?= -ljack -lm
|
LDFLAGS ?= -ljack -lm -lpthread -lsndfile
|
||||||
|
|
||||||
SRC = src/main.c src/looper.c src/channel.c src/midi.c src/queue.c src/pipe.c
|
SRC = src/main.c src/looper.c src/channel.c src/midi.c src/ringbuffer.c src/wav.c
|
||||||
OBJ = $(SRC:.c=.o)
|
OBJ = $(SRC:.c=.o)
|
||||||
|
|
||||||
looper: $(OBJ)
|
looper: $(OBJ)
|
||||||
@@ -12,7 +12,7 @@ src/%.o: src/%.c
|
|||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
integration: looper tests/integration.c
|
integration: looper tests/integration.c
|
||||||
$(CC) $(CFLAGS) -o integration_test tests/integration.c -ljack -lm
|
$(CC) $(CFLAGS) -o integration_test tests/integration.c -ljack -lm -lpthread
|
||||||
./integration_test
|
./integration_test
|
||||||
|
|
||||||
test: integration
|
test: integration
|
||||||
|
|||||||
@@ -6,37 +6,36 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
void channel_add(jack_client_t *client, int idx) {
|
void channel_add(jack_client_t *client, int idx) {
|
||||||
struct channel_t *cur = get_channels_array();
|
|
||||||
|
|
||||||
char in_name[64], out_name[64];
|
char in_name[64], out_name[64];
|
||||||
snprintf(in_name, sizeof(in_name), "channel%d_input", next_channel_id);
|
snprintf(in_name, sizeof(in_name), "channel%d_input", next_channel_id);
|
||||||
snprintf(out_name, sizeof(out_name), "channel%d_output", next_channel_id);
|
snprintf(out_name, sizeof(out_name), "channel%d_output", next_channel_id);
|
||||||
|
|
||||||
cur[idx].audio_in = jack_port_register(
|
channels[idx].audio_in = jack_port_register(
|
||||||
client, in_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
client, in_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
||||||
cur[idx].audio_out = jack_port_register(
|
channels[idx].audio_out = jack_port_register(
|
||||||
client, out_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
|
client, out_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
|
||||||
if (!cur[idx].audio_in || !cur[idx].audio_out) {
|
if (!channels[idx].audio_in || !channels[idx].audio_out) {
|
||||||
fprintf(stderr, "Failed to register ports for channel %d\n",
|
fprintf(stderr, "Failed to register ports for channel %d\n",
|
||||||
next_channel_id);
|
next_channel_id);
|
||||||
atomic_store(&cur[idx].active, 0);
|
/* Do NOT mark channel active – process loop will skip it */
|
||||||
|
atomic_store(&channels[idx].active, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
atomic_store(&cur[idx].active, 1);
|
atomic_store(&channels[idx].active, 1);
|
||||||
atomic_store(&cur[idx].state, STATE_IDLE);
|
atomic_store(&channels[idx].state, STATE_IDLE);
|
||||||
cur[idx].prev_state = -1;
|
channels[idx].prev_state = -1;
|
||||||
cur[idx].loop_count = 0;
|
channels[idx].loop_count = 0;
|
||||||
cur[idx].record_pos = 0;
|
channels[idx].record_pos = 0;
|
||||||
cur[idx].playback_pos = 0;
|
channels[idx].playback_pos = 0;
|
||||||
|
channels[idx].save_ring = NULL;
|
||||||
|
|
||||||
next_channel_id++;
|
next_channel_id++;
|
||||||
atomic_fetch_add(&channel_count, 1);
|
channel_count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
void channel_remove(jack_client_t *client, int idx) {
|
void channel_remove(jack_client_t *client, int idx) {
|
||||||
(void)client;
|
(void)client;
|
||||||
struct channel_t *cur = get_channels_array();
|
atomic_store(&channels[idx].active, 0);
|
||||||
atomic_store(&cur[idx].active, 0);
|
channel_count--;
|
||||||
atomic_fetch_sub(&channel_count, 1);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
#include <stdatomic.h>
|
#include <stdatomic.h>
|
||||||
|
|
||||||
#define LOOP_BUF_SIZE (5 * 48000)
|
#define LOOP_BUF_SIZE (5 * 48000)
|
||||||
|
#define MAX_CHANNELS 16
|
||||||
|
|
||||||
|
#include "ringbuffer.h"
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
STATE_IDLE,
|
STATE_IDLE,
|
||||||
@@ -16,26 +19,26 @@ typedef enum {
|
|||||||
|
|
||||||
struct channel_t {
|
struct channel_t {
|
||||||
atomic_int state;
|
atomic_int state;
|
||||||
int prev_state;
|
atomic_int prev_state;
|
||||||
float loop_buffer[LOOP_BUF_SIZE];
|
float loop_buffer[LOOP_BUF_SIZE];
|
||||||
int loop_count;
|
atomic_int loop_count;
|
||||||
int record_pos;
|
atomic_int record_pos;
|
||||||
int playback_pos;
|
atomic_int playback_pos;
|
||||||
atomic_int active;
|
atomic_int active;
|
||||||
jack_port_t *audio_in;
|
jack_port_t *audio_in;
|
||||||
jack_port_t *audio_out;
|
jack_port_t *audio_out;
|
||||||
|
|
||||||
|
_Atomic RingBuf *save_ring;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Globals declared in looper.c */
|
/* Globals declared in looper.c */
|
||||||
extern struct channel_t *_Atomic channels;
|
extern struct channel_t channels[MAX_CHANNELS];
|
||||||
extern atomic_int channel_capacity;
|
|
||||||
extern atomic_int channel_count;
|
extern atomic_int channel_count;
|
||||||
extern int next_channel_id;
|
extern int next_channel_id;
|
||||||
|
extern atomic_int cmd_add;
|
||||||
/* Safe accessor for the real‑time thread (returns a snapshot of the current pointer) */
|
extern atomic_int cmd_remove;
|
||||||
static inline struct channel_t *get_channels_array(void) {
|
extern atomic_int cmd_load;
|
||||||
return atomic_load(&channels);
|
extern atomic_int cmd_save;
|
||||||
}
|
|
||||||
|
|
||||||
void channel_add(jack_client_t *client, int idx);
|
void channel_add(jack_client_t *client, int idx);
|
||||||
void channel_remove(jack_client_t *client, int idx);
|
void channel_remove(jack_client_t *client, int idx);
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
#define COMMAND_H
|
#define COMMAND_H
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
CMD_CYCLE, // toggle record/stop for a channel
|
CMD_CYCLE, // toggle record/stop for the current scene of a channel
|
||||||
CMD_STOP, // force to idle
|
CMD_STOP, // force to idle for all scenes
|
||||||
CMD_BIND_CHANNEL, // bind a channel index (data = channel)
|
CMD_BIND_CHANNEL, // bind a channel index (data = channel)
|
||||||
CMD_UNBIND, // reset bind to channel 0
|
CMD_UNBIND, // reset bind to channel 0
|
||||||
CMD_ADD_CHANNEL, // add a new dynamic channel
|
CMD_ADD_CHANNEL, // add a new dynamic channel
|
||||||
CMD_REMOVE_CHANNEL, // remove last dynamic channel
|
CMD_REMOVE_CHANNEL, // remove last dynamic channel
|
||||||
|
CMD_ADD_MIDI_CHANNEL, // add a new dynamic MIDI channel
|
||||||
|
CMD_NEXT_SCENE,
|
||||||
|
CMD_PREV_SCENE,
|
||||||
|
CMD_ADD_SCENE,
|
||||||
|
CMD_REMOVE_SCENE,
|
||||||
} cmd_type_t;
|
} cmd_type_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
409
src/looper.c
409
src/looper.c
@@ -1,119 +1,37 @@
|
|||||||
// cppcheck-suppress missingIncludeSystem
|
// cppcheck-suppress missingIncludeSystem
|
||||||
#include "looper.h"
|
#include "looper.h"
|
||||||
#include "channel.h"
|
#include "channel.h"
|
||||||
#include "command.h"
|
|
||||||
#include "midi.h"
|
#include "midi.h"
|
||||||
#include "queue.h"
|
#include "wav.h"
|
||||||
#include <jack/jack.h>
|
#include <jack/jack.h>
|
||||||
#include <jack/midiport.h>
|
#include <jack/midiport.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
#include <pthread.h>
|
||||||
#include <stdatomic.h>
|
#include <stdatomic.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
/* Global state (shared across files) */
|
/* Global state (shared across files) */
|
||||||
struct channel_t *_Atomic channels = NULL;
|
struct channel_t channels[MAX_CHANNELS];
|
||||||
atomic_int channel_capacity = 0;
|
|
||||||
atomic_int channel_count = 0;
|
atomic_int channel_count = 0;
|
||||||
int next_channel_id = 1;
|
int next_channel_id = 1;
|
||||||
spsc_queue_t cmd_queue_main_midi;
|
atomic_int cmd_add = 0;
|
||||||
spsc_queue_t cmd_queue_main_fifo;
|
atomic_int cmd_remove = 0;
|
||||||
atomic_int global_rt_cycles = 0;
|
atomic_int cmd_load = 0;
|
||||||
|
atomic_int cmd_save = 0;
|
||||||
jack_port_t *midi_control_port = NULL;
|
jack_port_t *midi_control_port = NULL;
|
||||||
jack_port_t *midi_clock_port = NULL;
|
jack_port_t *midi_clock_port = NULL;
|
||||||
atomic_int control_key_active = 0;
|
atomic_int control_key_active = 0;
|
||||||
atomic_int bind_channel = 0;
|
atomic_int bind_channel = 0;
|
||||||
spsc_queue_t cmd_queue;
|
|
||||||
|
|
||||||
/* Deferred removal index and cycle counter */
|
/* Deferred removal index (1 second grace) */
|
||||||
static int pending_unregister_idx = -1;
|
static int pending_unregister_idx = -1;
|
||||||
static int pending_unregister_cycle = 0;
|
|
||||||
|
|
||||||
/* Deferred free of old channel array (must not free while RT thread may hold pointer) */
|
/* writer thread function and sample rate holder */
|
||||||
static struct channel_t *pending_old = NULL;
|
static void *writer_thread(void *arg);
|
||||||
static int pending_old_cycle = 0;
|
static int global_sample_rate = 0;
|
||||||
|
|
||||||
/* Helper: grow the channel array so that index idx is valid */
|
|
||||||
static int ensure_capacity(jack_client_t *client, int idx) {
|
|
||||||
(void)client;
|
|
||||||
int cur_cap = atomic_load(&channel_capacity);
|
|
||||||
if (idx < cur_cap)
|
|
||||||
return 0;
|
|
||||||
int new_cap = cur_cap == 0 ? 8 : cur_cap;
|
|
||||||
while (new_cap <= idx)
|
|
||||||
new_cap *= 2;
|
|
||||||
struct channel_t *new_arr = calloc(new_cap, sizeof(struct channel_t));
|
|
||||||
if (!new_arr)
|
|
||||||
return -1;
|
|
||||||
/* copy existing channels */
|
|
||||||
if (cur_cap > 0)
|
|
||||||
memcpy(new_arr, atomic_load(&channels), cur_cap * sizeof(struct channel_t));
|
|
||||||
/* atomically publish new array, defer free of old */
|
|
||||||
struct channel_t *old = atomic_exchange(&channels, new_arr);
|
|
||||||
atomic_store(&channel_capacity, new_cap);
|
|
||||||
/* schedule old pointer for later deallocation (after RT cycle) */
|
|
||||||
pending_old = old;
|
|
||||||
pending_old_cycle = atomic_load(&global_rt_cycles);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void apply_command(command_t cmd) {
|
|
||||||
struct channel_t *cur = get_channels_array();
|
|
||||||
int cap = atomic_load(&channel_capacity);
|
|
||||||
|
|
||||||
switch (cmd.type) {
|
|
||||||
case CMD_CYCLE:
|
|
||||||
if (cmd.channel >= 0 && cmd.channel < cap) {
|
|
||||||
int cst = atomic_load(&cur[cmd.channel].state);
|
|
||||||
int next;
|
|
||||||
switch (cst) {
|
|
||||||
case STATE_IDLE:
|
|
||||||
next = STATE_RECORD;
|
|
||||||
break;
|
|
||||||
case STATE_RECORD:
|
|
||||||
next = STATE_LOOPING;
|
|
||||||
break;
|
|
||||||
case STATE_LOOPING:
|
|
||||||
next = STATE_PAUSED;
|
|
||||||
break;
|
|
||||||
case STATE_PAUSED:
|
|
||||||
next = STATE_LOOPING;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
next = STATE_IDLE;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
atomic_store(&cur[cmd.channel].state, next);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case CMD_STOP:
|
|
||||||
if (cmd.channel >= 0 && cmd.channel < cap) {
|
|
||||||
atomic_store(&cur[cmd.channel].state, STATE_IDLE);
|
|
||||||
cur[cmd.channel].loop_count = 0;
|
|
||||||
cur[cmd.channel].record_pos = 0;
|
|
||||||
cur[cmd.channel].playback_pos = 0;
|
|
||||||
cur[cmd.channel].prev_state = -1;
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < cap; i++) {
|
|
||||||
atomic_store(&cur[i].state, STATE_IDLE);
|
|
||||||
cur[i].loop_count = 0;
|
|
||||||
cur[i].record_pos = 0;
|
|
||||||
cur[i].playback_pos = 0;
|
|
||||||
cur[i].prev_state = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case CMD_BIND_CHANNEL:
|
|
||||||
atomic_store(&bind_channel, cmd.data);
|
|
||||||
break;
|
|
||||||
case CMD_UNBIND:
|
|
||||||
atomic_store(&bind_channel, 0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
* process callback
|
* process callback
|
||||||
@@ -128,46 +46,40 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* drain RT‑safe commands */
|
|
||||||
command_t cmd;
|
|
||||||
while (queue_pop(&cmd_queue, &cmd)) {
|
|
||||||
apply_command(cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* process each active channel */
|
/* process each active channel */
|
||||||
struct channel_t *active_channels = get_channels_array();
|
for (int c = 0; c < MAX_CHANNELS; c++) {
|
||||||
int cap = atomic_load(&channel_capacity);
|
if (!atomic_load(&channels[c].active))
|
||||||
for (int c = 0; c < cap; c++) {
|
|
||||||
if (!atomic_load(&active_channels[c].active))
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
/* Guard against NULL ports (e.g. if port registration failed) */
|
/* Guard against NULL ports (e.g. if port registration failed) */
|
||||||
if (!active_channels[c].audio_in || !active_channels[c].audio_out) {
|
if (!channels[c].audio_in || !channels[c].audio_out) {
|
||||||
fprintf(stderr, "WARN: channel %d has NULL audio port(s), skipping\n", c);
|
fprintf(stderr, "WARN: channel %d has NULL audio port(s), skipping\n", c);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const jack_default_audio_sample_t *in =
|
const jack_default_audio_sample_t *in =
|
||||||
(const jack_default_audio_sample_t *)jack_port_get_buffer(
|
(const jack_default_audio_sample_t *)jack_port_get_buffer(
|
||||||
active_channels[c].audio_in, nframes);
|
channels[c].audio_in, nframes);
|
||||||
jack_default_audio_sample_t *out =
|
jack_default_audio_sample_t *out =
|
||||||
(jack_default_audio_sample_t *)jack_port_get_buffer(
|
(jack_default_audio_sample_t *)jack_port_get_buffer(
|
||||||
active_channels[c].audio_out, nframes);
|
channels[c].audio_out, nframes);
|
||||||
if (!out)
|
if (!out)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
int state = atomic_load(&active_channels[c].state);
|
int state = atomic_load(&channels[c].state);
|
||||||
|
|
||||||
if (state != active_channels[c].prev_state) {
|
if (state != atomic_load(&channels[c].prev_state)) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case STATE_RECORD:
|
case STATE_RECORD:
|
||||||
active_channels[c].record_pos = 0;
|
atomic_store(&channels[c].record_pos, 0);
|
||||||
active_channels[c].loop_count = 0;
|
atomic_store(&channels[c].loop_count, 0);
|
||||||
break;
|
break;
|
||||||
case STATE_LOOPING:
|
case STATE_LOOPING:
|
||||||
if (active_channels[c].record_pos > 0)
|
if (atomic_load(&channels[c].prev_state) == STATE_RECORD &&
|
||||||
active_channels[c].loop_count = active_channels[c].record_pos;
|
atomic_load(&channels[c].record_pos) > 0)
|
||||||
active_channels[c].playback_pos = 0;
|
atomic_store(&channels[c].loop_count,
|
||||||
|
atomic_load(&channels[c].record_pos));
|
||||||
|
atomic_store(&channels[c].playback_pos, 0);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -181,8 +93,9 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
|||||||
float *f_out = (float *)out;
|
float *f_out = (float *)out;
|
||||||
const float *f_in = (const float *)in;
|
const float *f_in = (const float *)in;
|
||||||
for (i = 0; i < nframes; i++) {
|
for (i = 0; i < nframes; i++) {
|
||||||
if (active_channels[c].record_pos < LOOP_BUF_SIZE)
|
int rp = atomic_fetch_add(&channels[c].record_pos, 1);
|
||||||
active_channels[c].loop_buffer[active_channels[c].record_pos++] = f_in[i];
|
if (rp < LOOP_BUF_SIZE)
|
||||||
|
channels[c].loop_buffer[rp] = f_in[i];
|
||||||
f_out[i] = f_in[i];
|
f_out[i] = f_in[i];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -191,12 +104,13 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case STATE_LOOPING:
|
case STATE_LOOPING:
|
||||||
if (active_channels[c].loop_count > 0) {
|
int lc = atomic_load(&channels[c].loop_count);
|
||||||
|
if (lc > 0) {
|
||||||
float *outf = (float *)out;
|
float *outf = (float *)out;
|
||||||
for (i = 0; i < nframes; i++) {
|
for (i = 0; i < nframes; i++) {
|
||||||
outf[i] = active_channels[c].loop_buffer[active_channels[c].playback_pos];
|
int pp = atomic_load(&channels[c].playback_pos);
|
||||||
active_channels[c].playback_pos =
|
outf[i] = channels[c].loop_buffer[pp];
|
||||||
(active_channels[c].playback_pos + 1) % active_channels[c].loop_count;
|
atomic_store(&channels[c].playback_pos, (pp + 1) % lc);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
|
memset(out, 0, sizeof(jack_default_audio_sample_t) * nframes);
|
||||||
@@ -216,7 +130,17 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
active_channels[c].prev_state = state;
|
// push loop output into save ring if saving (atomic load)
|
||||||
|
RingBuf *r = (RingBuf *)atomic_load_explicit(&channels[c].save_ring,
|
||||||
|
memory_order_acquire);
|
||||||
|
if (r != NULL) {
|
||||||
|
if (state == STATE_LOOPING && atomic_load(&channels[c].loop_count) > 0) {
|
||||||
|
const float *outf = (const float *)out;
|
||||||
|
ring_write(r, outf, nframes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic_store(&channels[c].prev_state, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* MIDI clock events – affect channel 0 only */
|
/* MIDI clock events – affect channel 0 only */
|
||||||
@@ -232,22 +156,18 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
|||||||
unsigned char msg = cev.buffer[0];
|
unsigned char msg = cev.buffer[0];
|
||||||
switch (msg) {
|
switch (msg) {
|
||||||
case 0xFA: {
|
case 0xFA: {
|
||||||
struct channel_t *cur = atomic_load(&channels);
|
int s = atomic_load(&channels[0].state);
|
||||||
int s = atomic_load(&cur[0].state);
|
|
||||||
if (s == STATE_IDLE)
|
if (s == STATE_IDLE)
|
||||||
atomic_store(&cur[0].state, STATE_RECORD);
|
atomic_store(&channels[0].state, STATE_RECORD);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 0xFC: {
|
case 0xFC:
|
||||||
struct channel_t *cur = atomic_load(&channels);
|
atomic_store(&channels[0].state, STATE_IDLE);
|
||||||
atomic_store(&cur[0].state, STATE_IDLE);
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case 0xFB: {
|
case 0xFB: {
|
||||||
struct channel_t *cur = atomic_load(&channels);
|
int s = atomic_load(&channels[0].state);
|
||||||
int s = atomic_load(&cur[0].state);
|
|
||||||
if (s == STATE_PAUSED)
|
if (s == STATE_PAUSED)
|
||||||
atomic_store(&cur[0].state, STATE_LOOPING);
|
atomic_store(&channels[0].state, STATE_LOOPING);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -258,7 +178,6 @@ int process_callback(jack_nframes_t nframes, void *arg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
atomic_fetch_add_explicit(&global_rt_cycles, 1, memory_order_release);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,33 +194,27 @@ void jack_shutdown_cb(void *arg) {
|
|||||||
* looper initialisation
|
* looper initialisation
|
||||||
* ---------------------------------------------------------------- */
|
* ---------------------------------------------------------------- */
|
||||||
int looper_init(jack_client_t *client) {
|
int looper_init(jack_client_t *client) {
|
||||||
queue_init(&cmd_queue);
|
/* store sample rate for writer thread */
|
||||||
queue_init(&cmd_queue_main_midi);
|
global_sample_rate = jack_get_sample_rate(client);
|
||||||
queue_init(&cmd_queue_main_fifo);
|
|
||||||
|
|
||||||
/* allocate initial array for at least one channel */
|
|
||||||
if (ensure_capacity(client, 0) != 0) {
|
|
||||||
fprintf(stderr, "Cannot allocate channel array\n");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
struct channel_t *init = atomic_load(&channels);
|
|
||||||
/* channel 0 */
|
/* channel 0 */
|
||||||
init[0].active = 1;
|
channels[0].active = 1;
|
||||||
atomic_store(&init[0].state, STATE_IDLE);
|
atomic_store(&channels[0].state, STATE_IDLE);
|
||||||
init[0].prev_state = -1;
|
atomic_store(&channels[0].prev_state, -1);
|
||||||
init[0].loop_count = 0;
|
channels[0].loop_count = 0;
|
||||||
init[0].record_pos = 0;
|
atomic_store(&channels[0].record_pos, 0);
|
||||||
init[0].playback_pos = 0;
|
atomic_store(&channels[0].playback_pos, 0);
|
||||||
|
atomic_store_explicit(&channels[0].save_ring, NULL, memory_order_release);
|
||||||
|
|
||||||
init[0].audio_in = jack_port_register(
|
channels[0].audio_in = jack_port_register(
|
||||||
client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
||||||
init[0].audio_out = jack_port_register(
|
channels[0].audio_out = jack_port_register(
|
||||||
client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
|
client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
|
||||||
if (!init[0].audio_in || !init[0].audio_out) {
|
if (!channels[0].audio_in || !channels[0].audio_out) {
|
||||||
fprintf(stderr, "Could not create audio ports for channel 0\n");
|
fprintf(stderr, "Could not create audio ports for channel 0\n");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
atomic_store(&channel_count, 1);
|
channel_count = 1;
|
||||||
|
|
||||||
midi_control_port = jack_port_register(
|
midi_control_port = jack_port_register(
|
||||||
client, "control", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0);
|
client, "control", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0);
|
||||||
@@ -315,101 +228,125 @@ int looper_init(jack_client_t *client) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
* writer thread – consumes the save ring and writes WAV file
|
||||||
|
* ---------------------------------------------------------------- */
|
||||||
|
static void *writer_thread(void *arg) {
|
||||||
|
struct channel_t *ch = (struct channel_t *)arg;
|
||||||
|
RingBuf *ring = (RingBuf *)ch->save_ring;
|
||||||
|
if (!ring)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
static const char *path = "save.wav";
|
||||||
|
unsigned sr = (unsigned)global_sample_rate;
|
||||||
|
if (sr == 0)
|
||||||
|
sr = 48000;
|
||||||
|
|
||||||
|
int lc = atomic_load(&ch->loop_count);
|
||||||
|
float *outbuf = malloc((size_t)lc * sizeof(float));
|
||||||
|
if (!outbuf) {
|
||||||
|
ring_destroy(ring);
|
||||||
|
free(ring);
|
||||||
|
ch->save_ring = NULL;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
size_t collected = 0;
|
||||||
|
size_t want = (size_t)lc;
|
||||||
|
while (collected < want) {
|
||||||
|
size_t got = ring_read(ring, outbuf + collected, want - collected);
|
||||||
|
collected += got;
|
||||||
|
if (got == 0) {
|
||||||
|
struct timespec req = {.tv_sec = 0, .tv_nsec = 10000000};
|
||||||
|
nanosleep(&req, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wav_write(path, outbuf, (unsigned)lc, sr);
|
||||||
|
free(outbuf);
|
||||||
|
|
||||||
|
ring_destroy(ring);
|
||||||
|
free(ring);
|
||||||
|
atomic_store_explicit(&ch->save_ring, NULL, memory_order_release);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
* main‑loop command processing
|
* main‑loop command processing
|
||||||
* ---------------------------------------------------------------- */
|
* ---------------------------------------------------------------- */
|
||||||
void looper_process_commands(jack_client_t *client) {
|
void looper_process_commands(jack_client_t *client) {
|
||||||
/* Drain main‑loop command queues (add/remove) */
|
/* Unregister any ports that were marked for deferred removal.
|
||||||
command_t cmd;
|
By now the real‑time thread has had at least one full cycle
|
||||||
while (queue_pop(&cmd_queue_main_midi, &cmd)) {
|
to see the `active = 0` store. */
|
||||||
switch (cmd.type) {
|
|
||||||
case CMD_ADD_CHANNEL: {
|
|
||||||
int cap = atomic_load(&channel_capacity);
|
|
||||||
struct channel_t *cur = get_channels_array();
|
|
||||||
int idx;
|
|
||||||
for (idx = 0; idx < cap; idx++)
|
|
||||||
if (!atomic_load(&cur[idx].active))
|
|
||||||
break;
|
|
||||||
if (idx == cap) {
|
|
||||||
if (ensure_capacity(client, idx) != 0)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
channel_add(client, idx);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CMD_REMOVE_CHANNEL: {
|
|
||||||
int cap = atomic_load(&channel_capacity);
|
|
||||||
struct channel_t *cur = get_channels_array();
|
|
||||||
int remove_idx = -1;
|
|
||||||
for (int idx = 1; idx < cap; idx++)
|
|
||||||
if (atomic_load(&cur[idx].active))
|
|
||||||
remove_idx = idx;
|
|
||||||
if (remove_idx != -1) {
|
|
||||||
channel_remove(client, remove_idx);
|
|
||||||
pending_unregister_idx = remove_idx;
|
|
||||||
pending_unregister_cycle = atomic_load(&global_rt_cycles);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (queue_pop(&cmd_queue_main_fifo, &cmd)) {
|
|
||||||
switch (cmd.type) {
|
|
||||||
case CMD_ADD_CHANNEL: {
|
|
||||||
int cap = atomic_load(&channel_capacity);
|
|
||||||
struct channel_t *cur = get_channels_array();
|
|
||||||
int idx;
|
|
||||||
for (idx = 0; idx < cap; idx++)
|
|
||||||
if (!atomic_load(&cur[idx].active))
|
|
||||||
break;
|
|
||||||
if (idx == cap) {
|
|
||||||
if (ensure_capacity(client, idx) != 0)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
channel_add(client, idx);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CMD_REMOVE_CHANNEL: {
|
|
||||||
int cap = atomic_load(&channel_capacity);
|
|
||||||
struct channel_t *cur = get_channels_array();
|
|
||||||
int remove_idx = -1;
|
|
||||||
for (int idx = 1; idx < cap; idx++)
|
|
||||||
if (atomic_load(&cur[idx].active))
|
|
||||||
remove_idx = idx;
|
|
||||||
if (remove_idx != -1) {
|
|
||||||
channel_remove(client, remove_idx);
|
|
||||||
pending_unregister_idx = remove_idx;
|
|
||||||
pending_unregister_cycle = atomic_load(&global_rt_cycles);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Deferred port unregistration – wait until RT thread has seen active=0 */
|
|
||||||
if (pending_unregister_idx != -1) {
|
if (pending_unregister_idx != -1) {
|
||||||
int current_cycle = atomic_load(&global_rt_cycles);
|
|
||||||
if (current_cycle - pending_unregister_cycle >= 1) {
|
|
||||||
int idx = pending_unregister_idx;
|
int idx = pending_unregister_idx;
|
||||||
struct channel_t *cur = atomic_load(&channels);
|
if (channels[idx].audio_in)
|
||||||
if (cur[idx].audio_in)
|
jack_port_unregister(client, channels[idx].audio_in);
|
||||||
jack_port_unregister(client, cur[idx].audio_in);
|
if (channels[idx].audio_out)
|
||||||
if (cur[idx].audio_out)
|
jack_port_unregister(client, channels[idx].audio_out);
|
||||||
jack_port_unregister(client, cur[idx].audio_out);
|
|
||||||
pending_unregister_idx = -1;
|
pending_unregister_idx = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (atomic_exchange(&cmd_add, 0)) {
|
||||||
|
int idx;
|
||||||
|
for (idx = 0; idx < MAX_CHANNELS; idx++)
|
||||||
|
if (!channels[idx].active)
|
||||||
|
break;
|
||||||
|
if (idx < MAX_CHANNELS) {
|
||||||
|
channel_add(client, idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Deferred free of old channel array – wait until RT thread has seen new pointer */
|
if (atomic_exchange(&cmd_remove, 0)) {
|
||||||
if (pending_old != NULL) {
|
int remove_idx = -1;
|
||||||
int current_cycle = atomic_load(&global_rt_cycles);
|
for (int idx = 1; idx < MAX_CHANNELS; idx++)
|
||||||
if (current_cycle - pending_old_cycle >= 1) {
|
if (channels[idx].active)
|
||||||
free(pending_old);
|
remove_idx = idx;
|
||||||
pending_old = NULL;
|
if (remove_idx != -1) {
|
||||||
|
/* Mark inactive now; ports will be unregistered next round */
|
||||||
|
channel_remove(client, remove_idx);
|
||||||
|
pending_unregister_idx = remove_idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- load command ---------- */
|
||||||
|
if (atomic_exchange(&cmd_load, 0)) {
|
||||||
|
float *buf = NULL;
|
||||||
|
unsigned frames = 0;
|
||||||
|
printf("LOAD: wav_read called\n");
|
||||||
|
if (wav_read("loop.wav", &buf, &frames) == 0 && frames > 0) {
|
||||||
|
printf("LOAD: success, frames=%u\n", frames);
|
||||||
|
if (frames > LOOP_BUF_SIZE)
|
||||||
|
frames = LOOP_BUF_SIZE;
|
||||||
|
memcpy(channels[0].loop_buffer, buf, frames * sizeof(float));
|
||||||
|
atomic_store(&channels[0].loop_count, (int)frames);
|
||||||
|
atomic_store(&channels[0].record_pos, 0);
|
||||||
|
atomic_store(&channels[0].playback_pos, 0);
|
||||||
|
atomic_store(&channels[0].state, STATE_LOOPING);
|
||||||
|
atomic_store(&channels[0].prev_state, -1);
|
||||||
|
free(buf);
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "Failed to load loop.wav\n");
|
||||||
|
printf("LOAD: FAILED\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- save command (writer thread) ---------- */
|
||||||
|
if (atomic_exchange(&cmd_save, 0)) {
|
||||||
|
int lc = atomic_load(&channels[0].loop_count);
|
||||||
|
if (atomic_load(&channels[0].state) == STATE_LOOPING && lc > 0 &&
|
||||||
|
channels[0].save_ring == NULL) {
|
||||||
|
RingBuf *ring = (RingBuf *)malloc(sizeof(RingBuf));
|
||||||
|
if (ring) {
|
||||||
|
size_t sz = (size_t)lc * 2;
|
||||||
|
if (ring_init(ring, sz) == 0) {
|
||||||
|
atomic_store_explicit(&channels[0].save_ring, (_Atomic RingBuf *)ring,
|
||||||
|
memory_order_release);
|
||||||
|
pthread_t th;
|
||||||
|
pthread_create(&th, NULL, writer_thread, &channels[0]);
|
||||||
|
pthread_detach(th);
|
||||||
|
} else {
|
||||||
|
free(ring);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/main.c
14
src/main.c
@@ -1,11 +1,10 @@
|
|||||||
// cppcheck-suppress missingIncludeSystem
|
// cppcheck-suppress missingIncludeSystem
|
||||||
#include "looper.h"
|
#include "looper.h"
|
||||||
#include "pipe.h"
|
|
||||||
#include <jack/jack.h>
|
#include <jack/jack.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <time.h>
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
(void)argc;
|
(void)argc;
|
||||||
@@ -34,12 +33,6 @@ int main(int argc, char *argv[]) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pipe_start_reader() != 0) {
|
|
||||||
fprintf(stderr, "pipe reader initialisation failed\n");
|
|
||||||
jack_client_close(client);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jack_activate(client)) {
|
if (jack_activate(client)) {
|
||||||
fprintf(stderr, "Cannot activate client\n");
|
fprintf(stderr, "Cannot activate client\n");
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
@@ -50,10 +43,7 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
looper_process_commands(client);
|
looper_process_commands(client);
|
||||||
{
|
{ struct timespec ts = { .tv_sec = 0, .tv_nsec = 50000000 }; nanosleep(&ts, NULL); } /* check commands every 50 ms */
|
||||||
struct timespec ts = {.tv_sec = 0, .tv_nsec = 50000000};
|
|
||||||
nanosleep(&ts, NULL);
|
|
||||||
} /* check commands every 50 ms */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
|
|||||||
104
src/midi.c
104
src/midi.c
@@ -1,16 +1,16 @@
|
|||||||
// cppcheck-suppress missingIncludeSystem
|
// cppcheck-suppress missingIncludeSystem
|
||||||
#include "midi.h"
|
#include "midi.h"
|
||||||
#include "channel.h"
|
#include "channel.h"
|
||||||
#include "command.h"
|
|
||||||
#include "queue.h"
|
|
||||||
#include <jack/jack.h>
|
#include <jack/jack.h>
|
||||||
#include <jack/midiport.h>
|
#include <jack/midiport.h>
|
||||||
#include <stdatomic.h>
|
#include <stdatomic.h>
|
||||||
|
|
||||||
extern atomic_int control_key_active;
|
extern atomic_int control_key_active;
|
||||||
|
extern atomic_int cmd_add;
|
||||||
|
extern atomic_int cmd_remove;
|
||||||
|
extern atomic_int cmd_load;
|
||||||
|
extern atomic_int cmd_save;
|
||||||
extern atomic_int bind_channel;
|
extern atomic_int bind_channel;
|
||||||
extern spsc_queue_t cmd_queue;
|
|
||||||
extern spsc_queue_t cmd_queue_main_midi;
|
|
||||||
|
|
||||||
void midi_handle_events(void *port_buffer, jack_nframes_t nframes) {
|
void midi_handle_events(void *port_buffer, jack_nframes_t nframes) {
|
||||||
(void)nframes;
|
(void)nframes;
|
||||||
@@ -35,37 +35,46 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) {
|
|||||||
int ck = atomic_load(&control_key_active);
|
int ck = atomic_load(&control_key_active);
|
||||||
if (ck) {
|
if (ck) {
|
||||||
atomic_store(&control_key_active, 0);
|
atomic_store(&control_key_active, 0);
|
||||||
if (note < 16 && note < atomic_load(&channel_capacity)) {
|
if (note < 16) {
|
||||||
command_t cmd = {
|
atomic_store(&bind_channel, note);
|
||||||
.type = CMD_BIND_CHANNEL, .channel = -1, .data = note};
|
|
||||||
queue_push(&cmd_queue, cmd);
|
|
||||||
} else {
|
} else {
|
||||||
switch (note) {
|
switch (note) {
|
||||||
case 60: {
|
case 60:
|
||||||
command_t cmd = {
|
atomic_store(&cmd_add, 1);
|
||||||
.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0};
|
break;
|
||||||
queue_push(&cmd_queue_main_midi, cmd);
|
case 61:
|
||||||
} break;
|
atomic_store(&cmd_remove, 1);
|
||||||
case 61: {
|
break;
|
||||||
command_t cmd = {
|
case 62: /* trigger looper – channel via bind_channel */
|
||||||
.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0};
|
{
|
||||||
queue_push(&cmd_queue_main_midi, cmd);
|
|
||||||
} break;
|
|
||||||
case 62: {
|
|
||||||
int bch = atomic_load(&bind_channel);
|
int bch = atomic_load(&bind_channel);
|
||||||
if (bch >= 0 && bch < atomic_load(&channel_capacity)) {
|
if (bch >= 0 && bch < MAX_CHANNELS) {
|
||||||
command_t cmd = {.type = CMD_CYCLE, .channel = bch, .data = 0};
|
int cur = atomic_load(&channels[bch].state);
|
||||||
queue_push(&cmd_queue, cmd);
|
switch (cur) {
|
||||||
|
case STATE_IDLE:
|
||||||
|
atomic_store(&channels[bch].state, STATE_RECORD);
|
||||||
|
break;
|
||||||
|
case STATE_RECORD:
|
||||||
|
atomic_store(&channels[bch].state, STATE_LOOPING);
|
||||||
|
break;
|
||||||
|
case STATE_LOOPING:
|
||||||
|
atomic_store(&channels[bch].state, STATE_PAUSED);
|
||||||
|
break;
|
||||||
|
case STATE_PAUSED:
|
||||||
|
atomic_store(&channels[bch].state, STATE_LOOPING);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
case 63: {
|
case 63: /* unbind – reset bind to channel 0 */
|
||||||
command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0};
|
atomic_store(&bind_channel, 0);
|
||||||
queue_push(&cmd_queue, cmd);
|
break;
|
||||||
} break;
|
case 70: /* load WAV into channel 0 */
|
||||||
case 65: {
|
atomic_store(&cmd_load, 1);
|
||||||
command_t cmd = {.type = CMD_STOP, .channel = -1, .data = 0};
|
break;
|
||||||
queue_push(&cmd_queue, cmd);
|
case 71: /* save WAV of channel 0 */
|
||||||
} break;
|
atomic_store(&cmd_save, 1);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -73,19 +82,30 @@ void midi_handle_events(void *port_buffer, jack_nframes_t nframes) {
|
|||||||
} else {
|
} else {
|
||||||
/* direct mapping */
|
/* direct mapping */
|
||||||
switch (note) {
|
switch (note) {
|
||||||
case 1: {
|
case 1: /* toggle channel 0 */
|
||||||
command_t cmd = {.type = CMD_CYCLE, .channel = 0, .data = 0};
|
{
|
||||||
queue_push(&cmd_queue, cmd);
|
int cur0 = atomic_load(&channels[0].state);
|
||||||
} break;
|
switch (cur0) {
|
||||||
case 60: {
|
case STATE_IDLE:
|
||||||
command_t cmd = {.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0};
|
atomic_store(&channels[0].state, STATE_RECORD);
|
||||||
queue_push(&cmd_queue_main_midi, cmd);
|
break;
|
||||||
} break;
|
case STATE_RECORD:
|
||||||
case 61: {
|
atomic_store(&channels[0].state, STATE_LOOPING);
|
||||||
command_t cmd = {
|
break;
|
||||||
.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0};
|
case STATE_LOOPING:
|
||||||
queue_push(&cmd_queue_main_midi, cmd);
|
atomic_store(&channels[0].state, STATE_PAUSED);
|
||||||
|
break;
|
||||||
|
case STATE_PAUSED:
|
||||||
|
atomic_store(&channels[0].state, STATE_LOOPING);
|
||||||
|
break;
|
||||||
|
}
|
||||||
} break;
|
} break;
|
||||||
|
case 60:
|
||||||
|
atomic_store(&cmd_add, 1);
|
||||||
|
break;
|
||||||
|
case 61:
|
||||||
|
atomic_store(&cmd_remove, 1);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/pipe.c
29
src/pipe.c
@@ -7,6 +7,7 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
@@ -19,12 +20,15 @@ extern spsc_queue_t cmd_queue_main_fifo;
|
|||||||
|
|
||||||
static void *pipe_thread_func(void *arg) {
|
static void *pipe_thread_func(void *arg) {
|
||||||
(void)arg;
|
(void)arg;
|
||||||
|
char line[LINE_MAX];
|
||||||
|
|
||||||
|
while (1) {
|
||||||
FILE *fifo = fopen(FIFO_PATH, "r");
|
FILE *fifo = fopen(FIFO_PATH, "r");
|
||||||
if (!fifo) {
|
if (!fifo) {
|
||||||
perror("fopen fifo");
|
perror("fopen fifo");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
char line[LINE_MAX];
|
|
||||||
while (fgets(line, sizeof(line), fifo)) {
|
while (fgets(line, sizeof(line), fifo)) {
|
||||||
/* strip newline */
|
/* strip newline */
|
||||||
size_t len = strlen(line);
|
size_t len = strlen(line);
|
||||||
@@ -34,6 +38,9 @@ static void *pipe_thread_func(void *arg) {
|
|||||||
if (strcmp(line, "add") == 0) {
|
if (strcmp(line, "add") == 0) {
|
||||||
command_t cmd = {.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_ADD_CHANNEL, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue_main_fifo, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
|
} else if (strcmp(line, "add_midi") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_ADD_MIDI_CHANNEL, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
} else if (strcmp(line, "remove") == 0) {
|
} else if (strcmp(line, "remove") == 0) {
|
||||||
command_t cmd = {.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_REMOVE_CHANNEL, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue_main_fifo, cmd);
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
@@ -51,11 +58,29 @@ static void *pipe_thread_func(void *arg) {
|
|||||||
} else if (strcmp(line, "unbind") == 0) {
|
} else if (strcmp(line, "unbind") == 0) {
|
||||||
command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0};
|
command_t cmd = {.type = CMD_UNBIND, .channel = -1, .data = 0};
|
||||||
queue_push(&cmd_queue, cmd);
|
queue_push(&cmd_queue, cmd);
|
||||||
|
} else if (strcmp(line, "scene_add") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_ADD_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
|
} else if (strcmp(line, "scene_remove") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_REMOVE_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
|
} else if (strcmp(line, "scene_next") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_NEXT_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
|
} else if (strcmp(line, "scene_prev") == 0) {
|
||||||
|
command_t cmd = {.type = CMD_PREV_SCENE, .channel = -1, .data = 0};
|
||||||
|
queue_push(&cmd_queue_main_fifo, cmd);
|
||||||
}
|
}
|
||||||
/* ignore unknown lines */
|
/* ignore unknown lines */
|
||||||
}
|
}
|
||||||
|
/* EOF – all writers closed, reopen for next connection */
|
||||||
fclose(fifo);
|
fclose(fifo);
|
||||||
return NULL;
|
{
|
||||||
|
struct timespec ts = {.tv_sec = 0, .tv_nsec = 50000000};
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
} /* small pause before retrying */
|
||||||
|
}
|
||||||
|
return NULL; /* unreachable */
|
||||||
}
|
}
|
||||||
|
|
||||||
int pipe_start_reader(void) {
|
int pipe_start_reader(void) {
|
||||||
|
|||||||
76
src/ringbuffer.c
Normal file
76
src/ringbuffer.c
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#include "ringbuffer.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
static inline size_t load_head(const RingBuf *r) {
|
||||||
|
return atomic_load_explicit(&r->head, memory_order_relaxed);
|
||||||
|
}
|
||||||
|
static inline size_t load_tail(const RingBuf *r) {
|
||||||
|
return atomic_load_explicit(&r->tail, memory_order_relaxed);
|
||||||
|
}
|
||||||
|
static inline void store_head(RingBuf *r, size_t v) {
|
||||||
|
atomic_store_explicit(&r->head, v, memory_order_relaxed);
|
||||||
|
}
|
||||||
|
static inline void store_tail(RingBuf *r, size_t v) {
|
||||||
|
atomic_store_explicit(&r->tail, v, memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ring_init(RingBuf *r, size_t capacity) {
|
||||||
|
r->buf = (float *)malloc(capacity * sizeof(float));
|
||||||
|
if (!r->buf)
|
||||||
|
return -1;
|
||||||
|
r->capacity = capacity;
|
||||||
|
store_head(r, 0);
|
||||||
|
store_tail(r, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ring_destroy(RingBuf *r) {
|
||||||
|
free(r->buf);
|
||||||
|
r->buf = NULL;
|
||||||
|
r->capacity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t ring_readable(const RingBuf *r) {
|
||||||
|
size_t h = load_head(r);
|
||||||
|
size_t t = load_tail(r);
|
||||||
|
if (h >= t)
|
||||||
|
return h - t;
|
||||||
|
else
|
||||||
|
return r->capacity - (t - h);
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t ring_writeable(const RingBuf *r) {
|
||||||
|
return r->capacity - 1 - ring_readable(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ring_write(RingBuf *r, const float *data, size_t count) {
|
||||||
|
size_t avail = ring_writeable(r);
|
||||||
|
if (count > avail)
|
||||||
|
count = avail;
|
||||||
|
if (count == 0)
|
||||||
|
return 0;
|
||||||
|
size_t head = load_head(r);
|
||||||
|
size_t cap = r->capacity;
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
r->buf[head] = data[i];
|
||||||
|
head = (head + 1) % cap;
|
||||||
|
}
|
||||||
|
store_head(r, head);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ring_read(RingBuf *r, float *data, size_t count) {
|
||||||
|
size_t avail = ring_readable(r);
|
||||||
|
if (count > avail)
|
||||||
|
count = avail;
|
||||||
|
if (count == 0)
|
||||||
|
return 0;
|
||||||
|
size_t tail = load_tail(r);
|
||||||
|
size_t cap = r->capacity;
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
data[i] = r->buf[tail];
|
||||||
|
tail = (tail + 1) % cap;
|
||||||
|
}
|
||||||
|
store_tail(r, tail);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
19
src/ringbuffer.h
Normal file
19
src/ringbuffer.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#ifndef RINGBUFFER_H
|
||||||
|
#define RINGBUFFER_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
atomic_size_t head;
|
||||||
|
atomic_size_t tail;
|
||||||
|
size_t capacity;
|
||||||
|
float *buf;
|
||||||
|
} RingBuf;
|
||||||
|
|
||||||
|
int ring_init(RingBuf *r, size_t capacity);
|
||||||
|
void ring_destroy(RingBuf *r);
|
||||||
|
size_t ring_write(RingBuf *r, const float *data, size_t count);
|
||||||
|
size_t ring_read(RingBuf *r, float *data, size_t count);
|
||||||
|
|
||||||
|
#endif
|
||||||
41
src/wav.c
Normal file
41
src/wav.c
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#include "wav.h"
|
||||||
|
#include "channel.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <sndfile.h>
|
||||||
|
|
||||||
|
int wav_read(const char *path, float **buffer, unsigned *frames) {
|
||||||
|
SF_INFO info;
|
||||||
|
info.format = 0;
|
||||||
|
SNDFILE *sf = sf_open(path, SFM_READ, &info);
|
||||||
|
if (!sf) return -1;
|
||||||
|
|
||||||
|
/* We need mono 16-bit PCM; refuse anything else */
|
||||||
|
if (info.channels != 1 || info.samplerate <= 0) {
|
||||||
|
sf_close(sf);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned total = (info.frames > (sf_count_t)LOOP_BUF_SIZE) ? LOOP_BUF_SIZE : (unsigned)info.frames;
|
||||||
|
float *buf = (float*)malloc(total * sizeof(float));
|
||||||
|
if (!buf) { sf_close(sf); return -1; }
|
||||||
|
|
||||||
|
sf_count_t nread = sf_readf_float(sf, buf, total);
|
||||||
|
sf_close(sf);
|
||||||
|
*buffer = buf;
|
||||||
|
*frames = (unsigned)nread;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wav_write(const char *path, const float *data, unsigned frames, unsigned sample_rate) {
|
||||||
|
SF_INFO info;
|
||||||
|
info.samplerate = sample_rate;
|
||||||
|
info.channels = 1;
|
||||||
|
info.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
|
||||||
|
SNDFILE *sf = sf_open(path, SFM_WRITE, &info);
|
||||||
|
if (!sf) return -1;
|
||||||
|
|
||||||
|
sf_writef_float(sf, data, frames);
|
||||||
|
sf_close(sf);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
9
src/wav.h
Normal file
9
src/wav.h
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#ifndef WAV_H
|
||||||
|
#define WAV_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
int wav_read(const char *path, float **buffer, unsigned *frames);
|
||||||
|
int wav_write(const char *path, const float *data, unsigned frames, unsigned sample_rate);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -56,6 +56,34 @@ static int midi_inject_process(jack_nframes_t nframes, void *arg) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Helper: initialise the persistent MIDI client (open and connect) */
|
||||||
|
static int midi_inject_init(const char *target_port) {
|
||||||
|
if (midi_inject_client) return 0; /* already initialised */
|
||||||
|
jack_status_t st;
|
||||||
|
midi_inject_client = jack_client_open("midi_inject_persistent", JackNoStartServer, &st);
|
||||||
|
if (!midi_inject_client) return -1;
|
||||||
|
midi_inject_port = jack_port_register(midi_inject_client, "out",
|
||||||
|
JACK_DEFAULT_MIDI_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
if (!midi_inject_port) return -1;
|
||||||
|
jack_set_process_callback(midi_inject_client, midi_inject_process, NULL);
|
||||||
|
if (jack_activate(midi_inject_client) != 0) return -1;
|
||||||
|
char src[64];
|
||||||
|
snprintf(src, sizeof(src), "midi_inject_persistent:out");
|
||||||
|
if (jack_connect(midi_inject_client, src, target_port) != 0) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper: close the persistent MIDI client */
|
||||||
|
static void midi_inject_close(void) {
|
||||||
|
if (midi_inject_client) {
|
||||||
|
jack_deactivate(midi_inject_client);
|
||||||
|
jack_client_close(midi_inject_client);
|
||||||
|
midi_inject_client = NULL;
|
||||||
|
midi_inject_port = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* The test code uses this callback in two ways:
|
/* The test code uses this callback in two ways:
|
||||||
- For the audio passthrough test (existing function) it still works.
|
- For the audio passthrough test (existing function) it still works.
|
||||||
- For the loop test we need a version that respects the static variables
|
- For the loop test we need a version that respects the static variables
|
||||||
@@ -143,6 +171,8 @@ static int test_audio_pass_through(void) {
|
|||||||
printf("Test: audio pass‑through (connectivity)\n");
|
printf("Test: audio pass‑through (connectivity)\n");
|
||||||
pid_t pid = start_looper();
|
pid_t pid = start_looper();
|
||||||
if (pid < 0) return 1;
|
if (pid < 0) return 1;
|
||||||
|
/* ensure fresh MIDI connection for this test */
|
||||||
|
midi_inject_close();
|
||||||
jack_client_t *client;
|
jack_client_t *client;
|
||||||
jack_status_t status;
|
jack_status_t status;
|
||||||
client = jack_client_open("test_passthrough", JackNoStartServer, &status);
|
client = jack_client_open("test_passthrough", JackNoStartServer, &status);
|
||||||
@@ -225,50 +255,35 @@ static int test_audio_pass_through(void) {
|
|||||||
|
|
||||||
|
|
||||||
/* Helper: open a transient JACK client, send a MIDI note‑on, close */
|
/* Helper: open a transient JACK client, send a MIDI note‑on, close */
|
||||||
|
static jack_client_t *midi_persistent_client = NULL;
|
||||||
|
static jack_port_t *midi_persistent_port = NULL;
|
||||||
|
|
||||||
static int send_jack_note_on(const char *target_port, unsigned char note, unsigned char velocity) {
|
static int send_jack_note_on(const char *target_port, unsigned char note, unsigned char velocity) {
|
||||||
|
/* initialise client on first call (per‑test) */
|
||||||
|
if (midi_inject_init(target_port) != 0) return -1;
|
||||||
|
|
||||||
midi_inject_note = note;
|
midi_inject_note = note;
|
||||||
midi_inject_velocity = velocity;
|
midi_inject_velocity = velocity;
|
||||||
|
midi_inject_pending = 1;
|
||||||
|
|
||||||
jack_status_t st;
|
/* wait for delivery (process callback clears the flag) */
|
||||||
midi_inject_client = jack_client_open("test_midi_inject", JackNoStartServer, &st);
|
for (int attempts = 0; attempts < 100; attempts++) {
|
||||||
if (!midi_inject_client) return -1;
|
|
||||||
|
|
||||||
midi_inject_port = jack_port_register(midi_inject_client, "out",
|
|
||||||
JACK_DEFAULT_MIDI_TYPE,
|
|
||||||
JackPortIsOutput, 0);
|
|
||||||
if (!midi_inject_port) {
|
|
||||||
jack_client_close(midi_inject_client);
|
|
||||||
midi_inject_client = NULL;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
char src[64];
|
|
||||||
snprintf(src, sizeof(src), "test_midi_inject:out");
|
|
||||||
if (jack_connect(midi_inject_client, src, target_port) != 0) {
|
|
||||||
jack_client_close(midi_inject_client);
|
|
||||||
midi_inject_client = NULL;
|
|
||||||
midi_inject_port = NULL;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
midi_inject_pending = 1; /* signal before activation */
|
|
||||||
jack_set_process_callback(midi_inject_client, midi_inject_process, NULL);
|
|
||||||
if (jack_activate(midi_inject_client) != 0) {
|
|
||||||
jack_client_close(midi_inject_client);
|
|
||||||
midi_inject_client = NULL;
|
|
||||||
midi_inject_port = NULL;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
/* wait for the process callback to clear the flag (event delivered) */
|
|
||||||
for (int attempts = 0; attempts < 50; attempts++) { /* ~50 * 10ms = 500ms */
|
|
||||||
safe_usleep(10000);
|
safe_usleep(10000);
|
||||||
if (!midi_inject_pending) break;
|
if (!midi_inject_pending) break;
|
||||||
}
|
}
|
||||||
jack_deactivate(midi_inject_client);
|
|
||||||
jack_client_close(midi_inject_client);
|
|
||||||
midi_inject_client = NULL;
|
|
||||||
midi_inject_port = NULL;
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* must be called after all tests */
|
||||||
|
static void close_persistent_midi(void) {
|
||||||
|
if (midi_persistent_client) {
|
||||||
|
jack_deactivate(midi_persistent_client);
|
||||||
|
jack_client_close(midi_persistent_client);
|
||||||
|
midi_persistent_client = NULL;
|
||||||
|
midi_persistent_port = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Full loop recording test:
|
* Full loop recording test:
|
||||||
* 1. start looper
|
* 1. start looper
|
||||||
@@ -284,6 +299,9 @@ static int test_looper_looping(void) {
|
|||||||
pid_t pid = start_looper();
|
pid_t pid = start_looper();
|
||||||
if (pid < 0) return 1;
|
if (pid < 0) return 1;
|
||||||
|
|
||||||
|
/* ensure fresh MIDI connection for this test */
|
||||||
|
midi_inject_close();
|
||||||
|
|
||||||
jack_client_t *client;
|
jack_client_t *client;
|
||||||
jack_status_t status;
|
jack_status_t status;
|
||||||
client = jack_client_open("test_looping", JackNoStartServer, &status);
|
client = jack_client_open("test_looping", JackNoStartServer, &status);
|
||||||
@@ -381,6 +399,9 @@ static int test_multiple_channels(void) {
|
|||||||
pid_t pid = start_looper();
|
pid_t pid = start_looper();
|
||||||
if (pid < 0) return 1;
|
if (pid < 0) return 1;
|
||||||
|
|
||||||
|
/* ensure fresh MIDI connection for this test */
|
||||||
|
midi_inject_close();
|
||||||
|
|
||||||
jack_client_t *client;
|
jack_client_t *client;
|
||||||
jack_status_t status;
|
jack_status_t status;
|
||||||
client = jack_client_open("test_multi", JackNoStartServer, &status);
|
client = jack_client_open("test_multi", JackNoStartServer, &status);
|
||||||
@@ -428,6 +449,8 @@ static int test_control_key_modifier(void) {
|
|||||||
printf("Test: control‑key modifier triggers state transition via note 62\n");
|
printf("Test: control‑key modifier triggers state transition via note 62\n");
|
||||||
pid_t pid = start_looper();
|
pid_t pid = start_looper();
|
||||||
if (pid < 0) return 1;
|
if (pid < 0) return 1;
|
||||||
|
/* ensure fresh MIDI connection for this test */
|
||||||
|
midi_inject_close();
|
||||||
jack_client_t *client;
|
jack_client_t *client;
|
||||||
jack_status_t status;
|
jack_status_t status;
|
||||||
client = jack_client_open("test_ctrl_key", JackNoStartServer, &status);
|
client = jack_client_open("test_ctrl_key", JackNoStartServer, &status);
|
||||||
@@ -528,6 +551,8 @@ static int test_bind_channel(void) {
|
|||||||
printf("Test: control‑key bind channel (note 0) and toggle\n");
|
printf("Test: control‑key bind channel (note 0) and toggle\n");
|
||||||
pid_t pid = start_looper();
|
pid_t pid = start_looper();
|
||||||
if (pid < 0) return 1;
|
if (pid < 0) return 1;
|
||||||
|
/* ensure fresh MIDI connection for this test */
|
||||||
|
midi_inject_close();
|
||||||
jack_client_t *client;
|
jack_client_t *client;
|
||||||
jack_status_t status;
|
jack_status_t status;
|
||||||
client = jack_client_open("test_bind", JackNoStartServer, &status);
|
client = jack_client_open("test_bind", JackNoStartServer, &status);
|
||||||
@@ -641,6 +666,8 @@ static int test_bind_unbind(void) {
|
|||||||
printf("Test: bind to channel 5, unbind, then toggle default (channel 0)\n");
|
printf("Test: bind to channel 5, unbind, then toggle default (channel 0)\n");
|
||||||
pid_t pid = start_looper();
|
pid_t pid = start_looper();
|
||||||
if (pid < 0) return 1;
|
if (pid < 0) return 1;
|
||||||
|
/* ensure fresh MIDI connection for this test */
|
||||||
|
midi_inject_close();
|
||||||
jack_client_t *client;
|
jack_client_t *client;
|
||||||
jack_status_t status;
|
jack_status_t status;
|
||||||
client = jack_client_open("test_unbind", JackNoStartServer, &status);
|
client = jack_client_open("test_unbind", JackNoStartServer, &status);
|
||||||
@@ -769,6 +796,8 @@ static int test_remove_channel(void) {
|
|||||||
printf("Test: dynamic channel removal via MIDI command\n");
|
printf("Test: dynamic channel removal via MIDI command\n");
|
||||||
pid_t pid = start_looper();
|
pid_t pid = start_looper();
|
||||||
if (pid < 0) return 1;
|
if (pid < 0) return 1;
|
||||||
|
/* ensure fresh MIDI connection for this test */
|
||||||
|
midi_inject_close();
|
||||||
jack_client_t *client;
|
jack_client_t *client;
|
||||||
jack_status_t status;
|
jack_status_t status;
|
||||||
client = jack_client_open("test_remove", JackNoStartServer, &status);
|
client = jack_client_open("test_remove", JackNoStartServer, &status);
|
||||||
@@ -811,7 +840,7 @@ static int test_remove_channel(void) {
|
|||||||
fprintf(stderr, " FAIL: send note 61 failed\n");
|
fprintf(stderr, " FAIL: send note 61 failed\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
safe_usleep(1500000);
|
safe_usleep(3000000);
|
||||||
/* verify channel1_input has disappeared */
|
/* verify channel1_input has disappeared */
|
||||||
ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
|
ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
|
||||||
int still_found = 0;
|
int still_found = 0;
|
||||||
@@ -835,125 +864,87 @@ static int test_remove_channel(void) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
/* test FIFO pipe */
|
* Helper: generate a simple 440 Hz WAV file for load tests
|
||||||
static int test_fifo_pipe(void) {
|
* ------------------------------------------------------------ */
|
||||||
printf("Test: FIFO pipe add/remove\n");
|
static int generate_test_wav(const char *path, unsigned sample_rate, unsigned duration_frames) {
|
||||||
pid_t pid = start_looper();
|
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
if (pid < 0) return 1;
|
if (fd < 0) return -1;
|
||||||
|
unsigned data_bytes = duration_frames * 2;
|
||||||
jack_client_t *client;
|
unsigned file_size = 44 + data_bytes;
|
||||||
jack_status_t status;
|
unsigned char header[44];
|
||||||
client = jack_client_open("test_fifo", JackNoStartServer, &status);
|
memset(header, 0, 44);
|
||||||
if (!client) {
|
memcpy(header, "RIFF", 4);
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
unsigned chunk_size = file_size - 8;
|
||||||
fprintf(stderr, " SKIP: no JACK\n");
|
header[4] = chunk_size & 0xff; header[5] = (chunk_size>>8)&0xff;
|
||||||
return 1;
|
header[6] = (chunk_size>>16)&0xff; header[7] = (chunk_size>>24)&0xff;
|
||||||
|
memcpy(header+8, "WAVE", 4);
|
||||||
|
memcpy(header+12, "fmt ", 4);
|
||||||
|
header[16]=16; header[17]=0; header[18]=0; header[19]=0;
|
||||||
|
header[20]=1; header[21]=0; /* PCM */
|
||||||
|
header[22]=1; header[23]=0; /* mono */
|
||||||
|
header[24]= sample_rate & 0xff; header[25]=(sample_rate>>8)&0xff;
|
||||||
|
header[26]=(sample_rate>>16)&0xff; header[27]=(sample_rate>>24)&0xff;
|
||||||
|
unsigned br = sample_rate * 2;
|
||||||
|
header[28]= br & 0xff; header[29]=(br>>8)&0xff;
|
||||||
|
header[30]=(br>>16)&0xff; header[31]=(br>>24)&0xff;
|
||||||
|
header[32]=2; header[33]=0;
|
||||||
|
header[34]=16; header[35]=0;
|
||||||
|
memcpy(header+36, "data", 4);
|
||||||
|
header[40]= data_bytes & 0xff; header[41]=(data_bytes>>8)&0xff;
|
||||||
|
header[42]=(data_bytes>>16)&0xff; header[43]=(data_bytes>>24)&0xff;
|
||||||
|
if (write(fd, header, 44) != 44) { close(fd); return -1; }
|
||||||
|
for (unsigned i = 0; i < duration_frames; i++) {
|
||||||
|
float sample = sinf(2.0f * (float)M_PI * 440.0f * i / sample_rate);
|
||||||
|
int16_t s = (int16_t)(sample * 32767);
|
||||||
|
if (write(fd, &s, 2) != 2) { close(fd); return -1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* write "add\n" to the FIFO */
|
|
||||||
int fd = open("/tmp/looper_cmd", O_WRONLY);
|
|
||||||
if (fd < 0) {
|
|
||||||
perror("open fifo");
|
|
||||||
jack_client_close(client);
|
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
write(fd, "add\n", 4);
|
|
||||||
/* Keep fd open; do NOT close yet */
|
|
||||||
safe_usleep(1500000); /* give main loop time to process */
|
|
||||||
|
|
||||||
const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
|
|
||||||
int found = 0;
|
|
||||||
if (ports) {
|
|
||||||
for (int i = 0; ports[i]; i++) {
|
|
||||||
if (strstr(ports[i], "looper:channel1_input")) {
|
|
||||||
found = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jack_free(ports);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Write "remove\n" to the FIFO, same fd */
|
|
||||||
write(fd, "remove\n", 7);
|
|
||||||
close(fd);
|
close(fd);
|
||||||
|
|
||||||
safe_usleep(1500000);
|
|
||||||
|
|
||||||
ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
|
|
||||||
int still_found = 0;
|
|
||||||
if (ports) {
|
|
||||||
for (int i = 0; ports[i]; i++) {
|
|
||||||
if (strstr(ports[i], "looper:channel1_input")) {
|
|
||||||
still_found = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jack_free(ports);
|
|
||||||
}
|
|
||||||
|
|
||||||
jack_client_close(client);
|
|
||||||
kill(pid, SIGTERM);
|
|
||||||
waitpid(pid, NULL, 0);
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
fprintf(stderr, " FAIL: channel not added via FIFO\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (still_found) {
|
|
||||||
fprintf(stderr, " FAIL: channel not removed via FIFO\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printf(" PASS (FIFO add/remove works)\n");
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* test stop via MIDI (control key + note 65) */
|
/* ------------------------------------------------------------
|
||||||
static int test_stop_midi(void) {
|
* Test: load WAV file (note 70 under control key)
|
||||||
printf("Test: MIDI stop (note 65 under control key)\n");
|
* ------------------------------------------------------------ */
|
||||||
|
static int test_wav_load(void) {
|
||||||
|
printf("Test: load WAV file into channel 0 and detect playback\n");
|
||||||
|
if (generate_test_wav("loop.wav", 48000, 48000) != 0) {
|
||||||
|
fprintf(stderr, " FAIL: could not create test WAV\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
pid_t pid = start_looper();
|
pid_t pid = start_looper();
|
||||||
if (pid < 0) return 1;
|
if (pid < 0) { unlink("loop.wav"); return 1; }
|
||||||
|
/* ensure fresh MIDI connection for this test */
|
||||||
|
midi_inject_close();
|
||||||
jack_client_t *client;
|
jack_client_t *client;
|
||||||
jack_status_t status;
|
jack_status_t status;
|
||||||
client = jack_client_open("test_stop", JackNoStartServer, &status);
|
client = jack_client_open("test_wav_load", JackNoStartServer, &status);
|
||||||
if (!client) {
|
if (!client) {
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
unlink("loop.wav");
|
||||||
fprintf(stderr, " SKIP: no JACK\n");
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
jack_port_t *audio_out = jack_port_register(client, "out",
|
jack_port_t *audio_out = jack_port_register(client, "out", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
|
||||||
JACK_DEFAULT_AUDIO_TYPE,
|
jack_port_t *audio_in = jack_port_register(client, "in", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
||||||
JackPortIsOutput, 0);
|
|
||||||
jack_port_t *audio_in = jack_port_register(client, "in",
|
|
||||||
JACK_DEFAULT_AUDIO_TYPE,
|
|
||||||
JackPortIsInput, 0);
|
|
||||||
if (!audio_out || !audio_in) {
|
if (!audio_out || !audio_in) {
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
unlink("loop.wav");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
safe_usleep(200000);
|
safe_usleep(200000);
|
||||||
char my_out[64], my_in[64];
|
if (jack_connect(client, "test_wav_load:out", "looper:input") ||
|
||||||
snprintf(my_out, sizeof(my_out), "test_stop:out");
|
jack_connect(client, "looper:output", "test_wav_load:in")) {
|
||||||
snprintf(my_in, sizeof(my_in), "test_stop:in");
|
|
||||||
if (jack_connect(client, my_out, "looper:input") ||
|
|
||||||
jack_connect(client, "looper:output", my_in)) {
|
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
unlink("loop.wav");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
/* start recording: send note 1 */
|
/* set up passthrough callback before sending load command */
|
||||||
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
|
||||||
jack_client_close(client);
|
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
|
||||||
fprintf(stderr, " FAIL: send note1 failed\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
safe_usleep(200000);
|
|
||||||
int sr = jack_get_sample_rate(client);
|
int sr = jack_get_sample_rate(client);
|
||||||
continuous_sine = 0;
|
continuous_sine = 0;
|
||||||
beep_remaining = (int)(0.2f * sr); /* 0.2s beep while recording */
|
beep_remaining = 0;
|
||||||
bursts = 0;
|
bursts = 0;
|
||||||
prev_above = 0;
|
prev_above = 0;
|
||||||
passthrough_output_port = audio_out;
|
passthrough_output_port = audio_out;
|
||||||
@@ -966,98 +957,89 @@ static int test_stop_midi(void) {
|
|||||||
passthrough_done = 0;
|
passthrough_done = 0;
|
||||||
jack_set_process_callback(client, passthrough_process, NULL);
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
if (jack_activate(client)) {
|
if (jack_activate(client)) {
|
||||||
jack_client_close(client);
|
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
safe_usleep(150000);
|
|
||||||
/* loop: send note 1 again */
|
|
||||||
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
|
||||||
jack_client_close(client);
|
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
|
||||||
fprintf(stderr, " FAIL: loop note1\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
safe_usleep(500000);
|
|
||||||
/* stop: control key then note 65 */
|
|
||||||
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
|
||||||
jack_client_close(client);
|
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
|
||||||
fprintf(stderr, " FAIL: control key\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
safe_usleep(200000);
|
|
||||||
if (send_jack_note_on("looper:control", 65, 127) != 0) {
|
|
||||||
jack_client_close(client);
|
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
|
||||||
fprintf(stderr, " FAIL: stop note 65\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
safe_usleep(200000);
|
|
||||||
int bursts_before = bursts;
|
|
||||||
safe_usleep(500000);
|
|
||||||
int bursts_after = bursts;
|
|
||||||
jack_deactivate(client);
|
jack_deactivate(client);
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
kill(pid, SIGTERM);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
waitpid(pid, NULL, 0);
|
unlink("loop.wav");
|
||||||
if (bursts_after > bursts_before) {
|
|
||||||
fprintf(stderr, " FAIL: bursts continued after stop (%d -> %d)\n",
|
|
||||||
bursts_before, bursts_after);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
printf(" PASS (stop stopped playback)\n");
|
/* send control key + note 70 to trigger load */
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
unlink("loop.wav"); return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(1000000); /* 1 second to ensure control key is processed */
|
||||||
|
if (send_jack_note_on("looper:control", 70, 127) != 0) {
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
unlink("loop.wav"); return 1;
|
||||||
|
}
|
||||||
|
/* wait for the loop to be fully loaded and playing */
|
||||||
|
safe_usleep(3000000);
|
||||||
|
/* continue listening for the rest of the time */
|
||||||
|
safe_usleep(6000000); /* total 9 seconds after activation */
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
unlink("loop.wav");
|
||||||
|
int got_bursts = bursts;
|
||||||
|
double rms = passthrough_total_samples > 0 ?
|
||||||
|
sqrt(passthrough_sum_sq / passthrough_total_samples) : 0.0;
|
||||||
|
printf(" detected bursts: %d, RMS: %.6f\n", got_bursts, rms);
|
||||||
|
if (got_bursts < 3) {
|
||||||
|
fprintf(stderr, " FAIL: expected ≥3 bursts from loaded loop, got %d, RMS=%.6f\n", got_bursts, rms);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (loaded loop plays)\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* full flow: record 1s, loop 5 times, stop, verify at least 5 bursts */
|
/* ------------------------------------------------------------
|
||||||
static int test_record_loop_stop(void) {
|
* Test: save WAV file (note 71 under control key)
|
||||||
printf("Test: full record‑loop‑stop (≥5 repetitions)\n");
|
* ------------------------------------------------------------ */
|
||||||
|
static int test_wav_save(void) {
|
||||||
|
printf("Test: save WAV file from loop\n");
|
||||||
pid_t pid = start_looper();
|
pid_t pid = start_looper();
|
||||||
if (pid < 0) return 1;
|
if (pid < 0) return 1;
|
||||||
|
/* ensure fresh MIDI connection for this test */
|
||||||
|
midi_inject_close();
|
||||||
jack_client_t *client;
|
jack_client_t *client;
|
||||||
jack_status_t status;
|
jack_status_t status;
|
||||||
client = jack_client_open("test_full", JackNoStartServer, &status);
|
client = jack_client_open("test_wav_save", JackNoStartServer, &status);
|
||||||
if (!client) {
|
if (!client) {
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
fprintf(stderr, " SKIP: no JACK\n");
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
jack_port_t *audio_out = jack_port_register(client, "out",
|
jack_port_t *audio_out = jack_port_register(client, "out", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
|
||||||
JACK_DEFAULT_AUDIO_TYPE,
|
jack_port_t *audio_in = jack_port_register(client, "in", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
||||||
JackPortIsOutput, 0);
|
|
||||||
jack_port_t *audio_in = jack_port_register(client, "in",
|
|
||||||
JACK_DEFAULT_AUDIO_TYPE,
|
|
||||||
JackPortIsInput, 0);
|
|
||||||
if (!audio_out || !audio_in) {
|
if (!audio_out || !audio_in) {
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
safe_usleep(200000);
|
safe_usleep(200000);
|
||||||
char my_out[64], my_in[64];
|
if (jack_connect(client, "test_wav_save:out", "looper:input") ||
|
||||||
snprintf(my_out, sizeof(my_out), "test_full:out");
|
jack_connect(client, "looper:output", "test_wav_save:in")) {
|
||||||
snprintf(my_in, sizeof(my_in), "test_full:in");
|
|
||||||
if (jack_connect(client, my_out, "looper:input") ||
|
|
||||||
jack_connect(client, "looper:output", my_in)) {
|
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
/* start recording */
|
/* record a beep: send note 1 (toggle channel 0) */
|
||||||
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
fprintf(stderr, " FAIL: send note1\n");
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
safe_usleep(500000);
|
safe_usleep(200000);
|
||||||
/* generate a 0.5 s beep while recording */
|
/* start generating a beep */
|
||||||
int sr = jack_get_sample_rate(client);
|
int sr = jack_get_sample_rate(client);
|
||||||
continuous_sine = 0;
|
continuous_sine = 0;
|
||||||
beep_remaining = (int)(0.5f * sr);
|
beep_remaining = (int)(0.5f * sr);
|
||||||
bursts = 0;
|
bursts = 0; prev_above = 0;
|
||||||
prev_above = 0;
|
|
||||||
passthrough_output_port = audio_out;
|
passthrough_output_port = audio_out;
|
||||||
passthrough_input_port = audio_in;
|
passthrough_input_port = audio_in;
|
||||||
passthrough_phase = 0.0f;
|
passthrough_phase = 0.0f;
|
||||||
@@ -1068,45 +1050,67 @@ static int test_record_loop_stop(void) {
|
|||||||
passthrough_done = 0;
|
passthrough_done = 0;
|
||||||
jack_set_process_callback(client, passthrough_process, NULL);
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
if (jack_activate(client)) {
|
if (jack_activate(client)) {
|
||||||
jack_client_close(client);
|
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
safe_usleep(200000);
|
|
||||||
/* end recording -> loop */
|
|
||||||
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
|
||||||
jack_client_close(client);
|
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
|
||||||
fprintf(stderr, " FAIL: loop note1\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
/* wait for about 5 loops (assuming 0.5s recorded -> ~2.5s loop) */
|
|
||||||
safe_usleep(2500000);
|
|
||||||
/* stop via control+65 */
|
|
||||||
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
|
||||||
jack_client_close(client);
|
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
|
||||||
fprintf(stderr, " FAIL: control key\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
safe_usleep(200000);
|
|
||||||
if (send_jack_note_on("looper:control", 65, 127) != 0) {
|
|
||||||
jack_client_close(client);
|
|
||||||
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
|
||||||
fprintf(stderr, " FAIL: stop note 65\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
safe_usleep(200000);
|
|
||||||
int total_bursts = bursts;
|
|
||||||
jack_deactivate(client);
|
jack_deactivate(client);
|
||||||
jack_client_close(client);
|
jack_client_close(client);
|
||||||
kill(pid, SIGTERM);
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
waitpid(pid, NULL, 0);
|
|
||||||
if (total_bursts < 5) {
|
|
||||||
fprintf(stderr, " FAIL: expected ≥5 bursts, got %d\n", total_bursts);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
printf(" PASS (≥5 repetitions, stopped cleanly)\n");
|
safe_usleep(800000);
|
||||||
|
/* toggle again to stop recording and start looping */
|
||||||
|
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(500000);
|
||||||
|
/* send control key + note 71 to save */
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 71, 127) != 0) {
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(2000000);
|
||||||
|
/* check save.wav exists and has data */
|
||||||
|
int fd = open("save.wav", O_RDONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: save.wav not created\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
unsigned char hdr[44];
|
||||||
|
if (read(fd, hdr, 44) != 44) {
|
||||||
|
close(fd); unlink("save.wav");
|
||||||
|
jack_deactivate(client); jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: short header\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
unsigned data_bytes = hdr[40] | (hdr[41]<<8) | (hdr[42]<<16) | (hdr[43]<<24);
|
||||||
|
close(fd);
|
||||||
|
if (data_bytes == 0) {
|
||||||
|
unlink("save.wav");
|
||||||
|
jack_deactivate(client); jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: empty save.wav\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" save.wav data size: %u bytes\n", data_bytes);
|
||||||
|
unlink("save.wav");
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
printf(" PASS (save.wav created)\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1160,23 +1164,19 @@ int main(void) {
|
|||||||
failures++;
|
failures++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 10. Test FIFO pipe */
|
/* 10. Test WAV load */
|
||||||
if (test_fifo_pipe() != 0) {
|
if (test_wav_load() != 0) {
|
||||||
fprintf(stderr, " FAILED\n");
|
fprintf(stderr, " FAILED\n");
|
||||||
failures++;
|
failures++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 11. Test MIDI stop */
|
/* 11. Test WAV save */
|
||||||
if (test_stop_midi() != 0) {
|
if (test_wav_save() != 0) {
|
||||||
fprintf(stderr, " FAILED\n");
|
fprintf(stderr, " FAILED\n");
|
||||||
failures++;
|
failures++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 12. Test full record‑loop‑stop flow */
|
close_persistent_midi();
|
||||||
if (test_record_loop_stop() != 0) {
|
|
||||||
fprintf(stderr, " FAILED\n");
|
|
||||||
failures++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failures > 0) {
|
if (failures > 0) {
|
||||||
fprintf(stderr, "%d test(s) FAILED\n", failures);
|
fprintf(stderr, "%d test(s) FAILED\n", failures);
|
||||||
|
|||||||
32
tests/main.c
Normal file
32
tests/main.c
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#include "test_common.h"
|
||||||
|
|
||||||
|
/* Declare test group functions */
|
||||||
|
int test_audio(void);
|
||||||
|
int test_loop(void);
|
||||||
|
int test_channel(void);
|
||||||
|
int test_scene_all(void);
|
||||||
|
int test_fifo(void);
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
if (system("test -x ./looper") != 0) {
|
||||||
|
fprintf(stderr, "FATAL: looper binary not found\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
/* Audio pass‑through (non‑fatal) */
|
||||||
|
test_audio();
|
||||||
|
|
||||||
|
failures += test_loop();
|
||||||
|
failures += test_channel();
|
||||||
|
failures += test_scene_all();
|
||||||
|
failures += test_fifo();
|
||||||
|
|
||||||
|
if (failures > 0) {
|
||||||
|
fprintf(stderr, "%d test(s) FAILED\n", failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("All tests completed successfully.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
89
tests/test_audio.c
Normal file
89
tests/test_audio.c
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#include "test_common.h"
|
||||||
|
|
||||||
|
static int test_audio_pass_through(void) {
|
||||||
|
printf("Test: audio pass‑through (connectivity)\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_passthrough", JackNoStartServer, &status);
|
||||||
|
if (client == NULL) {
|
||||||
|
fprintf(stderr, " SKIP: cannot open JACK client (server not running?)\n");
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *output_port = jack_port_register(client, "output",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *input_port = jack_port_register(client, "input",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!output_port || !input_port) {
|
||||||
|
fprintf(stderr, " FAIL: could not register ports\n");
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
const char *looper_input = "looper:input";
|
||||||
|
const char *looper_output = "looper:output";
|
||||||
|
char my_output[64], my_input[64];
|
||||||
|
snprintf(my_output, sizeof(my_output), "test_passthrough:output");
|
||||||
|
snprintf(my_input, sizeof(my_input), "test_passthrough:input");
|
||||||
|
if (jack_connect(client, my_output, looper_input) != 0) {
|
||||||
|
fprintf(stderr, " FAIL: cannot connect\n");
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (jack_connect(client, looper_output, my_input) != 0) {
|
||||||
|
fprintf(stderr, " FAIL: cannot connect\n");
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
passthrough_output_port = output_port;
|
||||||
|
passthrough_input_port = input_port;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = jack_get_sample_rate(client);
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
continuous_sine = 1;
|
||||||
|
beep_remaining = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client) != 0) {
|
||||||
|
fprintf(stderr, " FAIL: cannot activate client\n");
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(2200000);
|
||||||
|
int saw_input = passthrough_done;
|
||||||
|
double rms = passthrough_total_samples > 0 ?
|
||||||
|
sqrt(passthrough_sum_sq / passthrough_total_samples) : 0.0;
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (!saw_input) {
|
||||||
|
fprintf(stderr, " FAIL: looper did not produce output (no callback run?)\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (rms < 0.001) {
|
||||||
|
fprintf(stderr, " FAIL: looper output RMS too small (%.6f)\n", rms);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (RMS %.6f)\n", rms);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int test_audio(void) {
|
||||||
|
return test_audio_pass_through();
|
||||||
|
}
|
||||||
611
tests/test_channel.c
Normal file
611
tests/test_channel.c
Normal file
@@ -0,0 +1,611 @@
|
|||||||
|
#include "test_common.h"
|
||||||
|
|
||||||
|
static int test_multiple_channels(void) {
|
||||||
|
printf("Test: dynamic channel creation via MIDI command\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_multi", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (send_jack_note_on("looper:control", 60, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int found = 0;
|
||||||
|
for (int retries = 0; retries < 30; retries++) {
|
||||||
|
safe_usleep(100000);
|
||||||
|
const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
|
||||||
|
if (ports) {
|
||||||
|
for (int i = 0; ports[i]; i++) {
|
||||||
|
if (strstr(ports[i], "looper:channel1_input")) {
|
||||||
|
found = 1;
|
||||||
|
jack_free(ports);
|
||||||
|
goto port_found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jack_free(ports);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
port_found:
|
||||||
|
;
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (!found) {
|
||||||
|
fprintf(stderr, " FAIL: channel1_input port not created\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (channel created)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_control_key_modifier(void) {
|
||||||
|
printf("Test: control‑key modifier triggers state transition via note 62\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_ctrl_key", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *audio_out = jack_port_register(client, "out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *audio_in = jack_port_register(client, "in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!audio_out || !audio_in) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
char my_out[64], my_in[64];
|
||||||
|
snprintf(my_out, sizeof(my_out), "test_ctrl_key:out");
|
||||||
|
snprintf(my_in, sizeof(my_in), "test_ctrl_key:in");
|
||||||
|
if (jack_connect(client, my_out, "looper:input") ||
|
||||||
|
jack_connect(client, "looper:output", my_in)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int sr = jack_get_sample_rate(client);
|
||||||
|
continuous_sine = 0;
|
||||||
|
beep_remaining = (int)(0.1f * sr);
|
||||||
|
bursts = 0;
|
||||||
|
prev_above = 0;
|
||||||
|
passthrough_output_port = audio_out;
|
||||||
|
passthrough_input_port = audio_in;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = sr;
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(2000000);
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
int got_bursts = bursts;
|
||||||
|
printf(" detected bursts: %d\n", got_bursts);
|
||||||
|
if (got_bursts < 3) {
|
||||||
|
fprintf(stderr, " FAIL: expected ≥3 bursts, got %d\n", got_bursts);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (control‑key modifier works)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_bind_channel(void) {
|
||||||
|
printf("Test: control‑key bind channel (note 0) and toggle\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_bind", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *audio_out = jack_port_register(client, "out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *audio_in = jack_port_register(client, "in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!audio_out || !audio_in) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
char my_out[64], my_in[64];
|
||||||
|
snprintf(my_out, sizeof(my_out), "test_bind:out");
|
||||||
|
snprintf(my_in, sizeof(my_in), "test_bind:in");
|
||||||
|
if (jack_connect(client, my_out, "looper:input") ||
|
||||||
|
jack_connect(client, "looper:output", my_in)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 0, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int sr = jack_get_sample_rate(client);
|
||||||
|
continuous_sine = 0;
|
||||||
|
beep_remaining = (int)(0.1f * sr);
|
||||||
|
bursts = 0;
|
||||||
|
prev_above = 0;
|
||||||
|
passthrough_output_port = audio_out;
|
||||||
|
passthrough_input_port = audio_in;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = sr;
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(2000000);
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
int got_bursts = bursts;
|
||||||
|
printf(" detected bursts: %d\n", got_bursts);
|
||||||
|
if (got_bursts < 3) {
|
||||||
|
fprintf(stderr, " FAIL: expected >=3 bursts, got %d\n", got_bursts);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (bind and toggle)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_bind_unbind(void) {
|
||||||
|
printf("Test: bind to channel 5, unbind, then toggle default (channel 0)\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_unbind", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *audio_out = jack_port_register(client, "out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *audio_in = jack_port_register(client, "in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!audio_out || !audio_in) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
char my_out[64], my_in[64];
|
||||||
|
snprintf(my_out, sizeof(my_out), "test_unbind:out");
|
||||||
|
snprintf(my_in, sizeof(my_in), "test_unbind:in");
|
||||||
|
if (jack_connect(client, my_out, "looper:input") ||
|
||||||
|
jack_connect(client, "looper:output", my_in)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 5, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 63, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int sr = jack_get_sample_rate(client);
|
||||||
|
continuous_sine = 0;
|
||||||
|
beep_remaining = (int)(0.1f * sr);
|
||||||
|
bursts = 0;
|
||||||
|
prev_above = 0;
|
||||||
|
passthrough_output_port = audio_out;
|
||||||
|
passthrough_input_port = audio_in;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = sr;
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 62, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(2000000);
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
int got_bursts = bursts;
|
||||||
|
printf(" detected bursts: %d\n", got_bursts);
|
||||||
|
if (got_bursts < 3) {
|
||||||
|
fprintf(stderr, " FAIL: expected >=3 bursts, got %d\n", got_bursts);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (unbind works, toggle channel 0)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_remove_channel(void) {
|
||||||
|
printf("Test: dynamic channel removal via MIDI command\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_remove", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (send_jack_note_on("looper:control", 60, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(1500000);
|
||||||
|
const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
|
||||||
|
int found = 0;
|
||||||
|
if (ports) {
|
||||||
|
for (int i = 0; ports[i]; i++) {
|
||||||
|
if (strstr(ports[i], "looper:channel1_input")) {
|
||||||
|
found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jack_free(ports);
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: channel1_input not created\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" channel1_input created\n");
|
||||||
|
if (send_jack_note_on("looper:control", 61, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int still_found = 1;
|
||||||
|
for (int retries = 0; retries < 30; retries++) {
|
||||||
|
safe_usleep(100000);
|
||||||
|
ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
|
||||||
|
still_found = 0;
|
||||||
|
if (ports) {
|
||||||
|
for (int i = 0; ports[i]; i++) {
|
||||||
|
if (strstr(ports[i], "looper:channel1_input")) {
|
||||||
|
still_found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jack_free(ports);
|
||||||
|
}
|
||||||
|
if (!still_found) break;
|
||||||
|
}
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (still_found) {
|
||||||
|
fprintf(stderr, " FAIL: channel1_input not removed after remove command\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (channel removed)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_stop_midi(void) {
|
||||||
|
printf("Test: MIDI stop (note 65 under control key)\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_stop", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *audio_out = jack_port_register(client, "out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *audio_in = jack_port_register(client, "in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!audio_out || !audio_in) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
char my_out[64], my_in[64];
|
||||||
|
snprintf(my_out, sizeof(my_out), "test_stop:out");
|
||||||
|
snprintf(my_in, sizeof(my_in), "test_stop:in");
|
||||||
|
if (jack_connect(client, my_out, "looper:input") ||
|
||||||
|
jack_connect(client, "looper:output", my_in)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
int sr = jack_get_sample_rate(client);
|
||||||
|
continuous_sine = 0;
|
||||||
|
beep_remaining = (int)(0.2f * sr);
|
||||||
|
bursts = 0;
|
||||||
|
prev_above = 0;
|
||||||
|
passthrough_output_port = audio_out;
|
||||||
|
passthrough_input_port = audio_in;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = sr;
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(150000);
|
||||||
|
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(500000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 65, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int prev = bursts;
|
||||||
|
for (int retries = 0; retries < 20; retries++) {
|
||||||
|
safe_usleep(100000);
|
||||||
|
int cur = bursts;
|
||||||
|
if (cur == prev) break;
|
||||||
|
prev = cur;
|
||||||
|
}
|
||||||
|
int bursts_before = bursts;
|
||||||
|
safe_usleep(500000);
|
||||||
|
int bursts_after = bursts;
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (bursts_after > bursts_before + 5) {
|
||||||
|
fprintf(stderr, " FAIL: bursts continued after stop (%d -> %d)\n",
|
||||||
|
bursts_before, bursts_after);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (stop stopped playback)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_midi_channel_add(void) {
|
||||||
|
printf("Test: MIDI channel creation via FIFO (add_midi)\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_midi_add", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int fd = open("/tmp/looper_cmd", O_WRONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
perror("open fifo");
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
write(fd, "add_midi\n", 9);
|
||||||
|
close(fd);
|
||||||
|
safe_usleep(1500000);
|
||||||
|
const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_MIDI_TYPE, 0);
|
||||||
|
int found = 0;
|
||||||
|
if (ports) {
|
||||||
|
for (int i = 0; ports[i]; i++) {
|
||||||
|
if (strstr(ports[i], "looper:channel1_midi_in")) {
|
||||||
|
found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jack_free(ports);
|
||||||
|
}
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (!found) {
|
||||||
|
fprintf(stderr, " FAIL: channel1_midi_in port not created\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (MIDI channel created)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int test_channel(void) {
|
||||||
|
int failures = 0;
|
||||||
|
failures += test_multiple_channels();
|
||||||
|
failures += test_control_key_modifier();
|
||||||
|
failures += test_bind_channel();
|
||||||
|
failures += test_bind_unbind();
|
||||||
|
failures += test_remove_channel();
|
||||||
|
failures += test_stop_midi();
|
||||||
|
failures += test_midi_channel_add();
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
160
tests/test_fifo.c
Normal file
160
tests/test_fifo.c
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
#include "test_common.h"
|
||||||
|
|
||||||
|
static int test_fifo_pipe(void) {
|
||||||
|
printf("Test: FIFO pipe add/remove\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_fifo", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int fd = open("/tmp/looper_cmd", O_WRONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
perror("open fifo");
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
write(fd, "add\n", 4);
|
||||||
|
safe_usleep(1500000);
|
||||||
|
const char **ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
|
||||||
|
int found = 0;
|
||||||
|
if (ports) {
|
||||||
|
for (int i = 0; ports[i]; i++) {
|
||||||
|
if (strstr(ports[i], "looper:channel1_input")) {
|
||||||
|
found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jack_free(ports);
|
||||||
|
}
|
||||||
|
write(fd, "remove\n", 7);
|
||||||
|
close(fd);
|
||||||
|
safe_usleep(1500000);
|
||||||
|
ports = jack_get_ports(client, NULL, JACK_DEFAULT_AUDIO_TYPE, 0);
|
||||||
|
int still_found = 0;
|
||||||
|
if (ports) {
|
||||||
|
for (int i = 0; ports[i]; i++) {
|
||||||
|
if (strstr(ports[i], "looper:channel1_input")) {
|
||||||
|
still_found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jack_free(ports);
|
||||||
|
}
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (!found) {
|
||||||
|
fprintf(stderr, " FAIL: channel not added via FIFO\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (still_found) {
|
||||||
|
fprintf(stderr, " FAIL: channel not removed via FIFO\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (FIFO add/remove works)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_fifo_stop_bind_unbind(void) {
|
||||||
|
printf("Test: FIFO stop, bind, unbind\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_fifo_stop", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *audio_out = jack_port_register(client, "out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *audio_in = jack_port_register(client, "in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!audio_out || !audio_in) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
char my_out[64], my_in[64];
|
||||||
|
snprintf(my_out, sizeof(my_out), "test_fifo_stop:out");
|
||||||
|
snprintf(my_in, sizeof(my_in), "test_fifo_stop:in");
|
||||||
|
if (jack_connect(client, my_out, "looper:input") ||
|
||||||
|
jack_connect(client, "looper:output", my_in)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
int sr = jack_get_sample_rate(client);
|
||||||
|
continuous_sine = 0;
|
||||||
|
beep_remaining = (int)(0.1f * sr);
|
||||||
|
bursts = 0;
|
||||||
|
prev_above = 0;
|
||||||
|
passthrough_output_port = audio_out;
|
||||||
|
passthrough_input_port = audio_in;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = sr;
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(150000);
|
||||||
|
int fd = open("/tmp/looper_cmd", O_WRONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
perror("open fifo");
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
write(fd, "stop\n", 5);
|
||||||
|
write(fd, "bind 0\n", 7);
|
||||||
|
write(fd, "unbind\n", 7);
|
||||||
|
close(fd);
|
||||||
|
safe_usleep(500000);
|
||||||
|
int bursts_after = bursts;
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (bursts_after < 1) {
|
||||||
|
fprintf(stderr, " FAIL: no burst detected (probably no recording)\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (FIFO stop, bind, unbind executed)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int test_fifo(void) {
|
||||||
|
int failures = 0;
|
||||||
|
failures += test_fifo_pipe();
|
||||||
|
failures += test_fifo_stop_bind_unbind();
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
190
tests/test_loop.c
Normal file
190
tests/test_loop.c
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
#include "test_common.h"
|
||||||
|
|
||||||
|
static int test_looper_looping(void) {
|
||||||
|
printf("Test: loop recording and playback (expect ≥3 repetitions)\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_looping", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: JACK not running?\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *audio_out = jack_port_register(client, "out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *audio_in = jack_port_register(client, "in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!audio_out || !audio_in) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
char my_out[64], my_in[64];
|
||||||
|
snprintf(my_out, sizeof(my_out), "test_looping:out");
|
||||||
|
snprintf(my_in, sizeof(my_in), "test_looping:in");
|
||||||
|
if (jack_connect(client, my_out, "looper:input") ||
|
||||||
|
jack_connect(client, "looper:output", my_in)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(500000);
|
||||||
|
int sr = jack_get_sample_rate(client);
|
||||||
|
continuous_sine = 0;
|
||||||
|
beep_remaining = (int)(0.1f * sr);
|
||||||
|
bursts = 0;
|
||||||
|
prev_above = 0;
|
||||||
|
passthrough_output_port = audio_out;
|
||||||
|
passthrough_input_port = audio_in;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = sr;
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(150000);
|
||||||
|
safe_usleep(800000);
|
||||||
|
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(4000000);
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
int got_bursts = bursts;
|
||||||
|
printf(" detected bursts: %d\n", got_bursts);
|
||||||
|
if (got_bursts < 3) {
|
||||||
|
fprintf(stderr, " FAIL: expected ≥3 bursts, got %d\n", got_bursts);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (at least 3 repetitions)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_record_loop_stop(void) {
|
||||||
|
printf("Test: full record‑loop‑stop (≥5 repetitions)\n");
|
||||||
|
pid_t pid = start_looper();
|
||||||
|
if (pid < 0) return 1;
|
||||||
|
if (init_persistent_midi_client() != 0) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " FAIL: cannot initialise persistent MIDI client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_client_t *client;
|
||||||
|
jack_status_t status;
|
||||||
|
client = jack_client_open("test_full", JackNoStartServer, &status);
|
||||||
|
if (!client) {
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
fprintf(stderr, " SKIP: no JACK\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_port_t *audio_out = jack_port_register(client, "out",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsOutput, 0);
|
||||||
|
jack_port_t *audio_in = jack_port_register(client, "in",
|
||||||
|
JACK_DEFAULT_AUDIO_TYPE,
|
||||||
|
JackPortIsInput, 0);
|
||||||
|
if (!audio_out || !audio_in) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
char my_out[64], my_in[64];
|
||||||
|
snprintf(my_out, sizeof(my_out), "test_full:out");
|
||||||
|
snprintf(my_in, sizeof(my_in), "test_full:in");
|
||||||
|
if (jack_connect(client, my_out, "looper:input") ||
|
||||||
|
jack_connect(client, "looper:output", my_in)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(500000);
|
||||||
|
int sr = jack_get_sample_rate(client);
|
||||||
|
continuous_sine = 0;
|
||||||
|
beep_remaining = (int)(0.5f * sr);
|
||||||
|
bursts = 0;
|
||||||
|
prev_above = 0;
|
||||||
|
passthrough_output_port = audio_out;
|
||||||
|
passthrough_input_port = audio_in;
|
||||||
|
passthrough_phase = 0.0f;
|
||||||
|
passthrough_freq = 440.0f;
|
||||||
|
passthrough_sample_rate = sr;
|
||||||
|
passthrough_total_samples = 0;
|
||||||
|
passthrough_sum_sq = 0.0;
|
||||||
|
passthrough_done = 0;
|
||||||
|
jack_set_process_callback(client, passthrough_process, NULL);
|
||||||
|
if (jack_activate(client)) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 1, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(2500000);
|
||||||
|
if (send_jack_note_on("looper:control", 64, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
if (send_jack_note_on("looper:control", 65, 127) != 0) {
|
||||||
|
jack_client_close(client);
|
||||||
|
kill(pid, SIGTERM); waitpid(pid, NULL, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
safe_usleep(200000);
|
||||||
|
int total_bursts = bursts;
|
||||||
|
jack_deactivate(client);
|
||||||
|
jack_client_close(client);
|
||||||
|
cleanup_persistent_midi_client();
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (total_bursts < 5) {
|
||||||
|
fprintf(stderr, " FAIL: expected ≥5 bursts, got %d\n", total_bursts);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf(" PASS (≥5 repetitions, stopped cleanly)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int test_loop(void) {
|
||||||
|
int failures = 0;
|
||||||
|
failures += test_looper_looping();
|
||||||
|
failures += test_record_loop_stop();
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
0
tests/test_save.c
Normal file
0
tests/test_save.c
Normal file
0
tests/test_wav.c
Normal file
0
tests/test_wav.c
Normal file
1574
tests/unit_tests_tui.c
Normal file
1574
tests/unit_tests_tui.c
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user