From 0563e1da52404dba46b9033b0883c4327bf0444d Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Wed, 28 Sep 2022 12:16:55 +0200 Subject: [PATCH] audioconvert: add debug.wav-path to save wav --- spa/plugins/audioconvert/audioconvert.c | 51 ++++- spa/plugins/audioconvert/meson.build | 1 + spa/plugins/audioconvert/wavfile.c | 249 ++++++++++++++++++++++++ spa/plugins/audioconvert/wavfile.h | 41 ++++ 4 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 spa/plugins/audioconvert/wavfile.c create mode 100644 spa/plugins/audioconvert/wavfile.h diff --git a/spa/plugins/audioconvert/audioconvert.c b/spa/plugins/audioconvert/audioconvert.c index 2f979e670..869f2f415 100644 --- a/spa/plugins/audioconvert/audioconvert.c +++ b/spa/plugins/audioconvert/audioconvert.c @@ -29,6 +29,7 @@ #include "fmt-ops.h" #include "channelmix-ops.h" #include "resample.h" +#include "wavfile.h" #undef SPA_LOG_TOPIC_DEFAULT #define SPA_LOG_TOPIC_DEFAULT log_topic @@ -72,6 +73,7 @@ struct props { unsigned int resample_disabled:1; unsigned int resample_quality; double rate; + char wav_path[512]; }; static void props_reset(struct props *props) @@ -89,6 +91,7 @@ static void props_reset(struct props *props) props->resample_disabled = false; props->resample_quality = RESAMPLE_DEFAULT_QUALITY; props->rate = 1.0; + spa_zero(props->wav_path); } struct buffer { @@ -207,6 +210,8 @@ struct impl { float *scratch; float *tmp[2]; float *tmp_datas[2][MAX_PORTS]; + + struct wav_file *wav_file; }; #define CHECK_PORT(this,d,p) ((p) < this->dir[d].n_ports) @@ -637,6 +642,14 @@ static int impl_node_enum_params(void *object, int seq, spa_pod_builder_pop(&b, &f[1]); param = spa_pod_builder_pop(&b, &f[0]); break; + case 24: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_name, SPA_POD_String("debug.wav-path"), + SPA_PROP_INFO_description, SPA_POD_String("Path to WAV file"), + SPA_PROP_INFO_type, SPA_POD_String(p->wav_path), + SPA_PROP_INFO_params, SPA_POD_Bool(true)); + break; default: return 0; } @@ -709,6 +722,8 @@ static int impl_node_enum_params(void *object, int seq, spa_pod_builder_int(&b, this->dir[1].conv.noise_bits); spa_pod_builder_string(&b, "dither.method"); spa_pod_builder_string(&b, dither_method_info[this->dir[1].conv.method].label); + spa_pod_builder_string(&b, "debug.wav-path"); + spa_pod_builder_string(&b, p->wav_path); spa_pod_builder_pop(&b, &f[1]); param = spa_pod_builder_pop(&b, &f[0]); break; @@ -782,6 +797,10 @@ static int audioconvert_set_param(struct impl *this, const char *k, const char * spa_atou32(s, &this->dir[1].conv.noise_bits, 0); else if (spa_streq(k, "dither.method")) this->dir[1].conv.method = dither_method_from_label(s); + else if (spa_streq(k, "debug.wav-path")) { + spa_scnprintf(this->props.wav_path, + sizeof(this->props.wav_path), "%s", s ? s : ""); + } else return 0; return 1; @@ -2140,6 +2159,30 @@ static int impl_node_port_reuse_buffer(void *object, uint32_t port_id, uint32_t return 0; } +static void handle_wav(struct impl *this, void **src, uint32_t n_samples) +{ + if (SPA_UNLIKELY(this->props.wav_path[0])) { + if (this->wav_file == NULL) { + struct wav_file_info info; + + info.info = this->dir[this->direction].format; + + this->wav_file = wav_file_open(this->props.wav_path, + "w", &info); + if (this->wav_file == NULL) + spa_log_warn(this->log, "can't open wav path: %m"); + } + if (this->wav_file) { + wav_file_write(this->wav_file, src, n_samples); + } else { + spa_zero(this->props.wav_path); + } + } else if (this->wav_file != NULL) { + wav_file_close(this->wav_file); + this->wav_file = NULL; + } +} + static int channelmix_process_control(struct impl *this, struct port *ctrlport, void * SPA_RESTRICT dst[], const void * SPA_RESTRICT src[], uint32_t n_samples) @@ -2532,6 +2575,9 @@ static int impl_node_process(void *object) dst_remap = (void **)dst_datas; } + if (this->direction == SPA_DIRECTION_INPUT) + handle_wav(this, (void**)src_datas, n_samples); + dir = &this->dir[SPA_DIRECTION_INPUT]; if (!in_passthrough) { if (mix_passthrough && resample_passthrough && out_passthrough) @@ -2619,6 +2665,8 @@ static int impl_node_process(void *object) spa_log_trace_fp(this->log, "%p: output convert %d", this, n_samples); convert_process(&dir->conv, dst_datas, in_datas, n_samples); } + if (this->direction == SPA_DIRECTION_OUTPUT) + handle_wav(this, dst_datas, n_samples); spa_log_trace_fp(this->log, "%d/%d %d/%d %d->%d", this->in_offset, max_in, this->out_offset, max_out, n_samples, n_out); @@ -2752,7 +2800,8 @@ static int impl_clear(struct spa_handle *handle) convert_free(&this->dir[0].conv); if (this->dir[1].conv.free) convert_free(&this->dir[1].conv); - + if (this->wav_file != NULL) + wav_file_close(this->wav_file); return 0; } diff --git a/spa/plugins/audioconvert/meson.build b/spa/plugins/audioconvert/meson.build index f08527a5a..8482c3c0c 100644 --- a/spa/plugins/audioconvert/meson.build +++ b/spa/plugins/audioconvert/meson.build @@ -1,6 +1,7 @@ audioconvert_sources = [ 'audioadapter.c', 'audioconvert.c', + 'wavfile.c', 'plugin.c' ] diff --git a/spa/plugins/audioconvert/wavfile.c b/spa/plugins/audioconvert/wavfile.c new file mode 100644 index 000000000..b4c383e07 --- /dev/null +++ b/spa/plugins/audioconvert/wavfile.c @@ -0,0 +1,249 @@ +/* PipeWire + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +#include + +#include "wavfile.h" + +#define BLOCK_SIZE 4096 + +struct wav_file { + struct spa_audio_info info; + int fd; + const struct format_info *fi; + + uint32_t length; + + uint32_t stride; + uint32_t blocks; +}; + +static inline ssize_t write_data(struct wav_file *wf, const void *data, size_t size) +{ + ssize_t len; + len = write(wf->fd, data, size); + if (len > 0) + wf->length += len; + return len; +} + +static ssize_t writei(struct wav_file *wf, void **data, size_t samples) +{ + return write_data(wf, data[0], samples * wf->stride); +} + +typedef struct { + uint8_t v[3]; +} __attribute__ ((packed)) uint24_t; + +#define MAKE_WRITEN_FUNC(name, type) \ +static ssize_t name (struct wav_file *wf, void **data, size_t samples) \ +{ \ + uint32_t b, n, k, blocks = wf->blocks, chunk; \ + uint8_t buf[BLOCK_SIZE]; \ + ssize_t res = 0; \ + type **d = (type**)data; \ + uint32_t chunk_size = sizeof(buf) / (blocks * sizeof(type)); \ + for (n = 0; n < samples; ) { \ + type *p = (type*)buf; \ + chunk = SPA_MIN(samples - n, chunk_size); \ + for (k = 0; k < chunk; k++, n++) { \ + for (b = 0; b < blocks; b++) \ + *p++ = d[b][n]; \ + } \ + res += write_data(wf, buf, \ + chunk * blocks * sizeof(type)); \ + } \ + return res; \ +} + +MAKE_WRITEN_FUNC(writen_8, uint8_t); +MAKE_WRITEN_FUNC(writen_16, uint16_t); +MAKE_WRITEN_FUNC(writen_24, uint24_t); +MAKE_WRITEN_FUNC(writen_32, uint32_t); +MAKE_WRITEN_FUNC(writen_64, uint64_t); + +static inline int write_n(int fd, const void *buf, int count) +{ + return write(fd, buf, count) == (ssize_t)count ? count : -errno; +} + +static inline int write_le16(int fd, uint16_t val) +{ + uint8_t buf[2] = { val, val >> 8 }; + return write_n(fd, buf, 2); +} + +static inline int write_le32(int fd, uint32_t val) +{ + uint8_t buf[4] = { val, val >> 8, val >> 16, val >> 24 }; + return write_n(fd, buf, 4); +} + +#define MAKE_AUDIO_RAW(format,bits,planar,fmt,...) \ + { SPA_MEDIA_TYPE_audio, SPA_MEDIA_SUBTYPE_raw, format, bits, planar, fmt, __VA_ARGS__ } + +static struct format_info { + uint32_t media_type; + uint32_t media_subtype; + uint32_t format; + uint32_t bits; + bool planar; + uint32_t fmt; + ssize_t (*write) (struct wav_file *wf, void **data, size_t samples); +} format_info[] = { + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_U8P, 8, true, 1, writen_8), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_U8, 8, false, 1, writei), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S16P, 16, true, 1, writen_16), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S16_LE, 16, false, 1, writei), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S24P, 24, true, 1, writen_24), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S24_LE, 24, false, 1, writei), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S24_32P, 32, true, 1, writen_32), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S32P, 32, true, 1, writen_32), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S24_32_LE, 32, false, 1, writei), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_S32_LE, 32, false, 1, writei), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_F32P, 32, true, 3, writen_32), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_F32_LE, 32, false, 3, writei), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_F64P, 64, true, 3, writen_64), + MAKE_AUDIO_RAW(SPA_AUDIO_FORMAT_F64_LE, 32, false, 3, writei), +}; + +#define CHECK_RES(expr) if ((res = (expr)) < 0) return res + +static int write_headers(struct wav_file *wf) +{ + int res; + uint32_t channels, rate, bps, bits; + const struct format_info *fi = wf->fi; + + lseek(wf->fd, 0, SEEK_SET); + + rate = wf->info.info.raw.rate; + channels = wf->info.info.raw.channels; + bits = fi->bits; + bps = channels * bits / 8; + + CHECK_RES(write_n(wf->fd, "RIFF", 4)); + CHECK_RES(write_le32(wf->fd, wf->length == 0 ? (uint32_t)-1 : wf->length + 12 + 8 + 16)); + CHECK_RES(write_n(wf->fd, "WAVE", 4)); + CHECK_RES(write_n(wf->fd, "fmt ", 4)); + CHECK_RES(write_le32(wf->fd, 16)); + CHECK_RES(write_le16(wf->fd, fi->fmt)); /* format */ + CHECK_RES(write_le16(wf->fd, channels)); /* channels */ + CHECK_RES(write_le32(wf->fd, rate)); /* rate */ + CHECK_RES(write_le32(wf->fd, bps * rate)); /* bytes per sec */ + CHECK_RES(write_le16(wf->fd, bps)); /* bytes per samples */ + CHECK_RES(write_le16(wf->fd, bits)); /* bits per sample */ + CHECK_RES(write_n(wf->fd, "data", 4)); + CHECK_RES(write_le32(wf->fd, wf->length == 0 ? (uint32_t)-1 : wf->length)); + + return 0; +} + +static const struct format_info *find_info(struct wav_file_info *info) +{ + uint32_t i; + + if (info->info.media_type != SPA_MEDIA_TYPE_audio || + info->info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return NULL; + + for (i = 0; i < SPA_N_ELEMENTS(format_info); i++) { + if (format_info[i].format == info->info.info.raw.format) + return &format_info[i]; + } + return NULL; +} + +static int open_write(struct wav_file *wf, const char *filename, struct wav_file_info *info) +{ + int res; + const struct format_info *fi; + + fi = find_info(info); + if (fi == NULL) + return -ENOTSUP; + + if ((wf->fd = open(filename, O_WRONLY | O_CREAT | O_CLOEXEC | O_TRUNC, 0660)) < 0) { + res = -errno; + goto exit; + } + wf->info = info->info; + wf->fi = fi; + if (fi->planar) { + wf->stride = fi->bits / 8; + wf->blocks = info->info.info.raw.channels; + } else { + wf->stride = info->info.info.raw.channels * (fi->bits / 8); + wf->blocks = 1; + } + + res = write_headers(wf); +exit: + return res; +} + +struct wav_file * +wav_file_open(const char *filename, const char *mode, struct wav_file_info *info) +{ + int res; + struct wav_file *wf; + + wf = calloc(1, sizeof(struct wav_file)); + if (wf == NULL) + return NULL; + + if (spa_streq(mode, "w")) { + if ((res = open_write(wf, filename, info)) < 0) + goto exit_free; + } else { + res = -EINVAL; + goto exit_free; + } + return wf; + +exit_free: + free(wf); + errno = -res; + return NULL; +} + +int wav_file_close(struct wav_file *wf) +{ + int res; + + CHECK_RES(write_headers(wf)); + + close(wf->fd); + free(wf); + return 0; +} + +ssize_t wav_file_write(struct wav_file *wf, void **data, size_t samples) +{ + return wf->fi->write(wf, data, samples); +} diff --git a/spa/plugins/audioconvert/wavfile.h b/spa/plugins/audioconvert/wavfile.h new file mode 100644 index 000000000..7ecbddb2b --- /dev/null +++ b/spa/plugins/audioconvert/wavfile.h @@ -0,0 +1,41 @@ +/* PipeWire + * + * Copyright © 2022 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include + +#include +#include + +struct wav_file; + +struct wav_file_info { + struct spa_audio_info info; +}; + +struct wav_file * +wav_file_open(const char *filename, const char *mode, struct wav_file_info *info); + +int wav_file_close(struct wav_file *wf); + +ssize_t wav_file_write(struct wav_file *wf, void **data, size_t size);