Skip to content

Commit

Permalink
Add audio input node
Browse files Browse the repository at this point in the history
  • Loading branch information
mesca committed Nov 6, 2023
1 parent 76c77ab commit 0ef7704
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 2 deletions.
21 changes: 21 additions & 0 deletions examples/passthrough.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
graphs:
- nodes:
- id: input
module: timeflux_audio.nodes.device
class: Input
- id: ui
module: timeflux_ui.nodes.ui
class: UI
params:
settings:
monitor:
lineWidth: 1
- id: output
module: timeflux_audio.nodes.device
class: Output
edges:
- source: input
target: ui:audio
- source: input
target: output
rate: 10
69 changes: 67 additions & 2 deletions timeflux_audio/nodes/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,73 @@

import time
import numpy as np
import pandas as pd
import sounddevice as sd
from threading import Thread, Lock
from timeflux.core.node import Node


class Input(Node):
"""Audio input.
Attributes:
o (Port): Default output, provides DataFrame.
Args:
device (int|string): input device (numeric or string ID).
If none specified, will use the system default. Default: ``None``.
Example:
.. literalinclude:: /../examples/passthrough.yaml
:language: yaml
"""

def __init__(self, device=None):
self.device = device
self.logger.info(f"Available audio interfaces:\n{sd.query_devices()}")
self._data = np.empty((0, 1))
self._running = True
self._lock = Lock()
self._thread = Thread(target=self._loop).start()

def _callback(self, indata, frames, time, status):
if status:
self.logger.warning(status)
size = indata.shape[0]
if size > 0:
self._lock.acquire()
self._data = np.vstack((self._data, indata))
self._stop = pd.Timestamp.now(tz="UTC")
self._lock.release()

def _loop(self):
samplerate = sd.query_devices(self.device, "input")["default_samplerate"]
self.meta = {"rate": samplerate}
self._delta = 1 / samplerate
with sd.InputStream(
device=self.device,
channels=1,
callback=self._callback,
samplerate=samplerate,
):
while self._running:
sd.sleep(1)

def update(self):
self._lock.acquire()
if self._data.shape[0] > 0:
periods = self._data.shape[0]
start = self._stop - pd.Timedelta(self._delta * periods, "s")
timestamps = pd.date_range(start=start, end=self._stop, periods=periods)
self.o.set(self._data, timestamps, meta=self.meta)
self._data = np.empty((0, 1))
self._lock.release()

def terminate(self):
self._running = False


class Output(Node):
"""Audio output.
Expand All @@ -16,14 +78,17 @@ class Output(Node):
Args:
device (int|string): output device (numeric or string ID).
If none specified, will use the system default. Default: ``None``.
amplitude (float): audio volume.
Default: 1
Example:
.. literalinclude:: /../examples/sine.yaml
:language: yaml
"""

def __init__(self, device=None):
def __init__(self, device=None, amplitude=1):
self.device = device
self.amplitude = amplitude
self.logger.info(f"Available audio interfaces:\n{sd.query_devices()}")
self._data = np.empty((0, 1))
self._running = True
Expand All @@ -44,7 +109,6 @@ def _callback(self, outdata, frames, time, status):

def _loop(self):
samplerate = sd.query_devices(self.device, "output")["default_samplerate"]
self._start_idx = 0
with sd.OutputStream(
device=self.device,
channels=1,
Expand All @@ -56,6 +120,7 @@ def _loop(self):

def update(self):
if self.i.ready():
self.i.data *= self.amplitude
self._lock.acquire()
self._data = np.vstack((self._data, self.i.data.values))
self._lock.release()
Expand Down

0 comments on commit 0ef7704

Please sign in to comment.