Commit b4cdb725 authored by Stefan Westerfeld's avatar Stefan Westerfeld

Merge branch 'speed-detection-v2'

* speed-detection-v2:
  Produce better error messages for some commands.
  Use similar wording for --help options summary.
  Add verbose reporting to gen-speed-mk.sh.
  Fix speed detection for short payloads.
  TODO~
  Fix test script (keep watermarked files for which cmp failed).
  Use sync quality threshold for speed detection to minimize false positives.
  Fix comment.
  Add test script option to copy files if watermark detection fails.
  Merge analysis window generation into one function.
  Use n-best search for patient speed detection as well.
  Normalize speed detection quality scores.
  Export sync finder quality normalization function.
  Fix sync threshold comparision for equal sync quality scores.
  Merge sync finder / speed detection raw bit quality function.
  Fix bug in n-best selection (if three scores have the same value).
  Make test script exit with non-zero exit code if something goes wrong.
  TESTS: add test for audiowmark --key and --test-key
  Support audiowmark cmp --expect-matches 0 (true if no matches found).
  TESTS: use audiowmark cmp --expect-matches option
  Implement audiowmark cmp --expect-matches option for tests.
  Use smooth score function for speed detection to improve accuracy.
  TESTS: use audiowmark test function to improve debugging output
  TESTS: support make V=1 for make check
  TESTS: run sample-rate-test.sh during make check
  Add test for non-standard sample rates.
  Add audiowmark test-resample helper for sample rate testing.
  TESTS: add synchronization tests
  TESTS: add test for short payload
  BUILD: update EXTRA_DIST to make make distcheck work
  Fix option parser: single dash (-) is a valid filename (stdin / stdout).
  TESTS: add test for watermark add / cmp from pipe
  Improve error messages for command line argument parser.
  BUILD: add EXTRA_DIST to make make distcheck work
  Add scripts for make check.
  Add audiowmark helper commands for make check.
  Fix speed Makefile generator bugs.
  Fix overflow for time computation.
  SFInputStream: fixes related to number of frames.
	 - use size_t for n_frames -> support larger files
	 - map SF_COUNT_MAX to unknown length
  Support --try-speed in gen-speed-mk.sh.
  Support --try-speed in ber-test.sh.
  Document --detect-speed-patient.
  Update help to include --detect-speed-patient.
  TODO--
  Avoid message expansion in if --strict is used.
  TODO--
  Refactor payload parsing (avoid crashes for cmp with invalid bits).
  Introduce rounding for speed detection again, avoid lrint() though.
	While for long clips rounding doesn't help (much), it does for short
	clips.
  TODO~
  Test patient speed detection in speed test Makefile generator.
  Add patient speed detection to test script (AWM_SPEED_PATIENT=1).
  Add speed difference debugging output.
  Add speed range debugging function.
  Only accept --json option for cmp/get.
  Avoid lrint() in speed detection (rounding doesn't improve results).
  Reuse last value for speed compare bits start search (performance).
  Refactor speed detection compare loop into optimized extra function.
  Split speed detection compare loop into two loops (performance).
  Process sync bits in sorted order during speed detection (performance).
  Print total speed detection time for performance debugging.
  Update TODO.
  Cleanup window related FIXME in speed detection.
	Tests show: the hamming window is a good choice here.
  Refactor speed detection search.
  Only output speed detection timing information during cmp.
  Introduce SpeedSearch class.
  Throw away speed interpolation code.
  Get rid of extra speed_scan function.
  Cleanup SpeedSync API.
  Fix patient speed scan clip location size bug.
  Minor SpeedSync API cleanup.
  Refactor speed detection run_search() function.
  Refactor closest speed sync search.
  Fix problems / corner cases in speed detection n-best selection function.
  Skip 2nd search step for patient speed detection.
  Refactor speed_scan function.
  Initial implementation for --detect-speed-patient.
  Add extra function for speed score average computation.
  Reuse already prepared SpeedSync for 3rd speed detection step.
  Reduce search range for 3rd speed detection step.
  Implement n-best search for speed detection.
  Merge both speed scan passes into one function.
Signed-off-by: Stefan Westerfeld's avatarStefan Westerfeld <stefan@space.twc.de>
parents 1edb27ae 1b2e912e
SUBDIRS = src SUBDIRS = src tests
ACLOCAL_AMFLAGS = -I m4 ACLOCAL_AMFLAGS = -I m4
EXTRA_DIST = README.adoc Dockerfile EXTRA_DIST = README.adoc Dockerfile
...@@ -137,6 +137,7 @@ Use watermarking key from file <filename> (see <<key>>). ...@@ -137,6 +137,7 @@ Use watermarking key from file <filename> (see <<key>>).
Set the watermarking strength (see <<strength>>). Set the watermarking strength (see <<strength>>).
--detect-speed:: --detect-speed::
--detect-speed-patient::
Detect and correct replay speed difference (see <<speed>>). Detect and correct replay speed difference (see <<speed>>).
--json <file>:: --json <file>::
...@@ -302,6 +303,10 @@ the search is automatically run in parallel using many threads on systems with ...@@ -302,6 +303,10 @@ the search is automatically run in parallel using many threads on systems with
many cpu cores. So on good hardware it makes sense to always enable this option many cpu cores. So on good hardware it makes sense to always enable this option
to be robust to replay speed attacks. to be robust to replay speed attacks.
There are two versions of the speed detection algorithm, `--detect-speed` and
`--detect-speed-patient`. The difference is that the patient version takes
more cpu time to detect the speed, but produces more accurate results.
== Short Payload (experimental) == Short Payload (experimental)
By default, the watermark will store a 128-bit message. In this mode, we By default, the watermark will store a 128-bit message. In this mode, we
......
...@@ -102,7 +102,7 @@ AC_LANG_POP([C++]) ...@@ -102,7 +102,7 @@ AC_LANG_POP([C++])
# Less cluttered build output # Less cluttered build output
m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])])
AC_CONFIG_FILES([Makefile src/Makefile]) AC_CONFIG_FILES([Makefile src/Makefile tests/Makefile tests/test-common.sh])
AC_OUTPUT AC_OUTPUT
# Output summary message # Output summary message
......
speed detection possible improvements: speed detection possible improvements:
- refine with 128 sync step instead of 256?
- use bigger initial region (50s) --detect-speed-harder
- use window hann instead of hamming for sync bits (or all bits)?
- remove lrint and QInterpolator?
- detect silence instead of total volume? - detect silence instead of total volume?
- split big region into two smaller ones? - split big region into two smaller ones?
- connected search on even smaller regions? - connected search on even smaller regions?
- reorder memory access in compare to be more cache efficient
- n-best search?
- soft sync?
possible improvements: possible improvements:
- dynamic bit strength - dynamic bit strength
- merge infinite wav header generator
videowmark: videowmark:
opus length ffprobe -show_format phil.mkv opus length ffprobe -show_format phil.mkv
This diff is collapsed.
#!/bin/bash #!/bin/bash
# set -Eeuo pipefail # -x
set -Eeo pipefail
TRANSFORM=$1 TRANSFORM=$1
if [ "x$AWM_TRUNCATE" != "x" ]; then if [ "x$AWM_TRUNCATE" != "x" ]; then
AWM_REPORT=truncv AWM_REPORT=truncv
...@@ -23,6 +26,17 @@ if [ "x$AWM_PATTERN_BITS" == "x" ]; then ...@@ -23,6 +26,17 @@ if [ "x$AWM_PATTERN_BITS" == "x" ]; then
AWM_PATTERN_BITS=128 AWM_PATTERN_BITS=128
fi fi
audiowmark_cmp()
{
audiowmark cmp "$@" || {
if [ "x$AWM_FAIL_DIR" != "x" ]; then
mkdir -p $AWM_FAIL_DIR
SUM=$(sha1sum $1 | awk '{print $1;}')
cp -av $1 $AWM_FAIL_DIR/${AWM_FILE}.${SUM}.wav
fi
}
}
{ {
if [ "x$AWM_SET" == "xsmall" ]; then if [ "x$AWM_SET" == "xsmall" ]; then
ls test/T* ls test/T*
...@@ -80,13 +94,19 @@ do ...@@ -80,13 +94,19 @@ do
[ -z $SPEED_SEED ] && SPEED_SEED=0 [ -z $SPEED_SEED ] && SPEED_SEED=0
SPEED=$(audiowmark test-speed $SPEED_SEED --test-key $SEED) SPEED=$(audiowmark test-speed $SPEED_SEED --test-key $SEED)
((SPEED_SEED++)) SPEED_SEED=$((SPEED_SEED + 1))
echo in_speed $SPEED echo in_speed $SPEED
sox -D -V1 ${AWM_FILE}.wav ${AWM_FILE}.speed.wav speed $SPEED sox -D -V1 ${AWM_FILE}.wav ${AWM_FILE}.speed.wav speed $SPEED
mv ${AWM_FILE}.speed.wav ${AWM_FILE}.wav mv ${AWM_FILE}.speed.wav ${AWM_FILE}.wav
if [ "x$AWM_SPEED_PATIENT" != x ]; then
TEST_SPEED_ARGS="--detect-speed-patient --test-speed $SPEED"
elif [ "x$AWM_TRY_SPEED" != x ]; then
TEST_SPEED_ARGS="--try-speed $SPEED"
else
TEST_SPEED_ARGS="--detect-speed --test-speed $SPEED" TEST_SPEED_ARGS="--detect-speed --test-speed $SPEED"
fi
else else
TEST_SPEED_ARGS="" TEST_SPEED_ARGS=""
fi fi
...@@ -128,29 +148,29 @@ do ...@@ -128,29 +148,29 @@ do
for CLIP in $(seq $AWM_MULTI_CLIP) for CLIP in $(seq $AWM_MULTI_CLIP)
do do
audiowmark test-clip $OUT_FILE ${OUT_FILE}.clip.wav $((CLIP_SEED++)) $AWM_CLIP --test-key $SEED audiowmark test-clip $OUT_FILE ${OUT_FILE}.clip.wav $((CLIP_SEED++)) $AWM_CLIP --test-key $SEED
audiowmark cmp ${OUT_FILE}.clip.wav $PATTERN $AWM_PARAMS --test-key $SEED $TEST_CUT_ARGS $TEST_SPEED_ARGS audiowmark_cmp ${OUT_FILE}.clip.wav $PATTERN $AWM_PARAMS --test-key $SEED $TEST_CUT_ARGS $TEST_SPEED_ARGS
rm ${OUT_FILE}.clip.wav rm ${OUT_FILE}.clip.wav
echo echo
done done
elif [ "x$AWM_REPORT" == "xtruncv" ]; then elif [ "x$AWM_REPORT" == "xtruncv" ]; then
for TRUNC in $AWM_TRUNCATE for TRUNC in $AWM_TRUNCATE
do do
audiowmark cmp $OUT_FILE $PATTERN $AWM_PARAMS --test-key $SEED $TEST_CUT_ARGS $TEST_SPEED_ARGS --test-truncate $TRUNC | sed "s/^/$TRUNC /g" audiowmark_cmp $OUT_FILE $PATTERN $AWM_PARAMS --test-key $SEED $TEST_CUT_ARGS $TEST_SPEED_ARGS --test-truncate $TRUNC | sed "s/^/$TRUNC /g"
echo echo
done done
else else
audiowmark cmp $OUT_FILE $PATTERN $AWM_PARAMS --test-key $SEED $TEST_CUT_ARGS $TEST_SPEED_ARGS audiowmark_cmp $OUT_FILE $PATTERN $AWM_PARAMS --test-key $SEED $TEST_CUT_ARGS $TEST_SPEED_ARGS
echo echo
fi fi
rm -f ${AWM_FILE}.wav $OUT_FILE # cleanup temp files rm -f ${AWM_FILE}.wav $OUT_FILE # cleanup temp files
done done
done | { done | {
if [ "x$AWM_REPORT" == "xfer" ]; then if [ "x$AWM_REPORT" == "xfer" ]; then
awk 'BEGIN { bad = n = 0 } $1 == "match_count" { if ($2 == 0) bad++; n++; } END { print bad, n, bad * 100.0 / n; }' awk 'BEGIN { bad = n = 0 } $1 == "match_count" { if ($2 == 0) bad++; n++; } END { print bad, n, bad * 100.0 / (n > 0 ? n : 1); }'
elif [ "x$AWM_REPORT" == "xferv" ]; then elif [ "x$AWM_REPORT" == "xferv" ]; then
awk 'BEGIN { bad = n = 0 } { print "###", $0; } $1 == "match_count" { if ($2 == 0) bad++; n++; } END { print bad, n, bad * 100.0 / n; }' awk 'BEGIN { bad = n = 0 } { print "###", $0; } $1 == "match_count" { if ($2 == 0) bad++; n++; } END { print bad, n, bad * 100.0 / (n > 0 ? n : 1); }'
elif [ "x$AWM_REPORT" == "xsync" ]; then elif [ "x$AWM_REPORT" == "xsync" ]; then
awk 'BEGIN { bad = n = 0 } $1 == "sync_match" { bad += (3 - $2) / 3.0; n++; } END { print bad, n, bad * 100.0 / n; }' awk 'BEGIN { bad = n = 0 } $1 == "sync_match" { bad += (3 - $2) / 3.0; n++; } END { print bad, n, bad * 100.0 / (n > 0 ? n : 1); }'
elif [ "x$AWM_REPORT" == "xsyncv" ]; then elif [ "x$AWM_REPORT" == "xsyncv" ]; then
awk '{ print "###", $0; } $1 == "sync_match" { correct += $2; missing += 3 - $2; incorrect += $3-$2; print "correct:", correct, "missing:", missing, "incorrect:", incorrect; }' awk '{ print "###", $0; } $1 == "sync_match" { correct += $2; missing += 3 - $2; incorrect += $3-$2; print "correct:", correct, "missing:", missing, "incorrect:", incorrect; }'
elif [ "x$AWM_REPORT" == "xtruncv" ]; then elif [ "x$AWM_REPORT" == "xtruncv" ]; then
......
...@@ -3,20 +3,24 @@ ...@@ -3,20 +3,24 @@
SEEDS=$(seq 10) SEEDS=$(seq 10)
STRENGTHS="10 15" STRENGTHS="10 15"
CLIPS="15 30" CLIPS="15 30"
MODES="0 1 2"
echo -n "all:" echo -n "all:"
for SEED in $SEEDS for SEED in $SEEDS
do do
for MODE in $MODES
do
for STRENGTH in $STRENGTHS for STRENGTH in $STRENGTHS
do do
# clips # clips
for CLIP in $CLIPS for CLIP in $CLIPS
do do
echo -n " speed-$CLIP-$STRENGTH-$SEED" echo -n " speed-$CLIP-$STRENGTH-$MODE-$SEED"
done done
# full file # full file
echo -n " speed-full-$STRENGTH-$SEED" echo -n " speed-full-$STRENGTH-$MODE-$SEED"
done
done done
done done
...@@ -25,22 +29,32 @@ echo ...@@ -25,22 +29,32 @@ echo
for SEED in $SEEDS for SEED in $SEEDS
do do
for MODE in $MODES
do
if [ "x$MODE" == "x0" ]; then
MODE_ARGS=""
elif [ "x$MODE" == "x1" ]; then
MODE_ARGS="AWM_SPEED_PATIENT=1"
elif [ "x$MODE" == "x2" ]; then
MODE_ARGS="AWM_TRY_SPEED=1"
fi
for STRENGTH in $STRENGTHS for STRENGTH in $STRENGTHS
do do
# clips # clips
for CLIP in $CLIPS for CLIP in $CLIPS
do do
FILE="speed-$CLIP-$STRENGTH-$SEED" FILE="speed-$CLIP-$STRENGTH-$MODE-$SEED"
echo "$FILE:" echo "$FILE:"
echo -e "\t( cd ..; AWM_RAND_PATTERN=1 AWM_SET=huge2 AWM_PARAMS='--strength $STRENGTH' AWM_SPEED=1 AWM_SPEED_PRE_MP3=128 AWM_CLIP='$CLIP' AWM_SEEDS=$SEED AWM_FILE='t-$FILE' ber-test.sh mp3 128 ) >x$FILE" echo -e "\t( cd ..; AWM_REPORT=ferv AWM_RAND_PATTERN=1 AWM_SET=huge2 AWM_PARAMS_ADD='--strength $STRENGTH' AWM_SPEED=1 $MODE_ARGS AWM_SPEED_PRE_MP3=128 AWM_CLIP='$CLIP' AWM_SEEDS=$SEED AWM_FILE='t-$FILE' ber-test.sh mp3 128 ) >x$FILE"
echo -e "\tmv x$FILE $FILE" echo -e "\tmv x$FILE $FILE"
echo echo
done done
# full file # full file
FILE="speed-full-$STRENGTH-$SEED" FILE="speed-full-$STRENGTH-$MODE-$SEED"
echo "$FILE:" echo "$FILE:"
echo -e "\t( cd ..; AWM_RAND_PATTERN=1 AWM_SET=huge2 AWM_PARAMS='--strength $STRENGTH' AWM_SPEED=1 AWM_SPEED_PRE_MP3=128 AWM_SEEDS=$SEED AWM_FILE='t-$FILE' ber-test.sh mp3 128 ) >x$FILE" echo -e "\t( cd ..; AWM_REPORT=ferv AWM_RAND_PATTERN=1 AWM_SET=huge2 AWM_PARAMS_ADD='--strength $STRENGTH' AWM_SPEED=1 $MODE_ARGS AWM_SPEED_PRE_MP3=128 AWM_SEEDS=$SEED AWM_FILE='t-$FILE' ber-test.sh mp3 128 ) >x$FILE"
echo -e "\tmv x$FILE $FILE" echo -e "\tmv x$FILE $FILE"
echo echo
done done
done
done done
...@@ -68,7 +68,7 @@ SFInputStream::open (std::function<SNDFILE* (SF_INFO *)> open_func) ...@@ -68,7 +68,7 @@ SFInputStream::open (std::function<SNDFILE* (SF_INFO *)> open_func)
} }
m_n_channels = sfinfo.channels; m_n_channels = sfinfo.channels;
m_n_values = sfinfo.frames * sfinfo.channels; m_n_frames = (sfinfo.frames == SF_COUNT_MAX) ? N_FRAMES_UNKNOWN : sfinfo.frames;
m_sample_rate = sfinfo.samplerate; m_sample_rate = sfinfo.samplerate;
switch (sfinfo.format & SF_FORMAT_SUBMASK) switch (sfinfo.format & SF_FORMAT_SUBMASK)
......
...@@ -42,7 +42,7 @@ private: ...@@ -42,7 +42,7 @@ private:
SNDFILE *m_sndfile = nullptr; SNDFILE *m_sndfile = nullptr;
int m_n_channels = 0; int m_n_channels = 0;
int m_n_values = 0; size_t m_n_frames = 0;
int m_bit_depth = 0; int m_bit_depth = 0;
int m_sample_rate = 0; int m_sample_rate = 0;
bool m_read_float_data = false; bool m_read_float_data = false;
...@@ -74,7 +74,7 @@ public: ...@@ -74,7 +74,7 @@ public:
size_t size_t
n_frames() const override n_frames() const override
{ {
return m_n_values / m_n_channels; return m_n_frames;
} }
}; };
......
...@@ -76,6 +76,7 @@ SyncFinder::init_up_down (const WavData& wav_data, Mode mode) ...@@ -76,6 +76,7 @@ SyncFinder::init_up_down (const WavData& wav_data, Mode mode)
} }
} }
/* safe to call from any thread */
double double
SyncFinder::normalize_sync_quality (double raw_quality) SyncFinder::normalize_sync_quality (double raw_quality)
{ {
...@@ -89,6 +90,29 @@ SyncFinder::normalize_sync_quality (double raw_quality) ...@@ -89,6 +90,29 @@ SyncFinder::normalize_sync_quality (double raw_quality)
return raw_quality / min (Params::water_delta, 0.080) / 2.9; return raw_quality / min (Params::water_delta, 0.080) / 2.9;
} }
/* safe to call from any thread */
double
SyncFinder::bit_quality (float umag, float dmag, int bit)
{
const int expect_data_bit = bit & 1; /* expect 010101 */
/* convert avoiding bias, raw_bit < 0 => 0 bit received; raw_bit > 0 => 1 bit received */
double raw_bit;
if (umag == 0 || dmag == 0)
{
raw_bit = 0;
}
else if (umag < dmag)
{
raw_bit = 1 - umag / dmag;
}
else
{
raw_bit = dmag / umag - 1;
}
return expect_data_bit ? raw_bit : -raw_bit;
}
double double
SyncFinder::sync_decode (const WavData& wav_data, const size_t start_frame, SyncFinder::sync_decode (const WavData& wav_data, const size_t start_frame,
const vector<float>& fft_out_db, const vector<float>& fft_out_db,
...@@ -118,24 +142,7 @@ SyncFinder::sync_decode (const WavData& wav_data, const size_t start_frame, ...@@ -118,24 +142,7 @@ SyncFinder::sync_decode (const WavData& wav_data, const size_t start_frame,
frame_bit_count++; frame_bit_count++;
} }
} }
/* convert avoiding bias, raw_bit < 0 => 0 bit received; raw_bit > 0 => 1 bit received */ sync_quality += bit_quality (umag, dmag, bit) * frame_bit_count;
double raw_bit;
if (umag == 0 || dmag == 0)
{
raw_bit = 0;
}
else if (umag < dmag)
{
raw_bit = 1 - umag / dmag;
}
else
{
raw_bit = dmag / umag - 1;
}
const int expect_data_bit = bit & 1; /* expect 010101 */
const double q = expect_data_bit ? raw_bit : -raw_bit;
sync_quality += q * frame_bit_count;
bit_count += frame_bit_count; bit_count += frame_bit_count;
} }
if (bit_count) if (bit_count)
...@@ -227,9 +234,10 @@ SyncFinder::sync_select_by_threshold (vector<Score>& sync_scores) ...@@ -227,9 +234,10 @@ SyncFinder::sync_select_by_threshold (vector<Score>& sync_scores)
if (i + 1 < sync_scores.size()) if (i + 1 < sync_scores.size())
q_next = sync_scores[i + 1].quality; q_next = sync_scores[i + 1].quality;
if (sync_scores[i].quality > q_last && sync_scores[i].quality > q_next) if (sync_scores[i].quality >= q_last && sync_scores[i].quality >= q_next)
{ {
selected_scores.emplace_back (sync_scores[i]); selected_scores.emplace_back (sync_scores[i]);
i++; // score with quality q_next cannot be a local maximum
} }
} }
} }
......
...@@ -81,7 +81,6 @@ private: ...@@ -81,7 +81,6 @@ private:
std::vector<std::vector<FrameBit>> sync_bits; std::vector<std::vector<FrameBit>> sync_bits;
void init_up_down (const WavData& wav_data, Mode mode); void init_up_down (const WavData& wav_data, Mode mode);
double normalize_sync_quality (double raw_quality);
double sync_decode (const WavData& wav_data, const size_t start_frame, double sync_decode (const WavData& wav_data, const size_t start_frame,
const std::vector<float>& fft_out_db, const std::vector<float>& fft_out_db,
const std::vector<char>& have_frames, const std::vector<char>& have_frames,
...@@ -99,6 +98,9 @@ private: ...@@ -99,6 +98,9 @@ private:
public: public:
std::vector<Score> search (const WavData& wav_data, Mode mode); std::vector<Score> search (const WavData& wav_data, Mode mode);
std::vector<std::vector<FrameBit>> get_sync_bits (const WavData& wav_data, Mode mode); std::vector<std::vector<FrameBit>> get_sync_bits (const WavData& wav_data, Mode mode);
static double bit_quality (float umag, float dmag, int bit);
static double normalize_sync_quality (double raw_quality);
private: private:
void sync_fft (const WavData& wav_data, void sync_fft (const WavData& wav_data,
size_t index, size_t index,
......
...@@ -591,30 +591,9 @@ info_format (const string& label, const RawFormat& format) ...@@ -591,30 +591,9 @@ info_format (const string& label, const RawFormat& format)
int int
add_stream_watermark (AudioInputStream *in_stream, AudioOutputStream *out_stream, const string& bits, size_t zero_frames) add_stream_watermark (AudioInputStream *in_stream, AudioOutputStream *out_stream, const string& bits, size_t zero_frames)
{ {
auto bitvec = bit_str_to_vec (bits); auto bitvec = parse_payload (bits);
if (bitvec.empty()) if (bitvec.empty())
{
error ("audiowmark: cannot parse bits %s\n", bits.c_str());
return 1;
}
if (Params::payload_short && bitvec.size() != Params::payload_size)
{
error ("audiowmark: number of message bits must match payload size (%zd bits)\n", Params::payload_size);
return 1;
}
if (bitvec.size() > Params::payload_size)
{
error ("audiowmark: number of bits in message '%s' larger than payload size\n", bits.c_str());
return 1; return 1;
}
if (bitvec.size() < Params::payload_size)
{
/* expand message automatically; good for testing, maybe not so good for the final product */
vector<int> expanded_bitvec;
for (size_t i = 0; i < Params::payload_size; i++)
expanded_bitvec.push_back (bitvec[i % bitvec.size()]);
bitvec = expanded_bitvec;
}
/* sanity checks */ /* sanity checks */
if (in_stream->sample_rate() != out_stream->sample_rate()) if (in_stream->sample_rate() != out_stream->sample_rate())
...@@ -638,8 +617,8 @@ add_stream_watermark (AudioInputStream *in_stream, AudioOutputStream *out_stream ...@@ -638,8 +617,8 @@ add_stream_watermark (AudioInputStream *in_stream, AudioOutputStream *out_stream
} }
else else
{ {
int orig_seconds = in_stream->n_frames() / in_stream->sample_rate(); size_t orig_seconds = in_stream->n_frames() / in_stream->sample_rate();
info ("Time: %d:%02d\n", orig_seconds / 60, orig_seconds % 60); info ("Time: %zd:%02zd\n", orig_seconds / 60, orig_seconds % 60);
} }
info ("Sample Rate: %d\n", in_stream->sample_rate()); info ("Sample Rate: %d\n", in_stream->sample_rate());
info ("Channels: %d\n", in_stream->n_channels()); info ("Channels: %d\n", in_stream->n_channels());
......
...@@ -20,6 +20,10 @@ ...@@ -20,6 +20,10 @@
#include "convcode.hh" #include "convcode.hh"
#include "shortcode.hh" #include "shortcode.hh"
using std::string;
using std::vector;
using std::complex;
int Params::frames_per_bit = 2; int Params::frames_per_bit = 2;
double Params::water_delta = 0.01; double Params::water_delta = 0.01;
bool Params::mix = true; bool Params::mix = true;
...@@ -27,6 +31,7 @@ bool Params::hard = false; // hard decode bits? (soft decoding is b ...@@ -27,6 +31,7 @@ bool Params::hard = false; // hard decode bits? (soft decoding is b
bool Params::snr = false; // compute/show snr while adding watermark bool Params::snr = false; // compute/show snr while adding watermark
bool Params::strict = false; bool Params::strict = false;
bool Params::detect_speed = false; bool Params::detect_speed = false;
bool Params::detect_speed_patient = false;
double Params::try_speed = -1; double Params::try_speed = -1;
double Params::test_speed = -1; double Params::test_speed = -1;
int Params::have_key = 0; int Params::have_key = 0;
...@@ -36,6 +41,7 @@ int Params::test_cut = 0; // for sync test ...@@ -36,6 +41,7 @@ int Params::test_cut = 0; // for sync test
bool Params::test_no_sync = false; // disable sync bool Params::test_no_sync = false; // disable sync
bool Params::test_no_limiter = false; // disable limiter bool Params::test_no_limiter = false; // disable limiter
int Params::test_truncate = 0; int Params::test_truncate = 0;
int Params::expect_matches = -1;
Format Params::input_format = Format::AUTO; Format Params::input_format = Format::AUTO;
Format Params::output_format = Format::AUTO; Format Params::output_format = Format::AUTO;
...@@ -45,37 +51,41 @@ RawFormat Params::raw_output_format; ...@@ -45,37 +51,41 @@ RawFormat Params::raw_output_format;
int Params::hls_bit_rate = 0; int Params::hls_bit_rate = 0;
std::string Params::json_output; string Params::json_output;
std::string Params::input_label; string Params::input_label;
std::string Params::output_label; string Params::output_label;
using std::vector;
using std::complex;
FFTAnalyzer::FFTAnalyzer (int n_channels) : FFTAnalyzer::FFTAnalyzer (int n_channels) :
m_n_channels (n_channels), m_n_channels (n_channels),
m_fft_processor (Params::frame_size) m_fft_processor (Params::frame_size)
{ {
/* generate analysis window */ m_window = gen_normalized_window (Params::frame_size);
m_window.resize (Params::frame_size); }
/* safe to call from any thread */
vector<float>
FFTAnalyzer::gen_normalized_window (size_t n_values)
{
vector<float> window (n_values);
/* generate analysis window */
double window_weight = 0; double window_weight = 0;
for (size_t i = 0; i < Params::frame_size; i++) for (size_t i = 0; i < n_values; i++)
{ {
const double fsize_2 = Params::frame_size / 2.0; const double n_values_2 = n_values / 2.0;
// const double win = window_cos ((i - fsize_2) / fsize_2); // const double win = window_cos ((i - n_values_2) / n_values_2);
const double win = window_hamming ((i - fsize_2) / fsize_2); const double win = window_hamming ((i - n_values_2) / n_values_2);
//const double win = 1; //const double win = 1;
m_window[i] = win; window[i] = win;
window_weight += win; window_weight += win;
} }
/* normalize window using window weight */ /* normalize window using window weight */
for (size_t i = 0; i < Params::frame_size; i++) for (size_t i = 0; i < n_values; i++)
{ {
m_window[i] *= 2.0 / window_weight; window[i] *= 2.0 / window_weight;
} }
return window;
} }
vector<vector<complex<float>>> vector<vector<complex<float>>>
...@@ -211,3 +221,33 @@ frame_count (const WavData& wav_data) ...@@ -211,3 +221,33 @@ frame_count (const WavData& wav_data)
{ {
return wav_data.n_values() / wav_data.n_channels() / Params::frame_size; return wav_data.n_values() / wav_data.n_channels() / Params::frame_size;
} }
vector<int>
parse_payload (const string& bits)
{
auto bitvec = bit_str_to_vec (bits);
if (bitvec.empty())
{
error ("audiowmark: cannot parse bits '%s'\n", bits.c_str());
return {};
}
if ((Params::payload_short || Params::strict) && bitvec.size() != Params::payload_size)
{
error ("audiowmark: number of message bits must match payload size (%zd bits)\n", Params::payload_size);
return {};
}
if (bitvec.size() > Params::payload_size)
{
error ("audiowmark: number of bits in message '%s' larger than payload size\n", bits.c_str());
return {};
}
if (bitvec.size() < Params::payload_size)
{
/* expand message automatically; good for testing, not so good in production (disabled by --strict) */
vector<int> expanded_bitvec;
for (size_t i = 0; i < Params::payload_size; i++)
expanded_bitvec.push_back (bitvec[i % bitvec.size()]);
bitvec = expanded_bitvec;
}
return bitvec;
}
...@@ -48,6 +48,7 @@ public: ...@@ -48,6 +48,7 @@ public:
static int have_key; static int have_key;
static bool detect_speed; static bool detect_speed;
static bool detect_speed_patient;
static double try_speed; // manual speed correction static double try_speed; // manual speed correction
static double test_speed; // for debugging --detect-speed static double test_speed; // for debugging --detect-speed
...@@ -70,6 +71,7 @@ public: ...@@ -70,6 +71,7 @@ public:
static bool test_no_sync; static bool test_no_sync;
static bool test_no_limiter; static bool test_no_limiter;
static int test_truncate; static int test_truncate;
static int expect_matches;
static Format input_format; static Format input_format;
static Format output_format; static Format output_format;
...@@ -128,6 +130,8 @@ public: ...@@ -128,6 +130,8 @@ public:
std::vector<std::vector<std::complex<float>>> run_fft (const std::vector<float>& samples, size_t start_index); std::vector<std::vector<std::complex<float>>> run_fft (const std::vector<float>& samples, size_t start_index);
std::vector<std::vector<std::complex<float>>> fft_range (const std::vector<float>& samples, size_t start_index, size_t frame_count); std::vector<std::vector<std::complex<float>>> fft_range (const std::vector<float>& samples, size_t start_index, size_t frame_count);
static std::vector<float> gen_normalized_window (size_t n_values);
}; };
struct MixEntry struct MixEntry
...@@ -147,6 +151,8 @@ int frame_count (const WavData& wav_data); ...@@ -147,6 +151,8 @@ int frame_count (const WavData& wav_data);
int sync_frame_pos (int f); int sync_frame_pos (int f);
int data_frame_pos (int f); int data_frame_pos (int f);
std::vector<int> parse_payload (const std::string& str);
template<class T> std::vector<T> template<class T> std::vector<T>
randomize_bit_order (const std::vector<T>& bit_vec, bool encode) randomize_bit_order (const std::vector<T>& bit_vec, bool encode)
{ {
......
...@@ -264,19 +264,13 @@ public: ...@@ -264,19 +264,13 @@ public:
} }
} }
int int
print_match_count (const string& orig_pattern) print_match_count (const vector<int>& orig_bits)
{ {
int match_count = 0; int match_count = 0;
vector<int> orig_vec = bit_str_to_vec (orig_pattern);
for (auto p : patterns) for (auto p : patterns)
{ {
bool match = true; if (p.bit_vec == orig_bits)
for (size_t i = 0; i < p.bit_vec.size(); i++)
match = match && (p.bit_vec[i] == orig_vec[i % orig_vec.size()]);
if (match)
match_count++; match_count++;
} }
printf ("match_count %d %zd\n", match_count, patterns.size()); printf ("match_count %d %zd\n", match_count, patterns.size());
...@@ -587,7 +581,7 @@ public: ...@@ -587,7 +581,7 @@ public:
}; };
static int static int
decode_and_report (const WavData& wav_data, const string& orig_pattern) decode_and_report (const WavData& wav_data, const vector<int>& orig_bits)
{ {
ResultSet result_set; ResultSet result_set;
double speed = 1.0; double speed = 1.0;
...@@ -601,10 +595,10 @@ decode_and_report (const WavData& wav_data, const string& orig_pattern) ...@@ -601,10 +595,10 @@ decode_and_report (const WavData& wav_data, const string& orig_pattern)
* The reason to do it this way is that the detected speed may be wrong (on short clips) * The reason to do it this way is that the detected speed may be wrong (on short clips)
* and we don't want to loose a successful clip decoder match in this case. * and we don't want to loose a successful clip decoder match in this case.
*/ */
if (Params::detect_speed || Params::try_speed > 0) if (Params::detect_speed || Params::detect_speed_patient || Params::try_speed > 0)
{ {
if (Params::detect_speed) if (Params::detect_speed || Params::detect_speed_patient)
speed = detect_speed (wav_data, !orig_pattern.empty()); speed = detect_speed (wav_data, !orig_bits.empty());
else else
speed = Params::try_speed; speed = Params::try_speed;
...@@ -639,21 +633,38 @@ decode_and_report (const WavData& wav_data, const string& orig_pattern) ...@@ -639,21 +633,38 @@ decode_and_report (const WavData& wav_data, const string& orig_pattern)
if (Params::json_output != "-") if (Params::json_output != "-")
result_set.print(); result_set.print();
if (!orig_pattern.empty()) if (!orig_bits.empty())
{ {
int match_count = result_set.print_match_count (orig_pattern); int match_count = result_set.print_match_count (orig_bits);
block_decoder.print_debug_sync(); block_decoder.print_debug_sync();
if (Params::expect_matches >= 0)
{
printf ("expect_matches %d\n", Params::expect_matches);
if (match_count != Params::expect_matches)
return 1;
}
else
{
if (!match_count) if (!match_count)
return 1; return 1;
} }
}
return 0; return 0;
} }
int int
get_watermark (const string& infile, const string& orig_pattern) get_watermark (const string& infile, const string& orig_pattern)
{ {
vector<int> orig_bitvec;
if (!orig_pattern.empty())
{
orig_bitvec = parse_payload (orig_pattern);
if (orig_bitvec.empty())
return 1;
}
WavData wav_data; WavData wav_data;
Error err = wav_data.load (infile); Error err = wav_data.load (infile);
if (err) if (err)
...@@ -675,10 +686,10 @@ get_watermark (const string& infile, const string& orig_pattern) ...@@ -675,10 +686,10 @@ get_watermark (const string& infile, const string& orig_pattern)
} }
if (wav_data.sample_rate() == Params::mark_sample_rate) if (wav_data.sample_rate() == Params::mark_sample_rate)
{ {
return decode_and_report (wav_data, orig_pattern); return decode_and_report (wav_data, orig_bitvec);
} }
else else
{ {
return decode_and_report (resample (wav_data, Params::mark_sample_rate), orig_pattern); return decode_and_report (resample (wav_data, Params::mark_sample_rate), orig_bitvec);
} }
} }
This diff is collapsed.
test-common.sh
check: detect-speed-test block-decoder-test clip-decoder-test \
pipe-test short-payload-test sync-test sample-rate-test \
key-test
EXTRA_DIST = detect-speed-test.sh block-decoder-test.sh clip-decoder-test.sh \
pipe-test.sh short-payload-test.sh sync-test.sh sample-rate-test.sh \
key-test.sh
detect-speed-test:
Q=1 $(top_srcdir)/tests/detect-speed-test.sh
block-decoder-test:
Q=1 $(top_srcdir)/tests/block-decoder-test.sh
clip-decoder-test:
Q=1 $(top_srcdir)/tests/clip-decoder-test.sh
pipe-test:
Q=1 $(top_srcdir)/tests/pipe-test.sh
short-payload-test:
Q=1 $(top_srcdir)/tests/short-payload-test.sh
sync-test:
Q=1 $(top_srcdir)/tests/sync-test.sh
sample-rate-test:
Q=1 $(top_srcdir)/tests/sample-rate-test.sh
key-test:
Q=1 $(top_srcdir)/tests/key-test.sh
#!/bin/bash
source test-common.sh
IN_WAV=block-decoder-test.wav
OUT_WAV=block-decoder-test-out.wav
audiowmark test-gen-noise $IN_WAV 200 44100
audiowmark_add $IN_WAV $OUT_WAV $TEST_MSG
audiowmark_cmp --expect-matches 5 $OUT_WAV $TEST_MSG
rm $IN_WAV $OUT_WAV
exit 0
#!/bin/bash
source test-common.sh
IN_WAV=clip-decoder-test.wav
OUT_WAV=clip-decoder-test-out.wav
CUT_WAV=clip-decoder-test-out-cut.wav
audiowmark test-gen-noise $IN_WAV 30 44100
audiowmark_add $IN_WAV $OUT_WAV $TEST_MSG
audiowmark_cmp --expect-matches 1 $OUT_WAV $TEST_MSG
# cut 1 second 300 samples
audiowmark cut-start $OUT_WAV $CUT_WAV 44300
audiowmark_cmp --expect-matches 1 $CUT_WAV $TEST_MSG
rm $IN_WAV $OUT_WAV $CUT_WAV
exit 0
#!/bin/bash
source test-common.sh
IN_WAV=detect-speed-test.wav
OUT_WAV=detect-speed-test-out.wav
OUTS_WAV=detect-speed-test-out-spd.wav
audiowmark test-gen-noise detect-speed-test.wav 30 44100
for SPEED in 0.9764 1.0 1.01
do
audiowmark_add $IN_WAV $OUT_WAV $TEST_MSG
audiowmark test-change-speed $OUT_WAV $OUTS_WAV $SPEED
audiowmark_cmp $OUTS_WAV $TEST_MSG --detect-speed --test-speed $SPEED
audiowmark_cmp $OUTS_WAV $TEST_MSG --detect-speed-patient --test-speed $SPEED
done
rm $IN_WAV $OUT_WAV $OUTS_WAV
exit 0
#!/bin/bash
source test-common.sh
IN_WAV=key-test.wav
KEY1=key-test-1.key
KEY2=key-test-2.key
OUT1_WAV=key-test-out1.wav
OUT2_WAV=key-test-out2.wav
TEST_MSG2=0123456789abcdef0123456789abcdef
audiowmark test-gen-noise $IN_WAV 30 44100
audiowmark gen-key $KEY1
audiowmark gen-key $KEY2
audiowmark_add --key $KEY1 $IN_WAV $OUT1_WAV $TEST_MSG
audiowmark_add --key $KEY2 $IN_WAV $OUT2_WAV $TEST_MSG2
# shouldn't be able to detect watermark without correct key
audiowmark_cmp --key $KEY1 --expect-matches 1 $OUT1_WAV $TEST_MSG
audiowmark_cmp --key $KEY2 --expect-matches 0 $OUT1_WAV $TEST_MSG
audiowmark_cmp --expect-matches 0 $OUT1_WAV $TEST_MSG
audiowmark_cmp --key $KEY2 --expect-matches 1 $OUT2_WAV $TEST_MSG2
audiowmark_cmp --key $KEY1 --expect-matches 0 $OUT2_WAV $TEST_MSG2
audiowmark_cmp --expect-matches 0 $OUT2_WAV $TEST_MSG2
rm $OUT1_WAV $OUT2_WAV
# double watermark with two different keys
audiowmark_add $IN_WAV $OUT1_WAV $TEST_MSG
audiowmark_add --test-key 42 $OUT1_WAV $OUT2_WAV $TEST_MSG2
audiowmark_cmp --expect-matches 1 $OUT2_WAV $TEST_MSG
audiowmark_cmp --test-key 42 --expect-matches 1 $OUT2_WAV $TEST_MSG2
rm $IN_WAV $KEY1 $KEY2 $OUT1_WAV $OUT2_WAV
exit 0
#!/bin/bash
source test-common.sh
IN_WAV=pipe-test.wav
OUT_WAV=pipe-test-out.wav
audiowmark test-gen-noise $IN_WAV 200 44100
cat $IN_WAV | audiowmark_add - - $TEST_MSG > $OUT_WAV || die "watermark from pipe failed"
audiowmark_cmp --expect-matches 5 $OUT_WAV $TEST_MSG
cat $OUT_WAV | audiowmark_cmp --expect-matches 5 - $TEST_MSG || die "watermark detection from pipe failed"
rm $IN_WAV $OUT_WAV
exit 0
#!/bin/bash
source test-common.sh
IN_WAV=sample-rate-test.wav
OUT_WAV=sample-rate-test-out.wav
OUT_48000_WAV=sample-rate-test-out-48000.wav
audiowmark test-gen-noise $IN_WAV 200 32000
audiowmark_add $IN_WAV $OUT_WAV $TEST_MSG
audiowmark_cmp --expect-matches 5 $OUT_WAV $TEST_MSG
audiowmark test-resample $OUT_WAV $OUT_48000_WAV 48000
audiowmark_cmp --expect-matches 5 $OUT_48000_WAV $TEST_MSG
rm $IN_WAV $OUT_WAV $OUT_48000_WAV
exit 0
#!/bin/bash
source test-common.sh
IN_WAV=short-playload-test.wav
OUT_WAV=short-playload-test-out.wav
audiowmark test-gen-noise $IN_WAV 200 44100
audiowmark_add --short 12 $IN_WAV $OUT_WAV abc
audiowmark_cmp --short 12 $OUT_WAV abc
audiowmark_add --short 16 $IN_WAV $OUT_WAV abcd
audiowmark_cmp --short 16 $OUT_WAV abcd
audiowmark_add --short 20 $IN_WAV $OUT_WAV abcde
audiowmark_cmp --short 20 $OUT_WAV abcde
rm $IN_WAV $OUT_WAV
exit 0
#!/bin/bash
source test-common.sh
IN_WAV=sync-test.wav
OUT_WAV=sync-test-out.wav
CUT_WAV=sync-test-cut.wav
audiowmark test-gen-noise $IN_WAV 200 44100
audiowmark_add $IN_WAV $OUT_WAV $TEST_MSG
# cut 20 seconds and 300 samples
audiowmark cut-start $OUT_WAV $CUT_WAV 882300
audiowmark_cmp --expect-matches 3 $CUT_WAV $TEST_MSG
rm $IN_WAV $OUT_WAV $CUT_WAV
exit 0
# program locations
AUDIOWMARK=@top_builddir@/src/audiowmark
TEST_MSG=f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0
# common shell functions
die()
{
echo >&2 "$0: $@"
exit 1
}
audiowmark()
{
if [ "x$Q" == "x1" ] && [ -z "$V" ]; then
AUDIOWMARK_Q="-q"
else
echo >&2 ==== audiowmark "$@" ====
fi
$AUDIOWMARK --strict "$@" || die "failed to run audiowmark $@"
}
audiowmark_add()
{
if [ "x$Q" == "x1" ] && [ -z "$V" ]; then
AUDIOWMARK_Q="-q"
else
echo >&2 ==== audiowmark add "$@" ====
fi
$AUDIOWMARK $AUDIOWMARK_Q --strict add "$@" || die "failed to watermark $@"
}
audiowmark_cmp()
{
if [ "x$Q" == "x1" ] && [ -z "$V" ]; then
AUDIOWMARK_OUT="/dev/null"
else
AUDIOWMARK_OUT="/dev/stdout"
echo >&2 ==== audiowmark cmp "$@" ====
fi
$AUDIOWMARK --strict cmp "$@" > $AUDIOWMARK_OUT || die "failed to detect watermark $@"
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment