You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

529 lines
18 KiB

  1. /*
  2. * aptdec - A lightweight FOSS (NOAA) APT decoder
  3. * Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2023 Xerbo (xerbo@protonmail.com)
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. #include <stdio.h>
  19. #include <stdlib.h>
  20. #include <string.h>
  21. #ifndef _MSC_VER
  22. #include <libgen.h>
  23. #else
  24. #include <windows.h>
  25. #endif
  26. #include <math.h>
  27. #include <sndfile.h>
  28. #include <time.h>
  29. #include <signal.h>
  30. #include <float.h>
  31. #include <aptdec.h>
  32. #include "argparse/argparse.h"
  33. #include "pngio.h"
  34. #include "util.h"
  35. // Maximum height of an APT image that can be decoded
  36. #define APTDEC_MAX_HEIGHT 3000
  37. #define STRING_SPLIT(src, dst, delim, n) \
  38. char *dst[n]; \
  39. size_t dst##_len = 0; \
  40. {char *token = strtok(src, delim); \
  41. while (token != NULL && dst##_len < n) { \
  42. dst[dst##_len++] = token; \
  43. token = strtok(NULL, delim); \
  44. }}
  45. typedef struct {
  46. char *type; // Output image type
  47. char *effects; // Effects on the image
  48. int satellite; // The satellite number
  49. int realtime; // Realtime decoding
  50. char *filename; // Output filename
  51. char *lut; // Filename of lut
  52. } options_t;
  53. typedef struct {
  54. SNDFILE *file;
  55. SF_INFO info;
  56. } SNDFILE_t;
  57. // Function declarations
  58. static int process_file(const char *path, options_t *opts);
  59. static int freq_from_filename(const char *filename);
  60. static int satid_from_freq(int freq);
  61. static size_t callback(float *samples, size_t count, void *context);
  62. static void write_line(writer_t *png, float *row);
  63. int array_contains(char **array, char *value, size_t n);
  64. static volatile int sigint_stop = 0;
  65. void sigint_handler(int signum) {
  66. (void)signum;
  67. sigint_stop = 1;
  68. }
  69. // Basename is unsupported by MSVC
  70. // This implementation is GNU style
  71. #ifdef _MSC_VER
  72. char *basename(const char *filename) {
  73. char *p = strrchr(filename, '/');
  74. return p ? p + 1 : (char *)filename;
  75. }
  76. #endif
  77. int main(int argc, const char **argv) {
  78. char version[128] = { 0 };
  79. get_version(version);
  80. printf("%s\n", version);
  81. // clang-format off
  82. options_t opts = {
  83. .type = "raw",
  84. .effects = "",
  85. .satellite = 0,
  86. .realtime = 0,
  87. .filename = "",
  88. .lut = "",
  89. };
  90. // clang-format on
  91. static const char *const usages[] = {
  92. "aptdec-cli [options] [[--] sources]",
  93. "aptdec-cli [sources]",
  94. NULL,
  95. };
  96. struct argparse_option options[] = {
  97. OPT_HELP(),
  98. OPT_GROUP("Image options"),
  99. OPT_STRING('i', "image", &opts.type, "set output image type (see the README for a list)", NULL, 0, 0),
  100. OPT_STRING('e', "effect", &opts.effects, "add an effect (see the README for a list)", NULL, 0, 0),
  101. OPT_GROUP("Satellite options"),
  102. OPT_INTEGER('s', "satellite", &opts.satellite, "satellite ID, must be either NORAD or 15/18/19", NULL, 0, 0),
  103. OPT_GROUP("Paths"),
  104. OPT_STRING('l', "lut", &opts.lut, "path to a LUT", NULL, 0, 0),
  105. OPT_STRING('o', "output", &opts.filename, "path of output image", NULL, 0, 0),
  106. OPT_GROUP("Misc"),
  107. OPT_BOOLEAN('r', "realtime", &opts.realtime, "decode in realtime", NULL, 0, 0),
  108. OPT_END(),
  109. };
  110. struct argparse argparse;
  111. argparse_init(&argparse, options, usages, 0);
  112. argparse_describe(&argparse,
  113. "\nA lightweight FOSS NOAA APT satellite imagery decoder.",
  114. "\nSee `README.md` for a full description of command line arguments and `LICENSE` for licensing conditions."
  115. );
  116. argc = argparse_parse(&argparse, argc, argv);
  117. if (argc == 0) {
  118. argparse_usage(&argparse);
  119. }
  120. if (argc > 1 && opts.realtime) {
  121. error("Cannot use -r/--realtime with multiple input files");
  122. }
  123. // Actually decode the files
  124. for (int i = 0; i < argc; i++) {
  125. if (opts.satellite == 25338) opts.satellite = 15;
  126. if (opts.satellite == 28654) opts.satellite = 18;
  127. if (opts.satellite == 33591) opts.satellite = 19;
  128. if (opts.satellite == 0) {
  129. int freq = freq_from_filename(argv[i]);
  130. if (freq == 0) {
  131. opts.satellite = 19;
  132. warning("Satellite not specified, defaulting to NOAA-19");
  133. } else {
  134. opts.satellite = satid_from_freq(freq);
  135. printf("Satellite not specified, choosing to NOAA-%i based on filename\n", opts.satellite);
  136. }
  137. }
  138. if (opts.satellite != 15 && opts.satellite != 18 && opts.satellite != 19) {
  139. error("Invalid satellite ID");
  140. }
  141. process_file(argv[i], &opts);
  142. }
  143. return 0;
  144. }
  145. static int process_file(const char *path, options_t *opts) {
  146. const char *path_basename = basename((char *)path);
  147. const char *dot = strrchr(path_basename, '.');
  148. char name[256] = { 0 };
  149. if (dot == NULL) {
  150. strncpy(name, path_basename, 255);
  151. } else {
  152. strncpy(name, path_basename, clamp_int(dot - path_basename, 0, 255));
  153. }
  154. // Set filename to time when reading from stdin
  155. if (strcmp(name, "-") == 0) {
  156. time_t t = time(NULL);
  157. strcpy(name, ctime(&t));
  158. }
  159. writer_t *realtime_png;
  160. if (opts->realtime) {
  161. char filename[269] = { 0 };
  162. sprintf(filename, "%s-decoding.png", name);
  163. realtime_png = writer_init(filename, APTDEC_REGION_FULL, APTDEC_MAX_HEIGHT, PNG_COLOR_TYPE_GRAY, "Unknown");
  164. // Capture Ctrl+C
  165. signal(SIGINT, sigint_handler);
  166. }
  167. // Create a libsndfile instance
  168. SNDFILE_t audioFile;
  169. audioFile.file = sf_open(path, SFM_READ, &audioFile.info);
  170. if (audioFile.file == NULL) {
  171. error_noexit("Could not open file");
  172. return 0;
  173. }
  174. printf("Input file: %s\n", path_basename);
  175. printf("Input sample rate: %d\n", audioFile.info.samplerate);
  176. // Create a libaptdec instances
  177. aptdec_t *aptdec = aptdec_init(audioFile.info.samplerate);
  178. if (aptdec == NULL) {
  179. sf_close(audioFile.file);
  180. error_noexit("Error initializing libaptdec, sample rate too high/low?");
  181. return 0;
  182. }
  183. // Decode image
  184. float *data = calloc(APTDEC_IMG_WIDTH * (APTDEC_MAX_HEIGHT+1), sizeof(float));
  185. size_t rows;
  186. for (rows = 0; rows < APTDEC_MAX_HEIGHT; rows++) {
  187. float *row = &data[rows * APTDEC_IMG_WIDTH];
  188. // Break the loop when there are no more samples or the process has been sent SIGINT
  189. if (aptdec_getrow(aptdec, row, callback, &audioFile) == 0 || sigint_stop) {
  190. break;
  191. }
  192. if (opts->realtime) {
  193. #pragma GCC diagnostic push
  194. #pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
  195. write_line(realtime_png, row);
  196. #pragma GCC diagnostic pop
  197. }
  198. fprintf(stderr, "Row: %zu/%zu\r", rows+1, audioFile.info.frames/audioFile.info.samplerate * 2);
  199. fflush(stderr);
  200. }
  201. printf("\n");
  202. // Close stream
  203. sf_close(audioFile.file);
  204. aptdec_free(aptdec);
  205. if (opts->realtime) {
  206. #pragma GCC diagnostic push
  207. #pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
  208. writer_free(realtime_png);
  209. #pragma GCC diagnostic pop
  210. char filename[269] = { 0 };
  211. sprintf(filename, "%s-decoding.png", name);
  212. remove(filename);
  213. }
  214. // Normalize
  215. int error;
  216. aptdec_image_t img = aptdec_normalize(data, rows, opts->satellite, &error);
  217. free(data);
  218. if (error) {
  219. error_noexit("Normalization failed");
  220. return 0;
  221. }
  222. // clang-format off
  223. const char *channel_name[] = { "?", "1", "2", "3A", "4", "5", "3B" };
  224. const char *channel_desc[] = {
  225. "unknown",
  226. "visible (0.58-0.68 um)",
  227. "near-infrared (0.725-1.0 um)",
  228. "near-infrared (1.58-1.64 um)",
  229. "thermal-infrared (10.3-11.3 um)",
  230. "thermal-infrared (11.5-12.5 um)",
  231. "mid-infrared (3.55-3.93 um)"
  232. };
  233. printf("Channel A: %s - %s\n", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
  234. printf("Channel B: %s - %s\n", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
  235. // clang-format on
  236. STRING_SPLIT(opts->type, images, ",", 10)
  237. STRING_SPLIT(opts->effects, effects, ",", 10)
  238. STRING_SPLIT(opts->filename, filenames, ",", 10)
  239. for (size_t i = 0; i < effects_len; i++) {
  240. if (strcmp(effects[i], "crop") == 0) {
  241. aptdec_crop(&img);
  242. } else if (strcmp(effects[i], "denoise") == 0) {
  243. aptdec_denoise(&img, APTDEC_REGION_CHA);
  244. aptdec_denoise(&img, APTDEC_REGION_CHB);
  245. } else if (strcmp(effects[i], "flip") == 0) {
  246. aptdec_flip(&img, APTDEC_REGION_CHA);
  247. aptdec_flip(&img, APTDEC_REGION_CHB);
  248. }
  249. }
  250. for (size_t i = 0; i < images_len; i++) {
  251. const char *base;
  252. if (i < filenames_len) {
  253. base = filenames[i];
  254. } else {
  255. base = name;
  256. }
  257. if (strcmp(images[i], "thermal") == 0) {
  258. if (img.ch[1] >= 4) {
  259. char filename[269] = { 0 };
  260. sprintf(filename, "%s-thermal.png", base);
  261. char description[128] = { 0 };
  262. sprintf(description, "Calibrated thermal image, channel %s - %s", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
  263. // Perform visible calibration
  264. aptdec_image_t _img = aptdec_image_clone(img);
  265. aptdec_calibrate_thermal(&_img, APTDEC_REGION_CHA);
  266. writer_t *writer = writer_init(filename, APTDEC_REGION_CHB, img.rows, PNG_COLOR_TYPE_RGB, description);
  267. writer_write_image_gradient(writer, &_img, aptdec_temperature_gradient);
  268. writer_free(writer);
  269. free(_img.data);
  270. } else {
  271. error_noexit("Could not generate thermal image, no infrared channel");
  272. }
  273. } else if (strcmp(images[i], "visible") == 0) {
  274. if (img.ch[0] <= 2) {
  275. char filename[269] = { 0 };
  276. sprintf(filename, "%s-visible.png", base);
  277. char description[128] = { 0 };
  278. sprintf(description, "Calibrated visible image, channel %s - %s", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
  279. // Perform visible calibration
  280. aptdec_image_t _img = aptdec_image_clone(img);
  281. aptdec_calibrate_visible(&_img, APTDEC_REGION_CHA);
  282. writer_t *writer = writer_init(filename, APTDEC_REGION_CHA, img.rows, PNG_COLOR_TYPE_GRAY, description);
  283. writer_write_image(writer, &_img);
  284. writer_free(writer);
  285. free(_img.data);
  286. } else {
  287. error_noexit("Could not generate visible image, no visible channel");
  288. }
  289. }
  290. }
  291. for (size_t i = 0; i < effects_len; i++) {
  292. if (strcmp(effects[i], "stretch") == 0) {
  293. aptdec_stretch(&img, APTDEC_REGION_CHA);
  294. aptdec_stretch(&img, APTDEC_REGION_CHB);
  295. } else if (strcmp(effects[i], "equalize") == 0) {
  296. aptdec_equalize(&img, APTDEC_REGION_CHA);
  297. aptdec_equalize(&img, APTDEC_REGION_CHB);
  298. }
  299. }
  300. for (size_t i = 0; i < images_len; i++) {
  301. const char *base;
  302. if (i < filenames_len) {
  303. base = filenames[i];
  304. } else {
  305. base = name;
  306. }
  307. if (strcmp(images[i], "raw") == 0) {
  308. char filename[269] = { 0 };
  309. sprintf(filename, "%s-raw.png", base);
  310. char description[128] = { 0 };
  311. sprintf(description,
  312. "Raw image, channel %s - %s / %s - %s",
  313. channel_name[img.ch[0]],
  314. channel_desc[img.ch[0]],
  315. channel_name[img.ch[1]],
  316. channel_desc[img.ch[1]]
  317. );
  318. writer_t *writer;
  319. if (array_contains(effects, "strip", effects_len)) {
  320. writer = writer_init(filename, (aptdec_region_t){0, APTDEC_CH_WIDTH*2}, img.rows, PNG_COLOR_TYPE_GRAY, description);
  321. aptdec_image_t _img = aptdec_image_clone(img);
  322. aptdec_strip(&_img);
  323. writer_write_image(writer, &_img);
  324. free(_img.data);
  325. } else {
  326. writer = writer_init(filename, APTDEC_REGION_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
  327. writer_write_image(writer, &img);
  328. }
  329. writer_free(writer);
  330. } else if (strcmp(images[i], "lut") == 0) {
  331. if (opts->lut != NULL && opts->lut[0] != '\0') {
  332. char filename[269] = { 0 };
  333. sprintf(filename, "%s-lut.png", base);
  334. char description[128] = { 0 };
  335. sprintf(description,
  336. "LUT image, channel %s - %s / %s - %s",
  337. channel_name[img.ch[0]],
  338. channel_desc[img.ch[0]],
  339. channel_name[img.ch[1]],
  340. channel_desc[img.ch[1]]
  341. );
  342. png_colorp lut = calloc(256*256, sizeof(png_color));
  343. if (read_lut(opts->lut, lut)) {
  344. writer_t *writer = writer_init(filename, APTDEC_REGION_CHA, img.rows, PNG_COLOR_TYPE_RGB, description);
  345. writer_write_image_lut(writer, &img, lut);
  346. writer_free(writer);
  347. }
  348. free(lut);
  349. } else {
  350. warning("Cannot create LUT image, missing -l/--lut");
  351. }
  352. } else if (strcmp(images[i], "a") == 0) {
  353. char filename[269] = { 0 };
  354. sprintf(filename, "%s-a.png", base);
  355. char description[128] = { 0 };
  356. sprintf(description, "Channel A: %s - %s", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
  357. writer_t *writer;
  358. if (array_contains(effects, "strip", effects_len)) {
  359. writer = writer_init(filename, (aptdec_region_t){0, APTDEC_CH_WIDTH}, img.rows, PNG_COLOR_TYPE_GRAY, description);
  360. aptdec_image_t _img = aptdec_image_clone(img);
  361. aptdec_strip(&_img);
  362. writer_write_image(writer, &_img);
  363. free(_img.data);
  364. } else {
  365. writer = writer_init(filename, APTDEC_REGION_CHA_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
  366. writer_write_image(writer, &img);
  367. }
  368. writer_free(writer);
  369. } else if (strcmp(images[i], "b") == 0) {
  370. char filename[269] = { 0 };
  371. sprintf(filename, "%s-b.png", base);
  372. char description[128] = { 0 };
  373. sprintf(description, "Channel B: %s - %s", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
  374. writer_t *writer;
  375. if (array_contains(effects, "strip", effects_len)) {
  376. writer = writer_init(filename, (aptdec_region_t){APTDEC_CH_WIDTH, APTDEC_CH_WIDTH}, img.rows, PNG_COLOR_TYPE_GRAY, description);
  377. aptdec_image_t _img = aptdec_image_clone(img);
  378. aptdec_strip(&_img);
  379. writer_write_image(writer, &_img);
  380. free(_img.data);
  381. } else {
  382. writer = writer_init(filename, APTDEC_REGION_CHB_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
  383. writer_write_image(writer, &img);
  384. }
  385. writer_free(writer);
  386. }
  387. }
  388. free(img.data);
  389. return 1;
  390. }
  391. static int freq_from_filename(const char *filename) {
  392. char frequency_text[10] = {'\0'};
  393. if (strlen(filename) >= 30+4 && strncmp(filename, "gqrx_", 5) == 0) {
  394. memcpy(frequency_text, &filename[21], 9);
  395. return atoi(frequency_text);
  396. } else if (strlen(filename) == 40+4 && strncmp(filename, "SDRSharp_", 9) == 0) {
  397. memcpy(frequency_text, &filename[26], 9);
  398. return atoi(frequency_text);
  399. } else if (strlen(filename) == 37+4 && strncmp(filename, "audio_", 6) == 0) {
  400. memcpy(frequency_text, &filename[6], 9);
  401. return atoi(frequency_text);
  402. }
  403. return 0;
  404. }
  405. static int satid_from_freq(int freq) {
  406. int differences[3] = {
  407. abs(freq - 137620000), // NOAA-15
  408. abs(freq - 137912500), // NOAA-18
  409. abs(freq - 137100000) // NOAA-19
  410. };
  411. int best = 0;
  412. for (size_t i = 0; i < 3; i++) {
  413. if (differences[i] < differences[best]) {
  414. best = i;
  415. }
  416. }
  417. const int lut[3] = {15, 18, 19};
  418. return lut[best];
  419. }
  420. // Read samples from a SNDFILE_t instance (passed through context)
  421. static size_t callback(float *samples, size_t count, void *context) {
  422. SNDFILE_t *file = (SNDFILE_t *)context;
  423. switch (file->info.channels) {
  424. case 1:
  425. return sf_read_float(file->file, samples, count);
  426. case 2: {
  427. float _samples[APTDEC_BUFFER_SIZE * 2];
  428. size_t read = sf_read_float(file->file, _samples, count * 2);
  429. for (size_t i = 0; i < count; i++) {
  430. // Average of left and right
  431. samples[i] = (_samples[i*2] + _samples[i*2 + 1]) / 2.0f;
  432. }
  433. return read / 2;
  434. }
  435. default:
  436. error_noexit("Only mono and stereo audio files are supported\n");
  437. return 0;
  438. }
  439. }
  440. // Write a line with very basic equalization
  441. static void write_line(writer_t *png, float *row) {
  442. float min = FLT_MAX;
  443. float max = FLT_MIN;
  444. for (int i = 0; i < APTDEC_IMG_WIDTH; i++) {
  445. if (row[i] < min) min = row[i];
  446. if (row[i] > max) max = row[i];
  447. }
  448. png_byte pixels[APTDEC_IMG_WIDTH];
  449. for (int i = 0; i < APTDEC_IMG_WIDTH; i++) {
  450. pixels[i] = clamp_int(roundf((row[i]-min) / (max-min) * 255.0f), 0, 255);
  451. }
  452. png_write_row(png->png, pixels);
  453. }
  454. int array_contains(char **array, char *value, size_t n) {
  455. for (size_t i = 0; i < n; i++) {
  456. if (strcmp(array[i], value) == 0) {
  457. return 1;
  458. }
  459. }
  460. return 0;
  461. }