Created
July 27, 2013 19:36
-
-
Save RavuAlHemio/6096006 to your computer and use it in GitHub Desktop.
Magnetophon: simulates the frequency oscillation in a broken tape deck or damaged tape using SoundTouch and SoX.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Simulates the frequency oscillation in a broken tape deck or damaged tape | |
* using SoundTouch and SoX. | |
* | |
* Released into the public domain. | |
* http://creativecommons.org/publicdomain/zero/1.0/ | |
*/ | |
#include <string> | |
#include <cassert> | |
#include <cmath> | |
#include <cstdio> | |
#include <cstring> | |
#include <unistd.h> | |
#include <sox.h> | |
#include <soundtouch/SoundTouch.h> | |
/** The buffer size when reading or writing a file. */ | |
static const size_t BUFSIZE = 1024; | |
/** The name of the program, generally as provided in argv[0]. */ | |
static const char *progname = "magnetophon"; | |
/** How to bend the signal? */ | |
enum class BendMode | |
{ | |
INVALID, | |
LINEAR, | |
SINE | |
}; | |
/** Parameters for Magnetophon operation. */ | |
struct Opts | |
{ | |
/** How to bend the signal? */ | |
BendMode bm; | |
/** How often should the function oscillate? */ | |
double period; | |
/** The file name from which to read. */ | |
const char *srcfn; | |
/** The file name in which to write. */ | |
const char *dstfn; | |
/** Construct with a few sane defaults. */ | |
Opts() : bm(BendMode::SINE), period(1.0), srcfn(nullptr), dstfn(nullptr) {} | |
}; | |
/** | |
* Convert the bend mode from a string into the corresponding enum | |
* value. | |
*/ | |
BendMode parseBendMode(const std::string & s) | |
{ | |
if (s == "lin" || s == "linear") | |
{ | |
return BendMode::LINEAR; | |
} | |
else if (s == "sin" || s == "sine") | |
{ | |
return BendMode::SINE; | |
} | |
return BendMode::INVALID; | |
} | |
/** Output a message about how this program is to be called. */ | |
static void usage(void) | |
{ | |
fprintf(stderr, "Usage: %s [-m lin|sin] [-p PERIOD] SOURCE TARGET\n", progname); | |
} | |
/** | |
* Parses a double-precision floating-point value. | |
* | |
* Uses strtod. | |
* | |
* @param str The string to parse. | |
* @param out The value is stored in the target of this pointer. | |
* @return Whether the conversion was successful. An error message is also | |
* printed on stderr if not. | |
*/ | |
static bool parseDouble(const char *str, double *out) | |
{ | |
assert(out != NULL); | |
char *end; | |
*out = strtod(str, &end); | |
if (*out == HUGE_VAL && errno == ERANGE) | |
{ | |
fprintf(stderr, "%s: '%s' is too large\n", progname, str); | |
return false; | |
} | |
else if (*out == -HUGE_VAL && errno == ERANGE) | |
{ | |
fprintf(stderr, "%s: '%s' is too small\n", progname, str); | |
return false; | |
} | |
else if (*out == 0.0 && errno == ERANGE) | |
{ | |
fprintf(stderr, "%s: '%s' is too minuscule\n", progname, str); | |
return false; | |
} | |
else if (end[0] != '\0') | |
{ | |
fprintf(stderr, "%s: '%s' contains invalid characters: '%s'\n", progname, str, end); | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Parse the command-line arguments. | |
* @param opts Reference to structure in which to store options. | |
* @param argc Number of arguments. | |
* @param argv Array of argument strings. | |
* @param Whether the parsing was successful. | |
*/ | |
static bool parseArgs(Opts *opts, int argc, char **argv) | |
{ | |
assert(opts != nullptr); | |
if (argc > 0) | |
{ | |
progname = argv[0]; | |
} | |
int c; | |
while ((c = getopt(argc, argv, "m:p:")) != -1) | |
{ | |
switch (c) | |
{ | |
case 'm': | |
opts->bm = parseBendMode(optarg); | |
break; | |
case 'p': | |
if (!parseDouble(optarg, &opts->period)) | |
{ | |
usage(); | |
return false; | |
} | |
break; | |
case '?': | |
usage(); | |
return false; | |
default: | |
assert(0 && "Unexpected option."); | |
return false; | |
} | |
} | |
if (opts->bm == BendMode::INVALID) | |
{ | |
fprintf(stderr, "%s: invalid bend mode\n", progname); | |
usage(); | |
return false; | |
} | |
if (opts->period <= 0.0) | |
{ | |
fprintf(stderr, "%s: period must be greater than 0.0\n", progname); | |
usage(); | |
return false; | |
} | |
if (argc - optind != 2) | |
{ | |
usage(); | |
return false; | |
} | |
opts->srcfn = argv[optind + 0]; | |
opts->dstfn = argv[optind + 1]; | |
/* FIXME: doesn't canonicalize paths */ | |
if (strcmp(opts->srcfn, opts->dstfn) == 0) | |
{ | |
fprintf(stderr, "%s: source and target must not be the same\n", progname); | |
return false; | |
} | |
return true; | |
} | |
struct SoxFormats | |
{ | |
SoxFormats() | |
{ | |
sox_format_init(); | |
} | |
~SoxFormats() | |
{ | |
sox_format_quit(); | |
} | |
}; | |
struct SoxRead | |
{ | |
sox_format_t *fmt; | |
SoxRead(const char *path) : fmt(nullptr) | |
{ | |
fmt = sox_open_read(path, NULL, NULL, NULL); | |
} | |
~SoxRead() | |
{ | |
if (fmt != nullptr) | |
{ | |
sox_close(fmt); | |
} | |
} | |
}; | |
struct SoxWrite | |
{ | |
sox_signalinfo_t sig; | |
sox_format_t *fmt; | |
SoxWrite(const char *path, double samrate, unsigned chans, unsigned bps) : sig(), fmt(nullptr) | |
{ | |
sig.rate = samrate; | |
sig.channels = chans; | |
sig.precision = bps; | |
sig.length = 0; | |
sig.mult = nullptr; | |
fmt = sox_open_write(path, &sig, NULL, NULL, NULL, NULL); | |
} | |
~SoxWrite() | |
{ | |
if (fmt != nullptr) | |
{ | |
sox_close(fmt); | |
} | |
} | |
}; | |
/** | |
* Calculates a correction factor according to the number of bits per sample. | |
*/ | |
__attribute__((pure)) | |
static inline float corrFactor(unsigned bps) | |
{ | |
assert(bps > 0); | |
float ret = 128.0f; | |
--bps; | |
while (--bps > 0) | |
{ | |
ret *= 256.0f; | |
} | |
return ret; | |
} | |
/** | |
* Converts an array of SoX samples into an array of floating-point samples (in | |
* the range [-1, 1]). | |
* @param flts Pointer to the destination array. | |
* @param sams Pointer to the source array. | |
* @param bps Bits per sample actually used. | |
* @param sz Number of elements -- must not be greater than the size of flts | |
* and sams. | |
*/ | |
static void soxSamplesToFloats(float *flts, const sox_sample_t *sams, unsigned bps, size_t sz) | |
{ | |
size_t i; | |
for (i = 0; i < sz; ++i) | |
{ | |
flts[i] = ((float)sams[i]) / corrFactor(bps); | |
} | |
} | |
/** | |
* Converts an array of SoX samples into an array of floating-point samples (in | |
* the range [-1, 1]). | |
* @param flts Pointer to the destination array. | |
* @param sams Pointer to the source array. | |
* @param bps Bits per sample to actually use. | |
* @param sz Number of elements -- must not be greater than the size of flts | |
* and sams. | |
*/ | |
static void floatsToSoxSamples(sox_sample_t *sams, const float *flts, unsigned bps, size_t sz) | |
{ | |
size_t i; | |
for (i = 0; i < sz; ++i) | |
{ | |
sams[i] = (sox_sample_t)(flts[i] * corrFactor(bps)); | |
} | |
} | |
/** | |
* The main entry point of Magnetophon. | |
* @param argc Number of arguments. | |
* @param argv Array of argument strings. | |
* @return Zero on success, non-zero on failure. | |
*/ | |
int main(int argc, char **argv) | |
{ | |
// parse options | |
Opts opts; | |
if (!parseArgs(&opts, argc, argv)) | |
{ | |
return 1; | |
} | |
// start SoX | |
SoxFormats sfs; | |
// start SoundTouch | |
soundtouch::SoundTouch st; | |
st.setRate(1); | |
st.setTempo(1); | |
// prepare some values | |
unsigned chans; | |
unsigned bps; | |
double samrate; | |
// with the file | |
{ | |
SoxRead rd = SoxRead(opts.srcfn); | |
if (rd.fmt == nullptr) | |
{ | |
fprintf(stderr, "%s: error opening %s\n", progname, opts.srcfn); | |
return 1; | |
} | |
// store important signal info | |
samrate = rd.fmt->signal.rate; | |
bps = rd.fmt->signal.precision; | |
chans = rd.fmt->signal.channels; | |
// configure SoundTouch | |
st.setChannels(chans); | |
st.setSampleRate(samrate); | |
// read the file's contents, feeding them to SoundTouch | |
{ | |
float flts[BUFSIZE]; | |
sox_sample_t sams[BUFSIZE]; | |
size_t rdsz; | |
float n = 0.0; | |
for (;;) | |
{ | |
rdsz = sox_read(rd.fmt, sams, BUFSIZE); | |
if (rdsz == 0) | |
{ | |
break; | |
} | |
soxSamplesToFloats(flts, sams, bps, rdsz); | |
st.putSamples(flts, rdsz / chans); | |
st.setRate(0.875f + (cosf(1e-10f * n) / 8.0f)); | |
n += rdsz * samrate * opts.period; | |
} | |
} | |
} | |
st.flush(); | |
// with the other file | |
{ | |
SoxWrite wr = SoxWrite(opts.dstfn, samrate, chans, bps); | |
if (wr.fmt == nullptr) | |
{ | |
fprintf(stderr, "%s: error opening %s\n", progname, opts.dstfn); | |
return 1; | |
} | |
// read from SoundTouch, write into file | |
{ | |
float flts[BUFSIZE]; | |
sox_sample_t sams[BUFSIZE]; | |
size_t wrsz; | |
for (;;) | |
{ | |
wrsz = st.receiveSamples(flts, BUFSIZE / chans) * chans; | |
if (wrsz == 0) | |
{ | |
break; | |
} | |
floatsToSoxSamples(sams, flts, bps, wrsz); | |
sox_write(wr.fmt, sams, wrsz); | |
} | |
} | |
} | |
// done, I guess | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment