portapack-mayhem/firmware/baseband/proc_pocsag2.cpp
Kyle Reed b28283271b
POCSAG clock detection 🐋 improvement(?) (#1442)
* WIP convergence
* Tighter code, allow for sample nudges during clock discovery.
2023-09-09 17:49:22 +02:00

423 lines
12 KiB
C++

/*
* Copyright (C) 1996 Thomas Sailer (sailer@ife.ee.ethz.ch, hb9jnx@hb9w.che.eu)
* Copyright (C) 2012-2014 Elias Oenal (multimon-ng@eliasoenal.com)
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "proc_pocsag2.hpp"
#include "event_m4.hpp"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstddef>
using namespace std;
namespace {
/* Count of bits that differ between the two values. */
uint8_t diff_bit_count(uint32_t left, uint32_t right) {
uint32_t diff = left ^ right;
uint8_t count = 0;
for (size_t i = 0; i < sizeof(diff) * 8; ++i) {
if (((diff >> i) & 0x1) == 1)
++count;
}
return count;
}
} // namespace
/* AudioNormalizer ***************************************/
void AudioNormalizer::execute_in_place(const buffer_f32_t& audio) {
// Decay min/max every second (@24kHz).
if (counter_ >= 24'000) {
// 90% decay factor seems to work well.
// This keeps large transients from wrecking the filter.
max_ *= 0.9f;
min_ *= 0.9f;
counter_ = 0;
calculate_thresholds();
}
counter_ += audio.count;
for (size_t i = 0; i < audio.count; ++i) {
auto& val = audio.p[i];
if (val > max_) {
max_ = val;
calculate_thresholds();
}
if (val < min_) {
min_ = val;
calculate_thresholds();
}
if (val >= t_hi_)
val = 1.0f;
else if (val <= t_lo_)
val = -1.0f;
else
val = 0.0;
}
}
void AudioNormalizer::calculate_thresholds() {
auto center = (max_ + min_) / 2.0f;
auto range = (max_ - min_) / 2.0f;
// 10% off center force either +/-1.0f.
// Higher == larger dead zone.
// Lower == more false positives.
auto threshold = range * 0.1;
t_hi_ = center + threshold;
t_lo_ = center - threshold;
}
/* BitQueue **********************************************/
void BitQueue::push(bool bit) {
data_ = (data_ << 1) | (bit ? 1 : 0);
if (count_ < max_size_) ++count_;
}
bool BitQueue::pop() {
if (count_ == 0) return false;
--count_;
return (data_ & (1 << count_)) != 0;
}
void BitQueue::reset() {
data_ = 0;
count_ = 0;
}
uint8_t BitQueue::size() const {
return count_;
}
uint32_t BitQueue::data() const {
return data_;
}
/* BitExtractor ******************************************/
void BitExtractor::extract_bits(const buffer_f32_t& audio) {
// Assumes input has been normalized +/- 1.0f.
// Positive == 0, Negative == 1.
for (size_t i = 0; i < audio.count; ++i) {
auto sample = audio.p[i];
if (current_rate_) {
if (current_rate_->handle_sample(sample)) {
auto value = (current_rate_->bits.data() & 1) == 1;
bits_.push(value);
}
} else {
// Feed sample to all known rates for clock detection.
for (auto& rate : known_rates_) {
if (rate.handle_sample(sample) &&
diff_bit_count(rate.bits.data(), clock_magic_number) <= 3) {
// Clock detected, continue with this rate.
rate.is_stable = true;
current_rate_ = &rate;
}
}
}
}
}
void BitExtractor::configure(uint32_t sample_rate) {
sample_rate_ = sample_rate;
// Build the baud rate info table based on the sample rate.
// Sampling at 2x the baud rate to synchronize to bit transitions
// without needing to know exact transition boundaries.
for (auto& rate : known_rates_)
rate.sample_interval = sample_rate / (2.0 * rate.baud_rate);
}
void BitExtractor::reset() {
current_rate_ = nullptr;
for (auto& rate : known_rates_)
rate.reset();
}
uint16_t BitExtractor::baud_rate() const {
return current_rate_ ? current_rate_->baud_rate : 0;
}
bool BitExtractor::RateInfo::handle_sample(float sample) {
samples_until_next -= 1;
// Time to process a sample?
if (samples_until_next > 0)
return false;
bool value = signbit(sample); // NB: negative == '1'
bool bit_pushed = false;
switch (state) {
case State::WaitForSample:
// Just need to wait for the first sample of the bit.
state = State::ReadyToSend;
break;
case State::ReadyToSend:
if (!is_stable && prev_value != value) {
// Still looking for the clock signal but found a transition.
// Nudge the next sample a bit to try avoiding pulse edges.
samples_until_next += (sample_interval / 8.0);
} else {
// Either the clock has been found or both samples were
// (probably) in the same pulse. Send the bit.
// TODO: Wider/more samples for noise reduction?
state = State::WaitForSample;
bit_pushed = true;
bits.push(value);
}
break;
}
// How long until the next sample?
samples_until_next += sample_interval;
prev_value = value;
return bit_pushed;
}
void BitExtractor::RateInfo::reset() {
state = State::WaitForSample;
samples_until_next = 0.0;
prev_value = false;
is_stable = false;
bits.reset();
}
/* CodewordExtractor *************************************/
void CodewordExtractor::process_bits() {
// Process all of the bits in the bits queue.
while (bits_.size() > 0) {
take_one_bit();
// Wait until data_ is full.
if (bit_count_ < data_bit_count)
continue;
// Wait for the sync frame.
if (!has_sync_) {
if (diff_bit_count(data_, sync_codeword) <= 2)
handle_sync(/*inverted=*/false);
else if (diff_bit_count(data_, ~sync_codeword) <= 2)
handle_sync(/*inverted=*/true);
continue;
}
save_current_codeword();
if (word_count_ == pocsag::batch_size)
handle_batch_complete();
}
}
void CodewordExtractor::flush() {
// Don't bother flushing if there's no pending data.
if (word_count_ == 0) return;
pad_idle();
handle_batch_complete();
}
void CodewordExtractor::reset() {
clear_data_bits();
has_sync_ = false;
inverted_ = false;
word_count_ = 0;
}
void CodewordExtractor::clear_data_bits() {
data_ = 0;
bit_count_ = 0;
}
void CodewordExtractor::take_one_bit() {
data_ = (data_ << 1) | bits_.pop();
if (bit_count_ < data_bit_count)
++bit_count_;
}
void CodewordExtractor::handle_sync(bool inverted) {
clear_data_bits();
has_sync_ = true;
inverted_ = inverted;
word_count_ = 0;
}
void CodewordExtractor::save_current_codeword() {
batch_[word_count_++] = inverted_ ? ~data_ : data_;
clear_data_bits();
}
void CodewordExtractor::handle_batch_complete() {
on_batch_(*this);
has_sync_ = false;
word_count_ = 0;
}
void CodewordExtractor::pad_idle() {
while (word_count_ < pocsag::batch_size)
batch_[word_count_++] = idle_codeword;
}
/* POCSAGProcessor ***************************************/
void POCSAGProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
// buffer has 2048 samples
// decim0 out: 2048/8 = 256 samples
// decim1 out: 256/8 = 32 samples
// channel out: 32/2 = 16 samples
// Get 24kHz audio
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
auto audio = demod.execute(channel_out, audio_buffer);
// Check if there's any signal in the audio buffer.
bool has_audio = squelch.execute(audio);
squelch_history = (squelch_history << 1) | (has_audio ? 1 : 0);
// Has there been any signal recently?
if (squelch_history == 0) {
// No recent signal, flush and prepare for next message.
if (word_extractor.current() > 0) {
flush();
reset();
send_stats();
}
// Clear the audio stream before sending.
for (size_t i = 0; i < audio.count; ++i)
audio.p[i] = 0.0;
audio_output.write(audio);
return;
}
// Filter out high-frequency noise then normalize.
lpf.execute_in_place(audio);
normalizer.execute_in_place(audio);
audio_output.write(audio);
// Decode the messages from the audio.
bit_extractor.extract_bits(audio);
word_extractor.process_bits();
// Update the status.
samples_processed += buffer.count;
if (samples_processed >= stat_update_threshold) {
send_stats();
samples_processed -= stat_update_threshold;
}
}
void POCSAGProcessor::on_message(const Message* const message) {
switch (message->id) {
case Message::ID::POCSAGConfigure:
configure();
break;
case Message::ID::NBFMConfigure: {
auto config = reinterpret_cast<const NBFMConfigureMessage*>(message);
squelch.set_threshold(config->squelch_level / 99.0);
break;
}
default:
break;
}
}
void POCSAGProcessor::configure() {
constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor;
constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor;
constexpr size_t channel_filter_output_fs = decim_1_output_fs / 2;
constexpr size_t demod_input_fs = channel_filter_output_fs;
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
channel_filter.configure(taps_11k0_channel.taps, 2);
demod.configure(demod_input_fs, 4'500); // FSK +/- 4k5Hz.
// Don't process the audio stream.
audio_output.configure(false);
bit_extractor.configure(demod_input_fs);
// Set ready to process data.
configured = true;
}
void POCSAGProcessor::flush() {
word_extractor.flush();
}
void POCSAGProcessor::reset() {
bits.reset();
bit_extractor.reset();
word_extractor.reset();
samples_processed = 0;
}
void POCSAGProcessor::send_stats() const {
POCSAGStatsMessage message(
word_extractor.current(), word_extractor.count(),
word_extractor.has_sync(), bit_extractor.baud_rate());
shared_memory.application_queue.push(message);
}
void POCSAGProcessor::send_packet() {
packet.set_flag(pocsag::PacketFlag::NORMAL);
packet.set_timestamp(Timestamp::now());
packet.set_bitrate(bit_extractor.baud_rate());
packet.set(word_extractor.batch());
POCSAGPacketMessage message(packet);
shared_memory.application_queue.push(message);
}
/* main **************************************************/
int main() {
EventDispatcher event_dispatcher{std::make_unique<POCSAGProcessor>()};
event_dispatcher.run();
return 0;
}