Skip to content

Commit

Permalink
Initial working version with input/output and documentation (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
aef- authored Apr 4, 2022
1 parent c0f19da commit 8fa27e8
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 45 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## v0.1.0 (2022-04-03)
* Enhancements
* First public release
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022 EleanorDAW
Copyright (c) 2022 Adrian Fraiha

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
31 changes: 24 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,36 @@ Elixir 1.12
Rust 1.56.1
```

## Set-up (OSX)
## Set-up
### OSX
```
brew install jack
brew services start jack
mix test
```

It may help to view what's going with JACK using a GUI like https://qjackctl.sourceforge.io/.
It may help to view what's going with JACK using a GUI like https://qjackctl.sourceforge.io/. If you want to capture sound this is simplest way to connect until an API is added to assist with this.


## Usage
This is an example of piping your capture to output, be wary of feedback. You have to explicity connect your capture with "ExJackDemo:in", if you're unsure how to do this, install QJackCTL.

```elixir
$ iex -s mix
> ExJack.Server.start_link(%{name: "ExJackDemo"})
> ExJack.Server.set_input_func(fn frames -> ExJack.Server.send_frames(frames) end )
```

## TODO
- [x] Play audio frames
- [ ] Documentation (generate ExDoc + usage with additional examples)
- [ ] Input access
- [ ] Additional tests/typespecs in Rust/Elixir
- [ ] Release initial version to Hex
### Road to stable version 1
The first three are necessary to make this library useable beyond hobby projects.
- [ ] Better support for expected frames per cycle from JACK
- [ ] Handle variable channels with definable sources
- [ ] Handle JACK notifications
- [ ] Handling for cases that drop the JACK client such as underruns.
- [ ] Additional tests in Elixir
- [ ] Additional tests in Rust
- [ ] MCU demo
- [ ] Improve documentation with additional examples
- [ ] Autocorrection for xruns
1 change: 1 addition & 0 deletions lib/ex_jack.ex
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
defmodule ExJack do
@moduledoc false
end
22 changes: 21 additions & 1 deletion lib/ex_jack/native.ex
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
defmodule ExJack.Native do
@moduledoc """
A Rustler NIF that interfaces with JACK. Use `ExJack.Server` instead.
While there are only minimal number of functions compared to the JACK API,
the majority of interfacing is done through messages sent between the NIF
thread and `ExJack.Server` GenServer process.
Typically when interfacing with ExJack, calling these methods directly isn't necessary
and you should instead, as a client, use `ExJack.Server`.
If you are interested in using this library, signatures and usage can be found in `ExJack.Server`.
"""

use Rustler, otp_app: :ex_jack, crate: "exjack"

@type options_t :: %{
name: String.t(),
auto_connect: boolean(),
use_callback: boolean()
}

@start_defaults %{
name: __MODULE__,
auto_connect: true,
use_callback: true
}

def start(opts \\ %{}) do
@spec start(options_t) :: any()
def start(opts) do
_start(Map.merge(@start_defaults, opts))
end

Expand Down
131 changes: 113 additions & 18 deletions lib/ex_jack/server.ex
Original file line number Diff line number Diff line change
@@ -1,56 +1,151 @@
defmodule ExJack.Server do
@moduledoc """
A GenServer module that interfaces with JACK audio API I/O.
There are two methods for outputting sound to JACK:
1. Calling `send_frames/1`
2. Setting an output function using `set_output_func/1`, which JACK
calls every time it wants frames.
At the moment, there is only one method of retrieving input data, which is to set
an input callback using `set_input_func/1`.
Latency will obviously vary and if you have a busy machine, expect xruns. xruns,
which is shorthand for overruns and underruns, occur when you either send too
many frames or not enough frames. If the CPU is busy doing some other work
and neglects to send frames to the soundcard, the soundcard buffer runs out of frames
to play. An underrun will then occur. You could send too many frames to the
soundcard. If you send more than its buffers can hold, the data will be lost. This
is an overrun.
"""

use GenServer

require Logger
defstruct handler: nil,
shutdown_handler: nil,
current_frame: 0,
output_func: &ExJack.Server.noop/1,
input_func: &ExJack.Server.noop/1

defstruct handler: nil, shutdown_handler: nil, current_frame: 0, callback: &ExJack.Server.noop/1
@type t :: %__MODULE__{
handler: any(),
shutdown_handler: any(),
current_frame: pos_integer(),
output_func: output_func_t,
input_func: input_func_t
}

def noop(_) do
[]
end
@type frames_t :: list(float())
@type output_func_t :: (Range.t() -> frames_t)
@type input_func_t :: (frames_t -> any())
@type options_t :: %{
name: String.t(),
use_callback: boolean(),
auto_connect: boolean(),
}

@doc """
Start the server.
JACK NIF will start a thread that runs the JACK client.
def start_link(%{name: _} = opts) do
It will auto-connect to two standard channels which you can modify
through JACK.
## Parameters
- name: Used to name the JACK node (suffixed with `:in` and `:out`)
e.g. If you pass `%{name: "HelloWorld"}`, you can interface with this
connection within JACK through `HelloWorld:in` and `HelloWorld:out`.
"""
@spec start_link(options_t) :: GenServer.server()
def start_link(%{name: _name} = opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end

@doc """
Set the callback function that JACK will call when it requests more frames.
"""
@spec set_output_func(output_func_t) :: :ok
def set_output_func(output_func) do
GenServer.cast(__MODULE__, {:set_output_func, output_func})
end

@doc """
Set the callback function that will receive input data from JACK each cycle.
The output of the function is currently not used for anything.
"""
@spec set_input_func(input_func_t) :: :ok
def set_input_func(input_func) do
GenServer.cast(__MODULE__, {:set_input_func, input_func})
end

@doc """
Sends a list of frames for JACK to play during its next cycle.
"""
@spec send_frames(frames_t) :: :ok
def send_frames(frames) do
unless Enum.empty?(frames) do
GenServer.cast(__MODULE__, {:send_frames, frames})
end
end

@impl true
def init(opts) do
{:ok, handler, shutdown_handler, _opts} = ExJack.Native.start(opts)

{:ok, %__MODULE__{handler: handler, shutdown_handler: shutdown_handler, current_frame: 0}}
end

def handle_cast({:set_callback, callback}, state) do
{:noreply, %{state | callback: callback}}
@impl true
@spec handle_cast({:set_output_func, output_func_t}, t()) :: {:noreply, t()}
def handle_cast({:set_output_func, output_func}, state) do
{:noreply, %{state | output_func: output_func}}
end

@impl true
@spec handle_cast({:set_input_func, output_func_t}, t()) :: {:noreply, t()}
def handle_cast({:set_input_func, output_func}, state) do
{:noreply, %{state | input_func: output_func}}
end

@impl true
@spec handle_cast({:send_frames, frames_t}, t()) :: {:noreply, t()}
def handle_cast({:send_frames, frames}, %{handler: handler} = state) do
ExJack.Native.send_frames(handler, frames)

{:noreply, state}
end

@impl true
@spec handle_info({:in_frames, frames_t}, t()) :: {:noreply, t()}
def handle_info({:in_frames, frames}, %{input_func: input_func} = state) do
input_func.(frames)

{:noreply, state}
end

@impl true
@spec handle_cast({:request, pos_integer()}, t()) :: {:noreply, __MODULE__.t()}
def handle_info(
{:request, requested_frames},
%{current_frame: current_frame, callback: callback} = state
%{current_frame: current_frame, output_func: output_func} = state
) do
end_frames = current_frame + requested_frames - 1
send_frames(callback.(current_frame..end_frames))
send_frames(output_func.(current_frame..end_frames))

{:noreply, %{state | current_frame: end_frames + 1}}
end

@impl true
def terminate(_reason, %{shutdown_handler: shutdown_handler}) do
ExJack.Native.stop(shutdown_handler)
:ok
end

def set_callback(callback) do
GenServer.cast(__MODULE__, {:set_callback, callback})
end

def send_frames(frames) do
unless Enum.empty?(frames) do
GenServer.cast(__MODULE__, {:send_frames, frames})
end
@doc false
def noop(_) do
[]
end
end
41 changes: 35 additions & 6 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
defmodule ExJack.MixProject do
use Mix.Project

@source_url "https://github.com/fraihaav/ex_jack"
@version "0.28.3"

def project do
[
app: :ex_jack,
version: "0.1.0",
version: @version,
source_url: @source_url,
elixir: "~> 1.13",
build_embedded: Mix.env() == :prod,
start_permanent: Mix.env() == :prod,
compilers: Mix.compilers(),
# rustler_crates: rustler_crates(),

# compilers: [:elixir_make] ++ Mix.compilers(),
# make_cwd: "c_src",
deps: deps()
deps: deps(),
docs: docs(),
package: package()
]
end

Expand All @@ -28,9 +30,36 @@ defmodule ExJack.MixProject do
defp deps do
[
# {:elixir_make, "~> 0.6", runtime: false},
{:dialyxir, "~> 1.0", only: [:dev], runtime: false},
{:ex_doc, "~> 0.22.1", only: :dev, runtime: false},
{:benchee, "~> 1.0", only: [:test, :dev]},
{:rustler, github: "hansihe/rustler", sparse: "rustler_mix"}
]
end

defp docs do
[
main: "readme",
extras: [
"README.md",
"LICENSE",
"CHANGELOG.md"
],
source_ref: "v#{@version}",
source_url: @source_url,
groups_for_modules: [],
skip_undefined_reference_warnings_on: ["CHANGELOG.md"]
]
end

defp package do
[
licenses: ["MIT"],
maintainers: ["Adrian Fraiha"],
links: %{
"GitHub" => @source_url,
"Changelog" => "https://hexdocs.pm/ex_jack/changelog.html"
}
]
end
end
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
%{
"benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"},
"elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ex_doc": {:hex, :ex_doc, "0.22.6", "0fb1e09a3e8b69af0ae94c8b4e4df36995d8c88d5ec7dbd35617929144b62c00", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "1e0aceda15faf71f1b0983165e6e7313be628a460e22a031e32913b98edbd638"},
"jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
Expand Down
1 change: 1 addition & 0 deletions native/exjack/src/atoms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ rustler::atoms! {
ok,
error,
request,
in_frames
}
Loading

0 comments on commit 8fa27e8

Please sign in to comment.