Merge pull request #72 from bskari/avconv

Fix Python interface and add support for broadcasting any avconv supported media
This commit is contained in:
F5OEO 2017-07-19 20:09:05 +02:00 committed by GitHub
commit 38079ca8bc
4 changed files with 257 additions and 121 deletions

View File

@ -4,7 +4,7 @@ from setuptools import setup, Extension
setup(
name='rpitx',
version='0.1',
version='0.2',
description='Raspyberry Pi radio transmitter',
author='Brandon Skari',
author_email='brandon@skari.org',
@ -13,16 +13,20 @@ setup(
Extension(
'_rpitx',
[
'src/python/_rpitxmodule.c',
'src/RpiTx.c',
'src/mailbox.c',
'src/RpiDma.c',
'src/RpiGpio.c',
],
'src/RpiTx.c',
'src/mailbox.c',
'src/python/_rpitxmodule.c',
'src/raspberry_pi_revision.c',
],
extra_link_args=['-lrt', '-lsndfile'],
),
],
),
],
packages=['rpitx'],
package_dir={'': 'src/python'},
install_requires=['pydub', 'wave'],
)
install_requires=[
'ffmpegwrapper==0.1-dev',
'pypi-libavwrapper',
],
)

View File

@ -1,9 +1,10 @@
#include <Python.h>
#include <assert.h>
#include <sndfile.h>
#include <endian.h>
#include <string.h>
#include <unistd.h>
#include "../RpiTx.h"
#include "../RpiGpio.h"
#define COUNT_OF(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x])))))
@ -18,55 +19,18 @@ struct module_state {
static struct module_state _state;
#endif
static void* sampleBase;
static sf_count_t sampleLength;
static sf_count_t sampleOffset;
static SNDFILE* sndFile;
static int bitRate;
// These methods used by libsndfile's virtual file open function
static sf_count_t virtualSndfileGetLength(void* unused) {
return sampleLength;
}
static sf_count_t virtualSndfileRead(void* const dest, const sf_count_t count, void* const userData) {
const sf_count_t bytesAvailable = sampleLength - sampleOffset;
const int numBytes = bytesAvailable > count ? count : bytesAvailable;
memcpy(dest, ((char*)userData) + sampleOffset, numBytes);
sampleOffset += numBytes;
return numBytes;
}
static sf_count_t virtualSndfileTell(void* const unused) {
return sampleOffset;
}
static sf_count_t virtualSndfileSeek(
const sf_count_t offset,
const int whence,
void* const unused
) {
switch (whence) {
case SEEK_CUR:
sampleOffset += offset;
break;
case SEEK_SET:
sampleOffset = offset;
break;
case SEEK_END:
sampleOffset = sampleLength - offset;
break;
default:
assert(!"Invalid whence");
}
return sampleOffset;
}
static int streamFileNo = 0;
static int sampleRate = 48000;
typedef struct {
double frequency;
uint32_t waitForThisSample;
} samplerf_t;
static size_t readFloat(int streamFileNo, float* wavBuffer, const size_t count);
/**
* Formats a chunk of an array of a mono 44k wav at a time and outputs IQ
* Formats a chunk of an array of a mono 48k wav at a time and outputs RF
* formatted array for broadcast.
*/
static ssize_t formatRfWrapper(void* const outBuffer, const size_t count) {
@ -81,10 +45,10 @@ static ssize_t formatRfWrapper(void* const outBuffer, const size_t count) {
const int excursion = 6000;
int numBytesWritten = 0;
samplerf_t samplerf;
samplerf.waitForThisSample = 1e9 / ((float)bitRate); //en 100 de nanosecond
samplerf.waitForThisSample = 1e9 / ((float)sampleRate); //en 100 de nanosecond
char* const out = outBuffer;
while (numBytesWritten < count) {
while (numBytesWritten <= count - sizeof(samplerf_t)) {
for (
;
numBytesWritten <= count - sizeof(samplerf_t) && wavOffset < wavFilled;
@ -99,52 +63,165 @@ static ssize_t formatRfWrapper(void* const outBuffer, const size_t count) {
assert(wavOffset <= wavFilled);
if (wavOffset == wavFilled) {
wavFilled = sf_readf_float(sndFile, wavBuffer, COUNT_OF(wavBuffer));
wavFilled = readFloat(streamFileNo, wavBuffer, COUNT_OF(wavBuffer));
if (wavFilled == 0) {
// End of file
return numBytesWritten;
}
wavOffset = 0;
}
}
return numBytesWritten;
}
static void reset(void) {
sampleOffset = 0;
static size_t readFloat(int streamFileNo, float* wavBuffer, const size_t count) {
// Samples are stored as 16 bit signed integers in range of -32768 to 32767
int16_t sample;
int i;
for (i = 0; i < count; ++i) {
const int byteCount = read(streamFileNo, &sample, sizeof(sample));
if (byteCount != sizeof(sample)) {
return i;
}
// TODO: I don't know if this should be dividing by 32767 or 32768. Probably
// doesn't matter too much.
*wavBuffer = ((float)le16toh(sample)) / 32768;
++wavBuffer;
}
return count;
}
static void dummyFunction(void) {
assert(0 && "dummyFunction should not be called");
}
#define RETURN_ERROR(errorMessage) \
PyErr_SetString(st->error, errorMessage); \
return NULL;
static PyObject*
_rpitx_broadcast_fm(PyObject* self, PyObject* args) {
float frequency;
assert(sizeof(sampleBase) == sizeof(unsigned long));
if (!PyArg_ParseTuple(args, "Lif", &sampleBase, &sampleLength, &frequency)) {
struct module_state *st = GETSTATE(self);
PyErr_SetString(st->error, "Invalid arguments");
return NULL;
struct module_state *st = GETSTATE(self);
if (!PyArg_ParseTuple(args, "if", &streamFileNo, &frequency)) {
RETURN_ERROR("Invalid arguments");
}
sampleOffset = 0;
char char4[4];
uint32_t uint32;
uint16_t uint16;
char letter;
SF_VIRTUAL_IO virtualIo = {
.get_filelen = virtualSndfileGetLength,
.seek = virtualSndfileSeek,
.read = virtualSndfileRead,
.write = NULL,
.tell = virtualSndfileTell
};
SF_INFO sfInfo;
sndFile = sf_open_virtual(&virtualIo, SFM_READ, &sfInfo, sampleBase);
if (sf_error(sndFile) != SF_ERR_NO_ERROR) {
char message[100];
snprintf(
message,
COUNT_OF(message),
"Unable to open sound file: %s",
sf_strerror(sndFile));
message[COUNT_OF(message) - 1] = '\0';
struct module_state *st = GETSTATE(self);
PyErr_SetString(st->error, message);
return NULL;
size_t byteCount = read(streamFileNo, char4, sizeof(char4));
if (byteCount != sizeof(char4) || strncmp(char4, "RIFF", 4) != 0) {
RETURN_ERROR("Not a WAV file");
}
// Skip chunk size
byteCount = read(streamFileNo, char4, sizeof(char4));
if (byteCount != sizeof(char4)) {
RETURN_ERROR("Not a WAV file");
}
byteCount = read(streamFileNo, char4, sizeof(char4));
if (byteCount != sizeof(char4) || strncmp(char4, "WAVE", 4) != 0) {
RETURN_ERROR("Not a WAV file");
}
byteCount = read(streamFileNo, char4, sizeof(char4));
if (byteCount != sizeof(char4) || strncmp(char4, "fmt ", 4) != 0) {
RETURN_ERROR("Not a WAV file");
}
byteCount = read(streamFileNo, &uint32, sizeof(uint32));
uint32 = le32toh(uint32);
// TODO: This value is coming out as 18 and I don't know why, so I'm
// skipping this check for now
/*
if (byteCount != sizeof(uint32) || uint32 != 16) {
RETURN_ERROR("Not a PCM WAV file");
}
*/
byteCount = read(streamFileNo, &uint16, sizeof(uint16));
uint16 = le16toh(uint16);
if (byteCount != sizeof(uint16) || uint16 != 1) {
RETURN_ERROR("Not an uncompressed WAV file");
}
byteCount = read(streamFileNo, &uint16, sizeof(uint16));
uint16 = le16toh(uint16);
if (byteCount != sizeof(uint16) || uint16 != 1) {
RETURN_ERROR("Not a mono WAV file");
}
byteCount = read(streamFileNo, &uint32, sizeof(uint32));
sampleRate = le32toh(uint32);
if (byteCount != sizeof(uint32) || sampleRate != 48000) {
RETURN_ERROR("Not a WAV file");
}
// Skip byte rate
byteCount = read(streamFileNo, &uint32, sizeof(uint32));
if (byteCount != sizeof(uint32)) {
RETURN_ERROR("Not a WAV file");
}
// Skip block align
byteCount = read(streamFileNo, &uint16, sizeof(uint16));
if (byteCount != sizeof(uint16)) {
RETURN_ERROR("Not a WAV file");
}
byteCount = read(streamFileNo, &uint16, sizeof(uint16));
uint16 = le16toh(uint16);
if (byteCount != sizeof(uint16) || uint16 != 16) {
RETURN_ERROR("Not a 16 bit WAV file");
}
// TODO: PCM WAV files have "data" here, but avconv spits out a bunch of extra
// parameters, starting with "LIST" and including the encoder I think. However,
// the marker "data" is still there where the data starts, so let's just skip
// to that.
byteCount = read(streamFileNo, &letter, sizeof(letter));
int dataLettersCount = 0;
while (byteCount == 1) {
switch (letter) {
case 'd':
dataLettersCount = 1;
break;
case 'a':
if (dataLettersCount == 1) {
++dataLettersCount;
} else if (dataLettersCount == 3) {
++dataLettersCount;
goto foundDataMarker;
} else {
dataLettersCount = 0;
}
break;
case 't':
if (dataLettersCount == 2) {
++dataLettersCount;
} else {
dataLettersCount = 0;
}
break;
default:
dataLettersCount = 0;
}
byteCount = read(streamFileNo, &letter, sizeof(letter));
}
if (dataLettersCount != 4) {
RETURN_ERROR("Not a WAV file");
}
foundDataMarker:
// Skip subchunk2 size
byteCount = read(streamFileNo, &uint32, sizeof(uint32));
if (byteCount != sizeof(uint32)) {
RETURN_ERROR("Not a WAV file");
}
bitRate = sfInfo.samplerate;
int skipSignals[] = {
SIGALRM,
@ -153,8 +230,8 @@ _rpitx_broadcast_fm(PyObject* self, PyObject* args) {
SIGWINCH, // Window resized
0
};
pitx_run(MODE_RF, bitRate, frequency * 1000.0, 0.0, 0, formatRfWrapper, reset, skipSignals);
sf_close(sndFile);
pitx_run(MODE_RF, sampleRate, frequency * 1000.0, 0.0, 0, formatRfWrapper, dummyFunction, skipSignals, 0);
Py_RETURN_NONE;
}
@ -166,10 +243,9 @@ static PyMethodDef _rpitx_methods[] = {
_rpitx_broadcast_fm,
METH_VARARGS,
"Low-level broadcasting.\n\n"
"Broadcast a WAV formatted 48KHz memory array.\n"
"Broadcast a WAV formatted 48KHz file from a pipe file descriptor.\n"
"Args:\n"
" address (int): Address of the memory array.\n"
" length (int): Length of the memory array.\n"
" pipe_file_no (int): The fileno of the pipe that the WAV is being written to.\n"
" frequency (float): The frequency, in MHz, to broadcast on.\n"
},
{NULL, NULL, 0, NULL}

View File

@ -1,4 +1,4 @@
"""Python interface to rpitx."""
# To avoid import pollution with ipython, hide functions in another module
from _hidden import broadcast_fm
from ._hidden import broadcast_fm

View File

@ -1,42 +1,98 @@
"""Hides imports and other irrelevant things so that ipython works nicely."""
from pydub import AudioSegment
import StringIO
import _rpitx
import array
import logging
import wave
import os
import subprocess
import sys
import threading
import _rpitx
import ffmpegwrapper
import libavwrapper
PIPE = 'pipe:1'
DUMMY_FILE = 'dummy-file.wav'
def broadcast_fm(file_, frequency):
"""Play a music file over FM."""
def broadcast_fm(media_file_name, frequency):
"""Broadcast a media file's audio over FM.
Args:
media_file_name (str): The file to broadcast.
frequency (float): The frequency, in MHz, to broadcast on."""
logging.basicConfig()
logger = logging.getLogger('rpitx')
def _reencode(file_name):
"""Returns an AudioSegment file reencoded to the proper WAV format."""
original = AudioSegment.from_file(file_name)
if original.channels > 2:
raise ValueError('Too many channels in sound file')
if original.channels == 2:
# TODO: Support stereo. For now, just overlay into mono.
logger.info('Reducing stereo channels to mono')
left, right = original.split_to_mono()
original = left.overlay(right)
# Python < 3.3 throws IOError instead of PermissionError
if sys.version_info.major <= 2 or (sys.version_info.major == 3 and sys.version_info.minor < 3):
Error = IOError
else:
Error = PermissionError
return original
# mailbox.c calls exit if /dev/mem can't be opened, which causes the avconv
# pipe to be left open and messes with the terminal, so try it here first
# just to be safe
try:
open('/dev/mem')
except Error:
raise Error(
'This program should be run as root. Try prefixing command with: sudo'
)
raw_audio_data = _reencode(file_)
dev_null = open(os.devnull, 'w')
if subprocess.call(('which', 'ffmpeg'), stdout=dev_null) == 0:
Input = ffmpegwrapper.Input
Output = ffmpegwrapper.Output
VideoCodec = ffmpegwrapper.VideoCodec
AudioCodec = ffmpegwrapper.AudioCodec
Stream = ffmpegwrapper.FFmpeg
command = 'ffmpeg'
elif subprocess.call(('which', 'avconv'), stdout=dev_null) == 0:
Input = libavwrapper.Input
Output = libavwrapper.Output
VideoCodec = libavwrapper.VideoCodec
AudioCodec = libavwrapper.AudioCodec
Stream = libavwrapper.AVConv
Stream.add_option = Stream.add_parameter
command = 'avconv'
else:
raise NotImplementedError(
'Broadcasting audio requires either avconv or ffmpeg to be installed\n'
'sudo apt install libav-tools'
)
wav_data = StringIO.StringIO()
wav_writer = wave.open(wav_data, 'w')
wav_writer.setnchannels(1)
wav_writer.setsampwidth(2)
wav_writer.setframerate(48000)
wav_writer.writeframes(raw_audio_data.raw_data)
wav_writer.close()
logger.debug('Using %s', command)
raw_array = array.array('c', wav_data.getvalue())
array_address, length = raw_array.buffer_info()
_rpitx.broadcast_fm(array_address, length, frequency)
input_media = Input(media_file_name)
codec = AudioCodec('pcm_s16le').frequence(48000).channels(1)
output_audio = Output(DUMMY_FILE, codec)
stream = Stream(command, input_media, output_audio)
command_line = list(stream)
# The format needs to be specified manually because we're writing to
# stderr and not a named file. Normally, it would infer the format from
# the file name extension. Also, ffmpeg will only write to a pipe if it's
# the last named argument.
command_line.remove(DUMMY_FILE)
command_line += ('-f', 'wav', PIPE)
logger.debug('Running command "%s"', ' '.join(command_line))
stream_process = subprocess.Popen(
command_line,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
def log_stdout():
"""Log output from the stream process."""
for line in stream_process.stderr:
logger.debug(line.rstrip())
thread = threading.Thread(target=log_stdout)
thread.start()
logger.debug('Calling broadcast_fm')
try:
_rpitx.broadcast_fm(stream_process.stdout.fileno(), frequency)
except Exception as exc:
stream_process.stdout.close()
thread.join()
raise exc
thread.join()