diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt
index 03d2629ffb374ae1b96cd50f47cab06299f8ed69..18d4c45e755642a49eb9b5dad4c2cdcd004c5e7a 100644
--- a/firmware/application/CMakeLists.txt
+++ b/firmware/application/CMakeLists.txt
@@ -217,7 +217,7 @@ set(CPPSRC
 	apps/ui_aprs_tx.cpp
 	apps/ui_bht_tx.cpp
 	apps/ui_coasterp.cpp
-	apps/ui_debug.cpp
+	# apps/ui_debug.cpp
 	apps/ui_encoders.cpp
 	apps/ui_fileman.cpp
 	apps/ui_freqman.cpp
diff --git a/firmware/application/apps/pocsag_app.cpp b/firmware/application/apps/pocsag_app.cpp
index b193218840f55fc9664abda8775e571ae5244386..6808e63175c4b12376baa375c9dfd04e76b7a668 100644
--- a/firmware/application/apps/pocsag_app.cpp
+++ b/firmware/application/apps/pocsag_app.cpp
@@ -142,7 +142,7 @@ void POCSAGAppView::on_packet(const POCSAGPacketMessage * message) {
 	std::string alphanum_text = "";
 	
 	if (message->packet.flag() != NORMAL)
-		console.writeln("\n\x1B\x0CRX ERROR: " + pocsag::flag_str(message->packet.flag()));
+		console.writeln("\n\x1B\x0CRC ERROR: " + pocsag::flag_str(message->packet.flag()));
 	else {
 		pocsag_decode_batch(message->packet, &pocsag_state);
 
diff --git a/firmware/application/apps/replay_app.cpp b/firmware/application/apps/replay_app.cpp
index be34e619de7ba3ed0b14746db3bffdd37df1fbbf..8bde0c2f7e72f06796d1b6bc798803fca729f5b1 100644
--- a/firmware/application/apps/replay_app.cpp
+++ b/firmware/application/apps/replay_app.cpp
@@ -161,6 +161,8 @@ void ReplayAppView::stop(const bool do_loop) {
 		radio::disable();
 		button_play.set_bitmap(&bitmap_play);
 	}
+	
+	ready_signal = false;
 }
 
 void ReplayAppView::handle_replay_thread_done(const uint32_t return_code) {
diff --git a/firmware/application/apps/ui_about.cpp b/firmware/application/apps/ui_about.cpp
index faecaa0a05373d88589b68770b3d99c07e97c6c9..ce09961bd41d85607cfb671f8e6062aeef69d1db 100644
--- a/firmware/application/apps/ui_about.cpp
+++ b/firmware/application/apps/ui_about.cpp
@@ -62,10 +62,11 @@ void CreditsWidget::on_hide() {
 void CreditsWidget::new_row(
 	const std::array<Color, 240>& pixel_row
 ) {
+	// Glitch be here (see comment in main.cpp)
 	const auto draw_y = display.scroll(-1);
 	
 	display.draw_pixels(
-		{ { 0, draw_y }, { 240, 1 } },
+		{ { 0, draw_y - 1 }, { 240, 1 } },
 		pixel_row
 	);
 }
diff --git a/firmware/application/apps/ui_encoders.cpp b/firmware/application/apps/ui_encoders.cpp
index 9ac1e3b5e99aefadc835a3b36eea3f0dd997b263..e15defd101b4a313974bf4a7688d6eb7616ebce2 100644
--- a/firmware/application/apps/ui_encoders.cpp
+++ b/firmware/application/apps/ui_encoders.cpp
@@ -102,7 +102,7 @@ void EncodersConfigView::on_type_change(size_t index) {
 	symfield_word.set_length(word_length);
 	size_t n = 0, i = 0;
 	while (n < word_length) {
-		symbol_type = encoder_def->word_format.at(i++);
+		symbol_type = encoder_def->word_format[i++];
 		if (symbol_type == 'A') {
 			symfield_word.set_symbol_list(n++, encoder_def->address_symbols);
 			format_string += 'A';
diff --git a/firmware/application/apps/ui_mictx.cpp b/firmware/application/apps/ui_mictx.cpp
index 54dca6b01b6618fd92738739abd1c28dba666ef7..fff65098522cce77376f9745900b1ae8f1a3da8c 100644
--- a/firmware/application/apps/ui_mictx.cpp
+++ b/firmware/application/apps/ui_mictx.cpp
@@ -51,11 +51,11 @@ void MicTXView::on_tx_progress(const bool done) {
 }
 
 void MicTXView::configure_baseband() {
-	baseband::set_audiotx_data(
+	baseband::set_audiotx_config(
 		sampling_rate / 20,		// Update vu-meter at 20Hz
 		transmitting ? transmitter_model.channel_bandwidth() : 0,
 		mic_gain,
-		TONES_F2D(tone_key_frequency(tone_key_index)),
+		TONES_F2D(tone_key_frequency(tone_key_index), sampling_rate),
 		0.2		// 20% mix
 	);
 }
diff --git a/firmware/application/apps/ui_soundboard.cpp b/firmware/application/apps/ui_soundboard.cpp
index 9dc3f86428ca92146951d42eb9472002197eff18..5902bfadf754e36a5992a8131ec210ddb5ff9a56 100644
--- a/firmware/application/apps/ui_soundboard.cpp
+++ b/firmware/application/apps/ui_soundboard.cpp
@@ -23,7 +23,6 @@
 // To prepare samples: for f in ./*.wav; do sox "$f" -r 48000 -c 1 -b8 --norm "conv/$f"; done
 
 #include "ui_soundboard.hpp"
-
 #include "lfsr_random.hpp"
 #include "string_format.hpp"
 #include "tonesets.hpp"
@@ -33,6 +32,7 @@ using namespace portapack;
 
 namespace ui {
 
+// TODO: Use Sharebrained's PRNG
 void SoundBoardView::do_random() {
 	uint32_t id;
 	
@@ -43,45 +43,42 @@ void SoundBoardView::do_random() {
 
 	play_sound(id);
 	
-	buttons[id % 18].focus();
-	page = id / 18;
+	buttons[id % 15].focus();
+	page = id / 15;
 	
 	refresh_buttons(id);
 }
 
-void SoundBoardView::prepare_audio() {
+bool SoundBoardView::is_active() const {
+	return (bool)replay_thread;
+}
+
+void SoundBoardView::stop(const bool do_loop) {
+	if( is_active() )
+		replay_thread.reset();
 	
-	if (sample_counter >= sample_duration) {
-		if (tx_mode == NORMAL) {
-			if (!check_loop.value()) {
-				pbar.set_value(0);
-				transmitter_model.disable();
-				return;
-			} else {
-				reader->rewind();
-				sample_counter = 0;
-			}
-		} else {
-			pbar.set_value(0);
-			transmitter_model.disable();
-			if (check_loop.value())
-				do_random();
-		}
+	if (do_loop && check_loop.value()) {
+		play_sound(playing_id);
+	} else {
+		radio::disable();
+		//button_play.set_bitmap(&bitmap_play);
 	}
-	
-	pbar.set_value(sample_counter);
+	ready_signal = false;
+}
 
-	auto bytes_read = reader->read(audio_buffer, 512).value();
-	
-	// Unsigned to signed, pretty stupid :/
-	for (size_t n = 0; n < bytes_read; n++)
-		audio_buffer[n] -= 0x80;
-	for (size_t n = bytes_read; n < 512; n++)
-		audio_buffer[n] = 0;
-	
-	sample_counter += 512;
+void SoundBoardView::handle_replay_thread_done(const uint32_t return_code) {
+	if (return_code == ReplayThread::END_OF_FILE) {
+		stop(true);
+	} else if (return_code == ReplayThread::READ_ERROR) {
+		stop(false);
+		file_error();
+	}
 	
-	baseband::set_fifo_data(audio_buffer);
+	progressbar.set_value(0);
+}
+
+void SoundBoardView::set_ready() {
+	ready_signal = true;
 }
 
 void SoundBoardView::focus() {
@@ -95,41 +92,60 @@ void SoundBoardView::on_tuning_frequency_changed(rf::Frequency f) {
 	transmitter_model.set_tuning_frequency(f);
 }
 
-void SoundBoardView::play_sound(uint16_t id) {
-	uint32_t tone_key_index;
-	uint32_t divider;
-
-	if (sounds[id].size == 0) return;
+void SoundBoardView::file_error() {
+	nav_.display_modal("Error", "File read error.");
+}
 
-	if (!reader->open(sounds[id].path)) return;
-	
-	sample_duration = sounds[id].sample_duration;
-	
-	pbar.set_max(sample_duration);
-	pbar.set_value(0);
+void SoundBoardView::play_sound(uint16_t id) {
+	uint32_t sample_rate = 0;
 	
-	sample_counter = 0;
+	auto reader = std::make_unique<WAVFileReader>();
+	uint32_t tone_key_index = options_tone_key.selected_index();
 	
-	prepare_audio();
+	stop(false);
+
+	if(!reader->open(sounds[id].path)) {
+		file_error();
+		return;
+	}
 	
-	transmitter_model.set_sampling_rate(1536000U);
-	transmitter_model.set_rf_amp(true);
-	transmitter_model.set_lna(40);
-	transmitter_model.set_vga(40);
-	transmitter_model.set_baseband_bandwidth(1750000);
-	transmitter_model.enable();
+	playing_id = id;
 	
-	tone_key_index = 0;	//options_tone_key.selected_index();
+	progressbar.set_max(reader->sample_count());
+	sample_rate = reader->sample_rate() * 32;
 	
-	divider = (1536000 / sounds[id].sample_rate) - 1;
+	if( reader ) {
+		//button_play.set_bitmap(&bitmap_stop);
+		baseband::set_sample_rate(sample_rate);
+		
+		replay_thread = std::make_unique<ReplayThread>(
+			std::move(reader),
+			read_size, buffer_count,
+			&ready_signal,
+			[](uint32_t return_code) {
+				ReplayThreadDoneMessage message { return_code };
+				EventDispatcher::send_message(message);
+			}
+		);
+	}
 	
-	baseband::set_audiotx_data(
-		divider,
+	baseband::set_audiotx_config(
+		0,
 		number_bw.value() * 1000,
 		10,
-		TONES_F2D(tone_key_frequency(tone_key_index)),
+		TONES_F2D(tone_key_frequency(tone_key_index), sample_rate),
 		0.2		// 20% mix
 	);
+	
+	radio::enable({
+		receiver_model.tuning_frequency(),
+		sample_rate,
+		1750000,
+		rf::Direction::Transmit,
+		receiver_model.rf_amp(),
+		static_cast<int8_t>(receiver_model.lna()),
+		static_cast<int8_t>(receiver_model.vga())
+	});
 }
 
 void SoundBoardView::show_infos(uint16_t id) {
@@ -144,7 +160,7 @@ void SoundBoardView::refresh_buttons(uint16_t id) {
 	text_page.set("Page " + to_string_dec_uint(page + 1) + "/" + to_string_dec_uint(max_page));
 	
 	for (auto& button : buttons) {
-		n_sound = (page * 18) + n;
+		n_sound = (page * 15) + n;
 		
 		button.id = n_sound;
 		
@@ -180,64 +196,57 @@ bool SoundBoardView::change_page(Button& button, const KeyEvent key) {
 	return false;
 }
 
+void SoundBoardView::on_tx_progress(const uint32_t progress) {
+	progressbar.set_value(progress);
+}
+
 SoundBoardView::SoundBoardView(
 	NavigationView& nav
 ) : nav_ (nav)
 {
-	std::vector<std::filesystem::path> file_list;
-	std::string title;
-	uint8_t c;
-	
-	reader = std::make_unique<WAVFileReader>();
+	auto reader = std::make_unique<WAVFileReader>();
+	uint8_t c = 0;
 	
-	file_list = scan_root_files(u"WAV", u"*.WAV");
-	
-	c = 0;
-	for (auto& path : file_list) {
-		if (reader->open(u"WAV/" + path.native())) {
-			if ((reader->channels() == 1) && (reader->bits_per_sample() == 8)) {
-				sounds[c].size = reader->data_size();
-				sounds[c].sample_duration = reader->data_size(); // / (reader->bits_per_sample() / 8);
-				sounds[c].sample_rate = reader->sample_rate();
-				//if (reader->bits_per_sample() > 8)
-				//	sounds[c].sixteenbit = true;
-				//else
-				//	sounds[c].sixteenbit = false;
-				sounds[c].ms_duration = reader->ms_duration();
-				sounds[c].path = u"WAV/" + path.native();
-				title = reader->title().substr(0, 20);
-				if (title != "")
-					sounds[c].title = title;
-				else
-					sounds[c].title = "-";
-				c++;
-				if (c == 54) break;		// Limit to 54 files (3 pages)
+	for(const auto& entry : std::filesystem::directory_iterator(u"WAV", u"*.WAV")) {
+		if( std::filesystem::is_regular_file(entry.status()) ) {
+			if (reader->open(u"WAV/" + entry.path().native())) {
+				if ((reader->channels() == 1) && (reader->bits_per_sample() == 8)) {
+					sounds[c].ms_duration = reader->ms_duration();
+					sounds[c].path = u"WAV/" + entry.path().native();
+					std::string title = reader->title().substr(0, 20);
+					if (title != "")
+						sounds[c].title = title;
+					else
+						sounds[c].title = "-";
+					c++;
+					if (c == 60) break;		// Limit to 60 files (4 pages)
+				}
 			}
 		}
 	}
 	
 	baseband::run_image(portapack::spi_flash::image_tag_audio_tx);
-
+	
 	max_sound = c;
-	max_page = (max_sound + 18 - 1) / 18;	// 3 * 6 = 18 buttons per page
+	max_page = (max_sound + 15 - 1) / 15;	// 3 * 5 = 15 buttons per page
 	
 	add_children({
 		&labels,
 		&field_frequency,
 		&number_bw,
-		//&options_tone_key,
+		&options_tone_key,
 		&text_title,
 		&text_page,
 		&text_duration,
-		&pbar,
+		&progressbar,
 		&check_loop,
 		&button_random,
 		&button_exit
 	});
-
-	//tone_keys_populate(options_tone_key);
-	//options_tone_key.set_selected_index(0);
-
+	
+	tone_keys_populate(options_tone_key);
+	options_tone_key.set_selected_index(0);
+	
 	const auto button_fn = [this](Button& button) {
 		tx_mode = NORMAL;
 		this->play_sound(button.id);
@@ -250,7 +259,7 @@ SoundBoardView::SoundBoardView(
 	const auto button_dir = [this](Button& button, const KeyEvent key) {
 		return change_page(button, key);
 	};
-
+	
 	// Generate buttons
 	size_t n = 0;
 	for(auto& button : buttons) {
@@ -260,8 +269,8 @@ SoundBoardView::SoundBoardView(
 		button.on_dir = button_dir;
 		button.set_parent_rect({
 			static_cast<Coord>((n % 3) * 78 + 3),
-			static_cast<Coord>((n / 3) * 30 + 24),
-			78, 30
+			static_cast<Coord>((n / 3) * 38 + 24),
+			78, 38
 		});
 		n++;
 	}
diff --git a/firmware/application/apps/ui_soundboard.hpp b/firmware/application/apps/ui_soundboard.hpp
index 5dd9fc88b0c28a4650d22f81b67a0686ba0b2b9c..5f9e65b075cc12a81d6478826d8695365b6cc2cf 100644
--- a/firmware/application/apps/ui_soundboard.hpp
+++ b/firmware/application/apps/ui_soundboard.hpp
@@ -26,6 +26,7 @@
 #include "ui.hpp"
 #include "ui_widget.hpp"
 #include "ui_font_fixed_8x16.hpp"
+#include "replay_thread.hpp"
 #include "baseband_api.hpp"
 #include "ui_receiver.hpp"
 #include "io_wave.hpp"
@@ -59,10 +60,6 @@ private:
 	
 	struct sound {
 		std::filesystem::path path { };
-		bool sixteenbit = false;
-		uint32_t sample_rate = 0;
-		uint32_t size = 0;
-		uint32_t sample_duration = 0;
 		uint32_t ms_duration = 0;
 		std::string title { };
 	};
@@ -73,13 +70,15 @@ private:
 	
 	uint32_t lfsr_v = 0x13377331;
 	
-	std::unique_ptr<WAVFileReader> reader { };
-	
-	sound sounds[54];			// 3 pages * 18 buttons
+	sound sounds[60];			// 5 pages * 12 buttons
 	uint32_t max_sound { };
 	uint8_t max_page { };
+	uint32_t playing_id { };
 
-	int8_t audio_buffer[512];
+	const size_t read_size { 2048 };	// Less ?
+	const size_t buffer_count { 3 };
+	std::unique_ptr<ReplayThread> replay_thread { };
+	bool ready_signal { false };
 	
 	Style style_a {
 		.font = font::fixed_8x16,
@@ -102,7 +101,7 @@ private:
 		.foreground = { 153, 102, 255 }
 	};
 
-	std::array<Button, 18> buttons { };
+	std::array<Button, 15> buttons { };
 	const Style * styles[4] = { &style_a, &style_b, &style_c, &style_d };
 	
 	void on_tuning_frequency_changed(rf::Frequency f);
@@ -112,8 +111,13 @@ private:
 	bool change_page(Button& button, const KeyEvent key);
 	void refresh_buttons(uint16_t id);
 	void play_sound(uint16_t id);
-	void prepare_audio();
 	void on_ctcss_changed(uint32_t v);
+	void stop(const bool do_loop);
+	bool is_active() const;
+	void set_ready();
+	void handle_replay_thread_done(const uint32_t return_code);
+	void file_error();
+	void on_tx_progress(const uint32_t progress);
 	
 	Labels labels {
 		{ { 10 * 8, 4 }, "BW:   kHz", Color::light_grey() }
@@ -138,21 +142,21 @@ private:
 	};
 	
 	Text text_title {
-		{ 1 * 8, 26 * 8, 20 * 8, 16 },
+		{ 1 * 8, 28 * 8, 20 * 8, 16 },
 		"-"
 	};
 	
 	Text text_page {
-		{ 22 * 8 - 4, 26 * 8, 8 * 8, 16 },
+		{ 22 * 8 - 4, 28 * 8, 8 * 8, 16 },
 		"Page -/-"
 	};
 	
 	Text text_duration {
-		{ 1 * 8, 30 * 8, 5 * 8, 16 }
+		{ 1 * 8, 31 * 8, 5 * 8, 16 }
 	};
 	
-	ProgressBar pbar {
-		{ 9 * 8, 30 * 8, 20 * 8, 16 }
+	ProgressBar progressbar {
+		{ 9 * 8, 31 * 8, 20 * 8, 16 }
 	};
 	
 	Checkbox check_loop {
@@ -171,15 +175,31 @@ private:
 		"Exit"
 	};
 	
+	MessageHandlerRegistration message_handler_replay_thread_error {
+		Message::ID::ReplayThreadDone,
+		[this](const Message* const p) {
+			const auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
+			this->handle_replay_thread_done(message.return_code);
+		}
+	};
+	
 	MessageHandlerRegistration message_handler_fifo_signal {
 		Message::ID::RequestSignal,
 		[this](const Message* const p) {
 			const auto message = static_cast<const RequestSignalMessage*>(p);
 			if (message->signal == RequestSignalMessage::Signal::FillRequest) {
-				this->prepare_audio();
+				this->set_ready();
 			}
 		}
 	};
+	
+	MessageHandlerRegistration message_handler_tx_progress {
+		Message::ID::TXProgress,
+		[this](const Message* const p) {
+			const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
+			this->on_tx_progress(message.progress);
+		}
+	};
 };
 
 } /* namespace ui */
diff --git a/firmware/application/apps/ui_tone_search.cpp b/firmware/application/apps/ui_tone_search.cpp
index c7c2e0fc87a11ef04170f12aacb543d79a8f88be..0f190d5523e4f6a6667bdf25cf8cacb3b39a3c71 100644
--- a/firmware/application/apps/ui_tone_search.cpp
+++ b/firmware/application/apps/ui_tone_search.cpp
@@ -45,7 +45,10 @@ ToneSearchView::ToneSearchView(
 	//baseband::run_image(portapack::spi_flash::image_tag_wideband_spectrum);
 	
 	add_children({
-		&labels
+		&labels,
+		&field_lna,
+		&field_vga,
+		&field_rf_amp,
 	});
 }
 
diff --git a/firmware/application/apps/ui_tone_search.hpp b/firmware/application/apps/ui_tone_search.hpp
index b32c9bff53d242bd260a0378d830e0156653a8f1..1d121138e8c1745d0222aae1ebba83e2cb1fe21a 100644
--- a/firmware/application/apps/ui_tone_search.hpp
+++ b/firmware/application/apps/ui_tone_search.hpp
@@ -40,7 +40,19 @@ private:
 	NavigationView& nav_;
 	
 	Labels labels {
-		{ { 1 * 8, 0 }, "Min:      Max:       LNA VGA", Color::light_grey() }
+		{ { 0 * 8, 0 * 8 }, "LNA:   VGA:   AMP:", Color::light_grey() }
+	};
+	
+	LNAGainField field_lna {
+		{ 4 * 8, 0 * 16 }
+	};
+	
+	VGAGainField field_vga {
+		{ 11 * 8, 0 * 16 }
+	};
+	
+	RFAmpField field_rf_amp {
+		{ 18 * 8, 0 * 16 }
 	};
 	
 	/*
diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp
index ef03ac9d5fdbd001f347d7b47bdb78f9c2b9395a..2416a65548ccbfbf1d6388a959b26be15a6e6fee 100644
--- a/firmware/application/baseband_api.cpp
+++ b/firmware/application/baseband_api.cpp
@@ -152,7 +152,7 @@ void kill_afsk() {
 	send_message(&message);
 }
 
-void set_audiotx_data(const uint32_t divider, const float deviation_hz, const float audio_gain,
+void set_audiotx_config(const uint32_t divider, const float deviation_hz, const float audio_gain,
 					const uint32_t tone_key_delta, const float tone_key_mix_weight) {
 	const AudioTXConfigMessage message {
 		divider,
@@ -240,7 +240,7 @@ void set_spectrum(const size_t sampling_rate, const size_t trigger) {
 
 void set_siggen_tone(const uint32_t tone) {
 	const SigGenToneMessage message {
-		TONES_F2D(tone)
+		TONES_F2D(tone, TONES_SAMPLERATE)
 	};
 	send_message(&message);
 }
diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp
index 22f6ead6eeb73ffe9fae53195c6e7188321e9191..c665ff6e4050a348141ecbeb6d6f3adb6bb25f66 100644
--- a/firmware/application/baseband_api.hpp
+++ b/firmware/application/baseband_api.hpp
@@ -60,7 +60,7 @@ void set_tones_config(const uint32_t bw, const uint32_t pre_silence, const uint1
 					const bool dual_tone, const bool audio_out);
 void kill_tone();
 void set_sstv_data(const uint8_t vis_code, const uint32_t pixel_duration);
-void set_audiotx_data(const uint32_t divider, const float deviation_hz, const float audio_gain,
+void set_audiotx_config(const uint32_t divider, const float deviation_hz, const float audio_gain,
 					const uint32_t tone_key_delta, const float tone_key_mix_weight);
 void set_fifo_data(const int8_t * data);
 void set_pitch_rssi(int32_t avg, bool enabled);
diff --git a/firmware/application/main.cpp b/firmware/application/main.cpp
index 63d7c1633ef5763d7fe1be9a2b9719c8d9395977..1b007486404b1243683e9dd73f0494422489c315 100755
--- a/firmware/application/main.cpp
+++ b/firmware/application/main.cpp
@@ -23,6 +23,10 @@
 // Color bitmaps generated with:
 // Gimp image > indexed colors (16), then "xxd -i *.bmp"
 
+// Note about available RAM:
+// Check what ends up in the BSS section by looking at the map files !
+// Use constexpr where possible or make sure const are in .cpp files, not headers !
+
 //TEST: Goertzel
 //TEST: Menuview refresh, seems to blink a lot
 //TEST: Check AFSK transmit end, skips last bits ?
@@ -31,8 +35,8 @@
 //BUG: (Workaround ok) CPLD-related rx ok, tx bad, see portapack.cpp lines 214+ to disable CPLD overlay
 //BUG: SCANNER Lock on frequency, if frequency jump, still locked on first one
 //BUG: SCANNER Multiple slices
+//GLITCH: The about view scroller sometimes misses lines because of a race condition between the display scrolling and drawing the line
 
-//TODO: Disable Nuoptix timecode TX, useless (almost)
 //TODO: Move Touchtunes remote to Custom remote
 //TODO: Use escapes \x1B to set colors in text, it works !
 //TODO: Open files in File Manager
diff --git a/firmware/application/protocols/encoders.hpp b/firmware/application/protocols/encoders.hpp
index b30b5deb8dda37e3e4f1b4591646b6341e80a283..d23b38f4913ed06854cc77589dc2530afe39015c 100644
--- a/firmware/application/protocols/encoders.hpp
+++ b/firmware/application/protocols/encoders.hpp
@@ -38,22 +38,22 @@ namespace encoders {
 	void bitstream_append(size_t& bitstream_length, uint32_t bit_count, uint32_t bits);
 
 	struct encoder_def_t {
-		std::string name;						// Encoder chip ref/name
-		std::string address_symbols;			// List of possible symbols like "01", "01F"...
-		std::string data_symbols;				// Same
+		char name[16];							// Encoder chip ref/name
+		char address_symbols[8];				// List of possible symbols like "01", "01F"...
+		char data_symbols[8];					// Same
 		uint16_t clk_per_symbol;				// Oscillator periods per symbol
 		uint16_t clk_per_fragment;				// Oscillator periods per symbol fragment (state)
-		std::vector<std::string> bit_format;	// List of fragments for each symbol in previous *_symbols list order
+		char bit_format[4][20];					// List of fragments for each symbol in previous *_symbols list order
 		uint8_t word_length;					// Total # of symbols (not counting sync)
-		std::string word_format;				// A for Address, D for Data, S for sync
-		std::string sync;						// Like bit_format
+		char word_format[32];					// A for Address, D for Data, S for sync
+		char sync[64];							// Like bit_format
 		uint32_t default_speed;					// Default encoder clk frequency (often set by shitty resistor)
 		uint8_t repeat_min;						// Minimum repeat count
 		uint16_t pause_symbols;					// Length of pause between repeats in symbols
 	};
 
 	// Warning ! If this is changed, make sure that ENCODER_UM3750 is still valid !
-	const encoder_def_t encoder_defs[ENC_TYPES_COUNT] = {
+	constexpr encoder_def_t encoder_defs[ENC_TYPES_COUNT] = {
 		// PT2260-R2
 		{
 			"2260-R2",
diff --git a/firmware/application/protocols/modems.hpp b/firmware/application/protocols/modems.hpp
index 3582dfe7cf2a93d270b8378987a6025d49080dc9..48dea3ee8689fcaf4d57d0e51bf1b53f5582985b 100644
--- a/firmware/application/protocols/modems.hpp
+++ b/firmware/application/protocols/modems.hpp
@@ -40,14 +40,14 @@ enum ModemModulation {
 };
 
 struct modem_def_t {
-	std::string name;
+	char name[16];
 	ModemModulation modulation;
 	uint16_t mark_freq;
 	uint16_t space_freq;
 	uint16_t baudrate;
 };
 
-const modem_def_t modem_defs[MODEM_DEF_COUNT] = {
+constexpr modem_def_t modem_defs[MODEM_DEF_COUNT] = {
 	{ "Bell202", 	AFSK,	1200,	2200, 	1200 },
 	{ "Bell103", 	AFSK,	1270,	1070, 	300 },
 	{ "V21",		AFSK,	980,	1180, 	300 },
diff --git a/firmware/application/replay_thread.cpp b/firmware/application/replay_thread.cpp
index 29f75cdd30dc184f750fef042084b3fe09bc0eee..c303f75dd2d8f7c9d26b37579638fcbf96fc3395 100644
--- a/firmware/application/replay_thread.cpp
+++ b/firmware/application/replay_thread.cpp
@@ -88,7 +88,7 @@ uint32_t ReplayThread::run() {
 		if (prefill_buffer == nullptr) {
 			buffers.put_app(prefill_buffer);
 		} else {
-			size_t blocks = 16384 / 512;
+			size_t blocks = config.read_size / 512;
 			
 			for (size_t c = 0; c < blocks; c++) {
 				auto read_result = reader->read(&((uint8_t*)prefill_buffer->data())[c * 512], 512);
@@ -97,7 +97,7 @@ uint32_t ReplayThread::run() {
 				}
 			}
 			
-			prefill_buffer->set_size(16384);
+			prefill_buffer->set_size(config.read_size);
 			
 			buffers.put(prefill_buffer);
 		}
diff --git a/firmware/application/string_format.hpp b/firmware/application/string_format.hpp
index 278088baaadc3368a8e1233052b27b43840facc9..c0164a0d91a5e16bd0b2005bc6c67e2189b48e02 100644
--- a/firmware/application/string_format.hpp
+++ b/firmware/application/string_format.hpp
@@ -41,7 +41,7 @@ const char unit_prefix[7] { 'n', 'u', 'm', 0, 'k', 'M', 'G' };
 
 // TODO: Allow l=0 to not fill/justify? Already using this way in ui_spectrum.hpp...
 std::string to_string_bin(const uint32_t n, const uint8_t l = 0);
-std::string to_string_dec_uint(const uint32_t n, const int32_t l = 0, const char fill = '0');
+std::string to_string_dec_uint(const uint32_t n, const int32_t l = 0, const char fill = ' ');
 std::string to_string_dec_int(const int32_t n, const int32_t l = 0, const char fill = 0);
 std::string to_string_hex(const uint64_t n, const int32_t l = 0);
 std::string to_string_hex_array(uint8_t * const array, const int32_t l = 0);
diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp
index 935d9245500736a6252acd21c3035d17d2e4876a..e4aae62cad2e54b4e75d5149a745d0ebc465e1a7 100644
--- a/firmware/application/ui_navigation.cpp
+++ b/firmware/application/ui_navigation.cpp
@@ -37,7 +37,7 @@
 #include "ui_aprs_tx.hpp"
 #include "ui_bht_tx.hpp"
 #include "ui_coasterp.hpp"
-#include "ui_debug.hpp"
+//#include "ui_debug.hpp"
 #include "ui_encoders.hpp"
 #include "ui_fileman.hpp"
 #include "ui_freqman.hpp"
@@ -329,20 +329,20 @@ void NavigationView::focus() {
 
 ReceiversMenuView::ReceiversMenuView(NavigationView& nav) {
 	add_items({
-		{ "ADS-B: Planes", 			ui::Color::green(),	&bitmap_icon_adsb,	[&nav](){ nav.push<ADSBRxView>(); }, },
-		{ "AIS:   Boats", 			ui::Color::green(),	&bitmap_icon_ais,	[&nav](){ nav.push<AISAppView>(); } },
-		{ "AFSK", 					ui::Color::yellow(),&bitmap_icon_receivers,	[&nav](){ nav.push<AFSKRxView>(); } },
-		{ "APRS", 					ui::Color::grey(),	&bitmap_icon_aprs,	[&nav](){ nav.push<NotImplementedView>(); } },
-		{ "Audio", 					ui::Color::green(),	&bitmap_icon_speaker,	[&nav](){ nav.push<AnalogAudioView>(false); } },
-		{ "DMR framing", 			ui::Color::grey(),	&bitmap_icon_dmr,	[&nav](){ nav.push<NotImplementedView>(); } },
-		{ "ERT:   Utility Meters", 	ui::Color::green(), &bitmap_icon_ert,	[&nav](){ nav.push<ERTAppView>(); } },
-		{ "POCSAG", 				ui::Color::green(),	&bitmap_icon_pocsag,	[&nav](){ nav.push<POCSAGAppView>(); } },
-		{ "SIGFOX", 				ui::Color::grey(),	&bitmap_icon_fox,	[&nav](){ nav.push<NotImplementedView>(); } }, // SIGFRXView
-		{ "LoRa", 					ui::Color::grey(),	&bitmap_icon_lora,	[&nav](){ nav.push<NotImplementedView>(); } },
-		{ "Radiosondes", 			ui::Color::yellow(),&bitmap_icon_sonde,	[&nav](){ nav.push<SondeView>(); } },
-		{ "SSTV", 					ui::Color::grey(), 	&bitmap_icon_sstv,	[&nav](){ nav.push<NotImplementedView>(); } },
-		{ "TETRA framing", 			ui::Color::grey(),	&bitmap_icon_tetra,	[&nav](){ nav.push<NotImplementedView>(); } },
-		{ "TPMS:  Cars", 			ui::Color::green(),	&bitmap_icon_tpms,	[&nav](){ nav.push<TPMSAppView>(); } },
+		{ "ADS-B: Planes", 			ui::Color::green(),	&bitmap_icon_adsb,	[&nav](){ nav.replace<ADSBRxView>(); }, },
+		{ "AIS:   Boats", 			ui::Color::green(),	&bitmap_icon_ais,	[&nav](){ nav.replace<AISAppView>(); } },
+		{ "AFSK", 					ui::Color::yellow(),&bitmap_icon_receivers,	[&nav](){ nav.replace<AFSKRxView>(); } },
+		{ "APRS", 					ui::Color::grey(),	&bitmap_icon_aprs,	[&nav](){ nav.replace<NotImplementedView>(); } },
+		{ "Audio", 					ui::Color::green(),	&bitmap_icon_speaker,	[&nav](){ nav.replace<AnalogAudioView>(false); } },
+		{ "DMR framing", 			ui::Color::grey(),	&bitmap_icon_dmr,	[&nav](){ nav.replace<NotImplementedView>(); } },
+		{ "ERT:   Utility Meters", 	ui::Color::green(), &bitmap_icon_ert,	[&nav](){ nav.replace<ERTAppView>(); } },
+		{ "POCSAG", 				ui::Color::green(),	&bitmap_icon_pocsag,	[&nav](){ nav.replace<POCSAGAppView>(); } },
+		{ "SIGFOX", 				ui::Color::grey(),	&bitmap_icon_fox,	[&nav](){ nav.replace<NotImplementedView>(); } }, // SIGFRXView
+		{ "LoRa", 					ui::Color::grey(),	&bitmap_icon_lora,	[&nav](){ nav.replace<NotImplementedView>(); } },
+		{ "Radiosondes", 			ui::Color::yellow(),&bitmap_icon_sonde,	[&nav](){ nav.replace<SondeView>(); } },
+		{ "SSTV", 					ui::Color::grey(), 	&bitmap_icon_sstv,	[&nav](){ nav.replace<NotImplementedView>(); } },
+		{ "TETRA framing", 			ui::Color::grey(),	&bitmap_icon_tetra,	[&nav](){ nav.replace<NotImplementedView>(); } },
+		{ "TPMS:  Cars", 			ui::Color::green(),	&bitmap_icon_tpms,	[&nav](){ nav.replace<TPMSAppView>(); } },
 	});
 	on_left = [&nav](){ nav.pop(); };
 	
@@ -383,7 +383,7 @@ UtilitiesMenuView::UtilitiesMenuView(NavigationView& nav) {
 		{ "File manager", 			ui::Color::yellow(),	&bitmap_icon_file,		[&nav](){ nav.push<FileManagerView>(); } },
 		{ "Notepad",				ui::Color::grey(),		&bitmap_icon_notepad,	[&nav](){ nav.push<NotImplementedView>(); } },
 		{ "Signal generator", 		ui::Color::green(), 	&bitmap_icon_cwgen,		[&nav](){ nav.push<SigGenView>(); } },
-		{ "Tone search", 			ui::Color::grey(), 		nullptr,				[&nav](){ nav.push<NotImplementedView>(); } },	// ToneSearchView
+		{ "Tone search", 			ui::Color::grey(), 		nullptr,				[&nav](){ nav.push<ToneSearchView>(); } },
 		{ "Whip antenna length",	ui::Color::yellow(),	nullptr,				[&nav](){ nav.push<WhipCalcView>(); } },
 		{ "Wipe SD card",			ui::Color::red(),		nullptr,				[&nav](){ nav.push<WipeSDView>(); } },
 	});
@@ -466,10 +466,12 @@ SystemView::SystemView(
 		(portapack::persistent_memory::ui_config() & 16)) {					// Login option
 		navigation_view.push<PlayDeadView>();
 	} else {*/
+	
 		if (portapack::persistent_memory::config_splash())
 			navigation_view.push<BMPView>();
 		else
 			navigation_view.push<SystemMenuView>();
+			
 	//}
 }
 
diff --git a/firmware/application/ui_navigation.hpp b/firmware/application/ui_navigation.hpp
index ee8f49e2c4d6b1e540c07a3d0c2eebecb953988f..925f0de6f4cd4223d4034ff0de8ccdb55130a7cb 100644
--- a/firmware/application/ui_navigation.hpp
+++ b/firmware/application/ui_navigation.hpp
@@ -70,8 +70,14 @@ public:
 	T* push(Args&&... args) {
 		return reinterpret_cast<T*>(push_view(std::unique_ptr<View>(new T(*this, std::forward<Args>(args)...))));
 	}
+	template<class T, class... Args>
+	T* replace(Args&&... args) {
+		pop();
+		return reinterpret_cast<T*>(push_view(std::unique_ptr<View>(new T(*this, std::forward<Args>(args)...))));
+	}
 	
 	void push(View* v);
+	void replace(View* v);
 
 	void pop();
 	void pop_modal();
diff --git a/firmware/baseband/proc_audiotx.cpp b/firmware/baseband/proc_audiotx.cpp
index de78d836e5865f1bbbd9eac9ead0c69d33f3ec9a..2dfbdf55bc33587aa8fd417b557101b659bb1ff3 100644
--- a/firmware/baseband/proc_audiotx.cpp
+++ b/firmware/baseband/proc_audiotx.cpp
@@ -29,70 +29,94 @@
 #include <cstdint>
 
 void AudioTXProcessor::execute(const buffer_c8_t& buffer){
-
-	// This is called at 1536000/2048 = 750Hz
 	
 	if (!configured) return;
-
-	for (size_t i = 0; i<buffer.count; i++) {
-		
-		// Audio preview sample generation @ 1536000/divider
-		if (!as) {
-			as = divider;
-			audio_fifo.out(out_sample);
-			sample = (int32_t)out_sample;
-			//preview_audio_buffer.p[ai++] = sample << 8;
-			
-			if ((audio_fifo.len() < 512) && (asked == false)) {
-				// Ask application to fill up fifo
-				shared_memory.application_queue.push(sig_message);
-				asked = true;
-			}
-		} else {
-			as--;
-		}
+	
+	if( stream ) {
+		const size_t bytes_to_read = (buffer.count / 32);	// /32 (oversampling) should be == 64
+		bytes_read += stream->read(audio_buffer.p, bytes_to_read);
+	}
+	
+	// Fill and "stretch"
+	for (size_t i = 0; i < buffer.count; i++) {
+		if (!(i & 31))
+			audio_sample = audio_buffer.p[i >> 5] - 0x80;
 		
-		sample = tone_gen.process(sample);
+		sample = tone_gen.process(audio_sample);
 		
 		// FM
 		delta = sample * fm_delta;
 		
 		phase += delta;
 		sphase = phase + (64 << 24);
-
+		
 		re = (sine_table_i8[(sphase & 0xFF000000U) >> 24]);
 		im = (sine_table_i8[(phase & 0xFF000000U) >> 24]);
 		
-		buffer.p[i] = {re, im};
+		buffer.p[i] = { (int8_t)re, (int8_t)im };
+	}
+	
+	spectrum_samples += buffer.count;
+	if( spectrum_samples >= spectrum_interval_samples ) {
+		spectrum_samples -= spectrum_interval_samples;
+		
+		txprogress_message.progress = bytes_read;	// Inform UI about progress
+		txprogress_message.done = false;
+		shared_memory.application_queue.push(txprogress_message);
 	}
 	
 	//AudioOutput::fill_audio_buffer(preview_audio_buffer, true);
 }
 
-void AudioTXProcessor::on_message(const Message* const msg) {
-	const auto message = *reinterpret_cast<const AudioTXConfigMessage*>(msg);
-	
-	switch(msg->id) {
+void AudioTXProcessor::on_message(const Message* const message) {
+	switch(message->id) {
 		case Message::ID::AudioTXConfig:
-			fm_delta = message.deviation_hz * (0xFFFFFFULL / baseband_fs);
-			divider = message.divider;
-			as = 0;
-			
-			tone_gen.configure(message.tone_key_delta, message.tone_key_mix_weight);
+			audio_config(*reinterpret_cast<const AudioTXConfigMessage*>(message));
+			break;
 
-			configured = true;
+		case Message::ID::ReplayConfig:
+			configured = false;
+			bytes_read = 0;
+			replay_config(*reinterpret_cast<const ReplayConfigMessage*>(message));
+			break;
+		
+		case Message::ID::SamplerateConfig:
+			samplerate_config(*reinterpret_cast<const SamplerateConfigMessage*>(message));
 			break;
 		
 		case Message::ID::FIFOData:
-			audio_fifo.in(static_cast<const FIFODataMessage*>(msg)->data, 512);
-			asked = false;
+			configured = true;
 			break;
-
+		
 		default:
 			break;
 	}
 }
 
+void AudioTXProcessor::audio_config(const AudioTXConfigMessage& message) {
+	fm_delta = message.deviation_hz * (0xFFFFFFULL / baseband_fs);
+	
+	tone_gen.configure(message.tone_key_delta, message.tone_key_mix_weight);
+}
+
+void AudioTXProcessor::samplerate_config(const SamplerateConfigMessage& message) {
+	baseband_fs = message.sample_rate;
+	baseband_thread.set_sampling_rate(baseband_fs);
+	spectrum_interval_samples = baseband_fs / 20;
+}
+
+void AudioTXProcessor::replay_config(const ReplayConfigMessage& message) {
+	if( message.config ) {
+		
+		stream = std::make_unique<StreamOutput>(message.config);
+		
+		// Tell application that the buffers and FIFO pointers are ready, prefill
+		shared_memory.application_queue.push(sig_message);
+	} else {
+		stream.reset();
+	}
+}
+
 int main() {
 	EventDispatcher event_dispatcher { std::make_unique<AudioTXProcessor>() };
 	event_dispatcher.run();
diff --git a/firmware/baseband/proc_audiotx.hpp b/firmware/baseband/proc_audiotx.hpp
index 711b63782abf96b6517221420bd24c2153311b66..b15f8524c96a3dc68f093fe9d1d9cb9b2469ea22 100644
--- a/firmware/baseband/proc_audiotx.hpp
+++ b/firmware/baseband/proc_audiotx.hpp
@@ -23,10 +23,10 @@
 #ifndef __PROC_AUDIOTX_H__
 #define __PROC_AUDIOTX_H__
 
-#include "fifo.hpp"
 #include "baseband_processor.hpp"
 #include "baseband_thread.hpp"
 #include "tone_gen.hpp"
+#include "stream_output.hpp"
 
 class AudioTXProcessor : public BasebandProcessor {
 public:
@@ -35,27 +35,29 @@ public:
 	void on_message(const Message* const msg) override;
 
 private:
-	static constexpr size_t baseband_fs = 1536000U;
-	
-	bool configured = false;
+	size_t baseband_fs = 0;
 	
 	BasebandThread baseband_thread { baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Transmit };
 	
-	int8_t audio_fifo_data[2048] { };
-	FIFO<int8_t> audio_fifo = { audio_fifo_data, 11 };	// 43ms @ 48000Hz
+	std::array<uint8_t, 64> audio { };
+	const buffer_t<uint8_t> audio_buffer {
+		audio.data(),
+		audio.size(),
+		baseband_fs / 8
+	};
+	
+	std::unique_ptr<StreamOutput> stream { };
 	
 	ToneGen tone_gen { };
 	
 	uint32_t fm_delta { 0 };
-	uint32_t divider { };
-	uint32_t as { 0 };
 	uint32_t phase { 0 }, sphase { 0 };
 	int8_t out_sample { };
-	int32_t sample { 0 }, delta { };
-	
+	int32_t sample { 0 }, audio_sample { 0 }, delta { };
 	int8_t re { 0 }, im { 0 };
 	
-	bool asked { false };
+	size_t spectrum_interval_samples = 0;
+	size_t spectrum_samples = 0;
 
 	//int16_t audio_data[64];
 	/*const buffer_s16_t preview_audio_buffer {
@@ -63,6 +65,14 @@ private:
 		sizeof(int16_t)*64
 	};*/
 	
+	bool configured { false };
+	uint32_t bytes_read { 0 };
+	
+	void samplerate_config(const SamplerateConfigMessage& message);
+	void audio_config(const AudioTXConfigMessage& message);
+	void replay_config(const ReplayConfigMessage& message);
+	
+	TXProgressMessage txprogress_message { };
 	RequestSignalMessage sig_message { RequestSignalMessage::Signal::FillRequest };
 };
 
diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp
index be1d7e21294393bbbc40e95901aac6edfe18a3b2..37ba5af3f7942f7e9e83fca8b299ecc20dc3d552 100644
--- a/firmware/common/message.hpp
+++ b/firmware/common/message.hpp
@@ -96,16 +96,16 @@ public:
 		SigGenConfig = 43,
 		SigGenTone = 44,
 		
-		POCSAGPacket = 50,
-		ADSBFrame = 51,
-		AFSKData = 52,
-		TestAppPacket = 53,
+		POCSAGPacket = 45,
+		ADSBFrame = 46,
+		AFSKData = 47,
+		TestAppPacket = 48,
 		
-		RequestSignal = 60,
-		FIFOData = 61,
+		RequestSignal = 49,
+		FIFOData = 50,
 		
-		AudioLevelReport = 70,
-		CodedSquelch = 71,
+		AudioLevelReport = 51,
+		CodedSquelch = 52,
 		MAX
 	};
 
diff --git a/firmware/common/morse.cpp b/firmware/common/morse.cpp
index 6b7c14af954239e94ab9b7fa710165786d53a61a..bc8ae318e72f5d86d2d2ff6aa58fcfe010daa7d5 100644
--- a/firmware/common/morse.cpp
+++ b/firmware/common/morse.cpp
@@ -78,7 +78,7 @@ size_t morse_encode(std::string& message, const uint32_t time_unit_ms,
 	// Setup tone "symbols"
 	for (c = 0; c < 5; c++) {
 		if (c < 2)
-			delta = TONES_F2D(tone);	// Dot and dash
+			delta = TONES_F2D(tone, TONES_SAMPLERATE);	// Dot and dash
 		else
 			delta = 0;					// Pause
 		
diff --git a/firmware/common/morse.hpp b/firmware/common/morse.hpp
index 00384a08cbf4edb8845f03d1616fa8df2a62cf5a..7522a097c62e82bf7fd88139066584985a41ae74 100644
--- a/firmware/common/morse.hpp
+++ b/firmware/common/morse.hpp
@@ -45,7 +45,7 @@ const uint32_t morse_symbols[5] = {
 size_t morse_encode(std::string& message, const uint32_t time_unit_ms,
 	const uint32_t tone, uint32_t * const time_units);
 
-const std::string foxhunt_codes[11] = {
+constexpr char foxhunt_codes[11][4] = {
 	{ "MOE" },	// -----.
 	{ "MOI" },	// -----..
 	{ "MOS" },	// -----...
@@ -60,7 +60,7 @@ const std::string foxhunt_codes[11] = {
 };
 
 // 0=dot 1=dash
-const uint16_t morse_ITU[63] = {
+constexpr uint16_t morse_ITU[63] = {
 						//    Code    Size
 	0b1010110000000110,	// !: 101011- 110
 	0b0100100000000110,	// ": 010010- 110
@@ -127,7 +127,7 @@ const uint16_t morse_ITU[63] = {
 	0b0011010000000110	// _: 001101- 110
 };
 
-const uint16_t prosigns[12] = {
+constexpr uint16_t prosigns[12] = {
 						//    	  Code		Size
 	0b0001110000001001,	// <SOS>: 000111000	1001
 	0b0101000000000100,	// <AA>:  0101----- 0100
diff --git a/firmware/common/sstv.hpp b/firmware/common/sstv.hpp
index c4d72ec04156770132c53f65cb9b581f277cb519..6ab35532c4e831a1e5810bf9cf959cae6ae44c8b 100644
--- a/firmware/common/sstv.hpp
+++ b/firmware/common/sstv.hpp
@@ -44,7 +44,7 @@ enum sstv_color_seq {
 #define SSTV_MODES_NB 6
 
 // From http://www.graphics.stanford.edu/~seander/bithacks.html, nice !
-inline uint8_t sstv_parity(uint8_t code) {
+constexpr inline uint8_t sstv_parity(uint8_t code) {
 	uint8_t out = code;
 	out ^= code >> 4;
 	out &= 0x0F;
@@ -63,7 +63,7 @@ struct sstv_scanline {
 };
 
 struct sstv_mode {
-	std::string name;
+	char name[16];
 	uint8_t vis_code;
 	bool color;					// Unused for now
 	sstv_color_seq color_sequence;
@@ -78,7 +78,7 @@ struct sstv_mode {
 	//std::pair<uint16_t, uint16_t> luma_range;
 };
 
-const sstv_mode sstv_modes[SSTV_MODES_NB] = {
+constexpr sstv_mode sstv_modes[SSTV_MODES_NB] = {
 	{ "Scottie 1", 	sstv_parity(60),	true, SSTV_COLOR_GBR, 320, 256, SSTV_MS2S(0.4320),	true, 2, true, SSTV_MS2S(9), SSTV_MS2S(1.5) },
 	{ "Scottie 2", 	sstv_parity(56),	true, SSTV_COLOR_GBR, 320, 256, SSTV_MS2S(0.2752),	true, 2, true, SSTV_MS2S(9), SSTV_MS2S(1.5) },
 	{ "Scottie DX",	sstv_parity(76),	true, SSTV_COLOR_GBR, 320, 256, SSTV_MS2S(1.08), 	true, 2, true, SSTV_MS2S(9), SSTV_MS2S(1.5) },
diff --git a/firmware/common/tonesets.hpp b/firmware/common/tonesets.hpp
index d1d9851f49ba7b4a1e5d435dcdf734e5d10ac741..876ef7e6ea72d7e70d72f4901dfa6cf2aa6d92cd 100644
--- a/firmware/common/tonesets.hpp
+++ b/firmware/common/tonesets.hpp
@@ -26,38 +26,37 @@
 #include <memory>
 
 #define TONES_SAMPLERATE 1536000
-#define TONES_DELTA_COEF ((1ULL << 32) / TONES_SAMPLERATE)
-
-#define TONES_F2D(f) (uint32_t)(f * TONES_DELTA_COEF)
+#define TONES_DELTA_COEF(sr) ((1ULL << 32) / sr)
+#define TONES_F2D(f, sr) (uint32_t)(f * TONES_DELTA_COEF(sr))
 
 #define BEEP_TONES_NB 6
 
-#define DTMF_C0	TONES_F2D(1209)
-#define DTMF_C1	TONES_F2D(1336)
-#define DTMF_C2	TONES_F2D(1477)
-#define DTMF_C3	TONES_F2D(1633)
-#define DTMF_R0	TONES_F2D(697)
-#define DTMF_R1	TONES_F2D(770)
-#define DTMF_R2	TONES_F2D(852)
-#define DTMF_R3	TONES_F2D(941)
+#define DTMF_C0	TONES_F2D(1209, TONES_SAMPLERATE)
+#define DTMF_C1	TONES_F2D(1336, TONES_SAMPLERATE)
+#define DTMF_C2	TONES_F2D(1477, TONES_SAMPLERATE)
+#define DTMF_C3	TONES_F2D(1633, TONES_SAMPLERATE)
+#define DTMF_R0	TONES_F2D(697, TONES_SAMPLERATE)
+#define DTMF_R1	TONES_F2D(770, TONES_SAMPLERATE)
+#define DTMF_R2	TONES_F2D(852, TONES_SAMPLERATE)
+#define DTMF_R3	TONES_F2D(941, TONES_SAMPLERATE)
 
 const std::array<uint32_t, 16> ccir_deltas = {
-	TONES_F2D(1981),
-	TONES_F2D(1124),
-	TONES_F2D(1197),
-	TONES_F2D(1275),
-	TONES_F2D(1358),
-	TONES_F2D(1446),
-	TONES_F2D(1540),
-	TONES_F2D(1640),
-	TONES_F2D(1747),
-	TONES_F2D(1860),
-	TONES_F2D(2400),
-	TONES_F2D(930),
-	TONES_F2D(2247),
-	TONES_F2D(991),
-	TONES_F2D(2110),
-	TONES_F2D(1055)
+	TONES_F2D(1981, TONES_SAMPLERATE),
+	TONES_F2D(1124, TONES_SAMPLERATE),
+	TONES_F2D(1197, TONES_SAMPLERATE),
+	TONES_F2D(1275, TONES_SAMPLERATE),
+	TONES_F2D(1358, TONES_SAMPLERATE),
+	TONES_F2D(1446, TONES_SAMPLERATE),
+	TONES_F2D(1540, TONES_SAMPLERATE),
+	TONES_F2D(1640, TONES_SAMPLERATE),
+	TONES_F2D(1747, TONES_SAMPLERATE),
+	TONES_F2D(1860, TONES_SAMPLERATE),
+	TONES_F2D(2400, TONES_SAMPLERATE),
+	TONES_F2D(930, TONES_SAMPLERATE),
+	TONES_F2D(2247, TONES_SAMPLERATE),
+	TONES_F2D(991, TONES_SAMPLERATE),
+	TONES_F2D(2110, TONES_SAMPLERATE),
+	TONES_F2D(1055, TONES_SAMPLERATE)
 };
 
 // 0123456789ABCD#*
@@ -81,50 +80,50 @@ const uint32_t dtmf_deltas[16][2] = {
 };
 
 const std::array<uint32_t, 16> eia_deltas = {
-	TONES_F2D(600),
-	TONES_F2D(741),
-	TONES_F2D(882),
-	TONES_F2D(1023),
-	TONES_F2D(1164),
-	TONES_F2D(1305),
-	TONES_F2D(1446),
-	TONES_F2D(1587),
-	TONES_F2D(1728),
-	TONES_F2D(1869),
-	TONES_F2D(2151),
-	TONES_F2D(2433),
-	TONES_F2D(2010),
-	TONES_F2D(2292),
-	TONES_F2D(459),
-	TONES_F2D(1091)
+	TONES_F2D(600, TONES_SAMPLERATE),
+	TONES_F2D(741, TONES_SAMPLERATE),
+	TONES_F2D(882, TONES_SAMPLERATE),
+	TONES_F2D(1023, TONES_SAMPLERATE),
+	TONES_F2D(1164, TONES_SAMPLERATE),
+	TONES_F2D(1305, TONES_SAMPLERATE),
+	TONES_F2D(1446, TONES_SAMPLERATE),
+	TONES_F2D(1587, TONES_SAMPLERATE),
+	TONES_F2D(1728, TONES_SAMPLERATE),
+	TONES_F2D(1869, TONES_SAMPLERATE),
+	TONES_F2D(2151, TONES_SAMPLERATE),
+	TONES_F2D(2433, TONES_SAMPLERATE),
+	TONES_F2D(2010, TONES_SAMPLERATE),
+	TONES_F2D(2292, TONES_SAMPLERATE),
+	TONES_F2D(459, TONES_SAMPLERATE),
+	TONES_F2D(1091, TONES_SAMPLERATE)
 };
 
 const std::array<uint32_t, 16> zvei_deltas = {
-	TONES_F2D(2400),
-	TONES_F2D(1060),
-	TONES_F2D(1160),
-	TONES_F2D(1270),
-	TONES_F2D(1400),
-	TONES_F2D(1530),
-	TONES_F2D(1670),
-	TONES_F2D(1830),
-	TONES_F2D(2000),
-	TONES_F2D(2200),
-	TONES_F2D(2800),
-	TONES_F2D(810),
-	TONES_F2D(970),
-	TONES_F2D(885),
-	TONES_F2D(2600),
-	TONES_F2D(680)
+	TONES_F2D(2400, TONES_SAMPLERATE),
+	TONES_F2D(1060, TONES_SAMPLERATE),
+	TONES_F2D(1160, TONES_SAMPLERATE),
+	TONES_F2D(1270, TONES_SAMPLERATE),
+	TONES_F2D(1400, TONES_SAMPLERATE),
+	TONES_F2D(1530, TONES_SAMPLERATE),
+	TONES_F2D(1670, TONES_SAMPLERATE),
+	TONES_F2D(1830, TONES_SAMPLERATE),
+	TONES_F2D(2000, TONES_SAMPLERATE),
+	TONES_F2D(2200, TONES_SAMPLERATE),
+	TONES_F2D(2800, TONES_SAMPLERATE),
+	TONES_F2D(810, TONES_SAMPLERATE),
+	TONES_F2D(970, TONES_SAMPLERATE),
+	TONES_F2D(885, TONES_SAMPLERATE),
+	TONES_F2D(2600, TONES_SAMPLERATE),
+	TONES_F2D(680, TONES_SAMPLERATE)
 };
 
 const uint32_t beep_deltas[BEEP_TONES_NB] = {
-	TONES_F2D(1475),
-	TONES_F2D(740),
-	TONES_F2D(587),
-	TONES_F2D(1109),
-	TONES_F2D(831),
-	TONES_F2D(740)
+	TONES_F2D(1475, TONES_SAMPLERATE),
+	TONES_F2D(740, TONES_SAMPLERATE),
+	TONES_F2D(587, TONES_SAMPLERATE),
+	TONES_F2D(1109, TONES_SAMPLERATE),
+	TONES_F2D(831, TONES_SAMPLERATE),
+	TONES_F2D(740, TONES_SAMPLERATE)
 };
 
 #endif/*__TONESETS_H__*/
diff --git a/firmware/tools/world_map.py b/firmware/tools/world_map.py
new file mode 100644
index 0000000000000000000000000000000000000000..9066664db10be9c20644480f8df557e17c8e575f
--- /dev/null
+++ b/firmware/tools/world_map.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+
+# Copyright (C) 2017 Furrtek
+#
+# 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.
+#
+
+from __future__ import print_function
+import sys
+import struct
+from PIL import Image
+
+outfile = open('../../sdcard/world_map.bin', 'wb')
+
+im = Image.open("../../sdcard/world_map.jpg")
+pix = im.load()
+
+outfile.write(struct.pack('H', im.size[0]))
+outfile.write(struct.pack('H', im.size[1]))
+
+for y in range (0, im.size[1]):
+	line = ''
+	for x in range (0, im.size[0]):
+		# RRRRRGGGGGGBBBBB
+		#pixel_lcd = (pix[x, y][0] >> 3) << 11
+		#pixel_lcd |= (pix[x, y][1] >> 2) << 5
+		#pixel_lcd |= (pix[x, y][2] >> 3)
+		#         RRRGGGBB to
+		# RRR00GGG000BB000
+		pixel_lcd = (pix[x, y][0] >> 5) << 5
+		pixel_lcd |= (pix[x, y][1] >> 5) << 2
+		pixel_lcd |= (pix[x, y][2] >> 6)
+		line += struct.pack('B', pixel_lcd)
+	outfile.write(line)
+	print(str(y) + '/' + str(im.size[1]) + '\r', end="")
diff --git a/sdcard/world_map.jpg b/sdcard/world_map.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..a57b8291683fe35a9058ebda1282d5634d90ff9a
Binary files /dev/null and b/sdcard/world_map.jpg differ