Skip to content

Commit

Permalink
Merge pull request #2 from vkottler/dev/streaming
Browse files Browse the repository at this point in the history
Dev/streaming
  • Loading branch information
vkottler authored Mar 4, 2024
2 parents b71b5f1 + 2bccf08 commit 45dffab
Show file tree
Hide file tree
Showing 20 changed files with 423 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
- run: |
mk python-release owner=vkottler \
repo=quasimoto version=0.2.0
repo=quasimoto version=0.2.1
if: |
matrix.python-version == '3.11'
&& matrix.system == 'ubuntu-latest'
Expand Down
9 changes: 8 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
[MAIN]
ignored-modules=pyaudio

[DESIGN]
max-args=6
max-args=7
max-attributes=8

[MESSAGES CONTROL]
disable=too-few-public-methods
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
=====================================
generator=datazen
version=3.1.4
hash=c8fbe39d4a560fc68d4d3bba34f88ccd
hash=018543b1acb813e792af40f264f6ea54
=====================================
-->

# quasimoto ([0.2.0](https://pypi.org/project/quasimoto/))
# quasimoto ([0.2.1](https://pypi.org/project/quasimoto/))

[![python](https://img.shields.io/pypi/pyversions/quasimoto.svg)](https://pypi.org/project/quasimoto/)
![Build Status](https://github.com/vkottler/quasimoto/workflows/Python%20Package/badge.svg)
Expand Down
2 changes: 1 addition & 1 deletion config
3 changes: 3 additions & 0 deletions local/configs/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ commands:
mypy_local: |
[mypy-scipy.*]
ignore_missing_imports = True
[mypy-pyaudio.*]
ignore_missing_imports = True
2 changes: 1 addition & 1 deletion local/variables/package.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
major: 0
minor: 2
patch: 0
patch: 1
entry: quasimoto
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ strict_equality = False
# quasimoto-specific configurations.
[mypy-scipy.*]
ignore_missing_imports = True

[mypy-pyaudio.*]
ignore_missing_imports = True
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta:__legacy__"

[project]
name = "quasimoto"
version = "0.2.0"
version = "0.2.1"
description = "A lossless audio generator."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
4 changes: 2 additions & 2 deletions quasimoto/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# =====================================
# generator=datazen
# version=3.1.4
# hash=8efc396041b8d6d582b53a5468ffda71
# hash=9bcedc43070f45384f7b98092917a200
# =====================================

"""
Expand All @@ -10,4 +10,4 @@

DESCRIPTION = "A lossless audio generator."
PKG_NAME = "quasimoto"
VERSION = "0.2.0"
VERSION = "0.2.1"
27 changes: 21 additions & 6 deletions quasimoto/sampler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import math
from typing import TypeVar

# third-party
from runtimepy.primitives import Double

# internal
from quasimoto.wave.writer import DEFAULT_BITS, DEFAULT_SAMPLE_RATE

Expand All @@ -25,18 +28,21 @@ def __init__(
duration_s: float = None,
frequency: float = DEFAULT_FREQUENCY,
time: float = 0.0,
amplitude: float = 1.0,
) -> None:
"""Initialize this instance."""

# Can be changed after initialization.
self.frequency = frequency
self.frequency = Double(value=frequency)
self.amplitude = Double(value=amplitude)
self.duration_s = duration_s

# Runtime state.
self.time = time

# Constants / final.
self.sample_rate = sample_rate
self.period = 1.0 / self.sample_rate
self.time = time

# Note: this assumed signed + zero-centered.
self.num_bits = num_bits
self.scalar = (2 ** (self.num_bits - 1)) - 1
Expand All @@ -48,10 +54,14 @@ def __copy__(self: T) -> T:
num_bits=self.num_bits,
sample_rate=self.sample_rate,
duration_s=self.duration_s,
frequency=self.frequency,
frequency=self.frequency.value,
time=self.time,
)

def harmonic(self, index: int) -> float:
"""Get a harmonic frequency based on this instance's frequency."""
return float(2**index) * self.frequency.value

def copy(self: T, harmonic: int = None, duration_s: float = None) -> T:
"""Get a copy of this instance."""

Expand All @@ -60,7 +70,7 @@ def copy(self: T, harmonic: int = None, duration_s: float = None) -> T:
if duration_s is not None:
result.duration_s = duration_s
if harmonic is not None:
result.frequency = float(2**harmonic) * self.frequency
result.frequency.value = result.harmonic(harmonic)

return result

Expand All @@ -78,7 +88,12 @@ def advance(self, steps: int = 1) -> bool:

def sin(self, now: float) -> int:
"""Get a raw sin value sample."""
return int(self.scalar * math.sin(math.tau * now * self.frequency))

return int(
self.scalar
* self.amplitude.value
* math.sin(math.tau * now * self.frequency.value)
)

def value(self, now: float) -> int:
"""Get the next value."""
Expand Down
8 changes: 2 additions & 6 deletions quasimoto/wave/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

# third-party
from runtimepy.codec.protocol import Protocol
from runtimepy.primitives.byte_order import ByteOrder
from vcorelib.logging import LoggerMixin

# internal
Expand All @@ -17,17 +16,14 @@
class FormatMixin(LoggerMixin):
"""A class mixin for classes that use wave format data."""

byte_order = WaveFormat.protocol.array.byte_order

def __init__(self) -> None:
"""Initialize this instance."""

super().__init__()
self.format = WaveFormat.instance()

@property
def byte_order(self) -> ByteOrder:
"""Get the byte order for this format."""
return self.format.array.byte_order

@property
def channels(self) -> int:
"""Get the number of channels in this stream."""
Expand Down
22 changes: 17 additions & 5 deletions quasimoto/wave/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from contextlib import contextmanager
import os
from pathlib import Path
from typing import Iterable, Iterator
from typing import BinaryIO, Iterable, Iterator

# third-party
from runtimepy.primitives import Int16
Expand Down Expand Up @@ -58,6 +58,21 @@ def __init__(
# Write 'data' chunk header.
ChunkType.DATA.to_stream(self.riff.stream)

@classmethod
def to_bytes(cls, value: int) -> bytes:
"""Convert a sample value to bytes."""

# Note the underlying type assumption.
return Int16.kind.encode(value, byte_order=cls.byte_order)

@classmethod
def to_stream(cls, stream: BinaryIO, value: int) -> int:
"""Write a value to a stream."""

data = cls.to_bytes(value)
stream.write(data)
return len(data)

def write(self, samples: Iterable[tuple[int, ...]]) -> None:
"""Write samples to the output."""

Expand All @@ -72,10 +87,7 @@ def write(self, samples: Iterable[tuple[int, ...]]) -> None:
size = 0
for sample in samples:
for point in sample:
Int16.kind.write(
point, self.riff.stream, byte_order=self.byte_order
)
size += 2
size += self.to_stream(self.riff.stream, point)

self.riff.write_size(size, seek=size_pos)

Expand Down
2 changes: 2 additions & 0 deletions scripts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
portaudio
pyaudio
42 changes: 42 additions & 0 deletions scripts/common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/bash

set -e

pushd "$CWD" >/dev/null || exit

# Get PortAudio source.
if [ ! -d portaudio ]; then
git clone https://github.com/PortAudio/portaudio
sudo apt-get install libasound-dev
fi

# Get PyAudio source.
if [ ! -d pyaudio ]; then
git clone https://people.csail.mit.edu/hubert/git/pyaudio.git
fi

VENV="venv$PYTHON_VERSION"
PIP="$VENV/bin/pip"
PYTHON="$VENV/bin/python"

setup_venv() {
$PIP install -e "$REPO"
$PIP install -e pyaudio
}

if [ ! -d "$VENV" ]; then
"python$PYTHON_VERSION" -m venv "$VENV"
test -f "$PYTHON"

"$PIP" install -U pip
setup_venv
fi

on_exit() {
popd >/dev/null || exit
}

trap on_exit EXIT

SAMPLE_WAV="$REPO/tests/data/valid/vonbass.wav"
test -f "$SAMPLE_WAV"
14 changes: 14 additions & 0 deletions scripts/port_audio.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash

REPO=$(git rev-parse --show-toplevel)
CWD=$REPO/scripts
source "$CWD/common.sh"

pushd portaudio >/dev/null || exit

./configure
make clean
make -j
sudo make install

popd >/dev/null || exit
73 changes: 73 additions & 0 deletions scripts/stream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
A module for streaming raw data to pyaudio.
"""

# built-in
from contextlib import contextmanager
from io import BytesIO
import sys
import time
from typing import Iterator

# third-party
import pyaudio

# internal
from quasimoto.sampler import Sampler
from quasimoto.wave import WaveWriter


@contextmanager
def get_pyaudio() -> Iterator[pyaudio.PyAudio]:
"""Get a PyAudio instance."""

audio = pyaudio.PyAudio()
try:
yield audio
finally:
audio.terminate()


def main(argv: list[str]) -> int:
"""The program's main entry."""

left = Sampler()
right = left.copy(harmonic=-1)

def callback(in_data, frame_count, time_info, status) -> bytes:
"""Called when stream needs more data?"""

# Need to figure out what these are.
del in_data
del time_info
del status

with BytesIO() as stream:
for _ in range(frame_count):
WaveWriter.to_stream(stream, next(left))
WaveWriter.to_stream(stream, next(right))
return (stream.getvalue(), pyaudio.paContinue)

with get_pyaudio() as audio:
stream = audio.open(
format=audio.get_format_from_width(left.num_bits // 8),
channels=2,
rate=left.sample_rate,
output=True,
stream_callback=callback,
)

keep_going = True

while keep_going and stream.is_active():
try:
time.sleep(0.1)
except KeyboardInterrupt:
stream.close()
keep_going = False

return 0


if __name__ == "__main__":
sys.exit(main(sys.argv))
13 changes: 13 additions & 0 deletions scripts/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash

REPO=$(git rev-parse --show-toplevel)
CWD=$REPO/scripts
source "$CWD/common.sh"

# Can use this to verify sound output works.
# $PYTHON pyaudio/examples/play_wave.py "$SAMPLE_WAV"

set -x
$PYTHON stream.py
echo "Exited $?."
set +x
10 changes: 10 additions & 0 deletions tasks/default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
app:
- tasks.dev.main
- runtimepy.net.apps.wait_for_stop

factories:
- {name: tasks.dev.Stereo}

tasks:
- {name: stereo, factory: stereo, period_s: 0.02}
Loading

0 comments on commit 45dffab

Please sign in to comment.