Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: GlasgowEmbedded/glasgow
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 9d4981388ee4
Choose a base ref
...
head repository: GlasgowEmbedded/glasgow
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: c0a74e8eba8d
Choose a head ref
  • 1 commit
  • 2 files changed
  • 1 contributor

Commits on Aug 7, 2019

  1. applet.audio.dac: add support for connecting to a PCM source socket.

    Works with Linux desktop!
    whitequark committed Aug 7, 2019
    Copy the full SHA
    c0a74e8 View commit details
Showing with 83 additions and 16 deletions.
  1. +63 −14 software/glasgow/applet/audio/dac/__init__.py
  2. +20 −2 software/glasgow/support/endpoint.py
77 changes: 63 additions & 14 deletions software/glasgow/applet/audio/dac/__init__.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,46 @@
import logging
import asyncio
import argparse
from migen import *

from ....support.endpoint import *
from ....gateware.clockgen import *
from ... import *


class SigmaDeltaDACChannel(Module):
def __init__(self, output, bits):
def __init__(self, output, bits, signed):
self.stb = Signal()

self.level = Signal(bits)
self.update = Signal()

###

level_u = Signal(bits)
if signed:
self.comb += level_u.eq(self.level - (1 << (bits - 1)))
else:
self.comb += level_u.eq(self.level)

accum = Signal(bits)
level_r = Signal(bits)
self.sync += [
If(self.stb,
Cat(accum, output).eq(accum + level_r)
),
If(self.update,
level_r.eq(self.level)
level_r.eq(level_u)
)
]


class AudioDACSubtarget(Module):
def __init__(self, pads, out_fifo, pulse_cyc, sample_cyc, width):
def __init__(self, pads, out_fifo, pulse_cyc, sample_cyc, width, signed):
assert width in (1, 2)

channels = [SigmaDeltaDACChannel(output, bits=width * 8) for output in pads.o_t.o]
channels = [SigmaDeltaDACChannel(output, bits=width * 8, signed=signed)
for output in pads.o_t.o]
self.submodules += channels

self.submodules.clkgen = ClockGen(pulse_cyc)
@@ -103,8 +112,8 @@ class AudioDACApplet(GlasgowApplet, name="audio-dac"):
Play sound using a 1-bit sigma-delta DAC, i.e. pulse density modulation.
Currently, the supported sample formats are:
* 1..16 channel unsigned 8-bit,
* 1..16 channel unsigned 16-bit little endian.
* 1..16 channel signed/unsigned 8-bit,
* 1..16 channel signed/unsigned 16-bit little endian.
Other formats may be converted to it using:
@@ -113,7 +122,16 @@ class AudioDACApplet(GlasgowApplet, name="audio-dac"):
For example, to play an ogg file:
$ sox samples.ogg -c 2 -r 48000 samples.u16
$ glasgow run audio-dac --pins-o 0,1 -r 48000 -w 2 samples.u16
$ glasgow run audio-dac --pins-o 0,1 -r 48000 -w 2 -u play samples.u16
To use the DAC as a PulseAudio sink, add the following line to default.pa:
load-module module-simple-protocol-tcp source=0 record=true rate=48000 channels=2 \
format=s16le port=12345
Then run:
$ glasgow run audio-dac --pins-o 0,1 -r 48000 -w 2 -s connect localhost:12345
"""

__pin_sets = ("o",)
@@ -133,6 +151,13 @@ def add_build_arguments(cls, parser, access):
parser.add_argument(
"-w", "--width", metavar="WIDTH", type=int, default=1,
help="set sample width to WIDTH bytes (default: %(default)d)")
g_signed = parser.add_mutually_exclusive_group(required=True)
g_signed.add_argument(
"-s", "--signed", dest="signed", default=False, action="store_true",
help="interpret samples as signed")
g_signed.add_argument(
"-u", "--unsigned", dest="signed", action="store_false",
help="interpret samples as unsigned")

def build(self, target, args):
self.mux_interface = iface = target.multiplexer.claim_interface(self, args)
@@ -147,6 +172,7 @@ def build(self, target, args):
pulse_cyc=pulse_cyc,
sample_cyc=int(target.sys_clk_freq // args.sample_rate),
width=args.width,
signed=args.signed,
))
return subtarget

@@ -155,19 +181,42 @@ async def run(self, device, args):

@classmethod
def add_interact_arguments(cls, parser):
parser.add_argument(
# TODO(py3.7): add required=True
p_operation = parser.add_subparsers(dest="operation", metavar="OPERATION")

p_play = p_operation.add_parser(
"play", help="play PCM file")
p_play.add_argument(
"file", metavar="FILE", type=argparse.FileType("rb"),
help="read PCM data from FILE")
parser.add_argument(
p_play.add_argument(
"-l", "--loop", default=False, action="store_true",
help="loop the input samples")

p_connect = p_operation.add_parser(
"connect", help="connect to a PCM source")
ServerEndpoint.add_argument(p_connect, "pcm_endpoint")

async def interact(self, device, args, pcm_iface):
pcm_data = args.file.read()
while True:
await pcm_iface.write(pcm_data)
if not args.loop:
break
if args.operation == "play":
pcm_data = args.file.read()
while True:
await pcm_iface.write(pcm_data)
if not args.loop:
break

if args.operation == "connect":
proto, *proto_args = args.pcm_endpoint
if proto == "tcp":
reader, _ = await asyncio.open_connection(*proto_args)
elif proto == "unix":
reader, _ = await asyncio.open_unix_connection(*proto_args)
else:
assert False
while True:
data = await reader.read(512)
await pcm_iface.write(data)
await pcm_iface.flush(wait=False)

# -------------------------------------------------------------------------------------------------

22 changes: 20 additions & 2 deletions software/glasgow/support/endpoint.py
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
from .aobject import *


__all__ = ["ServerEndpoint"]
__all__ = ["ServerEndpoint", "ClientEndpoint"]


def endpoint(spec):
@@ -114,7 +114,7 @@ def _check_pushback(self):
self._log(logging.TRACE, "queue full, pausing reads")
self._transport.pause_reading()
self._read_paused = True
elif self._read_paused and self._queued < self._queue_size:
elif self._read_paused and self._queued < self._queue_size:
self._log(logging.TRACE, "queue not full, resuming reads")
self._transport.resume_reading()
self._read_paused = False
@@ -202,6 +202,24 @@ async def close(self):
if self._transport:
self._transport.close()


class ClientEndpoint(aobject, asyncio.Protocol):
@classmethod
def add_argument(cls, parser, name, default=None):
metavar = name.upper().replace("_", "-")
help = "connect to %s, either unix:PATH or tcp:HOST:PORT" % metavar
if default is None:
nargs = None
else:
nargs = "?"
help += " (default: %(default)s)"

parser.add_argument(
name, metavar=metavar, type=endpoint, nargs=nargs, default=default,
help=help)

# FIXME: finish this

# -------------------------------------------------------------------------------------------------

import unittest