diff --git a/setup.py b/setup.py index 14b491b..a565fa6 100644 --- a/setup.py +++ b/setup.py @@ -19,9 +19,9 @@ setup( 'src/RpiDma.c', 'src/RpiGpio.c', ], - extra_link_args=['-lrt'], + extra_link_args=['-lrt', '-lsndfile'], ), ], package_dir={'': 'src/python'}, - install_requires=['wave', 'pydub'], + install_requires=['pydub'], ) diff --git a/src/RpiTx.c b/src/RpiTx.c index 9968967..41584b7 100644 --- a/src/RpiTx.c +++ b/src/RpiTx.c @@ -1032,27 +1032,6 @@ static void resetFile(void) { } -static void* arrayBaseAddress; -static int arrayLength; -static int arrayPosition; -/** Wrapper around reading from memory with an interface similar to read. */ -ssize_t readArray(void *buffer, const size_t count) { - if (arrayPosition >= arrayLength) { - return 0; - } - const int left = arrayLength - arrayPosition; - const int numBytesToCopy = left > count ? count : left; - memcpy(buffer, arrayBaseAddress + arrayPosition, numBytesToCopy); - arrayPosition += numBytesToCopy; - return numBytesToCopy; -} -void setUpReadArray(void* baseAddress, size_t length) { - arrayBaseAddress = baseAddress; - arrayLength = length; - arrayPosition = 0; -} - - int main(int argc, char* argv[]) { diff --git a/src/RpiTx.h b/src/RpiTx.h index 716efe4..0671da6 100644 --- a/src/RpiTx.h +++ b/src/RpiTx.h @@ -24,8 +24,4 @@ int pitx_run( ); -/** Wrapper around reading from memory with an interface similar to read. */ -ssize_t readArray(void *buffer, const size_t count); -void setUpReadArray(void* baseAddress, size_t length); - #endif diff --git a/src/python/_rpitxmodule.c b/src/python/_rpitxmodule.c index b4b22e2..7649d19 100644 --- a/src/python/_rpitxmodule.c +++ b/src/python/_rpitxmodule.c @@ -1,10 +1,74 @@ #include +#include +#include + #include "../RpiTx.h" #include "../RpiGpio.h" +#define COUNT_OF(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x]))))) + + +static void* sampleBase; +static sf_count_t sampleLength; +static sf_count_t sampleOffset; +static SNDFILE* sndFile; + + +// These methods used by libsndfile's virtual file open function +sf_count_t virtualSndfileGetLength(void* unused) { + return sampleLength; +} +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, userData, numBytes); + sampleOffset += numBytes; + return numBytes; +} + + +typedef struct { + double frequency; + uint32_t waitForThisSample; +} samplerf_t; +/** + * Formats a chunk of an array of a mono 44k wav at a time and outputs IQ + * formatted array for broadcast. + */ +ssize_t formatIqWrapper(void* const outBuffer, size_t count) { + static float readBuffer[1024]; + const int excursion = 6000; + samplerf_t samplerf; + char* out = outBuffer; + + int readCount; + int k; + int totalBytesToRead = count / sizeof(samplerf_t); + int bytesToRead = totalBytesToRead; + if (bytesToRead > COUNT_OF(readBuffer)) { + bytesToRead = COUNT_OF(readBuffer); + } + int bytesWritten = 0; + while ((readCount = sf_readf_float(sndFile, readBuffer, bytesToRead))) { + for (k = 0; k < readCount; k++) { + const int x = readBuffer[k]; + samplerf.frequency = x * excursion * 2.0; + samplerf.waitForThisSample = 1e9 / 48000.0; //en 100 de nanosecond + memcpy(&out[bytesWritten], &samplerf, sizeof(samplerf_t)); + bytesWritten += sizeof(samplerf_t); + } + totalBytesToRead -= bytesToRead; + if (totalBytesToRead <= 0) { + break; + } + } + return bytesWritten; +} + + static PyObject* -_rpitx_broadcast(PyObject* self, PyObject* args) { +_rpitx_broadcast_fm(PyObject* self, PyObject* args) { int address; int length; float frequency; @@ -12,14 +76,29 @@ _rpitx_broadcast(PyObject* self, PyObject* args) { return NULL; } - setUpReadArray((void*)address, length); - pitx_run(MODE_IQ, 44000, frequency, 0.0, 0, readArray, NULL); + sampleBase = (void*)address; + sampleLength = length; + sampleOffset = 0; + + SF_VIRTUAL_IO virtualIo = { + .get_filelen = virtualSndfileGetLength, + .seek = NULL, + .read = virtualSndfileRead, + .write = NULL, + .tell = NULL + }; + SF_INFO sfInfo ; + sndFile = sf_open_virtual(&virtualIo, SFM_READ, &sfInfo, sampleBase); + + pitx_run(MODE_IQ, 44000, frequency, 0.0, 0, formatIqWrapper, NULL); + sf_close(sndFile); + Py_RETURN_NONE; } static PyMethodDef _rpitx_methods[] = { - {"broadcast", _rpitx_broadcast, METH_VARARGS, "Low-level broadcasting."}, + {"broadcast_fm", _rpitx_broadcast_fm, METH_VARARGS, "Low-level broadcasting."}, {NULL, NULL, 0, NULL} }; diff --git a/src/python/rpitx.py b/src/python/rpitx.py index e03dcde..403f24c 100644 --- a/src/python/rpitx.py +++ b/src/python/rpitx.py @@ -2,55 +2,43 @@ from pydub import AudioSegment import StringIO -import array import _rpitx -import wave +import array +import logging +import os -def play_fm(file_, frequency): +def broadcast_fm(file_, frequency): """Play a music file over FM.""" + logging.basicConfig() + logger = logging.getLogger('rpitx') + def _reencode(file_name): - """Returns a file-like object reencoded to the proper WAV format.""" - reencoded = StringIO.StringIO() - # AudioSegment doesn't support context managers either + """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) - original.export(reencoded, format='wav', bitrate='44k') - return reencoded + if ( + original.frame_rate != 48000 + # TODO: There should be a better way to check if it's wav + or not file_name.endswith('.wav') + ): + logger.debug('Reencoding file') + reencoded = StringIO.StringIO() + original.export(reencoded, format='wav', bitrate='44k') + return reencoded - encoded_file = None - if isinstance(file_, str): - if file_.endswith('.wav'): - with open(file_) as raw_file: - # wave.open doesn't support context managers, so we need to be - # careful about closing the file - wav_file = wave.open(raw_file, 'r') - num_channels = wav_file.getnchannels() - framerate = wav_file.getframerate() - sample_width = wav_file.getsampwidth() - wav_file.close() - - if ( - num_channels != 1 - or framerate != 1 - or sample_width != 2 - ): - encoded_file = _reencode(file_) - else: - encoded_file = AudioSegment.from_file(file_) - else: - encoded_file = _reencode(file_) - else: - encoded_file = _reencode(file_) + return original + encoded_file = _reencode(file_) raw_array = array.array('c') - raw_array.fromstring(str(encoded_file)) + raw_array.fromstring(encoded_file.raw_data) array_address, length = raw_array.buffer_info() - _rpitx.broadcast(array_address, length, frequency) + _rpitx.broadcast_fm(array_address, length, frequency)