Compare commits

..

No commits in common. "master" and "v0.4" have entirely different histories.
master ... v0.4

15 changed files with 95 additions and 233 deletions

View file

@ -1,40 +0,0 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python package
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest

View file

@ -1,9 +1,10 @@
language: python language: python
python: python:
- "3.6" - "2.7"
- "3.7" - "3.2"
- "3.8" - "3.3"
- "3.9" - "3.4"
- "3.5"
sudo: false sudo: false
install: "pip install -r requirements.txt" install: "pip install -r requirements.txt"
script: nosetests script: nosetests

View file

@ -1,6 +1,8 @@
SSTV generator in pure Python SSTV generator in pure Python
============================= =============================
[![Build Status](https://travis-ci.org/dnet/pySSTV.svg?branch=master)](https://travis-ci.org/dnet/pySSTV)
PySSTV generates SSTV modulated WAV files from any image that PIL can open 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 (PNG, JPEG, GIF, and many others). These WAV files then can be played by any
audio player connected to a shortwave radio for example. audio player connected to a shortwave radio for example.
@ -14,10 +16,9 @@ Command line usage
$ python -m pysstv -h $ python -m pysstv -h
usage: __main__.py [-h] usage: __main__.py [-h]
[--mode {MartinM1,MartinM2,ScottieS1,ScottieS2,ScottieDX,Robot36,PasokonP3,PasokonP5,PasokonP7,PD90,PD120,PD160,PD180,PD240,PD290,WraaseSC2120,WraaseSC2180,Robot8BW,Robot24BW}] [--mode {MartinM1,MartinM2,ScottieS1,ScottieS2,Robot36,PasokonP3,PasokonP5,PasokonP7,PD90,PD120,PD160,PD180,PD240,Robot8BW,Robot24BW}]
[--rate RATE] [--bits BITS] [--vox] [--fskid FSKID] [--rate RATE] [--bits BITS] [--vox] [--fskid FSKID]
[--chan CHAN] [--resize] [--keep-aspect-ratio] [--chan CHAN]
[--keep-aspect] [--resample {nearest,bicubic,lanczos}]
image.png output.wav image.png output.wav
Converts an image to an SSTV modulated WAV file. Converts an image to an SSTV modulated WAV file.
@ -26,23 +27,15 @@ Command line usage
image.png input image file name image.png input image file name
output.wav output WAV file name output.wav output WAV file name
options: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
--mode {MartinM1,MartinM2,ScottieS1,ScottieS2,ScottieDX,Robot36,PasokonP3,PasokonP5,PasokonP7,PD90,PD120,PD160,PD180,PD240,PD290,WraaseSC2120,WraaseSC2180,Robot8BW,Robot24BW} --mode {MartinM1,MartinM2,ScottieS1,ScottieS2,Robot36,PasokonP3,PasokonP5,PasokonP7,PD90,PD120D160,PD180,PD240,Robot8BW,Robot24BW}
image mode (default: Martin M1) image mode (default: Martin M1)
--rate RATE sampling rate (default: 48000) --rate RATE sampling rate (default: 48000)
--bits BITS bits per sample (default: 16) --bits BITS bits per sample (default: 16)
--vox add VOX tones at the beginning --vox add VOX tones at the beginning
--fskid FSKID add FSKID at the end --fskid FSKID add FSKID at the end
--chan CHAN number of channels (default: mono) --chan CHAN number of channels (default: mono)
--resize resize the image to the correct size
--keep-aspect-ratio keep the original aspect ratio when resizing
(and cut off excess pixels)
--keep-aspect keep the original aspect ratio when resizing
(not cut off excess pixels)
--resample {nearest,bicubic,lanczos}
which resampling filter to use for resizing
(see Pillow documentation)
Python interface Python interface
---------------- ----------------
@ -82,5 +75,6 @@ Useful links
Dependencies Dependencies
------------ ------------
- Python 3.5 or later - Python 2.7 (tested on 2.7.9; 2.6 might work, but test suite lacks support)
- Python Imaging Library (Debian/Ubuntu package: `python3-pil`) or 3.x (tested on 3.2, 3.3, 3.4 and 3.5)
- Python Imaging Library (Debian/Ubuntu package: `python-imaging`)

View file

@ -1,3 +0,0 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import print_function, division from __future__ import print_function
from PIL import Image from PIL import Image
from argparse import ArgumentParser from argparse import ArgumentParser
from sys import stderr from sys import stderr
@ -30,53 +30,10 @@ def main():
help='add FSKID at the end') help='add FSKID at the end')
parser.add_argument('--chan', dest='chan', type=int, parser.add_argument('--chan', dest='chan', type=int,
help='number of channels (default: mono)') help='number of channels (default: mono)')
parser.add_argument('--resize', dest='resize', action='store_true',
help='resize the image to the correct size')
parser.add_argument('--keep-aspect-ratio', dest='keep_aspect_ratio', action='store_true',
help='keep the original aspect ratio when resizing (and cut off excess pixels)')
parser.add_argument('--keep-aspect', dest='keep_aspect', action='store_true',
help='keep the original aspect ratio when resizing (not cut off excess pixels)')
parser.add_argument('--resample', dest='resample', default='lanczos',
choices=('nearest', 'bicubic', 'lanczos'),
help='which resampling filter to use for resizing (see Pillow documentation)')
args = parser.parse_args() args = parser.parse_args()
image = Image.open(args.img_file) image = Image.open(args.img_file)
mode = module_map[args.mode] mode = module_map[args.mode]
if args.resize and any(i != m for i, m in zip(image.size, (mode.WIDTH, mode.HEIGHT))): if not all(i >= m for i, m in zip(image.size, (mode.WIDTH, mode.HEIGHT))):
resample = getattr(Image, args.resample.upper())
if args.keep_aspect_ratio or args.keep_aspect:
orig_ratio = image.width / image.height
mode_ratio = mode.WIDTH / mode.HEIGHT
crop = orig_ratio != mode_ratio
else:
crop = False
if crop:
t = orig_ratio < mode_ratio
if args.keep_aspect:
t = orig_ratio > mode_ratio
if t:
w = mode.WIDTH
h = int(w / orig_ratio)
else:
h = mode.HEIGHT
w = int(orig_ratio * h)
else:
w = mode.WIDTH
h = mode.HEIGHT
image = image.resize((w, h), resample)
if args.keep_aspect:
newbg = Image.new('RGB', (mode.WIDTH, mode.HEIGHT))
if t:
newbg.paste(image, (0, int((mode.HEIGHT/2)-(h/2))))
else:
newbg.paste(image, (int((mode.WIDTH/2)-(w/2)), 0))
image = newbg.copy()
crop = False
if crop:
x = (image.width - mode.WIDTH) / 2
y = (image.height - mode.HEIGHT) / 2
image = image.crop((x, y, mode.WIDTH + x, mode.HEIGHT + y))
elif 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 ' print(('Image must be at least {m.WIDTH} x {m.HEIGHT} pixels '
'for mode {m.__name__}').format(m=mode), file=stderr) 'for mode {m.__name__}').format(m=mode), file=stderr)
raise SystemExit(1) raise SystemExit(1)

