/*
* aptdec - A lightweight FOSS (NOAA) APT decoder
* Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2023 Xerbo (xerbo@protonmail.com)
*
* 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 of the License, 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. If not, see .
*/
#include
#include
#include
#ifndef _MSC_VER
#include
#else
#include
#endif
#include
#include
#include
#include
#include
#include
#include "argparse/argparse.h"
#include "pngio.h"
#include "util.h"
// Maximum height of an APT image that can be decoded
#define APTDEC_MAX_HEIGHT 3000
#define STRING_SPLIT(src, dst, delim, n) \
char *dst[n]; \
size_t dst##_len = 0; \
{char *token = strtok(src, delim); \
while (token != NULL && dst##_len < n) { \
dst[dst##_len++] = token; \
token = strtok(NULL, delim); \
}}
typedef struct {
char *type; // Output image type
char *effects; // Effects on the image
int satellite; // The satellite number
int realtime; // Realtime decoding
char *filename; // Output filename
char *lut; // Filename of lut
} options_t;
typedef struct {
SNDFILE *file;
SF_INFO info;
} SNDFILE_t;
// Function declarations
static int process_file(const char *path, options_t *opts);
static int freq_from_filename(const char *filename);
static int satid_from_freq(int freq);
static size_t callback(float *samples, size_t count, void *context);
static void write_line(writer_t *png, float *row);
int array_contains(char **array, char *value, size_t n);
static volatile int sigint_stop = 0;
void sigint_handler(int signum) {
(void)signum;
sigint_stop = 1;
}
// Basename is unsupported by MSVC
// This implementation is GNU style
#ifdef _MSC_VER
char *basename(const char *filename) {
char *p = strrchr(filename, '/');
return p ? p + 1 : (char *)filename;
}
#endif
int main(int argc, const char **argv) {
char version[128] = { 0 };
get_version(version);
printf("%s\n", version);
// clang-format off
options_t opts = {
.type = "raw",
.effects = "",
.satellite = 0,
.realtime = 0,
.filename = "",
.lut = "",
};
// clang-format on
static const char *const usages[] = {
"aptdec-cli [options] [[--] sources]",
"aptdec-cli [sources]",
NULL,
};
struct argparse_option options[] = {
OPT_HELP(),
OPT_GROUP("Image options"),
OPT_STRING('i', "image", &opts.type, "set output image type (see the README for a list)", NULL, 0, 0),
OPT_STRING('e', "effect", &opts.effects, "add an effect (see the README for a list)", NULL, 0, 0),
OPT_GROUP("Satellite options"),
OPT_INTEGER('s', "satellite", &opts.satellite, "satellite ID, must be either NORAD or 15/18/19", NULL, 0, 0),
OPT_GROUP("Paths"),
OPT_STRING('l', "lut", &opts.lut, "path to a LUT", NULL, 0, 0),
OPT_STRING('o', "output", &opts.filename, "path of output image", NULL, 0, 0),
OPT_GROUP("Misc"),
OPT_BOOLEAN('r', "realtime", &opts.realtime, "decode in realtime", NULL, 0, 0),
OPT_END(),
};
struct argparse argparse;
argparse_init(&argparse, options, usages, 0);
argparse_describe(&argparse,
"\nA lightweight FOSS NOAA APT satellite imagery decoder.",
"\nSee `README.md` for a full description of command line arguments and `LICENSE` for licensing conditions."
);
argc = argparse_parse(&argparse, argc, argv);
if (argc == 0) {
argparse_usage(&argparse);
}
if (argc > 1 && opts.realtime) {
error("Cannot use -r/--realtime with multiple input files");
}
// Actually decode the files
for (int i = 0; i < argc; i++) {
if (opts.satellite == 25338) opts.satellite = 15;
if (opts.satellite == 28654) opts.satellite = 18;
if (opts.satellite == 33591) opts.satellite = 19;
if (opts.satellite == 0) {
int freq = freq_from_filename(argv[i]);
if (freq == 0) {
opts.satellite = 19;
warning("Satellite not specified, defaulting to NOAA-19");
} else {
opts.satellite = satid_from_freq(freq);
printf("Satellite not specified, choosing to NOAA-%i based on filename\n", opts.satellite);
}
}
if (opts.satellite != 15 && opts.satellite != 18 && opts.satellite != 19) {
error("Invalid satellite ID");
}
process_file(argv[i], &opts);
}
return 0;
}
static int process_file(const char *path, options_t *opts) {
const char *path_basename = basename((char *)path);
const char *dot = strrchr(path_basename, '.');
char name[256] = { 0 };
if (dot == NULL) {
strncpy(name, path_basename, 255);
} else {
strncpy(name, path_basename, clamp_int(dot - path_basename, 0, 255));
}
// Set filename to time when reading from stdin
if (strcmp(name, "-") == 0) {
time_t t = time(NULL);
strcpy(name, ctime(&t));
}
writer_t *realtime_png;
if (opts->realtime) {
char filename[269] = { 0 };
sprintf(filename, "%s-decoding.png", name);
realtime_png = writer_init(filename, APTDEC_REGION_FULL, APTDEC_MAX_HEIGHT, PNG_COLOR_TYPE_GRAY, "Unknown");
// Capture Ctrl+C
signal(SIGINT, sigint_handler);
}
// Create a libsndfile instance
SNDFILE_t audioFile;
audioFile.file = sf_open(path, SFM_READ, &audioFile.info);
if (audioFile.file == NULL) {
error_noexit("Could not open file");
return 0;
}
printf("Input file: %s\n", path_basename);
printf("Input sample rate: %d\n", audioFile.info.samplerate);
// Create a libaptdec instances
aptdec_t *aptdec = aptdec_init(audioFile.info.samplerate);
if (aptdec == NULL) {
sf_close(audioFile.file);
error_noexit("Error initializing libaptdec, sample rate too high/low?");
return 0;
}
// Decode image
float *data = calloc(APTDEC_IMG_WIDTH * (APTDEC_MAX_HEIGHT+1), sizeof(float));
size_t rows;
for (rows = 0; rows < APTDEC_MAX_HEIGHT; rows++) {
float *row = &data[rows * APTDEC_IMG_WIDTH];
// Break the loop when there are no more samples or the process has been sent SIGINT
if (aptdec_getrow(aptdec, row, callback, &audioFile) == 0 || sigint_stop) {
break;
}
if (opts->realtime) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
write_line(realtime_png, row);
#pragma GCC diagnostic pop
}
fprintf(stderr, "Row: %zu/%zu\r", rows+1, audioFile.info.frames/audioFile.info.samplerate * 2);
fflush(stderr);
}
printf("\n");
// Close stream
sf_close(audioFile.file);
aptdec_free(aptdec);
if (opts->realtime) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
writer_free(realtime_png);
#pragma GCC diagnostic pop
char filename[269] = { 0 };
sprintf(filename, "%s-decoding.png", name);
remove(filename);
}
// Normalize
int error;
aptdec_image_t img = aptdec_normalize(data, rows, opts->satellite, &error);
free(data);
if (error) {
error_noexit("Normalization failed");
return 0;
}
// clang-format off
const char *channel_name[] = { "?", "1", "2", "3A", "4", "5", "3B" };
const char *channel_desc[] = {
"unknown",
"visible (0.58-0.68 um)",
"near-infrared (0.725-1.0 um)",
"near-infrared (1.58-1.64 um)",
"thermal-infrared (10.3-11.3 um)",
"thermal-infrared (11.5-12.5 um)",
"mid-infrared (3.55-3.93 um)"
};
printf("Channel A: %s - %s\n", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
printf("Channel B: %s - %s\n", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
// clang-format on
STRING_SPLIT(opts->type, images, ",", 10)
STRING_SPLIT(opts->effects, effects, ",", 10)
STRING_SPLIT(opts->filename, filenames, ",", 10)
for (size_t i = 0; i < effects_len; i++) {
if (strcmp(effects[i], "crop") == 0) {
aptdec_crop(&img);
} else if (strcmp(effects[i], "denoise") == 0) {
aptdec_denoise(&img, APTDEC_REGION_CHA);
aptdec_denoise(&img, APTDEC_REGION_CHB);
} else if (strcmp(effects[i], "flip") == 0) {
aptdec_flip(&img, APTDEC_REGION_CHA);
aptdec_flip(&img, APTDEC_REGION_CHB);
}
}
for (size_t i = 0; i < images_len; i++) {
const char *base;
if (i < filenames_len) {
base = filenames[i];
} else {
base = name;
}
if (strcmp(images[i], "thermal") == 0) {
if (img.ch[1] >= 4) {
char filename[269] = { 0 };
sprintf(filename, "%s-thermal.png", base);
char description[128] = { 0 };
sprintf(description, "Calibrated thermal image, channel %s - %s", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
// Perform visible calibration
aptdec_image_t _img = aptdec_image_clone(img);
aptdec_calibrate_thermal(&_img, APTDEC_REGION_CHA);
writer_t *writer = writer_init(filename, APTDEC_REGION_CHB, img.rows, PNG_COLOR_TYPE_RGB, description);
writer_write_image_gradient(writer, &_img, aptdec_temperature_gradient);
writer_free(writer);
free(_img.data);
} else {
error_noexit("Could not generate thermal image, no infrared channel");
}
} else if (strcmp(images[i], "visible") == 0) {
if (img.ch[0] <= 2) {
char filename[269] = { 0 };
sprintf(filename, "%s-visible.png", base);
char description[128] = { 0 };
sprintf(description, "Calibrated visible image, channel %s - %s", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
// Perform visible calibration
aptdec_image_t _img = aptdec_image_clone(img);
aptdec_calibrate_visible(&_img, APTDEC_REGION_CHA);
writer_t *writer = writer_init(filename, APTDEC_REGION_CHA, img.rows, PNG_COLOR_TYPE_GRAY, description);
writer_write_image(writer, &_img);
writer_free(writer);
free(_img.data);
} else {
error_noexit("Could not generate visible image, no visible channel");
}
}
}
for (size_t i = 0; i < effects_len; i++) {
if (strcmp(effects[i], "stretch") == 0) {
aptdec_stretch(&img, APTDEC_REGION_CHA);
aptdec_stretch(&img, APTDEC_REGION_CHB);
} else if (strcmp(effects[i], "equalize") == 0) {
aptdec_equalize(&img, APTDEC_REGION_CHA);
aptdec_equalize(&img, APTDEC_REGION_CHB);
}
}
for (size_t i = 0; i < images_len; i++) {
const char *base;
if (i < filenames_len) {
base = filenames[i];
} else {
base = name;
}
if (strcmp(images[i], "raw") == 0) {
char filename[269] = { 0 };
sprintf(filename, "%s-raw.png", base);
char description[128] = { 0 };
sprintf(description,
"Raw image, channel %s - %s / %s - %s",
channel_name[img.ch[0]],
channel_desc[img.ch[0]],
channel_name[img.ch[1]],
channel_desc[img.ch[1]]
);
writer_t *writer;
if (array_contains(effects, "strip", effects_len)) {
writer = writer_init(filename, (aptdec_region_t){0, APTDEC_CH_WIDTH*2}, img.rows, PNG_COLOR_TYPE_GRAY, description);
aptdec_image_t _img = aptdec_image_clone(img);
aptdec_strip(&_img);
writer_write_image(writer, &_img);
free(_img.data);
} else {
writer = writer_init(filename, APTDEC_REGION_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
writer_write_image(writer, &img);
}
writer_free(writer);
} else if (strcmp(images[i], "lut") == 0) {
if (opts->lut != NULL && opts->lut[0] != '\0') {
char filename[269] = { 0 };
sprintf(filename, "%s-lut.png", base);
char description[128] = { 0 };
sprintf(description,
"LUT image, channel %s - %s / %s - %s",
channel_name[img.ch[0]],
channel_desc[img.ch[0]],
channel_name[img.ch[1]],
channel_desc[img.ch[1]]
);
png_colorp lut = calloc(256*256, sizeof(png_color));
if (read_lut(opts->lut, lut)) {
writer_t *writer = writer_init(filename, APTDEC_REGION_CHA, img.rows, PNG_COLOR_TYPE_RGB, description);
writer_write_image_lut(writer, &img, lut);
writer_free(writer);
}
free(lut);
} else {
warning("Cannot create LUT image, missing -l/--lut");
}
} else if (strcmp(images[i], "a") == 0) {
char filename[269] = { 0 };
sprintf(filename, "%s-a.png", base);
char description[128] = { 0 };
sprintf(description, "Channel A: %s - %s", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
writer_t *writer;
if (array_contains(effects, "strip", effects_len)) {
writer = writer_init(filename, (aptdec_region_t){0, APTDEC_CH_WIDTH}, img.rows, PNG_COLOR_TYPE_GRAY, description);
aptdec_image_t _img = aptdec_image_clone(img);
aptdec_strip(&_img);
writer_write_image(writer, &_img);
free(_img.data);
} else {
writer = writer_init(filename, APTDEC_REGION_CHA_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
writer_write_image(writer, &img);
}
writer_free(writer);
} else if (strcmp(images[i], "b") == 0) {
char filename[269] = { 0 };
sprintf(filename, "%s-b.png", base);
char description[128] = { 0 };
sprintf(description, "Channel B: %s - %s", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
writer_t *writer;
if (array_contains(effects, "strip", effects_len)) {
writer = writer_init(filename, (aptdec_region_t){APTDEC_CH_WIDTH, APTDEC_CH_WIDTH}, img.rows, PNG_COLOR_TYPE_GRAY, description);
aptdec_image_t _img = aptdec_image_clone(img);
aptdec_strip(&_img);
writer_write_image(writer, &_img);
free(_img.data);
} else {
writer = writer_init(filename, APTDEC_REGION_CHB_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
writer_write_image(writer, &img);
}
writer_free(writer);
}
}
free(img.data);
return 1;
}
static int freq_from_filename(const char *filename) {
char frequency_text[10] = {'\0'};
if (strlen(filename) >= 30+4 && strncmp(filename, "gqrx_", 5) == 0) {
memcpy(frequency_text, &filename[21], 9);
return atoi(frequency_text);
} else if (strlen(filename) == 40+4 && strncmp(filename, "SDRSharp_", 9) == 0) {
memcpy(frequency_text, &filename[26], 9);
return atoi(frequency_text);
} else if (strlen(filename) == 37+4 && strncmp(filename, "audio_", 6) == 0) {
memcpy(frequency_text, &filename[6], 9);
return atoi(frequency_text);
}
return 0;
}
static int satid_from_freq(int freq) {
int differences[3] = {
abs(freq - 137620000), // NOAA-15
abs(freq - 137912500), // NOAA-18
abs(freq - 137100000) // NOAA-19
};
int best = 0;
for (size_t i = 0; i < 3; i++) {
if (differences[i] < differences[best]) {
best = i;
}
}
const int lut[3] = {15, 18, 19};
return lut[best];
}
// Read samples from a SNDFILE_t instance (passed through context)
static size_t callback(float *samples, size_t count, void *context) {
SNDFILE_t *file = (SNDFILE_t *)context;
switch (file->info.channels) {
case 1:
return sf_read_float(file->file, samples, count);
case 2: {
float _samples[APTDEC_BUFFER_SIZE * 2];
size_t read = sf_read_float(file->file, _samples, count * 2);
for (size_t i = 0; i < count; i++) {
// Average of left and right
samples[i] = (_samples[i*2] + _samples[i*2 + 1]) / 2.0f;
}
return read / 2;
}
default:
error_noexit("Only mono and stereo audio files are supported\n");
return 0;
}
}
// Write a line with very basic equalization
static void write_line(writer_t *png, float *row) {
float min = FLT_MAX;
float max = FLT_MIN;
for (int i = 0; i < APTDEC_IMG_WIDTH; i++) {
if (row[i] < min) min = row[i];
if (row[i] > max) max = row[i];
}
png_byte pixels[APTDEC_IMG_WIDTH];
for (int i = 0; i < APTDEC_IMG_WIDTH; i++) {
pixels[i] = clamp_int(roundf((row[i]-min) / (max-min) * 255.0f), 0, 255);
}
png_write_row(png->png, pixels);
}
int array_contains(char **array, char *value, size_t n) {
for (size_t i = 0; i < n; i++) {
if (strcmp(array[i], value) == 0) {
return 1;
}
}
return 0;
}