diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..f566801 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2013 Andras Veres-Szentkiralyi + +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/README.md b/README.md new file mode 100644 index 0000000..cb85581 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +SSTV generator in pure Python +============================= + +PySSTV generates SSTV modulated WAV files from any image that PIL can open +(PNG, JPEG, GIF, and many others). These WAV files then can be played by any +audio player connected to a shortwave radio for example. + +My main motivation was to understand the internals of SSTV in practice, so +performance is far from optimal. I tried keeping the code readable, and only +performed such optimizations that wouldn't have complicated the codebase. + +Command line usage +------------------ + + usage: run.py [-h] + [--mode {MartinM2,MartinM1,Robot24BW,ScottieS2,ScottieS1,Robot8BW}] + [--rate RATE] [--bits BITS] + image.png output.wav + + Converts an image to an SSTV modulated WAV file. + + positional arguments: + image.png input image file name + output.wav output WAV file name + + optional arguments: + -h, --help show this help message and exit + --mode {MartinM2,MartinM1,Robot24BW,ScottieS2,ScottieS1,Robot8BW} + image mode (default: Martin M1) + --rate RATE sampling rate (default: 48000) + --bits BITS bits per sample (default: 16) + +Python interface +---------------- + +The `SSTV` class in the `sstv` module implements basic SSTV-related +functionality, and the classes of other modules such as `grayscale` and +`color` extend this. Most instances implement the following methods: + + - `__init__` takes a PIL image, the samples per second, and the bits per + sample as a parameter, but doesn't perform any hard calculations + - `gen_freq_bits` generates tuples that describe a sine wave segment with + frequency in Hz and duration in ms + - `gen_values` generates samples between -1 and +1, performing sampling + according to the samples per second value given during construction + - `gen_samples` generates discrete samples, performing quantization + according to the bits per sample value given during construction + - `write_wav` writes the whole image to a Microsoft WAV file + +The above methods all build upon those above them, for example `write_wav` +calls `gen_samples`, while latter calls `gen_values`, so typically, only +the first and the last, maybe the last two should be called directly, the +others are just listed here for the sake of completeness and to make the +flow easier to understand. + +License +------- + +The whole project is available under MIT license. + +Useful links +------------ + + - receive-only "counterpart": https://github.com/windytan/slowrx + - free SSTV handbook: http://www.sstv-handbook.com/ + +Dependencies +------------ + + - Python 2.7 (tested on 2.7.5) + - Python Imaging Library (Debian/Ubuntu package: `python-imaging`) diff --git a/color.py b/color.py index 3ebb6b5..2b9aeaf 100644 --- a/color.py +++ b/color.py @@ -70,3 +70,5 @@ class ScottieS2(ScottieS1): VIS_CODE = 0x38 SCAN = 88.064 - ScottieS1.INTER_CH_GAP WIDTH = 160 + +MODES = (MartinM1, MartinM2, ScottieS1, ScottieS2) diff --git a/grayscale.py b/grayscale.py index 10422a3..5cd4d37 100644 --- a/grayscale.py +++ b/grayscale.py @@ -26,8 +26,8 @@ class Robot8BW(GrayscaleSSTV): VIS_CODE = 0x02 WIDTH = 160 HEIGHT = 120 - SYNC = 10 - SCAN = 56 + SYNC = 7 + SCAN = 60 class Robot24BW(GrayscaleSSTV): @@ -36,3 +36,5 @@ class Robot24BW(GrayscaleSSTV): HEIGHT = 240 SYNC = 12 SCAN = 93 + +MODES = (Robot8BW, Robot24BW) diff --git a/run.py b/run.py new file mode 100644 index 0000000..f4da7f2 --- /dev/null +++ b/run.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +from __future__ import print_function +from PIL import Image +from argparse import ArgumentParser +from sys import stderr +import color, grayscale + +SSTV_MODULES = [color, grayscale] + +def main(): + module_map = build_module_map() + parser = ArgumentParser( + description='Converts an image to an SSTV modulated WAV file.') + parser.add_argument('img_file', metavar='image.png', + help='input image file name') + parser.add_argument('wav_file', metavar='output.wav', + help='output WAV file name') + parser.add_argument('--mode', dest='mode', default='MartinM1', choices=module_map, + help='image mode (default: Martin M1)') + parser.add_argument('--rate', dest='rate', type=int, default=48000, + help='sampling rate (default: 48000)') + parser.add_argument('--bits', dest='bits', type=int, default=16, + help='bits per sample (default: 16)') + args = parser.parse_args() + image = Image.open(args.img_file) + mode = module_map[args.mode] + if not all(i >= m for i, m in zip(image.size, (mode.WIDTH, mode.HEIGHT))): + print(('Image must be at least {m.WIDTH} x {m.HEIGHT} pixels ' + 'for mode {m.__name__}').format(m=mode), file=stderr) + raise SystemExit(1) + s = mode(image, args.rate, args.bits) + s.write_wav(args.wav_file) + +def build_module_map(): + module_map = {} + for module in SSTV_MODULES: + for mode in module.MODES: + module_map[mode.__name__] = mode + return module_map + + +if __name__ == '__main__': + main() diff --git a/sstv.py b/sstv.py index b860ae6..6388e7d 100644 --- a/sstv.py +++ b/sstv.py @@ -3,7 +3,8 @@ from __future__ import division, with_statement from math import sin, pi, floor from random import random -import struct +from contextlib import closing +import struct, wave FREQ_VIS_BIT1 = 1100 FREQ_SYNC = 1200 @@ -25,23 +26,13 @@ class SSTV(object): BITS_TO_STRUCT = {8: 'b', 16: 'h'} def write_wav(self, filename): - bytes_per_sec = self.bits // 8 fmt = '<' + self.BITS_TO_STRUCT[self.bits] data = ''.join(struct.pack(fmt, b) for b in self.gen_samples()) - payload = ''.join(( - 'WAVE', - 'fmt ', - struct.pack('