View file

@ -1,40 +1,39 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import division from __future__ import division
from six.moves import range, zip
from pysstv.sstv import byte_to_freq, FREQ_BLACK, FREQ_WHITE, FREQ_VIS_START from pysstv.sstv import byte_to_freq, FREQ_BLACK, FREQ_WHITE, FREQ_VIS_START
from pysstv.grayscale import GrayscaleSSTV from pysstv.grayscale import GrayscaleSSTV
from itertools import chain from itertools import chain
from enum import Enum
class Color(Enum): RED, GREEN, BLUE = range(3)
red = 0
green = 1
blue = 2
class ColorSSTV(GrayscaleSSTV): class ColorSSTV(GrayscaleSSTV):
def on_init(self): def on_init(self):
self.pixels = self.image.convert('RGB').load() self.pixels = self.image.load()
def encode_line(self, line): def encode_line(self, line):
msec_pixel = self.SCAN / self.WIDTH msec_pixel = self.SCAN / self.WIDTH
image = self.pixels image = self.pixels
for color in self.COLOR_SEQ: for index in self.COLOR_SEQ:
yield from self.before_channel(color) for item in self.before_channel(index):
yield item
for col in range(self.WIDTH): for col in range(self.WIDTH):
pixel = image[col, line] pixel = image[col, line]
freq_pixel = byte_to_freq(pixel[color.value]) freq_pixel = byte_to_freq(pixel[index])
yield freq_pixel, msec_pixel yield freq_pixel, msec_pixel
yield from self.after_channel(color) for item in self.after_channel(index):
yield item
def before_channel(self, color): def before_channel(self, index):
return [] return []
after_channel = before_channel after_channel = before_channel
class MartinM1(ColorSSTV): class MartinM1(ColorSSTV):
COLOR_SEQ = (Color.green, Color.blue, Color.red) COLOR_SEQ = (GREEN, BLUE, RED)
VIS_CODE = 0x2c VIS_CODE = 0x2c
WIDTH = 320 WIDTH = 320
HEIGHT = 256 HEIGHT = 256
@ -42,11 +41,11 @@ class MartinM1(ColorSSTV):
SCAN = 146.432 SCAN = 146.432
INTER_CH_GAP = 0.572 INTER_CH_GAP = 0.572
def before_channel(self, color): def before_channel(self, index):
if color is Color.green: if index == GREEN:
yield FREQ_BLACK, self.INTER_CH_GAP yield FREQ_BLACK, self.INTER_CH_GAP
def after_channel(self, color): def after_channel(self, index):
yield FREQ_BLACK, self.INTER_CH_GAP yield FREQ_BLACK, self.INTER_CH_GAP
@ -65,9 +64,10 @@ class ScottieS1(MartinM1):
def horizontal_sync(self): def horizontal_sync(self):
return [] return []
def before_channel(self, color): def before_channel(self, index):
if color is Color.red: if index == RED:
yield from MartinM1.horizontal_sync(self) for item in MartinM1.horizontal_sync(self):
yield item
yield FREQ_BLACK, self.INTER_CH_GAP yield FREQ_BLACK, self.INTER_CH_GAP
@ -77,12 +77,6 @@ class ScottieS2(ScottieS1):
WIDTH = 160 WIDTH = 160
class ScottieDX(ScottieS1):
VIS_CODE = 0x4c
# http://www.barberdsp.com/downloads/Dayton%20Paper.pdf
SCAN = 345.6000 - ScottieS1.INTER_CH_GAP
class Robot36(ColorSSTV): class Robot36(ColorSSTV):
VIS_CODE = 0x08 VIS_CODE = 0x08
WIDTH = 320 WIDTH = 320
@ -124,7 +118,7 @@ class PasokonP3(ColorSSTV):
Horizontal Sync - 25 time units of 1200 Hz. Horizontal Sync - 25 time units of 1200 Hz.
""" """
TIMEUNIT = 1000/4800. # ms TIMEUNIT = 1000/4800. # ms
COLOR_SEQ = (Color.red, Color.green, Color.blue) COLOR_SEQ = (RED, GREEN, BLUE)
VIS_CODE = 0x71 VIS_CODE = 0x71
WIDTH = 640 WIDTH = 640
HEIGHT = 480+16 HEIGHT = 480+16
@ -132,11 +126,11 @@ class PasokonP3(ColorSSTV):
SCAN = WIDTH * TIMEUNIT SCAN = WIDTH * TIMEUNIT
INTER_CH_GAP = 5 * TIMEUNIT INTER_CH_GAP = 5 * TIMEUNIT
def before_channel(self, color): def before_channel(self, index):
if color is Color.red: if index == self.COLOR_SEQ[0]:
yield FREQ_BLACK, self.INTER_CH_GAP yield FREQ_BLACK, self.INTER_CH_GAP
def after_channel(self, color): def after_channel(self, index):
yield FREQ_BLACK, self.INTER_CH_GAP yield FREQ_BLACK, self.INTER_CH_GAP
@ -166,7 +160,8 @@ class PD90(ColorSSTV):
def gen_image_tuples(self): def gen_image_tuples(self):
yuv = self.image.convert('YCbCr').load() yuv = self.image.convert('YCbCr').load()
for line in range(0, self.HEIGHT, 2): for line in range(0, self.HEIGHT, 2):
yield from self.horizontal_sync() for item in self.horizontal_sync():
yield item
yield FREQ_BLACK, self.PORCH yield FREQ_BLACK, self.PORCH
pixels0 = [yuv[col, line] for col in range(self.WIDTH)] pixels0 = [yuv[col, line] for col in range(self.WIDTH)]
pixels1 = [yuv[col, line + 1] for col in range(self.WIDTH)] pixels1 = [yuv[col, line + 1] for col in range(self.WIDTH)]
@ -200,56 +195,6 @@ class PD240(PD120):
VIS_CODE = 0x61 VIS_CODE = 0x61
PIXEL = 0.382 PIXEL = 0.382
class PD290(PD240):
VIS_CODE = 0x5e
WIDTH = 800
HEIGHT = 616
PIXEL = 0.286
MODES = (MartinM1, MartinM2, ScottieS1, ScottieS2, Robot36,
class WraaseSC2180(ColorSSTV): PasokonP3, PasokonP5, PasokonP7, PD90, PD120, PD160, PD180, PD240)
VIS_CODE = 0x37
WIDTH = 320
HEIGHT = 256
COLOR_SEQ = (Color.red, Color.green, Color.blue)
SYNC = 5.5225
PORCH = 0.5
SCAN = 235.0
def before_channel(self, color):
if color is Color.red:
yield FREQ_BLACK, self.PORCH
else:
return []
def after_channel(self, color):
return []
class WraaseSC2120(WraaseSC2180):
VIS_CODE = 0x3f
# NB: there are "authoritative" sounding documents that will tell you SC-2
# 120 uses red and blue channels that have half the line width of the
# green channel. Having spent several hours trying to nut out why SC2-120
# images weren't decoding in anything else, I can say this is utter
# bunkum. The line width is the same for all three channels, just
# shorter.
SCAN = 156.0
def before_channel(self, color):
# Not sure why, but SC2-120 decoding seems to need an extra few sync
# pulses to decode in QSSTV and slowrx. Take the extra pulse out, and
# it slants something chronic and QSSTV loses sync regularly even on
# DX mode. Put it in, and both decode reliably. Go figure. SC2-180
# works just fine without this extra pulse at the start of each
# channel.
yield FREQ_BLACK, self.PORCH
yield from super().before_channel(color)
MODES = (MartinM1, MartinM2, ScottieS1, ScottieS2, ScottieDX, Robot36,
PasokonP3, PasokonP5, PasokonP7, PD90, PD120, PD160, PD180, PD240,
PD290, WraaseSC2120, WraaseSC2180)

