diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..7239738 --- /dev/null +++ b/.clang-format @@ -0,0 +1,190 @@ +--- +Language: Cpp +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignArrayOfStructures: None +AlignConsecutiveMacros: None +AlignConsecutiveAssignments: None +AlignConsecutiveBitFields: None +AlignConsecutiveDeclarations: None +AlignEscapedNewlines: Right +AlignOperands: Align +AlignTrailingComments: false +AllowAllArgumentsOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortEnumsOnASingleLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: None +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: MultiLine +AttributeMacros: + - __capability +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: Always + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + BeforeLambdaBody: true + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeConceptDeclarations: true +BreakBeforeBraces: Allman +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: false +ColumnLimit: 0 +CommentPragmas: '^ IWYU pragma:' +QualifierAlignment: Leave +CompactNamespaces: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +PackConstructorInitializers: BinPack +BasedOnStyle: '' +ConstructorInitializerAllOnOneLineOrOnePerLine: false +AllowAllConstructorInitializersOnNextLine: true +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IfMacros: + - KJ_IF_MAYBE +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '(Test)?$' +IncludeIsMainSourceRegex: '' +IndentAccessModifiers: false +IndentCaseLabels: true +IndentCaseBlocks: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentExternBlock: AfterExternBlock +IndentRequires: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +InsertTrailingCommas: None +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +LambdaBodyIndentation: Signature +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakOpenParenthesis: 0 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PenaltyIndentedWhitespace: 0 +PointerAlignment: Right +PPIndentWidth: -1 +ReferenceAlignment: Pointer +ReflowComments: true +RemoveBracesLLVM: false +SeparateDefinitionBlocks: Leave +ShortNamespaceLines: 1 +SortIncludes: CaseSensitive +SortJavaStaticImport: Before +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeParensOptions: + AfterControlStatements: true + AfterForeachMacros: true + AfterFunctionDefinitionName: false + AfterFunctionDeclarationName: false + AfterIfMacros: true + AfterOverloadedOperator: false + BeforeNonEmptyParentheses: false +SpaceAroundPointerQualifiers: Default +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: Never +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +BitFieldColonSpacing: Both +Standard: Latest +StatementAttributeLikeMacros: + - Q_EMIT +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Never +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE + - NS_SWIFT_NAME + - CF_SWIFT_NAME +... diff --git a/.gitignore b/.gitignore index c6127b3..2af972e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,52 +1,11 @@ -# Prerequisites -*.d +panpi +panpi_x +panpi_sim +.vscode +gmon.out +prof_output -# Object files +*.d *.o -*.ko -*.obj -*.elf - -# Linker output -*.ilk *.map -*.exp - -# Precompiled Headers -*.gch -*.pch - -# Libraries -*.lib -*.a -*.la -*.lo - -# Shared objects (inc. Windows DLLs) -*.dll -*.so -*.so.* -*.dylib - -# Executables -*.exe -*.out -*.app -*.i*86 -*.x86_64 -*.hex - -# Debug files -*.dSYM/ -*.su -*.idb -*.pdb - -# Kernel Module Compile Results -*.mod* -*.cmd -.tmp_versions/ -modules.order -Module.symvers -Mkfile.old -dkms.conf +*.gcda diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bd29e33 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/blit-fonts"] + path = third_party/blit-fonts + url = https://github.com/azmr/blit-fonts diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..757e589 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +# =================================== PANPI =================================== +# Project Makefile +# (C) 2022 Ryan Suchocki +# http://suchocki.co.uk/ + +all: panpi + +VERSION := $(shell git describe --tags --dirty --always) + +override CFLAGS += -g -Og -pg -isystem third_party -std=gnu2x -Wall -Wextra -DVERSION=\"$(VERSION)\" +override LDFLAGS += -lm -lasound -lfftw3 -lX11 + +panpi: src/*.c + gcc $(CFLAGS) $(LDFLAGS) -o panpi $^ + +clean: + rm -f panpi + +run: clean panpi + ./panpi + +check: + @cppcheck -q --enable=all --inconclusive --suppress=missingInclude src + + @clang-format -n --style=file src/*.c src/*.h + + @echo "Check for headers without #pragma once" + @grep --color -L "#pragma once" src/*.h + +format: + clang-format -i --style=file src/*.c src/*.h diff --git a/README.md b/README.md index b48f3de..8a92522 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,145 @@ -# panpi -Pan-adapter for the Raspberry Pi +# PanPI +'Raspberry Pi' based *panadapter*. + +![screenshot](https://user-images.githubusercontent.com/278474/185504104-e697e979-ecaf-4e6b-b76a-1a81a1da90f1.png) + +## About + +PanPI is primarily an application which renders a realtime 'spectrogram' and 'waterfall' display based on an I/Q sample stream. While it is fully portable software, PanPI has been developed and tested on a Raspberry Pi (model 4B). + +PanPI is designed to perform a specific job very well, with minimal bells, whistles and other distractions. + +## Building and Running + +`$ git clone git@github.com:ryansuchocki/panpi.git` + +`$ cd panpi` + +`$ make` + +Customise `panpi.cfg` + +`$ ./panpi` + +## Background + +I started this project after finding myself in need of a self-contained panadapter unit for my Elecraft KX3 transceiver and being told (by Elecraft's UK distributor) that the companion 'PX3 Panadapter' is no longer available. Sadly: PX3s on the second-hand market seem to be rare to non-existent. + +I was pleased to find that some high quality pressed steel enclosures are available to fit the Raspberry Pi along with an inexpensive TFT display. I found that running established desktop SDR software such as 'Gqrx' on a small screen is *just about* feasible however performance is severely compromised and screen space is wasted. I also found a few existing software projects intended to render a straight-forward panadapter display however the software on offer was either poorly designed, unmaintained or had unnecessary and/or obsolete dependencies. + +## Platform + +The platform on which PanPI has been developed and tested is as follows: (Note that the software is portable and is in no way restricted to the following) + +### Hardware +* [Raspberry Pi 4B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) +* [Pi 4 metal case with 3.5" TFT display](https://www.mouser.co.uk/ProductDetail/DFRobot/FIT0820?qs=pBJMDPsKWf1f%252B%2Fx0HFXbyA%3D%3D) +* [96kHz stereo USB sound card](https://www.startech.com/en-gb/cards-adapters/icusbaudio2d) + +### Operating System +* [Arch Linux ARM](https://archlinuxarm.org/platforms/armv8/broadcom/raspberry-pi-4) +* The following packages are needed to build and run the software. The package versions used for development are recorded here but should not be taken as requirements: + * `git 2.37.1-1` + * `make 4.3-3.1` + * `gcc 12.1.0-2.1` + * `fftw 3.3.10-3` + * `libx11 1.8.1-3` + +### Configuration +The following lines were added to my `/boot/config.txt` file in order to get the display working. +``` +dtparam=spi=on + +dtoverlay=tft35a:rotate=270,speed=160000000,fps=60 +``` + +Note that the order of entries in `config.txt` is significant. My full `config.txt`, at time of writing, is as follows: + +``` +# /boot/config.txt +# See /boot/overlays/README for all available options + +dtparam=audio=on +dtparam=spi=on +dtparam=debug=7 + +dtoverlay=vc4-kms-v3d +dtoverlay=tft35a:rotate=270,speed=160000000,fps=60 + +initramfs initramfs-linux.img followkernel + +# Uncomment to enable bluetooth +#dtparam=krnbt=on + +[pi4] +# Run as fast as firmware / board allows +arm_boost=1 +``` + +![photo](https://user-images.githubusercontent.com/278474/186992126-67d05c45-e872-48e6-be8e-5f827d8043ff.jpg) + +## Design + +TODO + +C (fast) +Timeless +Minimal dependencies (maintainable) +No platform lock-in + +## Configuration + +PanPI is configured via the `panpi.cfg` file. This file follows a simple format whereby each valid line takes the form `key: value`. Lines not matching this format, or correctly formed lines with unrecognised keys, are simply ignored. + +After initial loading: PanPI "watches" the file for changes and, where possible, applies changes straight away to match the new configuration. This provides a rudimentary form of runtime control, but this is liable to change in the future. + +See comments within the file itself for details of the available configuration items. + +## Roadmap + +- MVP (Minimal Viable Product) + - [x] Hardware/OS selection for development + - [x] Capturing input samples using ALSA + - [x] Calculating FFT using fftw + - [x] Rendering a realtime 'spectrogram' display + - [x] Rendering a scrolling 'waterfall' display + - [x] Rendering to a cheap TFT screen using fbdev +- Goals for 'V1' + - [x] Rendering to a desktop window (using X11) for off-target development + - [ ] Runtime controls + - [ ] Zoom/span controls + - [ ] Useful documentation +- Possible future extensions + - [ ] Porting to a lower power microcontroller platform + - [ ] Alternative input sources (rtl-sdr?) + - [ ] Per-band IQ balance calibration + - [ ] Per-mode passband markers + - [ ] Per-band scaling memory + - [ ] Pre-amp compensation + - [ ] Spectrogram peak hold + +## License + +``` +MIT License + +Copyright (c) 2022 Ryan Suchocki + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/panpi.cfg b/panpi.cfg new file mode 100644 index 0000000..5ab07b1 --- /dev/null +++ b/panpi.cfg @@ -0,0 +1,40 @@ +# PanPI config file + +# Source type selection. Should be 'soundcard' or 'file' +source: soundcard + +# Device selection (only applies if the source type is 'soundcard') +# May be an ALSA identifier string (such as 'hw:1,0' or 'default) +# or the word 'auto', in which case PanPI tries to select the first +# available sound card which looks like an external (USB) one. +# device: auto + +# Source file path (only applies if the source type is 'file') +file: test/1min.dat + +# Capture sample rate +sample_rate: 96000 + +# Set to 'true' to render to an X11 window rather than direct +# to the framebuffer +# x_window: true + +# Capture gain level +capture_gain: 1.0 + +# Sharpness parameter for the DSP "DC offset" removal filter +dc_alpha: 0.999 + +# Upper and lower reference levels for the spectrogram and +# waterfall display. Use this to "zoom in/out" vertically. +refl: 8.0 +refh: 18.0 + +# Smoothing factors for the spectrogram display +sgam_spread: 1 +sgam_drag: 0.9 + +# Vertical scrolling speed divisor for the waterfall display +# The larger this number, the slower the waterfall moves and +# thus the larger the time period on display at once. +wfall_zoom: 5 diff --git a/src/capture.h b/src/capture.h new file mode 100644 index 0000000..efd4cb0 --- /dev/null +++ b/src/capture.h @@ -0,0 +1,44 @@ +/* =================================== PanPI =================================== + * IQ sample capture module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#pragma once + +#include "common.h" +#include "config.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +typedef struct +{ + int (*open)(unsigned sample_rate); + int (*close)(void); + int (*get)(complex double *buffer, unsigned n); +} capture_t; + +/******************************************************************************* + * API + ******************************************************************************/ + +static inline capture_t capture_init(const char *source) +{ + if (!strcmp(source, "soundcard")) + { + extern capture_t capture_soundcard; + return capture_soundcard; + } + else if (!strcmp(source, "file")) + { + extern capture_t capture_file; + return capture_file; + } + else + { + eprintf("Unknown source type '%s'\n", source); + exit(1); + } +} diff --git a/src/capture/capture_file.c b/src/capture/capture_file.c new file mode 100644 index 0000000..20ff1c6 --- /dev/null +++ b/src/capture/capture_file.c @@ -0,0 +1,114 @@ +/* =================================== PanPI =================================== + * IQ sample capture module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#include "capture.h" + +#include "common.h" +#include "config.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +/******************************************************************************* + * Prototypes + ******************************************************************************/ + +static int capture_file_open(unsigned sample_rate); +static int capture_file_close(void); +static int capture_file_get(complex double *buffer, unsigned n); +static inline char getc_wrapped(FILE *fp); + +/******************************************************************************* + * Variables + ******************************************************************************/ + +static FILE *capture_fp; +static unsigned capture_rate; +static int64_t capture_time; + +capture_t capture_file = { + .open = &capture_file_open, + .close = &capture_file_close, + .get = &capture_file_get, +}; + +/******************************************************************************* + * Code + ******************************************************************************/ + +static int capture_file_open(unsigned sample_rate) +{ + if (!strlen(config.file)) + { + eprintf("No source file configured\n"); + exit(-1); + } + capture_fp = fopen(config.file, "r"); + if (!capture_fp) + { + eprintf("Failed to open file '%s'\n", config.file); + exit(-1); + } + capture_rate = sample_rate; + capture_time = get_nanos(); + + return 0; +} + +static int capture_file_close(void) +{ + fclose(capture_fp); + return 0; +} + +static int capture_file_get(complex double *buffer, unsigned n) +{ + // Simulate blocking until the requested number of samples could + // have been received at the configured sample rate: + capture_time += (NANOSECONDS_PER_SECOND * n / capture_rate); + int64_t block = capture_time - (int64_t)get_nanos(); + if (block > 0) + { + nanosleep(&((struct timespec){ + .tv_sec = block / NANOSECONDS_PER_SECOND, + .tv_nsec = block % NANOSECONDS_PER_SECOND + }), NULL); + } + + while (n--) + { + int16_t i, q; + + ((char *)&q)[0] = getc_wrapped(capture_fp); + ((char *)&q)[1] = getc_wrapped(capture_fp); + ((char *)&i)[0] = getc_wrapped(capture_fp); + ((char *)&i)[1] = getc_wrapped(capture_fp); + + *buffer++ = CMPLX(i, q); + } + + return 0; +} + +static inline char getc_wrapped(FILE *fp) +{ + int c = getc(fp); + + if (c == EOF) + { + rewind(fp); + + c = getc(fp); + if (c == EOF) + { + eprintf("EOF despite rewinding file\n"); + exit(1); + } + } + + return (char)c; +} diff --git a/src/capture/capture_soundcard.c b/src/capture/capture_soundcard.c new file mode 100644 index 0000000..9c2452a --- /dev/null +++ b/src/capture/capture_soundcard.c @@ -0,0 +1,189 @@ +/* =================================== PanPI =================================== + * IQ sample capture module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#include "capture.h" + +#include "common.h" +#include "config.h" + +#include + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +#define SAMPLE_FORMAT SND_PCM_FORMAT_S16 + +#define SAMPLE_ACCESS SND_PCM_ACCESS_RW_INTERLEAVED + +#define N_CHANNELS 2 + +/******************************************************************************* + * Prototypes + ******************************************************************************/ + +static const char *get_audio_device(void); +static int capture_soundcard_open(unsigned sample_rate); +static int capture_soundcard_close(void); +static int capture_soundcard_get(complex double *buffer, unsigned n); + +/******************************************************************************* + * Variables + ******************************************************************************/ + +static snd_pcm_t *capture_handle; + +capture_t capture_soundcard = { + .open = &capture_soundcard_open, + .close = &capture_soundcard_close, + .get = &capture_soundcard_get, +}; + +/******************************************************************************* + * Code + ******************************************************************************/ + +static const char *get_audio_device(void) +{ + if (strcmp(config.device, "auto")) + { + return strdup(config.device); + } + + const char *result = NULL; + + snd_ctl_card_info_t *info; + snd_ctl_card_info_alloca(&info); + + int card_idx = -1; + while (snd_card_next(&card_idx) >= 0 && card_idx >= 0) + { + int err; + char *card_name; + if ((err = snd_card_get_name(card_idx, &card_name)) < 0) + { + eprintf("snd_card_get_name (%s)\n", snd_strerror(err)); + continue; + } + + if (strstr(card_name, "USB")) + { + static char identifier[32]; + sprintf(identifier, "hw:%d", card_idx); + result = strdup(identifier); + } + + free(card_name); + } + + if (result == NULL) + { + eprintf("Cannot find USB card\n"); + exit(1); + } + + printf("Using audio device '%s'\n", result); + + return result; +} + +static int capture_soundcard_open(unsigned sample_rate) +{ + int err; + + const char *device_str = get_audio_device(); + + if ((err = snd_pcm_open(&capture_handle, device_str, SND_PCM_STREAM_CAPTURE, 0)) < + 0) + { + eprintf("Cannot open audio device '%s' (%s)\n", device_str, snd_strerror(err)); + free((void *)device_str); + exit(1); + } + free((void *)device_str); + + snd_pcm_hw_params_t *hw_params; + snd_pcm_hw_params_alloca(&hw_params); + + if ((err = snd_pcm_hw_params_any(capture_handle, hw_params)) < 0) + { + eprintf("Cannot initialize hardware parameter structure (%s)\n", snd_strerror(err)); + exit(1); + } + + if ((err = snd_pcm_hw_params_set_access(capture_handle, hw_params, SAMPLE_ACCESS)) < 0) + { + eprintf("Cannot set access type (%s)\n", snd_strerror(err)); + exit(1); + } + + if ((err = snd_pcm_hw_params_set_format(capture_handle, hw_params, SAMPLE_FORMAT)) < 0) + { + eprintf("Cannot set sample format (%s)\n", snd_strerror(err)); + exit(1); + } + + if ((err = snd_pcm_hw_params_set_rate(capture_handle, hw_params, sample_rate, 0)) < 0) + { + eprintf("Cannot set sample rate (%s)\n", snd_strerror(err)); + exit(1); + } + + if ((err = snd_pcm_hw_params_set_channels(capture_handle, hw_params, N_CHANNELS)) < 0) + { + eprintf("Cannot set channel count (%s)\n", snd_strerror(err)); + exit(1); + } + + if ((err = snd_pcm_hw_params(capture_handle, hw_params)) < 0) + { + eprintf("Cannot set parameters (%s)\n", snd_strerror(err)); + exit(1); + } + + if ((err = snd_pcm_prepare(capture_handle)) < 0) + { + eprintf("Cannot prepare audio interface for use (%s)\n", snd_strerror(err)); + exit(1); + } + + if ((err = snd_pcm_start(capture_handle)) < 0) + { + eprintf("Start error: %s\n", snd_strerror(err)); + exit(1); + } + + return 0; +} + +static int capture_soundcard_close(void) +{ + snd_pcm_close(capture_handle); + + return 0; +} + +static int capture_soundcard_get(complex double *buffer, unsigned n) +{ + int err = 0; + + int16_t interleaved[n * 2]; + + if ((err = (int)snd_pcm_readi(capture_handle, interleaved, n)) != (int)n) + { + eprintf("Snd_pcm_readi(): %i (%s)\n", err, snd_strerror(err)); + return 1; + } + + // Convert interleaved integer samples to complex float: + for (unsigned i = 0; i < n; i++) + { + complex double sample = CMPLX(interleaved[2 * i + 1], interleaved[2 * i]); + buffer[i] = sample; + } + + return err; +} diff --git a/src/common.h b/src/common.h new file mode 100644 index 0000000..f3f4306 --- /dev/null +++ b/src/common.h @@ -0,0 +1,54 @@ +/* =================================== PanPI =================================== + * Project-wide dependencies and definitions + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/******************************************************************************* + * Definitions (Generic) + ******************************************************************************/ + +#define STRINGIZE(x) #x + +#define ARRAYLEN(x) (sizeof(x) / sizeof(x[0])) + +#define SQUARED(x) ((x) * (x)) + +#define MIN(x, y) ((y < x) ? (y) : (x)) +#define MAX(x, y) ((y > x) ? (y) : (x)) +#define EQMIN(x, y) x = MIN(x, y); +#define EQMAX(x, y) x = MAX(x, y); + +#define UNUSED(x) (void)(x) + +#define NANOSECONDS_PER_SECOND (1000000000L) + +#define eprintf(args...) fprintf(stderr, "ERROR: " args) + +/******************************************************************************* + * API + ******************************************************************************/ + +static inline int64_t get_nanos(void) +{ + // NB: will roll over after ~250 years... + struct timespec t; + clock_gettime(CLOCK_MONOTONIC_RAW, &t); + return (int64_t)t.tv_sec * NANOSECONDS_PER_SECOND + (int64_t)t.tv_nsec; +} diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..e488da9 --- /dev/null +++ b/src/config.c @@ -0,0 +1,205 @@ +/* =================================== PanPI =================================== + * Config management module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#include "config.h" + +#include "common.h" + +#include + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +#define CONFIG_FILEPATH "panpi.cfg" + +CONFIG_T config; + +static CONFIG_T defaults = { + .source = "soundcard", + .device = "auto", + .file = "", + .sample_rate = 96000, + .capture_gain = 1, + .dc_alpha = 0.999, + .x_window = false, + .refl = 0, + .refh = 20, + .sgam_spread = 1, + .sgam_drag = 0.9, + .wfall_zoom = 5, +}; + +struct +{ + const char *key; + enum + { + INT, + UINT, + DOUBLE, + STR, + BOOL + } type; + union + { + int *INT; + unsigned *UINT; + double *DOUBLE; + struct + { + size_t STRLEN; + char *STR; + }; + bool *BOOL; + }; +} map[] = { + {"source", STR, .STR = config.source, .STRLEN = sizeof(config.source)}, + {"device", STR, .STR = config.device, .STRLEN = sizeof(config.device)}, + {"file", STR, .STR = config.file, .STRLEN = sizeof(config.file)}, + {"sample_rate", UINT, .UINT = &config.sample_rate}, + {"capture_gain", DOUBLE, .DOUBLE = &config.capture_gain}, + {"dc_alpha", DOUBLE, .DOUBLE = &config.dc_alpha}, + {"x_window", BOOL, .BOOL = &config.x_window}, + {"refl", DOUBLE, .DOUBLE = &config.refl}, + {"refh", DOUBLE, .DOUBLE = &config.refh}, + {"sgam_spread", UINT, .UINT = &config.sgam_spread}, + {"sgam_drag", DOUBLE, .DOUBLE = &config.sgam_drag}, + {"wfall_zoom", UINT, .UINT = &config.wfall_zoom}, +}; + +/******************************************************************************* + * Prototypes + ******************************************************************************/ + +static void parse(void); + +/******************************************************************************* + * Variables + ******************************************************************************/ + +/******************************************************************************* + * Code + ******************************************************************************/ + +void parse(void) +{ + config = defaults; + + FILE *f = fopen(CONFIG_FILEPATH, "r"); + + char *linebuf; + + while (fscanf(f, " %m[^\n] ", &linebuf) == 1) + { + char *keybuf, *valbuf = NULL; + + if (sscanf(linebuf, " %m[^:\n]: %m[^\n] ", &keybuf, &valbuf) == 2) + { + for (unsigned i = 0; i < ARRAYLEN(map); i++) + { + if (!strcmp(map[i].key, keybuf)) + { + int parsed = 0; + + switch (map[i].type) + { + case INT: + parsed = sscanf(valbuf, "%i", map[i].INT); + break; + case UINT: + parsed = sscanf(valbuf, "%u", map[i].UINT); + break; + case DOUBLE: + parsed = sscanf(valbuf, "%lf", map[i].DOUBLE); + break; + case STR: + parsed = strlen(valbuf) < map[i].STRLEN; + if (parsed) + { + strcpy(map[i].STR, valbuf); + } + break; + case BOOL: + if (!strcmp(valbuf, "false")) + { + *map[i].BOOL = false; + parsed = 1; + } + else if (!strcmp(valbuf, "true")) + { + *map[i].BOOL = true; + parsed = 1; + } + break; + default: + eprintf("Unhandled case in parse\n"); + } + + if (parsed != 1) + { + eprintf("Cannot parse \"%s\" (\"%s\")\n", keybuf, valbuf); + } + + break; + } + } + } + + if (keybuf) free(keybuf); + if (valbuf) free(valbuf); + + free(linebuf); + } + + fclose(f); +} + +static int fd; + +void config_init(void) +{ + fd = inotify_init(); + if (fd < 0) + { + perror("Couldn't initialize inotify"); + } + + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); + + int wd = inotify_add_watch(fd, CONFIG_FILEPATH, IN_CLOSE_WRITE); + if (wd == -1) + { + eprintf("Failed to add file watch\n"); + } + + parse(); +} + +bool config_update(void) +{ + // NB there will be no 'name' as we're not watching a directory + struct inotify_event ev = {0}; + ssize_t length = read(fd, &ev, sizeof(ev)); + ev.len = 0; + if (length) + { + if (ev.mask & IN_CLOSE_WRITE) + { + parse(); + return true; + } + } + + return false; +} + +// /* Clean up*/ +// inotify_rm_watch( fd, wd ); +// close( fd ); + +// return 0; +// } diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..0451d41 --- /dev/null +++ b/src/config.h @@ -0,0 +1,40 @@ +/* =================================== PanPI =================================== + * Config management module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#pragma once + +#include "common.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +#define CONFIG_STR_LEN 32 + +typedef struct +{ + char source[CONFIG_STR_LEN]; + char device[CONFIG_STR_LEN]; + char file[CONFIG_STR_LEN]; + unsigned sample_rate; + double capture_gain; + double dc_alpha; + bool x_window; + double refl; + double refh; + unsigned sgam_spread; + double sgam_drag; + unsigned wfall_zoom; +} CONFIG_T; + +/******************************************************************************* + * API + ******************************************************************************/ + +extern CONFIG_T config; + +extern void config_init(void); +extern bool config_update(void); diff --git a/src/display.h b/src/display.h new file mode 100644 index 0000000..e9d6c4f --- /dev/null +++ b/src/display.h @@ -0,0 +1,23 @@ +/* =================================== PanPI =================================== + * Overall display rendering module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#pragma once + +#include "common.h" +#include "framebuffer.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +/******************************************************************************* + * API + ******************************************************************************/ + +void display_open(unsigned sample_rate); +void display_close(void); +void display_update_bg(unsigned sample_rate); +void display_update(double *amplitudes); diff --git a/src/display/display.c b/src/display/display.c new file mode 100644 index 0000000..368d2e3 --- /dev/null +++ b/src/display/display.c @@ -0,0 +1,139 @@ +/* =================================== PanPI =================================== + * Overall display rendering module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#include "display.h" + +#include "common.h" +#include "config.h" +#include "display.h" +#include "framebuffer.h" +#include "layout.h" +#include "spectrogram.h" +#include "text.h" +#include "waterfall.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +/******************************************************************************* + * Prototypes + ******************************************************************************/ + +static void render_debug_line(void); + +/******************************************************************************* + * Variables + ******************************************************************************/ + +static fb_t fb; +static fb_buf_t bg; + +/******************************************************************************* + * Code + ******************************************************************************/ + +void display_open(unsigned sample_rate) +{ + fb = fb_init(config.x_window); + + waterfall_init(); + + fb.open(); + display_update_bg(sample_rate); + fb.draw(); +} + +void display_close(void) +{ + fb.close(); +} + +void display_update_bg(unsigned sample_rate) +{ + spectrogram_render_bg(&bg); + + // Horizontal lines + for (int x = 3; x < FB_WIDTH - 3; x++) + for (int y = 0; y < 3; y++) + { + // Head + bg.xy[3 + y][x] = WHITE; + // Mid + bg.xy[SGAM_TOP + SGAM_HEIGHT + y][x] = WHITE; + // Foot + bg.xy[FB_HEIGHT - 4 - y][x] = WHITE; + } + + // Vertical lines + for (int y = 3; y < FB_HEIGHT - 4; y++) + for (int x = 0; x < 3; x++) + { + // Left + bg.xy[y][3 + x] = WHITE; + // Right + bg.xy[y][FB_WIDTH - 4 - x] = WHITE; + } + + // Head & foot bottom frequency pips + for (int x = 0; x < FB_WIDTH / 2; + x += (PIP_INTERVAL_HZ * FB_WIDTH / (int)sample_rate)) + for (int xx = x - 1; xx < x + 2; xx++) + for (int y = 0; y < 3; y++) + { + // Head left + bg.xy[y][FB_WIDTH / 2 - xx] = WHITE; + // Head right + bg.xy[y][FB_WIDTH / 2 + xx] = WHITE; + // Foot left + bg.xy[FB_HEIGHT - y - 1][FB_WIDTH / 2 - xx] = WHITE; + // Foot right + bg.xy[FB_HEIGHT - y - 1][FB_WIDTH / 2 + xx] = WHITE; + } +} + +void render_debug_line(void) +{ + static time_t t_last = 0; + static int count = 0; + static char debug_line[20]; + + count++; + time_t t_now = time(NULL); + + if (t_now > t_last) + { + snprintf(debug_line, 20, "%ifps", count); + count = 0; + t_last = t_now; + } + + render_text(fb.buf, debug_line, -7, -10, false, YELLOW); +} + +void display_update(double *amplitudes) +{ + spectrogram_update(amplitudes); + waterfall_update(amplitudes); + + static int64_t next_frame_due = 0; + + int64_t nanos = get_nanos(); + + if (nanos >= next_frame_due) + { + next_frame_due = next_frame_due ? next_frame_due : nanos; + next_frame_due += NANOSECONDS_PER_SECOND / TARGET_FPS; + + memcpy(fb.buf, &bg, sizeof(*fb.buf)); + + render_spectrogram(fb.buf); + render_waterfall(fb.buf); + render_debug_line(); + + fb.draw(); + } +} diff --git a/src/display/spectrogram.c b/src/display/spectrogram.c new file mode 100644 index 0000000..471dede --- /dev/null +++ b/src/display/spectrogram.c @@ -0,0 +1,102 @@ +/* =================================== PanPI =================================== + * Spectrogram display module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#include "spectrogram.h" + +#include "common.h" +#include "config.h" +#include "framebuffer.h" +#include "layout.h" +#include "text.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +/******************************************************************************* + * Prototypes + ******************************************************************************/ + +/******************************************************************************* + * Variables + ******************************************************************************/ + +static double smoothed_values[SGAM_WIDTH]; + +/******************************************************************************* + * Code + ******************************************************************************/ + +void spectrogram_render_bg(fb_buf_t *bg) +{ + // Vertical centre line + for (int x = 0; x < SGAM_WIDTH; x++) + { + for (int y = 0; y < SGAM_HEIGHT; y++) + { + bg->xy[SGAM_TOP + SGAM_HEIGHT - 1 - y][SGAM_LEFT + x] = + (x == SGAM_WIDTH / 2) ? CLINECOL : BGCOL; + } + } + + // Horizontal reference lines + for (int y = (int)(floor(config.refl + 1)); y < config.refh; y++) + { + int yyy = (int)((y - config.refl) * SGAM_HEIGHT / (config.refh - config.refl)); + for (int x = 8 + 8 + 8 + 1; (x + 3) < SGAM_WIDTH; x += 8) + for (int xx = x; xx < x + 3; xx++) + bg->xy[SGAM_TOP + SGAM_HEIGHT - 1 - yyy][SGAM_LEFT + xx] = + HLINECOL; + + char buf[5]; + snprintf(buf, 5, "%i", y); + + render_text(bg, buf, SGAM_LEFT + 6, SGAM_TOP + SGAM_HEIGHT - 1 - yyy, true, YELLOW); + } +} + +void spectrogram_update(const double *values) +{ + double sum = 0; + for (unsigned i = 0; i < config.sgam_spread * 2; i++) + { + sum += values[i]; + } + + for (unsigned i = config.sgam_spread; i < SGAM_WIDTH - config.sgam_spread; i++) + { + sum += values[i + config.sgam_spread]; + double this_amplitude = sum / (config.sgam_spread * 2 + 1); + sum -= values[i - config.sgam_spread]; + + smoothed_values[i] = smoothed_values[i] * config.sgam_drag; + smoothed_values[i] += this_amplitude * (1.0 - config.sgam_drag); + } +} + +void render_spectrogram(fb_buf_t *buf) +{ + int lastamp = 0; + for (int x = 0; x < SGAM_WIDTH; x++) + { + int col = SGAM_LEFT + x; + + int amp = (int)(smoothed_values[x] * SGAM_HEIGHT); + + // amp *= 0.244140625 * SGAM_HEIGHT; + EQMAX(amp, 0); + EQMIN(amp, SGAM_HEIGHT); + + int start = MIN(amp, lastamp); + int stop = MAX(amp, lastamp); + lastamp = amp; + + for (int y = start; y <= stop; y++) + { + buf->xy[SGAM_TOP + SGAM_HEIGHT - y - 1][col] = YELLOW; + } + } +} diff --git a/src/display/spectrogram.h b/src/display/spectrogram.h new file mode 100644 index 0000000..52517b6 --- /dev/null +++ b/src/display/spectrogram.h @@ -0,0 +1,22 @@ +/* =================================== PanPI =================================== + * Spectrogram display module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#pragma once + +#include "common.h" +#include "framebuffer.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +/******************************************************************************* + * API + ******************************************************************************/ + +extern void spectrogram_render_bg(fb_buf_t *bg); +extern void spectrogram_update(const double *values); +extern void render_spectrogram(fb_buf_t *buf); diff --git a/src/display/text.c b/src/display/text.c new file mode 100644 index 0000000..e38b7d8 --- /dev/null +++ b/src/display/text.c @@ -0,0 +1,52 @@ +/* =================================== PanPI =================================== + * Text rendering module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#include "text.h" + +#include "common.h" +#include "framebuffer.h" + +#define blit_pixel colour16_t +#include "blit-fonts/blit32.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +/******************************************************************************* + * Prototypes + ******************************************************************************/ + +/******************************************************************************* + * Variables + ******************************************************************************/ + +/******************************************************************************* + * Code + ******************************************************************************/ + +void render_text(fb_buf_t *buf, const char *text, int x, int y, bool v_centre, colour16_t colour) +{ + if (x < 0) + x += FB_WIDTH - (blit32_ADVANCE * (int)strlen(text)); + + if (y < 0) + y += FB_HEIGHT - blit32_HEIGHT; + + if (v_centre) + y -= blit32_HEIGHT / 2; + + blit32_TextExplicit( + (blit_pixel *)buf, + colour, + 1, + FB_WIDTH, + FB_HEIGHT, + blit_Clip, + x, + y, + (char *)text); +} diff --git a/src/display/text.h b/src/display/text.h new file mode 100644 index 0000000..caf58e4 --- /dev/null +++ b/src/display/text.h @@ -0,0 +1,20 @@ +/* =================================== PanPI =================================== + * Text rendering module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#pragma once + +#include "common.h" +#include "framebuffer.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +/******************************************************************************* + * API + ******************************************************************************/ + +extern void render_text(fb_buf_t *buf, const char *text, int x, int y, bool v_centre, colour16_t colour); diff --git a/src/display/waterfall.c b/src/display/waterfall.c new file mode 100644 index 0000000..2719505 --- /dev/null +++ b/src/display/waterfall.c @@ -0,0 +1,75 @@ +/* =================================== PanPI =================================== + * Waterfall rendering module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#include "waterfall.h" + +#include "common.h" +#include "config.h" +#include "framebuffer.h" +#include "layout.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +#define INTENS_LEVELS (255 - 0x17) + +/******************************************************************************* + * Prototypes + ******************************************************************************/ + +/******************************************************************************* + * Variables + ******************************************************************************/ + +static colour16_t waterfall[WFALL_HEIGHT][WFALL_WIDTH]; +static unsigned waterfall_i = 0; +static colour16_t wf_map[INTENS_LEVELS]; + +/******************************************************************************* + * Code + ******************************************************************************/ + +void waterfall_init(void) +{ + // Pre-compute colour values for each of the waterfall intensity levels + for (unsigned i = 0; i < INTENS_LEVELS; i++) + { + wf_map[i] = colour(i * 0, (uint8_t)(0x07 + i * 0x07 / 0x17), (uint8_t)(0x17 + i)); + } +} + +void waterfall_update(const double *values) +{ + static unsigned phase = 0; + + if (++phase == config.wfall_zoom) + { + phase = 0; + + for (unsigned x = 0; x < WFALL_WIDTH; x++) + { + int intens = (int)(values[x] * INTENS_LEVELS); + EQMAX(intens, 0); + waterfall[waterfall_i][x] = + (intens >= INTENS_LEVELS) ? WHITE : wf_map[intens]; + } + + waterfall_i = (waterfall_i + 1) % WFALL_HEIGHT; + } +} + +void render_waterfall(fb_buf_t *buf) +{ + for (unsigned x = 0; x < WFALL_WIDTH; x++) + { + for (unsigned y = 0; y < WFALL_HEIGHT; y++) + { + buf->xy[WFALL_TOP + WFALL_HEIGHT - 1 - y][x + WFALL_LEFT] = + waterfall[(waterfall_i + y + 1) % WFALL_HEIGHT][x]; + } + } +} diff --git a/src/display/waterfall.h b/src/display/waterfall.h new file mode 100644 index 0000000..01b2ed1 --- /dev/null +++ b/src/display/waterfall.h @@ -0,0 +1,22 @@ +/* =================================== PanPI =================================== + * Waterfall rendering module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#pragma once + +#include "common.h" +#include "framebuffer.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +/******************************************************************************* + * API + ******************************************************************************/ + +extern void waterfall_init(void); +extern void waterfall_update(const double *values); +extern void render_waterfall(fb_buf_t *buf); diff --git a/src/dsp.c b/src/dsp.c new file mode 100644 index 0000000..ebb30b9 --- /dev/null +++ b/src/dsp.c @@ -0,0 +1,96 @@ +/* =================================== PanPI =================================== + * Digital Signal Processing module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#include "dsp.h" + +#include "common.h" +#include "config.h" + +#include + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +/******************************************************************************* + * Prototypes + ******************************************************************************/ + +/******************************************************************************* + * Variables + ******************************************************************************/ + +static complex double *fft_in, *fft_out; +static fftw_plan fft_plan; + +static complex double dc = 0; +static unsigned fft_size = 0; + +/******************************************************************************* + * Code + ******************************************************************************/ + +void dsp_init(unsigned init_fft_size) +{ + fft_size = init_fft_size; + fft_in = fftw_alloc_complex(fft_size); + fft_out = fftw_alloc_complex(fft_size); + + fft_plan = + fftw_plan_dft_1d((int)fft_size, fft_in, fft_out, FFTW_FORWARD, FFTW_MEASURE); +} + +void dsp_free(void) +{ + fftw_destroy_plan(fft_plan); + fftw_free(fft_in); + fftw_free(fft_out); +} + +void dsp_process(const complex double *samples, double *results) +{ + // Prepare the samples for processing by FFTW: + for (unsigned i = 0; i < fft_size; i++) + { + complex double sample = samples[i]; + + // Apply any configured input gain: + sample *= config.capture_gain; + + // Calculate DC offset using a trivial IIR filter: + dc = (dc * config.dc_alpha) + (sample * (1 - config.dc_alpha)); + + // Store the sample with DC offset removed: + fft_in[i] = sample - dc; + } + + fftw_execute(fft_plan); + + // Prepare the FFT results for processing by the rest of panpi: + for (unsigned i = 0; i < fft_size; i++) + { + // Use an offset index so that 0Hz is shifted to the centre of the array: + unsigned idx = (i + fft_size / 2) % fft_size; + + // Calculate the magnitude squared of the complex frequency bin + // There's no point performing an expensive square root as it's + // just a factor of two after the upcoming log(). + double mag_sq = SQUARED(creal(fft_out[idx])) + SQUARED(cimag(fft_out[idx])); + + // Calculate the logarithm of the magnitude. The log base doesn't + // matter as it changes the result by a constant factor. + double log_mag = log(mag_sq); + + // Shift and scale the result using the configured upper and lower reference + // values: + log_mag -= config.refl; + log_mag /= (config.refh - config.refl); + + // Clip out any negative results at this stage to simplify downstream + // processing: + results[i] = MAX(log_mag, 0); + } +} diff --git a/src/dsp.h b/src/dsp.h new file mode 100644 index 0000000..d528e84 --- /dev/null +++ b/src/dsp.h @@ -0,0 +1,22 @@ +/* =================================== PanPI =================================== + * Digital Signal Processing module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#pragma once + +#include "common.h" +#include "config.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +/******************************************************************************* + * API + ******************************************************************************/ + +extern void dsp_init(unsigned init_fft_size); +extern void dsp_free(void); +extern void dsp_process(const complex double *samples, double *results); diff --git a/src/framebuffer.h b/src/framebuffer.h new file mode 100644 index 0000000..3312fb9 --- /dev/null +++ b/src/framebuffer.h @@ -0,0 +1,69 @@ +/* =================================== PanPI =================================== + * Linux framebuffer wrapper module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#pragma once + +#include "common.h" +#include "config.h" +#include "layout.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +typedef uint16_t colour16_t; + +static inline colour16_t colour(uint8_t red, uint8_t green, uint8_t blue) +{ + return (colour16_t)(((red >> 3) << 11) | + ((green >> 2) << 5) | + (blue >> 3)); +} + +static inline uint32_t colour16_to_24(colour16_t c) +{ + return ( + ((c & 0xF800) << 8) | + ((c & 0x7E0) << 5) | + ((c & 0x1F) << 3)); +} + +typedef union +{ + colour16_t xy[FB_HEIGHT][FB_WIDTH]; + colour16_t flat[FB_HEIGHT * FB_WIDTH]; +} fb_buf_t; + +typedef struct +{ + fb_buf_t *buf; + void (*open)(void); + void (*close)(void); + void (*draw)(void); +} fb_t; + +/******************************************************************************* + * API + ******************************************************************************/ + +static inline fb_t fb_init(bool use_x11) +{ + if (use_x11) + { +#if COMPILE_X11 + extern fb_t fb_x; + return fb_x; +#else + eprintf("PanPI was compiled without X11 support\n"); + exit(1); +#endif + } + else + { + extern fb_t fb_raw; + return fb_raw; + } +} diff --git a/src/framebuffer/framebuffer_X11.c b/src/framebuffer/framebuffer_X11.c new file mode 100644 index 0000000..6961cf1 --- /dev/null +++ b/src/framebuffer/framebuffer_X11.c @@ -0,0 +1,120 @@ +/* =================================== PanPI =================================== + * Linux framebuffer wrapper module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#include "common.h" +#include "framebuffer.h" +#include "layout.h" + +#include +#include +#include + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +/******************************************************************************* + * Prototypes + ******************************************************************************/ + +static void fb_x_open(void); +static void fb_x_close(void); +static void fb_x_draw(void); + +/******************************************************************************* + * Variables + ******************************************************************************/ + +static Display *display; +static Window window; +static Atom wm_delete_window; +static XImage *img; + +static fb_buf_t x_backbuf; + +fb_t fb_x = { + .buf = &x_backbuf, + .open = &fb_x_open, + .close = &fb_x_close, + .draw = &fb_x_draw, +}; + +/******************************************************************************* + * Code + ******************************************************************************/ + +static void fb_x_open(void) +{ + display = XOpenDisplay(NULL); + if (!display) + exit(1); + + window = XCreateWindow( + display, + DefaultRootWindow(display), + 0, + 0, + FB_WIDTH, + FB_HEIGHT, + 0, + CopyFromParent, + InputOutput, + CopyFromParent, + CWBackingStore | CWEventMask | CWBackPixel, + &((XSetWindowAttributes){ + .backing_store = Always, + .event_mask = StructureNotifyMask, + .background_pixel = BlackPixel(display, DefaultScreen(display))})); + + const char *s = "PanPI " VERSION; + XTextProperty prop; + XStringListToTextProperty((char **)&s, 1, &prop); + XSetWMProperties(display, window, &prop, &prop, NULL, 0, NULL, NULL, NULL); + + wm_delete_window = XInternAtom(display, "WM_DELETE_WINDOW", False); + XSetWMProtocols(display, window, &wm_delete_window, 1); + + img = XCreateImage( + display, + DefaultVisual(display, DefaultScreen(display)), + 24, + ZPixmap, + 0, + malloc(FB_WIDTH * FB_HEIGHT * 4), + FB_WIDTH, + FB_HEIGHT, + 32, + 0); + + XMapWindow(display, window); +} + +static void fb_x_close(void) +{ +} + +static void fb_x_draw(void) +{ + for (int y = 0; y < FB_HEIGHT; y++) + for (int x = 0; x < FB_WIDTH; x++) + XPutPixel(img, x, y, colour16_to_24(x_backbuf.xy[y][x])); + + XPutImage(display, window, DefaultGC(display, DefaultScreen(display)), img, 0, 0, 0, 0, FB_WIDTH, FB_HEIGHT); + + while (XPending(display)) + { + XEvent e; + XNextEvent(display, &e); + + switch (e.type) + { + case ClientMessage: + if ((Atom)e.xclient.data.l[0] == wm_delete_window) exit(0); + default: + break; + } + } +} diff --git a/src/framebuffer/framebuffer_raw.c b/src/framebuffer/framebuffer_raw.c new file mode 100644 index 0000000..74e99f5 --- /dev/null +++ b/src/framebuffer/framebuffer_raw.c @@ -0,0 +1,70 @@ +/* =================================== PanPI =================================== + * Linux framebuffer wrapper module + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#include "common.h" +#include "framebuffer.h" +#include "layout.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +#define FD_OPEN_FAILED (-1) + +/******************************************************************************* + * Prototypes + ******************************************************************************/ + +static void fb_raw_open(void); +static void fb_raw_close(void); +static void fb_raw_draw(void); + +/******************************************************************************* + * Variables + ******************************************************************************/ + +static int fbfd = FD_OPEN_FAILED; +static fb_buf_t *frontbuf = NULL; +static fb_buf_t raw_backbuf; + +fb_t fb_raw = { + .buf = &raw_backbuf, + .open = &fb_raw_open, + .close = &fb_raw_close, + .draw = &fb_raw_draw, +}; + +/******************************************************************************* + * Code + ******************************************************************************/ + +static void fb_raw_open(void) +{ + fbfd = open("/dev/fb0", O_RDWR); + if (fbfd == FD_OPEN_FAILED) + { + eprintf("Cannot open framebuffer device\n"); + exit(1); + } + + frontbuf = mmap(0, sizeof(fb_buf_t), PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0); + if (frontbuf == MAP_FAILED) + { + eprintf("Failed to map framebuffer device to memory\n"); + exit(1); + } +} + +static void fb_raw_close(void) +{ + munmap(frontbuf, sizeof(fb_buf_t)); + close(fbfd); +} + +static void fb_raw_draw(void) +{ + memcpy(frontbuf, &raw_backbuf, sizeof(*frontbuf)); +} diff --git a/src/layout.h b/src/layout.h new file mode 100644 index 0000000..7e0ac70 --- /dev/null +++ b/src/layout.h @@ -0,0 +1,40 @@ +/* =================================== PanPI =================================== + * Display layout/geometry definitions + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#pragma once + +#include "common.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +#define TARGET_FPS 60 + +#define FB_HEIGHT 320 +#define FB_WIDTH 480 + +#define SGAM_TOP 6 +#define SGAM_HEIGHT 152 +#define SGAM_LEFT 6 +#define SGAM_WIDTH 468 + +#define WFALL_TOP (SGAM_TOP + SGAM_HEIGHT + 3) +#define WFALL_HEIGHT (FB_HEIGHT - WFALL_TOP - 6) +#define WFALL_LEFT SGAM_LEFT +#define WFALL_WIDTH SGAM_WIDTH + +#define PIP_INTERVAL_HZ 10000 + +#define WHITE colour(255, 255, 255) +#define YELLOW colour(255, 255, 0) +#define BGCOL colour(0x00, 0x07, 0x17) +#define CLINECOL colour(0x50, 0x50, 0x50) +#define HLINECOL colour(0x50, 0x50, 0x50) + +/******************************************************************************* + * API + ******************************************************************************/ diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..eee3785 --- /dev/null +++ b/src/main.c @@ -0,0 +1,87 @@ +/* =================================== PanPI =================================== + * Main entry point + * (C) 2022 Ryan Suchocki + * http://suchocki.co.uk/ + */ + +#include "capture.h" +#include "common.h" +#include "config.h" +#include "display.h" +#include "dsp.h" +#include "layout.h" + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +#define FFT_SIZE SGAM_WIDTH // NB small prime factors + +/******************************************************************************* + * Prototypes + ******************************************************************************/ + +/******************************************************************************* + * Variables + ******************************************************************************/ + +static volatile bool should_run = true; + +unsigned sample_rate = 0; + +/******************************************************************************* + * Code + ******************************************************************************/ + +static void sigint_handler(int signum) +{ + UNUSED(signum); + + should_run = false; +} + +int main(int argc, const char *argv[]) +{ + UNUSED(argc); + UNUSED(argv); + + signal(SIGINT, sigint_handler); + + printf("PanPI " VERSION "\n"); + + config_init(); + sample_rate = config.sample_rate; + dsp_init(FFT_SIZE); + + display_open(sample_rate); + + capture_t capture = capture_init(config.source); + + capture.open(sample_rate); + + while (should_run) + { + static complex double samples[FFT_SIZE]; + capture.get(samples, FFT_SIZE); + + static double amplitudes[FFT_SIZE]; + dsp_process(samples, amplitudes); + + display_update(amplitudes); + + if (config_update()) + { + display_update_bg(sample_rate); + printf("BG\n"); + } + } + + printf("Goodbye\n"); + + capture.close(); + display_close(); + + dsp_free(); + + return 0; +} diff --git a/src/template.cfg b/src/template.cfg new file mode 100644 index 0000000..f6a62a9 --- /dev/null +++ b/src/template.cfg @@ -0,0 +1,37 @@ + +x_window: true +# Source type selection. Should be 'soundcard' or 'file' +# source: soundcard +source: file + +# Device selection (only applies if the source type is 'soundcard') +# May be an ALSA identifier string (such as 'hw:1,0' or 'default) +# or the word 'auto', in which case PanPI tries to select the first +# available sound card which looks like an external (USB) one. +# device: auto + +# Source file path (only applies if the source type is 'file') +file: test/1min.dat + +# Capture sample rate +sample_rate: 96000 + +# Capture gain level +capture_gain: 1.0 + +# Sharpness parameter for the DSP "DC offset" removal filter +dc_alpha: 0.999 + +# Upper and lower reference levels for the spectrogram and +# waterfall display. Use this to "zoom in/out" vertically. +refl: 8.0 +refh: 18.0 + +# Smoothing factors for the spectrogram display +sgam_spread: 1 +sgam_drag: 0.9 + +# Vertical scrolling speed divisor for the waterfall display +# The larger this number, the slower the waterfall moves and +# thus the larger the time period on display at once. +wfall_zoom: 5 diff --git a/third_party/blit-fonts b/third_party/blit-fonts new file mode 160000 index 0000000..8baf38b --- /dev/null +++ b/third_party/blit-fonts @@ -0,0 +1 @@ +Subproject commit 8baf38ba5c3baa923aca70eaabdd15c4d911a4db