mirror of https://github.com/F5OEO/rpitx
Merge pull request #72 from bskari/avconv
Fix Python interface and add support for broadcasting any avconv supported media
This commit is contained in:
commit
38079ca8bc
22
setup.py
22
setup.py
|
|
@ -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',
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue