From 5233ecc47a7574ed382800583ca4c6e123394a3d Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Tue, 28 May 2024 16:57:26 +0200 Subject: [PATCH 1/3] Start at an audio library and examples --- examples/Python/src/lib/Audio.lf | 80 ++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 examples/Python/src/lib/Audio.lf diff --git a/examples/Python/src/lib/Audio.lf b/examples/Python/src/lib/Audio.lf new file mode 100644 index 00000000..0a81b3cb --- /dev/null +++ b/examples/Python/src/lib/Audio.lf @@ -0,0 +1,80 @@ +/** + * @file + * @author Edward A. Lee + * @author Vincenzo Barbuto + * @brief Basic audio library for Lingua Franca Python target. + */ +target Python + +/** + * @brief A reactor that produces audio data captured from a microphone. + * + * The output will be a numpy array of arrays of audio samples, where entry + * in the outer array is the audio samples, one per channel. + * Currently, only one channel is supported. + * + * The logical time of the output is calculated based on the block_size, block_count, and sample_rate. + * This design assumes that the physical clock is synchronized to the microphone clock, otherwise + * it may block accessing the microphone, possibly leading to increasing lag. + */ +reactor Microphone(block_size=16000, sample_rate=16000, channels=1) { + + output audio_data + + # Use a logical action to get deterministic timestamps on audio output. + logical action get_audio_data + + # Input audio stream. + state audio_stream + # Count of blocks. + state blocks_count = 1 + + preamble {= + import sounddevice as sd + import numpy as np + =} + + reaction(startup) -> get_audio_data {= + self.audio_stream = self.sd.InputStream( + channels=self.channels, samplerate=self.sample_rate, blocksize=self.block_size) + self.audio_stream.start() # FIXME: Perhaps start in reaction to an input. + # Schedule the first output. + # The logical time of the output is calculated based on the block_size, block_count, and sample_rate. + t = self.block_size * SEC(1) // self.sample_rate # Sample interval. + get_audio_data.schedule(t) + =} + + reaction(get_audio_data) -> audio_data {= + try: + data, overflowed = self.audio_stream.read(self.block_size) + if overflowed: + print("Audio data lost between blocks.") + + if (len(data)): + audio_data.set(data) + except Exception as error: + print("Error reading audio data: ", type(error).__name__, " - ", error) + return + + # Schedule the next output. + # Because the sample interval may not be an exact multiple of one nanosecond, + # we calculate the next output time based on the number of blocks, the block size, and the sample rate. + # This relies on the fact that integers do not overflow in Python. + self.blocks_count += 1 + time_since_start = self.blocks_count * self.block_size * SEC(1) // self.sample_rate + t = time_since_start - lf.time.logical_elapsed() # Offset from current logical time. + get_audio_data.schedule(t) + =} + + reaction(shutdown) {= + self.audio_stream.close() + print("Shutting down Microphone reactor") + =} +} + +main reactor { + m = new Microphone() + reaction(m.audio_data) {= + print(m.audio_data.value, " at time ", lf.time.logical_elapsed()) + =} +} From 69e74465f34ba24d732e529c771df47a5390caea Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Thu, 30 May 2024 08:27:17 +0200 Subject: [PATCH 2/3] Start of an audio library and demonstrators --- examples/Python/src/Audio/Spectrum.lf | 34 ++++++++++ examples/Python/src/lib/Audio.lf | 88 +++++++++++------------- examples/Python/src/lib/Plotters.lf | 96 +++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 48 deletions(-) create mode 100644 examples/Python/src/Audio/Spectrum.lf create mode 100644 examples/Python/src/lib/Plotters.lf diff --git a/examples/Python/src/Audio/Spectrum.lf b/examples/Python/src/Audio/Spectrum.lf new file mode 100644 index 00000000..ed688e76 --- /dev/null +++ b/examples/Python/src/Audio/Spectrum.lf @@ -0,0 +1,34 @@ +target Python { + keepalive: true # Needed because of the physical action in the Microphone reactor. +} + +import Microphone from "../lib/Audio.lf" +import VectorPlot from "../lib/Plotters.lf" + +preamble {= + import numpy as np +=} + +reactor FFT { + input x + output y + + reaction(x) -> y {= + s = np.fft.rfft(x.value) + y.set(np.abs(s)) + =} +} + +main reactor { + m = new Microphone(block_size=1024) + f = new FFT() + p = new VectorPlot( + size = [10, 5], + title="Spectrum", + xlabel = "Frequency (Hz)", + ylabel="Magnitude", + ylim = [0, 10], + xrange = {= [0, 16000/2 + 1, 16000/1024] =}) + m.audio_data -> f.x + f.y -> p.y +} diff --git a/examples/Python/src/lib/Audio.lf b/examples/Python/src/lib/Audio.lf index 0a81b3cb..96fab7b3 100644 --- a/examples/Python/src/lib/Audio.lf +++ b/examples/Python/src/lib/Audio.lf @@ -1,79 +1,71 @@ /** * @file - * @author Edward A. Lee * @author Vincenzo Barbuto + * @author Edward A. Lee * @brief Basic audio library for Lingua Franca Python target. */ -target Python +target Python { + keepalive: true +} /** * @brief A reactor that produces audio data captured from a microphone. * - * The output will be a numpy array of arrays of audio samples, where entry - * in the outer array is the audio samples, one per channel. - * Currently, only one channel is supported. + * This reactor outputs a series of numpy arrays of audio samples. The size of each array is equal + * to the `block_size` parameter. Each array element will be a `float32` value if the number of + * channels is 1, or an array of `float32` values if the number of channels is greater than 1. * - * The logical time of the output is calculated based on the block_size, block_count, and sample_rate. - * This design assumes that the physical clock is synchronized to the microphone clock, otherwise - * it may block accessing the microphone, possibly leading to increasing lag. + * The logical time of the output is the physical time at which the audio hardware calls a callback + * function, which occurs when a buffer of length block_size has been filled with audio samples. + * + * If your machine has more than one microphone, you can select one by specifying the device index. + * To see what devices are available, run `python3 -m sounddevice`. + * + * To use this reactor, you must install the sounddevice and numpy libraries for Python. You can do + * this with `pip install sounddevice numpy`. */ -reactor Microphone(block_size=16000, sample_rate=16000, channels=1) { - +reactor Microphone(block_size=16000, sample_rate=16000, channels=1, device = {= None =}) { + physical action send_audio_data output audio_data - # Use a logical action to get deterministic timestamps on audio output. - logical action get_audio_data - - # Input audio stream. - state audio_stream - # Count of blocks. - state blocks_count = 1 - preamble {= import sounddevice as sd import numpy as np =} - reaction(startup) -> get_audio_data {= - self.audio_stream = self.sd.InputStream( - channels=self.channels, samplerate=self.sample_rate, blocksize=self.block_size) - self.audio_stream.start() # FIXME: Perhaps start in reaction to an input. - # Schedule the first output. - # The logical time of the output is calculated based on the block_size, block_count, and sample_rate. - t = self.block_size * SEC(1) // self.sample_rate # Sample interval. - get_audio_data.schedule(t) - =} + reaction(startup) -> send_audio_data {= + def callback(indata, frames, time, status): + if status: + print(status) + input_data = indata.astype(self.np.float32) + if (len(input_data)): + if self.channels == 1: + input_data = self.np.array(self.np.transpose(input_data)[0]) + send_audio_data.schedule(0, input_data) - reaction(get_audio_data) -> audio_data {= - try: - data, overflowed = self.audio_stream.read(self.block_size) - if overflowed: - print("Audio data lost between blocks.") - - if (len(data)): - audio_data.set(data) - except Exception as error: - print("Error reading audio data: ", type(error).__name__, " - ", error) - return + self.stream = self.sd.InputStream( + channels=self.channels, + samplerate=self.sample_rate, + callback=callback, + blocksize=self.block_size, + device=self.device) + self.stream.start() + =} - # Schedule the next output. - # Because the sample interval may not be an exact multiple of one nanosecond, - # we calculate the next output time based on the number of blocks, the block size, and the sample rate. - # This relies on the fact that integers do not overflow in Python. - self.blocks_count += 1 - time_since_start = self.blocks_count * self.block_size * SEC(1) // self.sample_rate - t = time_since_start - lf.time.logical_elapsed() # Offset from current logical time. - get_audio_data.schedule(t) + reaction(send_audio_data) -> audio_data {= + audio_data.set(send_audio_data.value) =} reaction(shutdown) {= - self.audio_stream.close() - print("Shutting down Microphone reactor") + if self.stream: + self.stream.close() =} } +/** @brief A test reactor that prints arrays of audio data captured from a microphone. */ main reactor { m = new Microphone() + reaction(m.audio_data) {= print(m.audio_data.value, " at time ", lf.time.logical_elapsed()) =} diff --git a/examples/Python/src/lib/Plotters.lf b/examples/Python/src/lib/Plotters.lf new file mode 100644 index 00000000..a289032d --- /dev/null +++ b/examples/Python/src/lib/Plotters.lf @@ -0,0 +1,96 @@ +/** + * @file + * @author Edward A. Lee + * @brief Reactors for plotting signals. + */ +target Python { + timeout: 10 s +} + +/** + * @brief A reactor that plots a sequence of vectors, where each vector plot replaces the previous + * one. + * + * The `size` parameter is a tuple of two values: the width and height of the plot in inches. + * + * The `title` parameter is a string that is displayed above the plot. The `xlabel` and `ylabel` + * parameters provide strings to label the x and y axes, respectively. + * + * The `ylim` parameter is a tuple of two values: the lower limit and the upper limit of the y axis. + * If no ylim is specified or it is not a tuple with two value, then the default range is determined + * by the limits of the first vector provided. + * + * The `xrange` parameter is a tuple of three values: the start of the x axis, the end of the x + * axis, and the step size. If `xrange` is `None` (the default), the x axis will be set to the + * length of the first input vector. + * + * To use this reactor, you must install the matplotlib and numpy libraries for Python. You can do + * this with `pip install matplotlib numpy`. See [matplotlib + * documentation](https://matplotlib.org/stable/) for more information. + */ +reactor VectorPlot( + size = {= None =}, + title = {= None =}, + xlabel = {= None =}, + xrange = {= None =}, + ylabel = {= None =}, + ylim = {= None =}) { + preamble {= + import matplotlib.pyplot as plt + import numpy as np + =} + + input y + state showing = False + state figure = {= None =} + state axes = {= None =} + state line1 = {= None =} + + reaction(y) {= + if not self.showing: + # First vector to plot. Set up the plot. + # to run GUI event loop + self.plt.ion() + self.figure, self.axes = self.plt.subplots(figsize=self.size) + if (self.title): + self.axes.set_title(self.title) + if (self.xlabel): + self.axes.set_xlabel(self.xlabel) + if (self.ylabel): + self.axes.set_ylabel(self.ylabel) + if (self.ylim) and len(self.ylim) == 2: + self.axes.set_ylim(self.ylim[0], self.ylim[1]) + + # Determine the x axis. + xrange = self.xrange + if not xrange: + xrange = [0, len(y.value), 1] + x = self.np.arange(xrange[0], xrange[1], xrange[2]) + + # Plot the data. + self.line1, = self.axes.plot(x, y.value) + + self.showing = True + else: + self.line1.set_ydata(y.value) + + self.figure.canvas.draw() + self.figure.canvas.flush_events() + =} +} + +main reactor { + preamble {= + import numpy as np + =} + state count = 1 + timer t(0, 1 s) + v = new VectorPlot(title = "Sine wave", xlabel = "X Axis", ylabel = "Y Axis") + + reaction(t) -> v.y {= + x = self.np.linspace(0, 2 * self.np.pi * self.count, 200) + y = self.np.sin(x) + v.y.set(y) + self.count += 1 + =} +} From 7945648a49c0b3621161e77f5e3c2105ba95f6a5 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Thu, 27 Jun 2024 13:16:00 -0700 Subject: [PATCH 3/3] Merged with main --- examples/C/src/lib/WebSocketServerString.lf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/C/src/lib/WebSocketServerString.lf b/examples/C/src/lib/WebSocketServerString.lf index cd0c55f3..3e72a827 100644 --- a/examples/C/src/lib/WebSocketServerString.lf +++ b/examples/C/src/lib/WebSocketServerString.lf @@ -74,14 +74,14 @@ reactor WebSocketServerString(hostport: int = 8080, initial_file: string = {= NU if (in_dynamic->is_present) { message_copy = (char*)malloc(strlen(in_dynamic->value)); strcpy(message_copy, in_dynamic->value); + to_send->length = strlen(in_dynamic->value); } else { message_copy = (char*)malloc(strlen(in_static->value)); strcpy(message_copy, in_static->value); + to_send->length = strlen(in_static->value); } to_send->message = message_copy; - to_send->length = strlen(in_dynamic->value); to_send->wsi = self->ws.wsi; - lf_set(server.send, to_send); } else { // Web socket is not connected.