ref: 4ff85f47015d72f28f9eeef85a4c8c7222300891
parent: 5bafb67083c07394616c04e67dab86a9ca40d698
author: qwx <qwx@sciops.net>
date: Sun Jan 29 00:41:47 EST 2023
add plan9-friendly native version via npe work on pcm audio only; use pcmconv(1) for resampling
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,17 @@
+</$objtype/mkfile
+TARG=stretch
+BIN=/$objtype/bin/audio
+
+OFILES=\
+ this.$O\
+ stretch.$O\
+
+HFILES=\
+ stretch.h\
+
+default:V: all
+
+</sys/src/cmd/mkone
+
+CFLAGS=$CFLAGS -p -I/sys/include/npe -D__plan9__ -D__${objtype}__\
+ -D_POSIX_SOURCE
--- /dev/null
+++ b/this.c
@@ -1,0 +1,234 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include "stretch.h"
+
+enum{
+ Nrate = 44100,
+ Nchan = 2,
+ Sampsz = Nchan * 2,
+};
+
+void
+usage(void)
+{
+ fprint(2, "usage: %s [OPTIONS] [FILE]\n"
+ "-r F stretch ratio, [0.25,4.0] default 1.0\n"
+ "-g F gap/silence stretch ratio (if different)\n"
+ "-u N upper freq period limit [40,44100[ default 333 Hz\n"
+ "-l N lower freq period limit, [20,[ default 55 Hz\n"
+ "-b N audio buffer/window length, [1,100] default 25 ms\n"
+ "-t N gap/silence threshold (dB re FS, default -40)\n"
+ "-c cycle through all ratios, starting higher\n"
+ "-C cycle through all ratios, starting lower\n"
+ "-d force dual instance even for shallow ratios\n"
+ "-s scale rate to preserve duration (not pitch)\n"
+ "-f fast pitch detection (default >= 32 kHz)\n"
+ "-n normal pitch detection (default < 32 kHz)\n",
+ argv0);
+ exits("usage");
+}
+
+double
+rms_level_dB (int16_t *audio, int samples, int channels)
+{
+ double rms_sum = 0.0;
+ int i;
+
+ if (channels == 1)
+ for (i = 0; i < samples; ++i)
+ rms_sum += (double) audio [i] * audio [i];
+ else
+ for (i = 0; i < samples; ++i) {
+ double average = (audio [i * 2] + audio [i * 2 + 1]) / 2.0;
+ rms_sum += average * average;
+ }
+
+ return log10 (rms_sum / samples / (32768.0 * 32767.0 * 0.5)) * 10.0;
+}
+
+void
+threadmain(int argc, char **argv)
+{
+ int n, m, fd, ofd, pfd[2], cycle, dual, fast, normal, doscale, lf, uf, wsz, flags, maxnsamp, silence, nibuf, min_period, max_period, non_silence_frames, silence_frames, used_silence_frames, max_generated_stretch, max_generated_flush, samples_to_stretch, consecutive_silence_frames, verbose;
+ char *r;
+ s16int *prebuf, *ibuf, *obuf;
+ u32int insamp, outsamp;
+ float max_ratio;
+ double level, ratio, gap, smin;
+ StretchHandle S;
+
+ fd = 0;
+ ofd = 1;
+ cycle = 0; /* cycle ratio, direction, 1: fw, 2: back */
+ dual = 0; /* force dual instance */
+ fast = 0; /* force fast pitch detection */
+ normal = 0; /* force normal pitch detection */
+ doscale = 0; /* scale rate to preserve duration */
+ lf = 55; /* freq lower bound */
+ uf = 333; /* freq upper bound */
+ gap = 0.0; /* gap/silence stretch ratio */
+ ratio = 1.0; /* stretch ratio */
+ wsz = 25; /* window size */
+ smin = -40; /* gap/silence threshold */
+ verbose = 0;
+ ARGBEGIN{
+ case 'C': cycle = 2; break;
+ case 'c': cycle = 1; break;
+ case 'd': dual = 1; break;
+ case 'f': fast = 1; break;
+ case 'g': gap = strtod(EARGF(usage()), nil); if(gap < 0.25 || gap > 4.0) usage(); break;
+ case 'l': lf = strtol(EARGF(usage()), nil, 10); if(lf < 20) usage(); break;
+ case 'n': normal = 1; break;
+ case 'r': ratio = strtod(EARGF(usage()), nil); if(ratio < 0.25 || ratio > 4.0) usage(); break;
+ case 's': doscale = 1; break;
+ case 't': smin = strtod(EARGF(usage()), nil); if(smin < -70 || smin > -10) usage(); break;
+ case 'u': uf = strtol(EARGF(usage()), nil, 10); if(uf < 40) usage(); break;
+ case 'v': verbose = 1; break;
+ case 'w': wsz = strtol(EARGF(usage()), nil, 10); if(wsz < 1 || wsz > 100) usage(); break;
+ case 'h':
+ default: usage(); break;
+ }ARGEND
+ if(*argv != nil && (fd = open(*argv, OREAD)) < 0)
+ usage();
+
+ insamp = 0;
+ outsamp = 0;
+ min_period = Nrate / uf;
+ max_period = Nrate / lf;
+ flags = 0;
+ silence = gap != 0.0 && gap != ratio && !cycle;
+ nibuf = Nrate * (wsz / 1000.0);
+ max_ratio = ratio;
+ if(dual || ratio < 0.5 || ratio > 2.0 ||
+ (silence && (gap < 0.5 || gap > 2.0)))
+ flags |= STRETCH_DUAL_FLAG;
+ if((fast || Nrate >= 32000) && !normal)
+ flags |= STRETCH_FAST_FLAG;
+ if(verbose){
+ fprint(2, "file sample rate is %d Hz (%s), buffer size is %d samples\n",
+ Nrate, Nchan == 2 ? "stereo" : "mono", nibuf);
+ fprint(2, "stretch period range = %d to %d, %d channels, %s, %s\n",
+ min_period, max_period, 2, (flags & STRETCH_FAST_FLAG) ? "fast mode" : "normal mode",
+ (flags & STRETCH_DUAL_FLAG) ? "dual instance" : "single instance");
+ }
+ if((S = stretch_init(min_period, max_period, Nchan, flags)) == NULL)
+ sysfatal("initialization failed");
+ if(cycle)
+ max_ratio = (flags & STRETCH_DUAL_FLAG) ? 4.0 : 2.0;
+ else if(silence && gap > max_ratio)
+ max_ratio = gap;
+ maxnsamp = stretch_output_capacity(S, nibuf, max_ratio);
+ obuf = prebuf = nil;
+ if((ibuf = malloc(nibuf * Sampsz)) == nil
+ || (obuf = malloc(maxnsamp * Sampsz)) == nil
+ || silence && (prebuf = malloc(nibuf * Sampsz)) == nil)
+ sysfatal("malloc: %r");
+ non_silence_frames = 0,
+ silence_frames = 0,
+ used_silence_frames = 0;
+ max_generated_stretch = 0,
+ max_generated_flush = 0;
+ samples_to_stretch = 0,
+ consecutive_silence_frames = 1;
+
+ if(doscale){
+ if(pipe(pfd) < 0)
+ sysfatal("pipe: %r");
+ switch(rfork(RFPROC|RFFDG)){
+ case -1:
+ sysfatal("rfork: %r");
+ case 0:
+ close(0);
+ close(pfd[1]);
+ dup(pfd[0], 0);
+ if((r = smprint("s16c2r%d", (u32int)(Nrate * ratio + 0.5))) == nil)
+ sysfatal("smprint: %r");
+ execl("/bin/audio/pcmconv", "pcmconv", "-i", r, "-o", "s16c2r44100", nil);
+ sysfatal("execl: %r");
+ default:
+ close(1);
+ close(pfd[0]);
+ ofd = pfd[1];
+ }
+ }
+ for(;;){
+ n = read(fd, silence ? prebuf : ibuf, Sampsz * nibuf);
+ n /= Sampsz;
+ if(n < 0)
+ sysfatal("read: %r");
+ if(!silence && n == 0)
+ break;
+ insamp += n;
+ if(silence){
+ if(n != 0){
+ level = rms_level_dB(prebuf, n, Nchan);
+ if(level > smin){
+ consecutive_silence_frames = 0;
+ non_silence_frames++;
+ }
+ else{
+ consecutive_silence_frames++;
+ silence_frames++;
+ }
+ }
+ }
+ else
+ samples_to_stretch = n;
+ if(cycle){
+ if(flags & STRETCH_DUAL_FLAG)
+ ratio = (sin((double) outsamp / Nrate / 2.0) *(cycle & 1 ? 1.875 : -1.875)) + 2.125;
+ else
+ ratio = (sin((double) outsamp / Nrate) * (cycle & 1 ? 0.75 : -0.75)) + 1.25;
+ }
+ if(samples_to_stretch){
+ if(consecutive_silence_frames >= 3){
+ m = stretch_samples(S, ibuf, samples_to_stretch, obuf, gap);
+ used_silence_frames++;
+ }else
+ m = stretch_samples(S, ibuf, samples_to_stretch, obuf, ratio);
+ if(m){
+ if(m > max_generated_stretch)
+ max_generated_stretch = m;
+ write(ofd, obuf, Sampsz * m);
+ outsamp += m;
+ if(m > maxnsamp)
+ sysfatal("sample generation overflow");
+ }
+ }
+ if(silence){
+ if(n){
+ memcpy(ibuf, prebuf, n * Sampsz);
+ samples_to_stretch = n;
+ }else
+ break;
+ }
+ }
+ for(;;){
+ if((n = stretch_flush(S, obuf)) == 0)
+ break;
+ if(n > max_generated_flush)
+ max_generated_flush = n;
+ if(n > maxnsamp)
+ sysfatal("flush overflow");
+ write(ofd, obuf, Sampsz * n);
+ outsamp += n / Sampsz;
+ }
+ if(insamp && verbose){
+ fprint(2, "done, %ud samples --> %ud samples (ratio = %.3f)\n",
+ insamp, outsamp, (double)outsamp / insamp);
+ if(doscale)
+ fprint(2, "sample rate changed from %d Hz to %ud Hz\n",
+ Nrate, (u32int)(Nrate * ratio + 0.5));
+ fprint(2, "max expected samples = %d, actually seen = %d stretch, %d flush\n",
+ maxnsamp, max_generated_stretch, max_generated_flush);
+ if(silence_frames || non_silence_frames) {
+ int total_frames = silence_frames + non_silence_frames;
+ fprint(2, "%d silence frames detected (%.2f%%), %d actually used (%.2f%%)\n",
+ silence_frames, silence_frames * 100.0 / total_frames,
+ used_silence_frames, used_silence_frames * 100.0 / total_frames);
+ }
+ }
+ stretch_deinit(S);
+ exits(nil);
+}