feat: add save/load thread and WAV file I/O for clip persistence
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
24
cli.c
24
cli.c
@@ -1,4 +1,5 @@
|
|||||||
#include "cli.h"
|
#include "cli.h"
|
||||||
|
#include "wav_io.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <strings.h>
|
#include <strings.h>
|
||||||
@@ -56,6 +57,8 @@ int cli_process_line(Engine *engine, const char *line) {
|
|||||||
printf(" toggle - Toggle play/pause\n");
|
printf(" toggle - Toggle play/pause\n");
|
||||||
printf(" clock internal|midi - Set clock source\n");
|
printf(" clock internal|midi - Set clock source\n");
|
||||||
printf(" bpm <value> - Set BPM (1.0-999.0)\n");
|
printf(" bpm <value> - Set BPM (1.0-999.0)\n");
|
||||||
|
printf(" load <clip> <file> - Load WAV file into clip\n");
|
||||||
|
printf(" save <clip> - Save clip to samples/clip_<N>.wav\n");
|
||||||
printf(" help - Show this help\n");
|
printf(" help - Show this help\n");
|
||||||
printf(" quit - Exit CLI\n");
|
printf(" quit - Exit CLI\n");
|
||||||
return 1;
|
return 1;
|
||||||
@@ -157,6 +160,27 @@ int cli_process_line(Engine *engine, const char *line) {
|
|||||||
printf("Unknown clock source: %s\n", source_str);
|
printf("Unknown clock source: %s\n", source_str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (strcasecmp(token, "load") == 0) {
|
||||||
|
char *clip_str = strtok(NULL, " \t");
|
||||||
|
char *filename = strtok(NULL, " \t");
|
||||||
|
if (!clip_str || !filename) {
|
||||||
|
printf("Usage: load <clip_index> <filename>\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int clip_idx = atoi(clip_str);
|
||||||
|
save_load_queue_push(&engine->save_load_queue, REQ_LOAD_CLIP, clip_idx, filename);
|
||||||
|
printf("Loading %s into clip %d...\n", filename, clip_idx);
|
||||||
|
}
|
||||||
|
else if (strcasecmp(token, "save") == 0) {
|
||||||
|
char *clip_str = strtok(NULL, " \t");
|
||||||
|
if (!clip_str) {
|
||||||
|
printf("Usage: save <clip_index>\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int clip_idx = atoi(clip_str);
|
||||||
|
save_load_queue_push(&engine->save_load_queue, REQ_SAVE_CLIP, clip_idx, "");
|
||||||
|
printf("Saving clip %d...\n", clip_idx);
|
||||||
|
}
|
||||||
else if (strcasecmp(token, "bpm") == 0) {
|
else if (strcasecmp(token, "bpm") == 0) {
|
||||||
char *bpm_str = strtok(NULL, " \t");
|
char *bpm_str = strtok(NULL, " \t");
|
||||||
if (!bpm_str) {
|
if (!bpm_str) {
|
||||||
|
|||||||
184
engine.c
184
engine.c
@@ -1,9 +1,14 @@
|
|||||||
#include "engine.h"
|
#include "engine.h"
|
||||||
|
#include "wav_io.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <stdatomic.h>
|
#include <stdatomic.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
static void process_queued_triggers(Engine *engine, jack_nframes_t current_frame);
|
static void process_queued_triggers(Engine *engine, jack_nframes_t current_frame);
|
||||||
@@ -243,6 +248,158 @@ void command_queue_init(CommandQueue *q) {
|
|||||||
atomic_store(&q->read_index, 0);
|
atomic_store(&q->read_index, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize save/load queue
|
||||||
|
void save_load_queue_init(SaveLoadQueue *q) {
|
||||||
|
atomic_store(&q->write_index, 0);
|
||||||
|
atomic_store(&q->read_index, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push a save/load request (called from audio thread)
|
||||||
|
int save_load_queue_push(SaveLoadQueue *q, SaveLoadType type, int clip_index, const char *filename) {
|
||||||
|
if (!q || !filename) return -1;
|
||||||
|
|
||||||
|
unsigned int write = atomic_load(&q->write_index);
|
||||||
|
unsigned int read = atomic_load(&q->read_index);
|
||||||
|
|
||||||
|
if ((write - read) >= MAX_QUEUED_COMMANDS) {
|
||||||
|
fprintf(stderr, "Save/Load queue full, dropping request\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int slot = write % MAX_QUEUED_COMMANDS;
|
||||||
|
q->buffer[slot].type = type;
|
||||||
|
q->buffer[slot].clip_index = clip_index;
|
||||||
|
strncpy(q->buffer[slot].filename, filename, sizeof(q->buffer[slot].filename) - 1);
|
||||||
|
q->buffer[slot].filename[sizeof(q->buffer[slot].filename) - 1] = '\0';
|
||||||
|
|
||||||
|
atomic_store(&q->write_index, write + 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop a save/load request (called from save/load thread)
|
||||||
|
int save_load_queue_pop(SaveLoadQueue *q, SaveLoadRequest *req) {
|
||||||
|
if (!q || !req) return -1;
|
||||||
|
|
||||||
|
unsigned int read = atomic_load(&q->read_index);
|
||||||
|
unsigned int write = atomic_load(&q->write_index);
|
||||||
|
|
||||||
|
if (read >= write) return 0; // Empty
|
||||||
|
|
||||||
|
unsigned int slot = read % MAX_QUEUED_COMMANDS;
|
||||||
|
*req = q->buffer[slot];
|
||||||
|
|
||||||
|
atomic_store(&q->read_index, read + 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save/Load thread function
|
||||||
|
void* save_load_thread_func(void *arg) {
|
||||||
|
Engine *engine = (Engine *)arg;
|
||||||
|
if (!engine) return NULL;
|
||||||
|
|
||||||
|
// Create samples directory if it doesn't exist
|
||||||
|
mkdir("samples", 0755);
|
||||||
|
|
||||||
|
while (engine->save_load_running) {
|
||||||
|
SaveLoadRequest req;
|
||||||
|
int ret = save_load_queue_pop(&engine->save_load_queue, &req);
|
||||||
|
|
||||||
|
if (ret == 1) {
|
||||||
|
char filepath[512];
|
||||||
|
|
||||||
|
switch (req.type) {
|
||||||
|
case REQ_SAVE_CLIP: {
|
||||||
|
if (req.clip_index < 0 || req.clip_index >= MAX_CLIPS) break;
|
||||||
|
Clip *clip = &engine->clips[req.clip_index];
|
||||||
|
|
||||||
|
// Build filename: samples/clip_<index>.wav
|
||||||
|
snprintf(filepath, sizeof(filepath), "samples/clip_%d.wav", req.clip_index);
|
||||||
|
|
||||||
|
if (clip->buffer && clip->buffer_size > 0) {
|
||||||
|
int result = save_wav_float(filepath, clip->buffer, clip->buffer_size, engine->sample_rate);
|
||||||
|
if (result == 0) {
|
||||||
|
printf("Saved clip %d to %s (%zu samples)\n", req.clip_index, filepath, clip->buffer_size);
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "Failed to save clip %d to %s\n", req.clip_index, filepath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case REQ_LOAD_CLIP: {
|
||||||
|
if (req.clip_index < 0 || req.clip_index >= MAX_CLIPS) break;
|
||||||
|
Clip *clip = &engine->clips[req.clip_index];
|
||||||
|
|
||||||
|
float *new_buffer = NULL;
|
||||||
|
size_t num_samples = 0;
|
||||||
|
unsigned int file_sample_rate = 0;
|
||||||
|
|
||||||
|
int result = load_wav_float(req.filename, &new_buffer, &num_samples, &file_sample_rate);
|
||||||
|
if (result == 0 && new_buffer && num_samples > 0) {
|
||||||
|
// Allocate a new buffer for the clip
|
||||||
|
float *clip_buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float));
|
||||||
|
if (!clip_buffer) {
|
||||||
|
free(new_buffer);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy samples (truncate if too long)
|
||||||
|
size_t copy_size = (num_samples < MAX_BUFFER_SIZE) ? num_samples : MAX_BUFFER_SIZE;
|
||||||
|
memcpy(clip_buffer, new_buffer, copy_size * sizeof(float));
|
||||||
|
|
||||||
|
// Atomically swap the clip's buffer
|
||||||
|
float *old_buffer = atomic_exchange(&clip->buffer, clip_buffer);
|
||||||
|
|
||||||
|
// Update clip state atomically
|
||||||
|
clip->state = CLIP_LOOPING;
|
||||||
|
clip->buffer_size = copy_size;
|
||||||
|
clip->write_position = copy_size;
|
||||||
|
clip->read_position = 0;
|
||||||
|
|
||||||
|
// Free old buffer and temporary buffer
|
||||||
|
if (old_buffer) free(old_buffer);
|
||||||
|
free(new_buffer);
|
||||||
|
|
||||||
|
printf("Loaded clip %d from %s (%zu samples, %u Hz)\n",
|
||||||
|
req.clip_index, req.filename, num_samples, file_sample_rate);
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "Failed to load %s into clip %d\n", req.filename, req.clip_index);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No requests, sleep a bit to avoid busy-waiting
|
||||||
|
usleep(1000); // 1ms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the save/load thread
|
||||||
|
int engine_start_save_load_thread(Engine *engine) {
|
||||||
|
if (!engine) return -1;
|
||||||
|
|
||||||
|
engine->save_load_running = true;
|
||||||
|
save_load_queue_init(&engine->save_load_queue);
|
||||||
|
|
||||||
|
if (pthread_create(&engine->save_load_thread, NULL, save_load_thread_func, engine) != 0) {
|
||||||
|
engine->save_load_running = false;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the save/load thread
|
||||||
|
void engine_stop_save_load_thread(Engine *engine) {
|
||||||
|
if (!engine) return;
|
||||||
|
|
||||||
|
engine->save_load_running = false;
|
||||||
|
pthread_join(engine->save_load_thread, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
// Submit command from frontend thread (non-blocking)
|
// Submit command from frontend thread (non-blocking)
|
||||||
int engine_submit_command(Engine *engine, CommandType type, int index, jack_nframes_t value) {
|
int engine_submit_command(Engine *engine, CommandType type, int index, jack_nframes_t value) {
|
||||||
if (!engine) return -1;
|
if (!engine) return -1;
|
||||||
@@ -309,6 +466,8 @@ void engine_process_commands(Engine *engine) {
|
|||||||
action.previous_read_position = clip->read_position;
|
action.previous_read_position = clip->read_position;
|
||||||
engine_push_undo_action(engine, &action);
|
engine_push_undo_action(engine, &action);
|
||||||
|
|
||||||
|
ClipState prev_state = clip->state;
|
||||||
|
|
||||||
switch (clip->state) {
|
switch (clip->state) {
|
||||||
case CLIP_EMPTY:
|
case CLIP_EMPTY:
|
||||||
clip->state = CLIP_RECORDING;
|
clip->state = CLIP_RECORDING;
|
||||||
@@ -330,6 +489,11 @@ void engine_process_commands(Engine *engine) {
|
|||||||
clip->read_position = 0;
|
clip->read_position = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-save when recording finishes (RECORDING -> LOOPING)
|
||||||
|
if (prev_state == CLIP_RECORDING && clip->state == CLIP_LOOPING) {
|
||||||
|
save_load_queue_push(&engine->save_load_queue, REQ_SAVE_CLIP, cmd.index, "");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +511,8 @@ void engine_process_commands(Engine *engine) {
|
|||||||
int clip_idx = CLIP_INDEX(cmd.index, ch);
|
int clip_idx = CLIP_INDEX(cmd.index, ch);
|
||||||
Clip *clip = &engine->clips[clip_idx];
|
Clip *clip = &engine->clips[clip_idx];
|
||||||
|
|
||||||
|
ClipState prev_state = clip->state;
|
||||||
|
|
||||||
switch (clip->state) {
|
switch (clip->state) {
|
||||||
case CLIP_EMPTY:
|
case CLIP_EMPTY:
|
||||||
clip->state = CLIP_RECORDING;
|
clip->state = CLIP_RECORDING;
|
||||||
@@ -368,6 +534,11 @@ void engine_process_commands(Engine *engine) {
|
|||||||
clip->read_position = 0;
|
clip->read_position = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-save when recording finishes
|
||||||
|
if (prev_state == CLIP_RECORDING && clip->state == CLIP_LOOPING) {
|
||||||
|
save_load_queue_push(&engine->save_load_queue, REQ_SAVE_CLIP, clip_idx, "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -788,6 +959,9 @@ int engine_init(Engine *engine, const char *client_name) {
|
|||||||
// Initialize command queue
|
// Initialize command queue
|
||||||
command_queue_init(&engine->command_queue);
|
command_queue_init(&engine->command_queue);
|
||||||
|
|
||||||
|
// Initialize save/load queue
|
||||||
|
save_load_queue_init(&engine->save_load_queue);
|
||||||
|
|
||||||
// Initialize atomic state mirrors
|
// Initialize atomic state mirrors
|
||||||
atomic_store(&engine->quantize_mode_atomic, (int)QUANTIZE_OFF);
|
atomic_store(&engine->quantize_mode_atomic, (int)QUANTIZE_OFF);
|
||||||
atomic_store(&engine->quantize_threshold_atomic, 0);
|
atomic_store(&engine->quantize_threshold_atomic, 0);
|
||||||
@@ -921,6 +1095,15 @@ int engine_start(Engine *engine) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
engine->running = true;
|
engine->running = true;
|
||||||
|
|
||||||
|
// Start save/load thread
|
||||||
|
if (engine_start_save_load_thread(engine) != 0) {
|
||||||
|
fprintf(stderr, "Failed to start save/load thread\n");
|
||||||
|
jack_deactivate(engine->client);
|
||||||
|
engine->running = false;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -928,6 +1111,7 @@ void engine_stop(Engine *engine) {
|
|||||||
if (!engine || !engine->client) return;
|
if (!engine || !engine->client) return;
|
||||||
|
|
||||||
engine->running = false;
|
engine->running = false;
|
||||||
|
engine_stop_save_load_thread(engine);
|
||||||
jack_deactivate(engine->client);
|
jack_deactivate(engine->client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
35
engine.h
35
engine.h
@@ -6,6 +6,7 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdatomic.h>
|
#include <stdatomic.h>
|
||||||
|
#include <pthread.h>
|
||||||
#include "transport.h"
|
#include "transport.h"
|
||||||
|
|
||||||
#define MAX_SCENES 8
|
#define MAX_SCENES 8
|
||||||
@@ -114,6 +115,25 @@ typedef struct {
|
|||||||
atomic_uint read_index;
|
atomic_uint read_index;
|
||||||
} CommandQueue;
|
} CommandQueue;
|
||||||
|
|
||||||
|
// Save/Load request types
|
||||||
|
typedef enum {
|
||||||
|
REQ_SAVE_CLIP,
|
||||||
|
REQ_LOAD_CLIP
|
||||||
|
} SaveLoadType;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
SaveLoadType type;
|
||||||
|
int clip_index;
|
||||||
|
char filename[256];
|
||||||
|
} SaveLoadRequest;
|
||||||
|
|
||||||
|
// Lock-free queue for save/load requests (audio thread -> save/load thread)
|
||||||
|
typedef struct {
|
||||||
|
SaveLoadRequest buffer[MAX_QUEUED_COMMANDS];
|
||||||
|
atomic_uint write_index;
|
||||||
|
atomic_uint read_index;
|
||||||
|
} SaveLoadQueue;
|
||||||
|
|
||||||
// Queued trigger for quantization
|
// Queued trigger for quantization
|
||||||
typedef struct QueuedTrigger {
|
typedef struct QueuedTrigger {
|
||||||
int clip_index;
|
int clip_index;
|
||||||
@@ -154,6 +174,11 @@ typedef struct {
|
|||||||
|
|
||||||
// Undo/Redo
|
// Undo/Redo
|
||||||
UndoHistory undo_history;
|
UndoHistory undo_history;
|
||||||
|
|
||||||
|
// Save/Load queue and thread
|
||||||
|
SaveLoadQueue save_load_queue;
|
||||||
|
pthread_t save_load_thread;
|
||||||
|
volatile bool save_load_running;
|
||||||
} Engine;
|
} Engine;
|
||||||
|
|
||||||
// Engine lifecycle
|
// Engine lifecycle
|
||||||
@@ -189,6 +214,16 @@ void engine_process_commands(Engine *engine);
|
|||||||
// Initialize command queue (exposed for testing)
|
// Initialize command queue (exposed for testing)
|
||||||
void command_queue_init(CommandQueue *q);
|
void command_queue_init(CommandQueue *q);
|
||||||
|
|
||||||
|
// Save/Load queue management
|
||||||
|
void save_load_queue_init(SaveLoadQueue *q);
|
||||||
|
int save_load_queue_push(SaveLoadQueue *q, SaveLoadType type, int clip_index, const char *filename);
|
||||||
|
int save_load_queue_pop(SaveLoadQueue *q, SaveLoadRequest *req);
|
||||||
|
|
||||||
|
// Save/Load thread
|
||||||
|
void* save_load_thread_func(void *arg);
|
||||||
|
int engine_start_save_load_thread(Engine *engine);
|
||||||
|
void engine_stop_save_load_thread(Engine *engine);
|
||||||
|
|
||||||
// Utility
|
// Utility
|
||||||
const char* clip_state_to_string(ClipState state);
|
const char* clip_state_to_string(ClipState state);
|
||||||
uint8_t clip_state_to_velocity(ClipState state);
|
uint8_t clip_state_to_velocity(ClipState state);
|
||||||
|
|||||||
22
tui.c
22
tui.c
@@ -1,4 +1,5 @@
|
|||||||
#include "tui.h"
|
#include "tui.h"
|
||||||
|
#include "wav_io.h"
|
||||||
#include <ncurses.h>
|
#include <ncurses.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -366,8 +367,27 @@ static bool handle_command_mode(void) {
|
|||||||
// Restore previous nodelay state before returning
|
// Restore previous nodelay state before returning
|
||||||
nodelay(stdscr, prev_nodelay);
|
nodelay(stdscr, prev_nodelay);
|
||||||
return true; // Quit
|
return true; // Quit
|
||||||
|
} else if (strncmp(cmd_buffer, "load ", 5) == 0) {
|
||||||
|
// :load <clip_index> <filename>
|
||||||
|
char *rest = cmd_buffer + 5;
|
||||||
|
int clip_idx = atoi(rest);
|
||||||
|
// Find filename after clip index
|
||||||
|
char *filename = rest;
|
||||||
|
while (*filename && *filename != ' ') filename++;
|
||||||
|
if (*filename) {
|
||||||
|
*filename = '\0';
|
||||||
|
filename++;
|
||||||
|
while (*filename == ' ') filename++;
|
||||||
|
if (*filename) {
|
||||||
|
// Submit load request via save/load queue
|
||||||
|
save_load_queue_push(&g_engine->save_load_queue, REQ_LOAD_CLIP, clip_idx, filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (strncmp(cmd_buffer, "save ", 5) == 0) {
|
||||||
|
// :save <clip_index>
|
||||||
|
int clip_idx = atoi(cmd_buffer + 5);
|
||||||
|
save_load_queue_push(&g_engine->save_load_queue, REQ_SAVE_CLIP, clip_idx, "");
|
||||||
}
|
}
|
||||||
// Add more commands here as needed
|
|
||||||
|
|
||||||
// Restore previous nodelay state before returning
|
// Restore previous nodelay state before returning
|
||||||
nodelay(stdscr, prev_nodelay);
|
nodelay(stdscr, prev_nodelay);
|
||||||
|
|||||||
231
wav_io.c
Normal file
231
wav_io.c
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
#include "wav_io.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// WAV file header structures (little-endian)
|
||||||
|
typedef struct {
|
||||||
|
char chunkID[4]; // "RIFF"
|
||||||
|
uint32_t chunkSize; // file size - 8
|
||||||
|
char format[4]; // "WAVE"
|
||||||
|
} WAVHeader;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char subchunk1ID[4]; // "fmt "
|
||||||
|
uint32_t subchunk1Size; // 16 for PCM
|
||||||
|
uint16_t audioFormat; // 1 = PCM, 3 = IEEE float
|
||||||
|
uint16_t numChannels;
|
||||||
|
uint32_t sampleRate;
|
||||||
|
uint32_t byteRate;
|
||||||
|
uint16_t blockAlign;
|
||||||
|
uint16_t bitsPerSample;
|
||||||
|
} FMTSubchunk;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char subchunk2ID[4]; // "data"
|
||||||
|
uint32_t subchunk2Size;
|
||||||
|
} DataSubchunk;
|
||||||
|
|
||||||
|
static void write_le16(FILE *f, uint16_t v) {
|
||||||
|
unsigned char buf[2];
|
||||||
|
buf[0] = v & 0xFF;
|
||||||
|
buf[1] = (v >> 8) & 0xFF;
|
||||||
|
fwrite(buf, 1, 2, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_le32(FILE *f, uint32_t v) {
|
||||||
|
unsigned char buf[4];
|
||||||
|
buf[0] = v & 0xFF;
|
||||||
|
buf[1] = (v >> 8) & 0xFF;
|
||||||
|
buf[2] = (v >> 16) & 0xFF;
|
||||||
|
buf[3] = (v >> 24) & 0xFF;
|
||||||
|
fwrite(buf, 1, 4, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint16_t read_le16(const unsigned char *buf) {
|
||||||
|
return (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t read_le32(const unsigned char *buf) {
|
||||||
|
return (uint32_t)buf[0] | ((uint32_t)buf[1] << 8) |
|
||||||
|
((uint32_t)buf[2] << 16) | ((uint32_t)buf[3] << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
int save_wav_float(const char *filename, const float *buffer, size_t num_samples, unsigned int sample_rate) {
|
||||||
|
if (!filename || !buffer || num_samples == 0) return -1;
|
||||||
|
|
||||||
|
FILE *f = fopen(filename, "wb");
|
||||||
|
if (!f) return -1;
|
||||||
|
|
||||||
|
// Calculate sizes
|
||||||
|
uint32_t data_size = (uint32_t)(num_samples * sizeof(float));
|
||||||
|
uint32_t chunk_size = 36 + data_size;
|
||||||
|
|
||||||
|
// Write RIFF header
|
||||||
|
fwrite("RIFF", 1, 4, f);
|
||||||
|
write_le32(f, chunk_size);
|
||||||
|
fwrite("WAVE", 1, 4, f);
|
||||||
|
|
||||||
|
// Write fmt subchunk
|
||||||
|
fwrite("fmt ", 1, 4, f);
|
||||||
|
write_le32(f, 16); // subchunk1Size
|
||||||
|
write_le16(f, 3); // audioFormat = IEEE float
|
||||||
|
write_le16(f, 1); // numChannels = mono
|
||||||
|
write_le32(f, sample_rate);
|
||||||
|
write_le32(f, sample_rate * sizeof(float)); // byteRate
|
||||||
|
write_le16(f, sizeof(float)); // blockAlign
|
||||||
|
write_le16(f, 32); // bitsPerSample
|
||||||
|
|
||||||
|
// Write data subchunk
|
||||||
|
fwrite("data", 1, 4, f);
|
||||||
|
write_le32(f, data_size);
|
||||||
|
|
||||||
|
// Write samples
|
||||||
|
fwrite(buffer, sizeof(float), num_samples, f);
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int load_wav_float(const char *filename, float **buffer, size_t *num_samples, unsigned int *sample_rate) {
|
||||||
|
if (!filename || !buffer || !num_samples || !sample_rate) return -1;
|
||||||
|
|
||||||
|
FILE *f = fopen(filename, "rb");
|
||||||
|
if (!f) return -1;
|
||||||
|
|
||||||
|
// Read RIFF header
|
||||||
|
unsigned char header[12];
|
||||||
|
if (fread(header, 1, 12, f) != 12) {
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memcmp(header, "RIFF", 4) != 0 || memcmp(header + 8, "WAVE", 4) != 0) {
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read chunks until we find fmt and data
|
||||||
|
uint16_t audio_format = 0;
|
||||||
|
uint16_t num_channels = 0;
|
||||||
|
uint32_t sample_rate_val = 0;
|
||||||
|
uint16_t bits_per_sample = 0;
|
||||||
|
uint32_t data_size = 0;
|
||||||
|
float *data_buffer = NULL;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
unsigned char chunk_header[8];
|
||||||
|
if (fread(chunk_header, 1, 8, f) != 8) break;
|
||||||
|
|
||||||
|
uint32_t chunk_size = read_le32(chunk_header + 4);
|
||||||
|
|
||||||
|
if (memcmp(chunk_header, "fmt ", 4) == 0) {
|
||||||
|
unsigned char fmt_data[16];
|
||||||
|
if (chunk_size < 16) {
|
||||||
|
fseek(f, chunk_size, SEEK_CUR);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (fread(fmt_data, 1, 16, f) != 16) break;
|
||||||
|
|
||||||
|
audio_format = read_le16(fmt_data);
|
||||||
|
num_channels = read_le16(fmt_data + 2);
|
||||||
|
sample_rate_val = read_le32(fmt_data + 4);
|
||||||
|
bits_per_sample = read_le16(fmt_data + 14);
|
||||||
|
|
||||||
|
// Skip any extra fmt data
|
||||||
|
if (chunk_size > 16) fseek(f, chunk_size - 16, SEEK_CUR);
|
||||||
|
} else if (memcmp(chunk_header, "data", 4) == 0) {
|
||||||
|
data_size = chunk_size;
|
||||||
|
|
||||||
|
// Allocate buffer
|
||||||
|
size_t num_frames = data_size / (bits_per_sample / 8) / num_channels;
|
||||||
|
data_buffer = (float *)calloc(num_frames, sizeof(float));
|
||||||
|
if (!data_buffer) {
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audio_format == 3 && bits_per_sample == 32) {
|
||||||
|
// IEEE float
|
||||||
|
size_t read_size = num_frames * num_channels * sizeof(float);
|
||||||
|
if (read_size > data_size) read_size = data_size;
|
||||||
|
float *temp = (float *)malloc(read_size);
|
||||||
|
if (!temp) {
|
||||||
|
free(data_buffer);
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (fread(temp, 1, read_size, f) != read_size) {
|
||||||
|
free(temp);
|
||||||
|
free(data_buffer);
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Mix to mono if stereo
|
||||||
|
if (num_channels == 1) {
|
||||||
|
memcpy(data_buffer, temp, num_frames * sizeof(float));
|
||||||
|
} else {
|
||||||
|
for (size_t i = 0; i < num_frames; i++) {
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (uint16_t ch = 0; ch < num_channels; ch++) {
|
||||||
|
sum += temp[i * num_channels + ch];
|
||||||
|
}
|
||||||
|
data_buffer[i] = sum / num_channels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(temp);
|
||||||
|
} else if (audio_format == 1 && bits_per_sample == 16) {
|
||||||
|
// 16-bit PCM
|
||||||
|
size_t read_size = num_frames * num_channels * sizeof(int16_t);
|
||||||
|
if (read_size > data_size) read_size = data_size;
|
||||||
|
int16_t *temp = (int16_t *)malloc(read_size);
|
||||||
|
if (!temp) {
|
||||||
|
free(data_buffer);
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (fread(temp, 1, read_size, f) != read_size) {
|
||||||
|
free(temp);
|
||||||
|
free(data_buffer);
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Convert to float and mix to mono
|
||||||
|
if (num_channels == 1) {
|
||||||
|
for (size_t i = 0; i < num_frames; i++) {
|
||||||
|
data_buffer[i] = (float)temp[i] / 32768.0f;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (size_t i = 0; i < num_frames; i++) {
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (uint16_t ch = 0; ch < num_channels; ch++) {
|
||||||
|
sum += (float)temp[i * num_channels + ch] / 32768.0f;
|
||||||
|
}
|
||||||
|
data_buffer[i] = sum / num_channels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(temp);
|
||||||
|
} else {
|
||||||
|
// Unsupported format
|
||||||
|
free(data_buffer);
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*buffer = data_buffer;
|
||||||
|
*num_samples = num_frames;
|
||||||
|
*sample_rate = sample_rate_val;
|
||||||
|
fclose(f);
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
// Skip unknown chunk
|
||||||
|
fseek(f, chunk_size, SEEK_CUR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, we didn't find data
|
||||||
|
if (data_buffer) free(data_buffer);
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
10
wav_io.h
Normal file
10
wav_io.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#ifndef WAV_IO_H
|
||||||
|
#define WAV_IO_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
int save_wav_float(const char *filename, const float *buffer, size_t num_samples, unsigned int sample_rate);
|
||||||
|
int load_wav_float(const char *filename, float **buffer, size_t *num_samples, unsigned int *sample_rate);
|
||||||
|
|
||||||
|
#endif
|
||||||
Reference in New Issue
Block a user