View file

@ -69,10 +69,10 @@ def main(sstv_class=None):
n += 1 n += 1
del lut del lut
m_start, m_len = gen_matches(same_as, history, n) m_start, m_len = gen_matches(same_as, history, n)
for i in range(same_as[m_start]): for i in xrange(same_as[m_start]):
yield history[i][0] yield history[i][0]
yield 'for (int row = 0; row < width * {0}; row += width) {{'.format(sstv.HEIGHT) yield 'for (int row = 0; row < width * {0}; row += width) {{'.format(sstv.HEIGHT)
for i in range(same_as[m_start], same_as[m_start] + m_len - 1): for i in xrange(same_as[m_start], same_as[m_start] + m_len - 1):
yield ' ' + history[i][1] yield ' ' + history[i][1]
yield '}' yield '}'
yield '}}\n\n#define FREQ_COUNT {0}'.format(n) yield '}}\n\n#define FREQ_COUNT {0}'.format(n)
@ -83,7 +83,7 @@ def gen_matches(same_as, history, n):
cur_start = None cur_start = None
cur_len = None cur_len = None
cur_end = None cur_end = None
for i in range(n): for i in xrange(n):
if cur_start is None: if cur_start is None:
tmp = same_as.get(i) tmp = same_as.get(i)
if tmp is not None: if tmp is not None:
@ -114,22 +114,22 @@ def test(img_file):
import struct import struct
exe = './codegen-test-executable' exe = './codegen-test-executable'
if not path.exists('stb_image.h'): if not path.exists('stb_image.h'):
from urllib.request import urlretrieve from urllib import urlretrieve
urlretrieve('https://raw.githubusercontent.com/nothings/stb/master/stb_image.h', 'stb_image.h') urlretrieve('https://raw.githubusercontent.com/nothings/stb/master/stb_image.h', 'stb_image.h')
try: try:
for sstv_class in supported: for sstv_class in supported:
print('Testing', sstv_class) print 'Testing', sstv_class
gcc = Popen(['gcc', '-xc', '-lm', '-o', exe, '-'], stdin=PIPE) gcc = Popen(['gcc', '-xc', '-lm', '-o', exe, '-'], stdin=PIPE)
start = datetime.now() start = datetime.now()
with open(path.join(path.dirname(__file__), 'codeman.c')) as cm: with open(path.join(path.dirname(__file__), 'codeman.c')) as cm:
c_src = cm.read().replace('#include "codegen.c"', '\n'.join(main(sstv_class))) c_src = cm.read().replace('#include "codegen.c"', '\n'.join(main(sstv_class)))
gcc.communicate(c_src) gcc.communicate(c_src)
gen_elapsed = datetime.now() - start gen_elapsed = datetime.now() - start
print(' - gengcc took', gen_elapsed) print ' - gengcc took', gen_elapsed
start = datetime.now() start = datetime.now()
gen = check_output([exe, img_file]) gen = check_output([exe, img_file])
native_elapsed = datetime.now() - start native_elapsed = datetime.now() - start
print(' - native took', native_elapsed) print ' - native took', native_elapsed
img = Image.open(img_file) img = Image.open(img_file)
sstv = sstv_class(img, 44100, 16) sstv = sstv_class(img, 44100, 16)
start = datetime.now() start = datetime.now()
@ -138,20 +138,20 @@ def test(img_file):
assert gen[n * 8:(n + 1) * 8] == struct.pack('ff', freq, msec) assert gen[n * 8:(n + 1) * 8] == struct.pack('ff', freq, msec)
except AssertionError: except AssertionError:
mode_name = sstv_class.__name__ mode_name = sstv_class.__name__
with open('/tmp/{0}-c.bin'.format(mode_name), 'wb') as f: with file('/tmp/{0}-c.bin'.format(mode_name), 'wb') as f:
f.write(gen) f.write(gen)
with open('/tmp/{0}-py.bin'.format(mode_name), 'wb') as f: with file('/tmp/{0}-py.bin'.format(mode_name), 'wb') as f:
for n, (freq, msec) in enumerate(sstv.gen_freq_bits()): for n, (freq, msec) in enumerate(sstv.gen_freq_bits()):
f.write(struct.pack('ff', freq, msec)) f.write(struct.pack('ff', freq, msec))
with open('/tmp/{0}.c'.format(mode_name), 'w') as f: with file('/tmp/{0}.c'.format(mode_name), 'w') as f:
f.write(c_src) f.write(c_src)
print((" ! Outputs are different, they've been saved to " print (" ! Outputs are different, they've been saved to "
"/tmp/{0}-{{c,py}}.bin, along with the C source code " "/tmp/{0}-{{c,py}}.bin, along with the C source code "
"in /tmp/{0}.c").format(mode_name)) "in /tmp/{0}.c").format(mode_name)
python_elapsed = datetime.now() - start python_elapsed = datetime.now() - start
print(' - python took', python_elapsed) print ' - python took', python_elapsed
print(' - speedup:', python_elapsed.total_seconds() / native_elapsed.total_seconds()) print ' - speedup:', python_elapsed.total_seconds() / native_elapsed.total_seconds()
print('OK') print 'OK'
finally: finally:
try: try:
remove(exe) remove(exe)
@ -164,4 +164,4 @@ if __name__ == '__main__':
if len(argv) > 2 and argv[1] == 'test': if len(argv) > 2 and argv[1] == 'test':
test(argv[2]) test(argv[2])
else: else:
print('\n'.join(main())) print '\n'.join(main())

