An audit rather than a feature, prompted by wanting this fit to hand to somebody else. Five options had no command-line flag written down in the table the manual is generated from -- location, hold, schedules, tile_url and speed_unit -- so five flags that exist were missing from the manual. Four of them did exist under other names and are now recorded; hold had no flag at all and has one. Four switches could be turned off from the command line and not back on: --no-lookup, --no-basemap, --no-airports and --no-labels had no positive halves, so an option turned off in the saved settings could not be turned on again for one run. All four now have both. And adsb, which opens the window and draws a map when it stops, could not be given any of the settings that decide what those look like: no --at, no --radius, no --tiles, no --map-brightness, no --width, --fps, --trail, --fade, --stale, --airports or --labels. It takes all of them now. The manual had no list of the aircraft options at all -- the ADS-B sections were hand-written prose -- so five of them appeared nowhere in it. It now generates an AIRCRAFT OPTIONS section from the same table the menu and the flags come from, and the readme carries a table of all thirty-four with their flags and defaults. Three tests hold the three of them together: one that every option records its flag, one that every flag the table claims actually exists on a command, and one that the readme names them all. The installing instructions now list every dependency rather than only the optional ones: the four Python packages with their names in Debian, Fedora and Arch, and librtlsdr, which is a C library and therefore the one thing pip cannot bring and a virtual environment cannot supply. What reaches a network is written down too -- which host, when, and which file it is cached in -- since somebody installing this on a metered or air-gapped machine has to be able to see that nothing is fetched behind their back. build-repo.sh needs dpkg-dev and apt-utils, which a minimal system does not have, and now says so. Verified rather than asserted: a clean virtual environment, pip install from this tree, and a real log read back through the installed command. It pulls seven wheels rather than the four the page claimed, the other three being what Rich brings with it. Also: matplotlib is gone from the readme's dependency table, nothing having imported it; ffmpeg and Qt are in it, both having been missing; ffmpeg is a Suggests on the package; and resume.sh is ignored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
463 lines
20 KiB
Markdown
463 lines
20 KiB
Markdown
# Installing bandsaunter
|
|
|
|
Step by step, from nothing to a scan running. The whole of it is four
|
|
dependencies and one C library; there is nothing to compile and no model to
|
|
download unless you want speech transcription.
|
|
|
|
If you are on Debian, Ubuntu or Mint, [build the package](#a-debian-ubuntu-mint-the-package)
|
|
— it is two commands and apt does the rest. Anywhere else, use
|
|
[a virtual environment](#b-anywhere-else-a-virtual-environment).
|
|
|
|
**You can try the whole program before buying or plugging in a receiver.**
|
|
`--simulate` runs the scanner against a synthetic band, and `bandsaunter adsb
|
|
--simulate` flies imaginary aircraft past an imaginary receiver. Neither needs
|
|
hardware or a network.
|
|
|
|
---
|
|
|
|
## What you need
|
|
|
|
| | |
|
|
|---|---|
|
|
| **Python** | 3.10 or newer |
|
|
| **A receiver** | any RTL2832U dongle (RTL-SDR Blog v3/v4, NESDR, generic) — *optional*, everything except live scanning works without one |
|
|
| **An aerial** | whatever came with the dongle to start; a band-cut aerial for anything serious |
|
|
| **Disk** | ~250 MB for the program and its dependencies; recordings are what actually fill a disk |
|
|
| **Network** | only for installing, and for callsign and aircraft lookups afterwards |
|
|
|
|
Operating systems: developed and packaged on Debian; runs on any Linux with
|
|
Python 3.10+, and on macOS with `brew install librtlsdr`. On Windows it will
|
|
run under WSL2 with USB passthrough, which is more trouble than it is worth —
|
|
a Raspberry Pi is the easier answer.
|
|
|
|
---
|
|
|
|
## A. Debian, Ubuntu, Mint: the package
|
|
|
|
```bash
|
|
git clone <the repository> bandsaunter
|
|
cd bandsaunter
|
|
./packaging/build-deb.sh # writes dist/bandsaunter_<version>_all.deb
|
|
sudo apt install ./dist/bandsaunter_*.deb
|
|
```
|
|
|
|
That is the whole of it. `build-deb.sh` needs only `dpkg-deb` and `fakeroot`,
|
|
both already present on a normal system; apt then pulls in numpy, scipy, rich,
|
|
PyYAML and librtlsdr itself.
|
|
|
|
The package also does the one piece of system setup that a `pip` install
|
|
cannot: it blacklists the DVB-T television driver, which claims these dongles
|
|
on sight. Read what it prints — if a receiver was plugged in during the
|
|
install, unplug and replug it, or run `sudo rmmod dvb_usb_rtl28xxu` once.
|
|
|
|
Uninstall with `sudo apt remove bandsaunter`, which also removes the blacklist.
|
|
|
|
### For several machines: your own apt repository
|
|
|
|
`./packaging/build-repo.sh` builds the application, a speech recogniser and a
|
|
model into a small apt repository under `dist/repo`, so that `apt install
|
|
bandsaunter` on every other machine brings transcription with it. See
|
|
**Install → From your own apt repository** in the README.
|
|
|
|
It needs two more tools than `build-deb.sh` does, neither of which is on a
|
|
minimal system:
|
|
|
|
```bash
|
|
sudo apt install dpkg-dev apt-utils # dpkg-scanpackages and apt-ftparchive
|
|
```
|
|
|
|
---
|
|
|
|
## B. Anywhere else: a virtual environment
|
|
|
|
**Debian, Ubuntu and Fedora refuse `pip install` outside a virtual
|
|
environment** (PEP 668: *"externally-managed-environment"*). That is the error
|
|
most people hit first, and a virtual environment is the answer:
|
|
|
|
```bash
|
|
sudo apt install librtlsdr0 # Debian/Ubuntu/Mint
|
|
# sudo dnf install rtl-sdr # Fedora
|
|
# sudo pacman -S rtl-sdr # Arch
|
|
# brew install librtlsdr # macOS
|
|
|
|
git clone <the repository> bandsaunter
|
|
cd bandsaunter
|
|
python3 -m venv .venv
|
|
. .venv/bin/activate
|
|
pip install . # a few wheels; under a minute
|
|
```
|
|
|
|
That installs seven wheels — numpy, scipy, rich and PyYAML, plus the three
|
|
Rich brings with it (Pygments, markdown-it-py, mdurl) — and puts two commands
|
|
on the path, `bandsaunter` and `saunterbrowse`. Nothing is built from source,
|
|
and it takes under a minute.
|
|
|
|
Use `pip install -e .` instead if you intend to change the code.
|
|
|
|
The commands only exist while the environment is active. To have them always,
|
|
install with [pipx](https://pipx.pypa.io) instead, which keeps its own
|
|
environment and puts the commands on your path permanently:
|
|
|
|
```bash
|
|
sudo apt install pipx && pipx install /path/to/bandsaunter
|
|
```
|
|
|
|
As a last resort — a throwaway machine, a container — `pip install
|
|
--break-system-packages .` overrides the refusal. It puts the dependencies in
|
|
among the system ones, which is what the warning is about.
|
|
|
|
---
|
|
|
|
## Check that it worked
|
|
|
|
```bash
|
|
bandsaunter --version
|
|
bandsaunter bands # the built-in US band plan; no hardware needed
|
|
bandsaunter scan -b 2m --simulate # a whole scan against a synthetic band
|
|
bandsaunter adsb --simulate --seconds 30
|
|
bandsaunter flights # draws what the last command heard
|
|
```
|
|
|
|
With a receiver plugged in:
|
|
|
|
```bash
|
|
bandsaunter devices # should list the dongle
|
|
bandsaunter devices --test # opens it and captures a block
|
|
bandsaunter # interactive setup, then scanning
|
|
```
|
|
|
|
`bandsaunter devices` is the command to run when something is wrong: it says
|
|
whether librtlsdr loaded, whether a dongle was found, and what to do about it
|
|
if not.
|
|
|
|
---
|
|
|
|
## Every dependency, in one table
|
|
|
|
**Required.** Four Python packages and one system library. The `.deb` and
|
|
`pip install` both pull the Python ones in; the system library is the only
|
|
thing either way that has to come from your distribution.
|
|
|
|
| | Debian/Ubuntu/Mint | Fedora | Arch | what needs it |
|
|
|---|---|---|---|---|
|
|
| Python 3.10+ | `python3` | `python3` | `python` | everything |
|
|
| NumPy | `python3-numpy` | `python3-numpy` | `python-numpy` | every signal path, every picture |
|
|
| SciPy | `python3-scipy` | `python3-scipy` | `python-scipy` | filtering, demodulation, classifying |
|
|
| Rich | `python3-rich` | `python3-rich` | `python-rich` | the menus and the live display |
|
|
| PyYAML | `python3-yaml` | `python3-pyyaml` | `python-yaml` | the settings file |
|
|
| **librtlsdr** | `librtlsdr0` | `rtl-sdr` | `rtl-sdr` | **talking to the dongle** |
|
|
|
|
**`librtlsdr` is the one manual dependency that matters.** It is a C library,
|
|
not a Python package, so `pip` cannot install it and no virtual environment
|
|
brings it with it. bandsaunter opens it with `ctypes` at runtime, trying
|
|
`librtlsdr.so.2`, `.so.0`, `.so`, `librtlsdr.dylib`, `rtlsdr.dll` and
|
|
`librtlsdr.dll` in that order.
|
|
Without it everything that does not touch the hardware still works — reading
|
|
logs, drawing maps, decoding recordings, the simulated sky — and anything that
|
|
does says so plainly instead of failing:
|
|
|
|
```sh
|
|
sudo apt install librtlsdr0 # Debian, Ubuntu, Mint
|
|
sudo dnf install rtl-sdr # Fedora
|
|
sudo pacman -S rtl-sdr # Arch
|
|
brew install librtlsdr # macOS
|
|
```
|
|
|
|
**Optional.** Everything below is genuinely optional. The program starts,
|
|
scans, records, identifies, decodes Morse, draws waterfalls and maps with none
|
|
of it, and says plainly when a feature is unavailable rather than failing.
|
|
|
|
| Install | Gives you | Without it |
|
|
|---|---|---|
|
|
| `sudo apt install python3-pyqt6` | the realtime aircraft window (`bandsaunter adsb --window`) | the terminal board still shows every aircraft, and the maps are still drawn afterwards |
|
|
| `sudo apt install ffmpeg` | `bandsaunter flights --out sky.mp4` writes a video | a GIF is written instead, and it says so |
|
|
| `sudo apt install espeak-ng` | clearer spoken timestamps on combined recordings, rendered about three times faster | a built-in formant synthesiser does the same job, less clearly |
|
|
| `sudo apt install rtl-sdr` | `rtl_test`, `rtl_sdr` and friends, for diagnosing the hardware | nothing missing from bandsaunter itself |
|
|
| `pip install faster-whisper` | speech transcription of recorded voice — see [the step-by-step below](#speech-transcription-step-by-step) (~250 MB installed, plus a 148 MB model) | transcription is off; scans report that no recogniser is installed |
|
|
| `pip install vosk` | a smaller, weaker recogniser (~10 MB plus a 40 MB model) | as above |
|
|
| `pip install openai-whisper` | the reference Whisper, slower and heavier than faster-whisper | as above |
|
|
| `pip install pocketsphinx` | a tiny recogniser, poor on radio audio | as above |
|
|
| whisper.cpp (`whisper-cli` on the PATH) | transcription with no Python dependencies at all | as above |
|
|
| `pip install pyte` | the terminal-resize tests | those tests skip |
|
|
| `pip install pytest` | running the test suite | you cannot run the tests |
|
|
|
|
`bandsaunter transcribe --list` says which recognisers it can actually see,
|
|
and which one it would use.
|
|
|
|
**Nothing is downloaded behind your back.** The things that reach a network do
|
|
so only when you ask, and each is cached on disk afterwards:
|
|
|
|
| What | Where from | When | Kept in |
|
|
|---|---|---|---|
|
|
| aircraft and route lookups | `api.adsbdb.com`, `hexdb.io` | while listening, unless `--no-lookup` | `~/.cache/bandsaunter/flights.json`, a month |
|
|
| amateur callsign lookups | `callook.info`, `api.hamdb.org` | when a scan hears a callsign, unless turned off | `~/.cache/bandsaunter/callsigns.json`, a month |
|
|
| map tiles | `tile.openstreetmap.org`, or `--tiles URL` | when a map is drawn, unless `--no-basemap` | `~/.cache/bandsaunter/tiles`, for ever |
|
|
| aerodrome positions | `overpass-api.de` | when a map is drawn, unless `--no-airports` | `~/.cache/bandsaunter/airports`, a month |
|
|
| flight schedules | the four paid services below | only if you have set a key | a month |
|
|
|
|
Every request identifies itself as `bandsaunter` and carries nothing but the
|
|
question — a callsign, a 24-bit address, or a box of the world. No identity,
|
|
no position, no key.
|
|
|
|
**The aircraft window needs Qt**, and any of four bindings will do — PyQt6,
|
|
PyQt5, PySide6 or PySide2 — because distributions disagree about which they
|
|
package. `python3-pyqt6` on Debian and Fedora, `python-pyqt6` on Arch, or
|
|
`pip install PyQt6` in the environment bandsaunter runs from. Without it the
|
|
window is the only thing missing, and the program says so rather than failing.
|
|
|
|
**Aircraft and callsign lookups need no installation**, only a network. They
|
|
ask public registers about a callsign or a 24-bit address and cache the
|
|
answers for a month; `--no-lookup` turns them off, and what the address and
|
|
the callsign say on their own is worked out offline either way.
|
|
|
|
### Optional: a paid schedule service
|
|
|
|
Nothing here needs installing either — these are accounts, not packages, and
|
|
all four are optional. They answer the one question the free registers cannot:
|
|
an airline runs the same flight number over several legs in a day, and a free
|
|
register holds one route per number, so it will often name somebody else's
|
|
leg. A schedule service holds the day's actual movements.
|
|
|
|
| Service | Sign up at | Set |
|
|
|---|---|---|
|
|
| FlightAware AeroAPI | <https://www.flightaware.com/commercial/aeroapi/> | `BANDSAUNTER_AEROAPI_KEY` |
|
|
| Flightradar24 | <https://fr24api.flightradar24.com/> | `BANDSAUNTER_FR24_TOKEN` |
|
|
| OAG Flight Info | <https://developer.oag.com/> | `BANDSAUNTER_OAG_KEY` |
|
|
| Cirium (FlightStats) | <https://developer.cirium.com/> | `BANDSAUNTER_CIRIUM_APP_ID` and `BANDSAUNTER_CIRIUM_APP_KEY` |
|
|
|
|
Put the ones you have in your shell profile:
|
|
|
|
```sh
|
|
echo 'export BANDSAUNTER_AEROAPI_KEY=your-key-here' >> ~/.bashrc
|
|
. ~/.bashrc
|
|
```
|
|
|
|
**Keys are read from the environment and never written to the settings file**,
|
|
on purpose: a settings file gets copied between machines and pasted into
|
|
messages asking for help, and an API key should not travel that way.
|
|
|
|
Any service whose key is set is asked; one whose key is not set is skipped
|
|
silently, and the free registers answer exactly as they did before.
|
|
`--schedules flightaware,oag` picks which to ask and in what order. Only the
|
|
callsign and the time are ever sent.
|
|
|
|
These readers were written from each service's published response format and
|
|
tested against it, but none has been run against a live service, because each
|
|
one needs a paid account. Each is written to return nothing rather than guess,
|
|
so a service that has changed its format costs you a route, not a scan.
|
|
|
|
---
|
|
|
|
## Speech transcription, step by step
|
|
|
|
This is the only part with a real download in it, and the only part Debian
|
|
cannot supply, so it gets its own section. Two pieces are needed: the
|
|
**recogniser** (a Python package) and the **model** it loads (the weights).
|
|
Scanning, recording, identifying and CW decoding all work without either.
|
|
|
|
### Step 1 — install the recogniser where bandsaunter can see it
|
|
|
|
The recogniser must go into the same environment bandsaunter itself runs
|
|
from, or bandsaunter will not find it.
|
|
|
|
If you installed into a virtual environment:
|
|
|
|
```bash
|
|
. .venv/bin/activate # the same one bandsaunter is in
|
|
pip install faster-whisper
|
|
```
|
|
|
|
If you installed for your user with `pip install --user` (the layout this
|
|
project was developed with, everything under `~/.local`):
|
|
|
|
```bash
|
|
pip install --user --break-system-packages faster-whisper
|
|
```
|
|
|
|
`--break-system-packages` is what gets past the PEP 668 refusal described
|
|
above; with `--user` it writes to `~/.local/lib/python3.x/site-packages` and
|
|
touches nothing the distribution owns.
|
|
|
|
This pulls in `ctranslate2`, `av`, `huggingface-hub`, `tokenizers` and
|
|
`onnxruntime` — about 250 MB installed, no compiler needed, all wheels.
|
|
|
|
### Step 2 — check bandsaunter can see it
|
|
|
|
```bash
|
|
bandsaunter transcribe --engines
|
|
```
|
|
|
|
```
|
|
engine installed how to get it
|
|
faster-whisper yes pip install faster-whisper (best on radio audio)
|
|
whisper no pip install openai-whisper
|
|
whisper-cli no whisper.cpp, no Python dependencies
|
|
vosk no pip install vosk (small, weaker on noisy audio)
|
|
pocketsphinx no pip install pocketsphinx (tiny, poor on radio audio)
|
|
auto would use faster-whisper
|
|
```
|
|
|
|
`installed: yes` against `faster-whisper` and a last line naming it is the
|
|
whole test. If it says `no` while pip says the package is there, they are in
|
|
different environments — see [When it does not work](#when-it-does-not-work).
|
|
|
|
### Step 3 — get the model
|
|
|
|
The model is **not** in the pip package. By default `base.en` is used, and
|
|
the first transcription downloads it from Hugging Face
|
|
(`Systran/faster-whisper-base.en`) into `~/.cache/huggingface/hub/`, about
|
|
140 MB. Everything after that is offline.
|
|
|
|
You can leave it to happen by itself, but a scan that appears to hang for a
|
|
minute on its first voice capture is unnerving, so it is better to fetch it
|
|
deliberately:
|
|
|
|
```bash
|
|
python3 -c "from faster_whisper import WhisperModel; WhisperModel('base.en', device='cpu', compute_type='int8')"
|
|
du -sh ~/.cache/huggingface # about 140 MB when it has finished
|
|
```
|
|
|
|
To watch what any engine is doing, including a download in progress:
|
|
|
|
```bash
|
|
BANDSAUNTER_ENGINE_OUTPUT=1 bandsaunter transcribe recording.wav --stdout
|
|
```
|
|
|
|
Model sizes, measured from the repository listings:
|
|
|
|
| Model | Download | Notes |
|
|
|---|---|---|
|
|
| `tiny.en` | 78 MB | fastest; misses words on noisy radio audio |
|
|
| `base.en` | 148 MB | **the default**; keeps up comfortably on an ordinary machine |
|
|
| `small.en` | 486 MB | noticeably better on weak signals, several times slower |
|
|
| `medium.en` | 1.5 GB | better again; wants a fast machine and patience |
|
|
| `large-v3` | 3.1 GB | multilingual, and slow on a CPU |
|
|
|
|
Everything runs **on the CPU**, at int8 — no GPU, no CUDA, no nvidia
|
|
packages, on purpose: a scanner should not need a graphics card, and int8 on
|
|
a CPU keeps up with a scan.
|
|
|
|
### Step 4 — turn it on
|
|
|
|
```bash
|
|
bandsaunter transcribe recording.wav --stdout # one file, straight away
|
|
bandsaunter transcribe ~/bandsaunter # a whole directory
|
|
bandsaunter scan -b 2m --transcribe # during a scan
|
|
bandsaunter config transcribe=true # or make that the default
|
|
bandsaunter config transcribe_model=small.en # and choose the model
|
|
```
|
|
|
|
During a scan, transcription runs on its own thread after each recording is
|
|
written, so it never holds up the sweep. Transcripts land beside the audio as
|
|
`<recording>_transcription.txt`, and `saunterbrowse` shows them.
|
|
|
|
### Doing it without a network, or on many machines
|
|
|
|
`./packaging/build-repo.sh` collects the recogniser and the model into two
|
|
Debian packages, so no machine you install on ever reaches the network:
|
|
|
|
```bash
|
|
MODEL=base.en ./packaging/build-repo.sh # any size from the table above
|
|
```
|
|
|
|
That produces `bandsaunter-transcribe` (the wheels, unpacked into
|
|
`/usr/lib/bandsaunter/vendor`) and `bandsaunter-model-base-en` (the weights,
|
|
in `/usr/share/bandsaunter/models/base.en`), and an apt repository serving
|
|
them alongside `bandsaunter` itself. Both are `Recommends`, so a plain `apt
|
|
install bandsaunter` from that repository brings the lot.
|
|
|
|
For a machine that is not using apt, copy the model directory anywhere and
|
|
point at it:
|
|
|
|
```bash
|
|
export BANDSAUNTER_MODEL_DIR=/opt/models # holds base.en/, small.en/ ...
|
|
bandsaunter config transcribe_model=/opt/models/base.en # or an outright path
|
|
```
|
|
|
|
A model name that matches a directory under `BANDSAUNTER_MODEL_DIR` is used
|
|
from disk; anything else is left to the recogniser to download.
|
|
|
|
### A smaller alternative
|
|
|
|
`vosk` is a tenth of the size and much weaker on the noisy, clipped audio
|
|
radio produces. It is worth it on a Raspberry Pi and not otherwise:
|
|
|
|
```bash
|
|
pip install vosk # ~10 MB
|
|
bandsaunter config transcribe_engine=vosk
|
|
```
|
|
|
|
It fetches its own small model on first use, by language; point
|
|
`transcribe_model` at an unpacked model directory to use a specific one.
|
|
|
|
---
|
|
|
|
## When it does not work
|
|
|
|
**`externally-managed-environment` from pip.** Use a virtual environment or
|
|
pipx, as above. This is the distribution protecting its own Python, not a
|
|
problem with bandsaunter.
|
|
|
|
**`bandsaunter transcribe --engines` says `no` although pip installed it.**
|
|
The recogniser went into a different environment from bandsaunter. Ask each
|
|
of them where it is and compare:
|
|
|
|
```bash
|
|
python3 -c "import bandsaunter, sys; print(sys.executable); print(bandsaunter.__file__)"
|
|
python3 -c "import faster_whisper; print(faster_whisper.__file__)"
|
|
```
|
|
|
|
Install the recogniser with the same `pip` that owns the first path — the
|
|
venv's `pip` for a venv install, `pip install --user --break-system-packages`
|
|
for a `~/.local` one. On a `.deb` install, bandsaunter runs as the system
|
|
`python3` and looks in `/usr/lib/bandsaunter/vendor` as well, which is what
|
|
the `bandsaunter-transcribe` package fills.
|
|
|
|
**The first transcription takes a minute and prints nothing.** It is
|
|
downloading the model. `BANDSAUNTER_ENGINE_OUTPUT=1` shows the progress, and
|
|
Step 3 above fetches it deliberately instead.
|
|
|
|
**`librtlsdr was not found`.** Install it: `librtlsdr0` on Debian/Ubuntu,
|
|
`rtl-sdr` on Fedora and Arch, `brew install librtlsdr` on macOS. Everything
|
|
that does not touch the receiver keeps working without it.
|
|
|
|
**`bandsaunter devices` finds nothing, or cannot open the dongle.** Almost
|
|
always the DVB-T driver, which claims these dongles the moment they are
|
|
plugged in:
|
|
|
|
```bash
|
|
echo 'blacklist dvb_usb_rtl28xxu' | sudo tee /etc/modprobe.d/blacklist-rtl.conf
|
|
sudo rmmod dvb_usb_rtl28xxu
|
|
```
|
|
|
|
Then unplug and replug the receiver. The `.deb` does this for you.
|
|
|
|
**Permission denied opening the device.** On Debian and Ubuntu the
|
|
`librtlsdr0` package installs the udev rule that grants access, so this is
|
|
rare; if you built librtlsdr yourself, you need the rule from its source tree
|
|
in `/etc/udev/rules.d`, then `sudo udevadm control --reload` and a replug.
|
|
|
|
**Nothing is recorded.** That is usually correct behaviour rather than a
|
|
fault: captures are kept only when they carry voice, decodable CW or an
|
|
identified digital scheme. `bandsaunter scan -b 2m --simulate` proves the
|
|
whole path end to end without an aerial.
|
|
|
|
---
|
|
|
|
## Running the tests
|
|
|
|
```bash
|
|
pip install pytest pyte
|
|
python3 -m pytest tests/ -q
|
|
```
|
|
|
|
About 1500 tests and around fifteen minutes; they need no hardware and no
|
|
network.
|
|
|
|
---
|
|
|
|
## Licence
|
|
|
|
GNU General Public License, version 3 or later. The full text is in
|
|
[LICENSE](LICENSE), and in an installed package at
|
|
`/usr/share/doc/bandsaunter/copyright`.
|