wegen/sauSynKlick/SynKlick.cc

559 lines
15 KiB
C++
Raw Normal View History

2014-08-30 03:27:40 +02:00
/*
SynKlick - repair synchronous skips in raw sound recording
2014-08-30 03:27:40 +02:00
(C) Ichthyostega (Hermann Vosseler), 2014
All code in this package is provided "as is" and WITHOUT ANY WARRANTY.
It may be copied and used under the terms and conditions of the
GNU PUBLIC LICENCE (GPL) Version 2 and above.
*********************************************************************/
#include <sndfile.h>
#include <stdio.h>
#include <string>
2014-08-30 19:26:17 +02:00
#include <vector>
#include <iostream>
2014-08-30 03:27:40 +02:00
#include <cassert>
2014-08-30 01:33:01 +02:00
#include <cstdlib>
#include <cmath>
2014-08-30 03:27:40 +02:00
using std::string;
using std::to_string;
2014-08-30 19:26:17 +02:00
using std::vector;
2014-08-30 03:27:40 +02:00
using std::cout;
using std::cerr;
using std::max;
using std::abs;
2014-08-30 03:27:40 +02:00
2014-09-05 02:28:51 +02:00
const size_t CHUNK_SIZE = 100000;
2014-08-30 19:26:17 +02:00
const size_t CONTEXT = 100;
const size_t BUFF_SIZE = CHUNK_SIZE + 2*CONTEXT;
2014-08-30 03:27:40 +02:00
const float EPSILON = 1.0f / (1 << 23); // one normalised sample step for 24bit samples
const float SILENCE_GATE = 4096.0f * EPSILON;// signal below this level (-33dB) is too low to detect jumps
const float TRIGGER_FACTOR = 6.0f; // discontinuity detected by local change compared to average change
const float CONFIRMATION_DEGREE = 0.66f; // degree of confirmation by multiple tracks for an event to be accepted
const uint AVERAGE_SAMPLES = 50; // number of past samples to use for average delta
const uint POS_LEEWAY = 3; // events within this timespan (samples) still count as "synchronous"
2014-08-30 03:27:40 +02:00
2014-09-05 02:28:51 +02:00
const bool VERBOSE = false; // set to true to trace the decision process
2014-08-30 03:27:40 +02:00
/* ==== Helpers ==== */
void
error (string msg)
{
cerr << "processing FAILED: "
<< msg << "\n\n";
exit(EXIT_FAILURE);
}
string
name_without_path (string rawName)
{
auto pos = rawName.rfind('/');
return (pos == string::npos)? rawName
: rawName.substr(pos+1);
}
void
reduceBufferPos (size_t & pos, size_t elementsToRemove)
{
if (pos < elementsToRemove)
pos = 0;
else
pos -= elementsToRemove;
2014-08-30 03:27:40 +02:00
}
/* ==== Abstraction: single channel to be processed ==== */
/**
* Helper to decide if a glitch event should be considered,
* while visiting multiple tracks at the same location.
*/
class EventScore
{
float weights_ = 0.0f;
float score_ = 0.0f;
uint confs_ = 0;
bool isHere_ = false;
static uint minConfs;
uint last_ = 0;
float fac_ = 0.0f;
public:
float
confirmationDegree()
{
if (0==weights_) return 0.0;
float avgScore = score_/weights_;
return 0.5 * (avgScore + 1.0);
}
operator bool()
{
return CONFIRMATION_DEGREE <= this->confirmationDegree()
&& confs_ >= minConfs
&& isHere_;
}
void
record (bool unclear, bool confirmed, bool noOffset, float factor)
{
fac_ = factor;
last_ = unclear? 0 : confirmed? 2 : 1; // record for diagnostics
if (unclear) return;
if (confirmed) ++confs_;
if (noOffset) isHere_ = true;
float weight = confirmed? (factor/TRIGGER_FACTOR)
: (0.0<factor? TRIGGER_FACTOR/factor : 10);
weights_ += weight;
score_ += weight * (confirmed? 1.0 : -1.0);
}
void
recordNeutral()
{
record (true, false, true, 0.0);
}
bool
possible()
{
return 0.0 < weights_ && 0.1 < confirmationDegree();
}
string
showLastUpdate()
{
return 0==last_? "neutral"
: (2==last_? "conf\t"
: "rej \t"
) + to_string(fac_) +'\t'+ to_string(confirmationDegree());
}
/**
* to avoid triggering by noise peaks, we require a confirmation
* by multiple channels; but the minimum fraction of confirming
* channels is lowered when processing a large number of channels.
*/
static void
adjustRequiredMinimalConfirmations (uint channelCount)
{
minConfs = 1 + uint(std::log2(channelCount));
}
};
2014-08-30 19:26:17 +02:00
/**
* single sound channel with the ability to investigate itself.
* Internally holds a work buffer to be filled through libsndfile.
* The #visit function performs the decision logic to detect a defect
* at the "current" position, including the next neighbouring samples.
*/
2014-08-30 19:26:17 +02:00
class Channel
2014-08-30 03:27:40 +02:00
{
2014-08-30 19:26:17 +02:00
SNDFILE *in_ = nullptr,
*out_ = nullptr;
SF_INFO info_in_,
info_out_;
2014-08-30 03:27:40 +02:00
float buff_[BUFF_SIZE];
size_t numSamples_ = 0;
size_t workPos_ = 0;
float ema_ = 1.0f / TRIGGER_FACTOR;
2014-08-30 03:27:40 +02:00
2014-08-30 19:26:17 +02:00
public:
string name_;
static bool hitEnd;
2014-08-30 03:27:40 +02:00
void
open (string name, string target)
{
this->name_ = name_without_path(name);
target += name_;
2014-08-30 03:27:40 +02:00
2014-08-30 19:26:17 +02:00
info_in_.format = 0;
in_ = sf_open (name.c_str(), SFM_READ, &info_in_);
if (!in_)
2014-08-30 03:27:40 +02:00
error ("could not open "+name+" -- " + sf_strerror (NULL));
if (1 != info_in_.channels)
error ("only Mono streams supported -- but '"+name_
+"' was found to have "+to_string(info_in_.channels)+" channels.");
2014-08-30 03:27:40 +02:00
2014-08-30 19:26:17 +02:00
info_out_.format = info_in_.format;
info_out_.channels = info_in_.channels;
info_out_.samplerate = info_in_.samplerate;
out_ = sf_open(target.c_str(), SFM_WRITE, &info_out_);
if (!out_)
2014-08-30 03:27:40 +02:00
error ("unable to create target "+target+" -- " + sf_strerror (NULL));
}
~Channel()
{
if (in_) sf_close (in_);
if (out_)
if (0 != sf_close (out_))
cerr << "WARNING: problems while closing target '" << name_
<< "' -- " << sf_strerror(out_) << "\n";
2014-08-30 03:27:40 +02:00
2014-08-30 19:26:17 +02:00
in_ = out_ = nullptr;
}
void
fillBuffer()
{
assert(numSamples_ < BUFF_SIZE);
float* startPos = buff_ + numSamples_;
size_t samplesToRead = BUFF_SIZE - numSamples_;
numSamples_ = sf_read_float(in_, startPos, samplesToRead);
if (numSamples_ < samplesToRead)
hitEnd = true; // causes the processing to end with this chunk.
numSamples_ += (startPos - buff_);
assert(workPos_ <= numSamples_);
2014-08-30 19:26:17 +02:00
}
void
writeBuffer()
{
size_t samplesToWrite = numSamples_ - (hitEnd? 0 : 2*CONTEXT);
size_t actuallyWritten = sf_write_float(out_, buff_, samplesToWrite);
if (actuallyWritten < samplesToWrite)
cerr << "Alarm: only "<<actuallyWritten<<" instead of "<<samplesToWrite
<<" samples written for " << name_ <<"\n";
if (!hitEnd)
// move processing context samples to the beginning of the buffer;
// they will be written together with the next chunk of data
{
for (uint i=0; i < 2*CONTEXT; ++i)
buff_[i] = buff_[i + actuallyWritten];
reduceBufferPos(numSamples_, actuallyWritten);
reduceBufferPos(workPos_, actuallyWritten);
}
assert(workPos_ <= numSamples_);
2014-08-30 19:26:17 +02:00
}
/**
* investigate a single sample position in the current buffer content.
* The delta to the preceding sample(s) is used to detect a signal glitch.
2014-08-30 19:26:17 +02:00
* @param i position of current sample
* @param event state of other tracks investigated thus far
* @return the event detection state including the results of investigating this sample.
* If the sample is positively an glitch event, and the state is undecided thus far,
* the event state is confirmed; if a glitch can not be confirmed, the state is
* cleared. Otherwise, the undecided state is passed on.
*/
void
visit (uint i, EventScore& event)
2014-08-30 19:26:17 +02:00
{
if (i + CONTEXT < numSamples_
&& CONTEXT <= i)
{
bool noOffset = fineTuneWorkPos (i);
float averageDelta = updateMovingAverage();
float changeFactor = delta(workPos_) / (averageDelta + EPSILON);
bool unclear = (sampleLevel() < SILENCE_GATE );
bool trigger = (changeFactor > TRIGGER_FACTOR);
event.record (unclear, trigger, noOffset, changeFactor);
}
else
event.recordNeutral();
2014-08-30 03:27:40 +02:00
}
2014-08-30 19:26:17 +02:00
/** if it ain't broken, don't fix it */
bool
reallyNeedsFix()
{
float changeFactor = delta(workPos_) / (ema_ + EPSILON);
return changeFactor > 0.25 * TRIGGER_FACTOR;
}
private:
float
sampleLevel()
{
auto curr = abs(buff_[workPos_]);
auto prev = abs(buff_[workPos_ - 1]);
return 0 < workPos_? max(prev, curr)
: curr;
}
float
delta(size_t pos)
{
if (0 < pos)
return abs(buff_[pos] - buff_[pos-1]);
else
return 0;
}
float
updateMovingAverage()
{
float old_ema = ema_; // avoid poisoning EMA used for detection
ema_ = (delta(workPos_) + ema_*(AVERAGE_SAMPLES - 1)) / AVERAGE_SAMPLES;
return old_ema;
}
/**
* sometimes the actual jump appears slightly offset on neighbouring tracks.
* We allow for some leeway by moving the work position to maximise delta.
* @return if the triggering event is at the current position
*/
bool
fineTuneWorkPos (size_t currPos)
{
size_t maxPos = 0;
float largestDelta = 0.0f;
for (size_t j=currPos; j < currPos+POS_LEEWAY && j < numSamples_; ++j)
{
float thisDelta = delta(j);
if (thisDelta > largestDelta)
{
largestDelta = thisDelta;
maxPos = j;
}
}
workPos_ = maxPos;
return (workPos_ == currPos);
}
float*
accessCurrentContext()
{
assert(CONTEXT <= workPos_);
assert(CONTEXT + workPos_ <= numSamples_);
return buff_ + (workPos_ - CONTEXT);
}
friend class Patch;
2014-08-30 03:27:40 +02:00
};
2014-08-30 19:26:17 +02:00
/**
* Fix a glitch detected in the corresponding channel.
2014-08-30 19:26:17 +02:00
* When the Patch is applied, internal work buffers are used
* to create two (partially synthetic) copies of the channel's signal
* which are then cross faded. The result is pushed back into the
* output of the underlying channel.
*/
class Patch
{
static const size_t LEN = 2 * CONTEXT;
float workBuff[LEN];
float* rawBuff;
void
reconstructMirroredSignal()
{
for (size_t i=0; i < LEN; ++i)
workBuff[i] = rawBuff[LEN-1 - i];
}
void
maybe_fix_Gap()
{
const size_t CENTRE = CONTEXT;
size_t i = CENTRE-1;
while (i > 0 && 0.0f == workBuff[i]) --i;
if (i < CENTRE-1)
fix_Gap_by_Interpolation(i, CENTRE);
i = CENTRE;
while (i < LEN-1 && 0.0f == workBuff[i]) ++i;
if (i > CENTRE)
fix_Gap_by_Interpolation(CENTRE-1, i);
}
2014-08-30 19:26:17 +02:00
void
fix_Gap_by_Interpolation(size_t s, size_t e)
2014-08-30 19:26:17 +02:00
{
assert (s+1 < e);
assert (0 <= s && s+1 < LEN);
assert (0 <= e && e < LEN);
float before = workBuff[s];
float after = workBuff[e];
for (size_t i = s+1; i<e; ++i)
{
float fade = float(i-s) / float(e-s);
workBuff[i] = rawBuff[LEN-1 - i]
= (1.0-fade)*before + fade*after;
}
2014-08-30 19:26:17 +02:00
}
void
mix_faded_with_mirroredSignal()
2014-08-30 19:26:17 +02:00
{
for (uint i=0; i < LEN; ++i)
{
float fade = (i<CONTEXT) ? CONTEXT - (i+1)
: i - CONTEXT;
fade = 1.0 - fade/CONTEXT;
// i=-1 -> 0
// i=CONTEXT-1 -> 1
// i=CONTEXT -> 1
// i=2*CONTEXT -> 0
float p = fade * 0.5f,
q = 1.0 - p;
rawBuff[i] = q*rawBuff[i] + p*workBuff[i];
}
2014-08-30 19:26:17 +02:00
}
public:
Patch (Channel& c)
: rawBuff{c.accessCurrentContext()}
{
reconstructMirroredSignal();
}
void
apply()
{
maybe_fix_Gap();
mix_faded_with_mirroredSignal();
}
2014-08-30 19:26:17 +02:00
};
2014-08-30 03:27:40 +02:00
/* ==== high-level operation ==== */
int num_sources = 0;
2014-08-30 19:26:17 +02:00
vector<Channel> streams(0);
bool Channel::hitEnd = false;
uint EventScore::minConfs = 1;
2014-08-30 03:27:40 +02:00
void
openStreams (char* name[], string targetDir)
{
assert (num_sources);
2014-08-30 19:26:17 +02:00
assert (0 == streams.size());
streams.resize(num_sources);
EventScore::adjustRequiredMinimalConfirmations (num_sources);
2014-08-30 03:27:40 +02:00
assert (targetDir.length());
if (targetDir.back() != '/')
2014-08-30 03:27:40 +02:00
targetDir += '/';
for (uint i=0; i < num_sources; ++i)
2014-08-30 19:26:17 +02:00
streams[i].open (name[i], targetDir);
2014-08-30 03:27:40 +02:00
}
void
closeStreams()
{
2014-08-30 19:26:17 +02:00
streams.clear();
}
void
eventConfirmationDiagnostic(EventScore& event, string name, size_t absPos)
{
static size_t trail = 0;
if (trail && trail != absPos) trail = 0;
if (!trail && event.possible())
{
trail = absPos;
cerr << "...\n";
}
if (trail)
cerr <<absPos<<"\t jump? "
<<name<<"\t :: "
<<event.showLastUpdate()
<<"\n";
}
2014-08-30 19:26:17 +02:00
void
process()
{
size_t absStartPos = 0; // for diagnostic messages
while (!Channel::hitEnd)
2014-08-30 19:26:17 +02:00
{
for (Channel& channel : streams)
channel.fillBuffer();
for (uint i=0; i < BUFF_SIZE; ++i)
2014-08-30 19:26:17 +02:00
{
EventScore event;
for (Channel& channel : streams)
{
channel.visit(i, event);
if (VERBOSE)
eventConfirmationDiagnostic (event, channel.name_, absStartPos+i);
}
if (event)
{
cout << "PATCH at: "<< absStartPos+i <<"\n";
for (Channel& channel : streams)
if (channel.reallyNeedsFix())
Patch(channel).apply();
}
2014-08-30 19:26:17 +02:00
}
2014-08-30 19:26:17 +02:00
for (Channel& channel : streams)
channel.writeBuffer();
absStartPos += CHUNK_SIZE;
2014-08-30 19:26:17 +02:00
}
2014-08-30 03:27:40 +02:00
}
2014-08-30 01:33:01 +02:00
2014-08-30 03:27:40 +02:00
int
main (int argc, char* argv[]) {
if (argc < 3 || string("-h") == argv[1])
{
cerr << "SynKlick <INPUTFILE> [<INPUTFILE2> ...] <TARGETDIR>\n"
<< "\nRepair a very special kind of defects in sound recordings:\n"
<< "synchronous skips in a set of input sound files. Possible events\n"
<< "will be detected by scanning the deltas between neighbouring samples\n"
<< "and confirmed by jumps at the same position in all files.\n"
<< "The jump will be amended by a short cross-blend of about 4.5ms.\n"
2014-08-30 03:27:40 +02:00
<< "\nA fixed copy of each file will be written to <TARGETDIR>\n\n";
return EXIT_FAILURE;
}
num_sources = argc - 2;
string targetDir = argv[argc-1];
openStreams (&argv[1], targetDir);
2014-08-30 19:26:17 +02:00
process();
2014-08-30 03:27:40 +02:00
closeStreams();
cout << "\nSUCCESS\n"
<< num_sources << " files read and processed --> "
<< targetDir << "\n\n";
return EXIT_SUCCESS;
2014-08-30 01:33:01 +02:00
}