View file

@ -2,17 +2,17 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
# copy to ~/.gimp-2.8/plug-ins/ # copy to ~/.gimp-2.8/plug-ins/
# dependencies: GIMP 2.8, python-imaging-tk, python-pyaudio # dependencies: GIMP 2.8, python-imaging-tk
from gimpfu import register, main, pdb, PF_BOOL, PF_STRING, PF_RADIO, CLIP_TO_IMAGE from gimpfu import register, main, pdb, PF_BOOL, PF_STRING, PF_RADIO, CLIP_TO_IMAGE
from PIL import Image, ImageTk from PIL import Image, ImageTk
from tkinter import Tk, Canvas, Button, Checkbutton, IntVar, Frame, LEFT, NW from Tkinter import Tk, Canvas, Button, Checkbutton, IntVar, Frame, LEFT, NW
from pysstv import __main__ as pysstv_main from pysstv import __main__ as pysstv_main
from pysstv.examples.pyaudio_sstv import PyAudioSSTV from pysstv.examples.pyaudio_sstv import PyAudioSSTV
from pysstv.sstv import SSTV from pysstv.sstv import SSTV
from itertools import repeat from itertools import repeat
from threading import Thread from threading import Thread
from queue import Queue, Empty from Queue import Queue, Empty
from time import sleep from time import sleep
import gimp, os import gimp, os
@ -118,12 +118,12 @@ class ProgressCanvas(Canvas):
self.height_ratio = 1 self.height_ratio = 1
width, height = image.size width, height = image.size
pixels = image.load() pixels = image.load()
RED, GREEN, BLUE = list(range(3)) RED, GREEN, BLUE = range(3)
self.colors = ['#{0:02x}{1:02x}{2:02x}'.format( self.colors = ['#{0:02x}{1:02x}{2:02x}'.format(
contrast(sum(pixels[x, y][RED] for x in range(width)) / width), contrast(sum(pixels[x, y][RED] for x in xrange(width)) / width),
contrast(sum(pixels[x, y][GREEN] for x in range(width)) / width), contrast(sum(pixels[x, y][GREEN] for x in xrange(width)) / width),
contrast(sum(pixels[x, y][BLUE] for x in range(width)) / width)) contrast(sum(pixels[x, y][BLUE] for x in xrange(width)) / width))
for y in range(height)] for y in xrange(height)]
if height / float(width) > 1.5: if height / float(width) > 1.5:
width *= 2 width *= 2
elif width < 200: elif width < 200:
@ -152,6 +152,9 @@ def contrast(value):
else: else:
return 255 - value return 255 - value
def set_ptt_pin(port, pin, state):
getattr(port, 'set' + pin)(state)
def transmit_current_image(image, drawable, mode, vox, fskid, ptt_port, ptt_pin, ptt_state): def transmit_current_image(image, drawable, mode, vox, fskid, ptt_port, ptt_pin, ptt_state):
sstv = MODULE_MAP[mode] sstv = MODULE_MAP[mode]
if ptt_port is not None: if ptt_port is not None:
@ -237,7 +240,7 @@ register(
"*", "*",
[ [
(PF_RADIO, "mode", "SSTV mode", "MartinM1", (PF_RADIO, "mode", "SSTV mode", "MartinM1",
tuple((n, n) for n in sorted(MODULE_MAP.keys()))), tuple((n, n) for n in sorted(MODULE_MAP.iterkeys()))),
(PF_BOOL, "vox", "Include VOX tones", True), (PF_BOOL, "vox", "Include VOX tones", True),
(PF_STRING, "fskid", "FSK ID", ""), (PF_STRING, "fskid", "FSK ID", ""),
(PF_RADIO, "ptt_port", "PTT port", None, (PF_RADIO, "ptt_port", "PTT port", None,

View file

@ -5,7 +5,7 @@ Demonstrates playing the generated samples directly using PyAudio
Tested on PyAudio 0.2.7 http://people.csail.mit.edu/hubert/pyaudio/ Tested on PyAudio 0.2.7 http://people.csail.mit.edu/hubert/pyaudio/
""" """
from __future__ import division
from pysstv.sstv import SSTV from pysstv.sstv import SSTV
from time import sleep from time import sleep
from itertools import islice from itertools import islice

View file

@ -8,9 +8,9 @@ simply copying/linking images to the directory or suing an SSTV
receiver such as slowrx or QSSTV. receiver such as slowrx or QSSTV.
""" """
from __future__ import print_function
from pyinotify import WatchManager, Notifier, ProcessEvent, IN_CREATE from pyinotify import WatchManager, Notifier, ProcessEvent, IN_CREATE
from .pyaudio_sstv import PyAudioSSTV from pyaudio_sstv import PyAudioSSTV
from pysstv.color import MartinM1, MartinM2, ScottieS1, ScottieS2 from pysstv.color import MartinM1, MartinM2, ScottieS1, ScottieS2
from pysstv.grayscale import Robot8BW, Robot24BW from pysstv.grayscale import Robot8BW, Robot24BW
from PIL import Image from PIL import Image
@ -44,13 +44,13 @@ class EventHandler(ProcessEvent):
def get_module_for_filename(filename): def get_module_for_filename(filename):
basename, _ = path.splitext(path.basename(filename)) basename, _ = path.splitext(path.basename(filename))
for mode, module in MODE_MAP.items(): for mode, module in MODE_MAP.iteritems():
if mode in basename: if mode in basename:
return module return module
def get_module_for_image(image): def get_module_for_image(image):
size = image.size size = image.size
for mode in MODE_MAP.values(): for mode in MODE_MAP.itervalues():
if all(i >= m for i, m in zip(size, (mode.WIDTH, mode.HEIGHT))): if all(i >= m for i, m in zip(size, (mode.WIDTH, mode.HEIGHT))):
return mode return mode

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import division from __future__ import division
from six.moves import range
from pysstv.sstv import SSTV, byte_to_freq from pysstv.sstv import SSTV, byte_to_freq
@ -10,8 +11,10 @@ class GrayscaleSSTV(SSTV):
def gen_image_tuples(self): def gen_image_tuples(self):
for line in range(self.HEIGHT): for line in range(self.HEIGHT):
yield from self.horizontal_sync() for item in self.horizontal_sync():
yield from self.encode_line(line) yield item
for item in self.encode_line(line):
yield item
def encode_line(self, line): def encode_line(self, line):
msec_pixel = self.SCAN / self.WIDTH msec_pixel = self.SCAN / self.WIDTH

View file

@ -1,6 +1,9 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import division, with_statement from __future__ import division, with_statement
from six.moves import range
from six.moves import map
from six.moves import zip
from math import sin, pi from math import sin, pi
from random import random from random import random
from contextlib import closing from contextlib import closing
@ -51,7 +54,7 @@ class SSTV(object):
wav.setnchannels(self.nchannels) wav.setnchannels(self.nchannels)
wav.setsampwidth(self.bits // 8) wav.setsampwidth(self.bits // 8)
wav.setframerate(self.samples_per_sec) wav.setframerate(self.samples_per_sec)
wav.writeframes(data) wav.writeframes(data.tostring())
def gen_samples(self): def gen_samples(self):
"""generates discrete samples from gen_values() """generates discrete samples from gen_values()
@ -113,7 +116,8 @@ class SSTV(object):
parity_freq = FREQ_VIS_BIT1 if num_ones % 2 == 1 else FREQ_VIS_BIT0 parity_freq = FREQ_VIS_BIT1 if num_ones % 2 == 1 else FREQ_VIS_BIT0
yield parity_freq, MSEC_VIS_BIT yield parity_freq, MSEC_VIS_BIT
yield FREQ_SYNC, MSEC_VIS_BIT # stop bit yield FREQ_SYNC, MSEC_VIS_BIT # stop bit
yield from self.gen_image_tuples() for freq_tuple in self.gen_image_tuples():
yield freq_tuple
for fskid_byte in map(ord, self.fskid_payload): for fskid_byte in map(ord, self.fskid_payload):
for _ in range(6): for _ in range(6):
bit = fskid_byte & 1 bit = fskid_byte & 1

View file

@ -1,8 +1,10 @@
import unittest import unittest
from io import BytesIO
from itertools import islice from itertools import islice
from six.moves import zip
import mock import mock
from mock import MagicMock from mock import MagicMock
from six import BytesIO
from six import PY2
import hashlib import hashlib
from pysstv import sstv from pysstv import sstv
@ -66,7 +68,8 @@ class TestSSTV(unittest.TestCase):
bio = BytesIO() bio = BytesIO()
bio.close = MagicMock() # ignore close() so we can .getvalue() bio.close = MagicMock() # ignore close() so we can .getvalue()
mock_open = MagicMock(return_value=bio) mock_open = MagicMock(return_value=bio)
with mock.patch('builtins.open', mock_open): ns = '__builtin__' if PY2 else 'builtins'
with mock.patch('{0}.open'.format(ns), mock_open):
self.s.write_wav('unittest.wav') self.s.write_wav('unittest.wav')
expected = 'dd7eed880ab3360fb79ce09c469deee2' expected = 'dd7eed880ab3360fb79ce09c469deee2'
data = bio.getvalue() data = bio.getvalue()

View file

@ -1,3 +1,4 @@
Pillow==10.3.0 six==1.10.0
Pillow==4.3.0
mock==1.0.1 mock==1.0.1
nose==1.3.0 nose==1.3.0

View file

@ -4,19 +4,14 @@ from setuptools import setup
setup( setup(
name='PySSTV', name='PySSTV',
version='0.5.7', version='0.4',
description='Python classes for generating Slow-scan Television transmissions', description='Python classes for generating Slow-scan Television transmissions',
author=u'András Veres-Szentkirályi', author=u'András Veres-Szentkirályi',
author_email='vsza@vsza.hu', author_email='vsza@vsza.hu',
url='https://github.com/dnet/pySSTV', url='https://github.com/dnet/pySSTV',
packages=['pysstv', 'pysstv.tests', 'pysstv.examples'], packages=['pysstv', 'pysstv.tests', 'pysstv.examples'],
entry_points={
'console_scripts': [
'pysstv = pysstv.__main__:main',
],
},
keywords='HAM SSTV slow-scan television Scottie Martin Robot Pasokon', keywords='HAM SSTV slow-scan television Scottie Martin Robot Pasokon',
install_requires = ['Pillow'], install_requires = ['Pillow', 'six'],
license='MIT', license='MIT',
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',
@ -26,5 +21,4 @@ setup(
'Operating System :: OS Independent', 'Operating System :: OS Independent',
], ],
long_description=open('README.md').read(), long_description=open('README.md').read(),
long_description_content_type="text/markdown",
) )