Skip to content

Commit

Permalink
feat: add audio related functions
Browse files Browse the repository at this point in the history
Co-authored-by: veliandev <[email protected]>
  • Loading branch information
shiryel and FatigueDev committed Aug 28, 2024
1 parent c179c93 commit ee44d7c
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 48 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![API Docs](https://img.shields.io/badge/api-docs-blue.svg?style=flat)](https://hexdocs.pm/rayex/)
[![Run Tests](https://github.com/shiryel/rayex/actions/workflows/test.yml/badge.svg)](https://github.com/shiryel/rayex/actions/workflows/test.yml)

[![BlueSky](https://img.shields.io/badge/bsky-follow-blue.svg?logo=data:image/svg%2bxml;base64,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)](shiryel.bsky.social)
[![BlueSky](https://img.shields.io/badge/bsky-follow-blue.svg?logo=data:image/svg%2bxml;base64,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)](https://shiryel.bsky.social)

Rayex provides Elixir NIF bindings to [Raylib](https://www.raylib.com/)

Expand Down
76 changes: 76 additions & 0 deletions c_src/rayex/rayex.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
#include "rayex.h"
#include <raylib.h>

/*
* Payloads let us ignore some structs when binding data from Elixir <-> C
* But will make the data unavailable on the elixir side!
*/
#define CREATE_UNIFEX_PAYLOAD_FOR(T, name) \
UnifexPayload create_##name##_unifex_payload(UnifexEnv *env, T value) { \
UnifexPayload payload; \
unifex_payload_alloc(env, UNIFEX_PAYLOAD_BINARY, sizeof(value), &payload); \
memcpy(payload.data, &value, payload.size); \
return payload; \
} \
\
T get_##name##_unifex_payload(UnifexEnv *env, UnifexPayload *in_payload) { \
UnifexPayload out_payload; \
unifex_payload_alloc(env, UNIFEX_PAYLOAD_BINARY, in_payload->size, \
&out_payload); \
memcpy(out_payload.data, in_payload->data, out_payload.size); \
T result; \
memcpy(&result, out_payload.data, out_payload.size); \
return result; \
}

// NOTE: "E_" when converting back to NIF struct

#define VECTOR2(v) ((Vector2){.x = v.x, .y = v.y})
Expand Down Expand Up @@ -581,12 +603,66 @@ UNIFEX_TERM draw_grid(UnifexEnv *env, int slices, double spacing) {

// Audio device management functions

UNIFEX_TERM init_audio_device(UnifexEnv *env) {
InitAudioDevice();
return init_audio_device_result_ok(env);
}

UNIFEX_TERM close_audio_device(UnifexEnv *env) {
CloseAudioDevice();
return close_audio_device_result_ok(env);
}

UNIFEX_TERM is_audio_device_ready(UnifexEnv *env) {
bool res = IsAudioDeviceReady();
return is_audio_device_ready_result(env, res);
}

UNIFEX_TERM set_master_volume(UnifexEnv *env, double volume) {
SetMasterVolume((float)volume);
return set_master_volume_result_ok(env);
}

UNIFEX_TERM get_master_volume(UnifexEnv *env) {
float res = GetMasterVolume();
return get_master_volume_result(env, res);
}

// Wave/Sound loading/unloading functions

CREATE_UNIFEX_PAYLOAD_FOR(Sound, sound)

UNIFEX_TERM load_sound(UnifexEnv *env, char *fileName) {
Sound sound = LoadSound(fileName);
UnifexPayload payload = create_sound_unifex_payload(env, sound);
UNIFEX_TERM result = load_sound_result(env, &payload);
unifex_payload_release(&payload);

return result;
}

// Wave/Sound management functions

// Music management functions

UNIFEX_TERM play_sound(UnifexEnv *env, UnifexPayload *payload) {
Sound sound = get_sound_unifex_payload(env, payload);
PlaySound(sound);
return play_sound_result_ok(env);
}

UNIFEX_TERM stop_sound(UnifexEnv *env, UnifexPayload *payload) {
Sound sound = get_sound_unifex_payload(env, payload);
StopSound(sound);
return stop_sound_result_ok(env);
}

UNIFEX_TERM is_sound_ready(UnifexEnv *env, UnifexPayload *payload) {
Sound sound = get_sound_unifex_payload(env, payload);
bool res = IsSoundReady(sound);
return is_sound_ready_result(env, res);
}

// AudioStream management functions

/***********
Expand Down
41 changes: 26 additions & 15 deletions c_src/rayex/rayex.spec.exs
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,24 @@ spec draw_grid(slices :: int, spacing :: float) :: :ok :: label
#########

# Audio device management functions
spec init_audio_device() :: :ok :: label
spec close_audio_device() :: :ok :: label
spec is_audio_device_ready() :: result :: bool
spec set_master_volume(volume :: float) :: :ok :: label
spec get_master_volume() :: volume :: float

# Wave/Sound loading/unloading functions

spec load_sound(file_name :: string) :: sound :: payload
dirty(:io, load_sound: 1)

spec is_sound_ready(sound :: payload) :: result :: bool

# Wave/Sound management functions

spec play_sound(sound :: payload) :: :ok :: label
spec stop_sound(sound :: payload) :: :ok :: label

# Music management functions

# AudioStream management functions
Expand Down Expand Up @@ -490,23 +503,21 @@ type wave :: %Rayex.Structs.Wave{
data: payload
}

# FIXME: ? https://github.com/raysan5/raylib/blob/master/src/raylib.h#L428
type r_audio_buffer :: %Rayex.Structs.RAudioBuffer{}

type audio_stream :: %Rayex.Structs.AudioStream{
buffer: [r_audio_buffer],
sample_rate: unsigned,
sample_size: unsigned,
channels: unsigned
}

type sound :: %Rayex.Structs.Sound{
stream: audio_stream,
frame_count: unsigned
}
# type audio_stream :: %Rayex.Structs.AudioStream{
# buffer: [r_audio_buffer],
# processor: [r_audio_processor],
# sample_rate: unsigned,
# sample_size: unsigned,
# channels: unsigned
# }
#
# type sound :: %Rayex.Structs.Sound{
# stream: audio_stream,
# frame_count: unsigned
# }

type music :: %Rayex.Structs.Music{
stream: audio_stream,
stream: payload,
frame_count: unsigned,
looping: bool,
ctx_type: int,
Expand Down
77 changes: 77 additions & 0 deletions examples/audio.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!elixir

Mix.install([
{:rayex, path: ".."}
])

defmodule Test do
@moduledoc false

alias Rayex.Structs, as: S

use Rayex
use Rayex.Keys

@camera_first_person 4
@color_white %S.Color{r: 245, g: 245, b: 245, a: 255}
@color_darkgray %S.Color{r: 80, g: 80, b: 80, a: 255}

def run do
init_window(800, 450, "raylib [core] example - 3d picking")
set_target_fps(60)

camera = %S.Camera3D{
position: %S.Vector3{x: 6.0, y: 6.0, z: 6.0},
target: %S.Vector3{x: 0.0, y: 2.0, z: 0.0},
up: %S.Vector3{x: 0.0, y: 1.0, z: 0.0},
fovy: 45.0,
# perspective projection
projection: 0
}

camera = update_camera(camera, @camera_first_person)
init_audio_device()
sound = load_sound("resources/audio/country.mp3")
play_sound(sound)

game_loop(%{camera: camera, sound: sound, pause: false})
end

defp game_loop(%{camera: camera, sound: sound, pause: pause} = state) do
camera = update_camera(camera, @camera_first_person)

pause =
if is_key_pressed?(key_space()) do
(!pause && stop_sound(sound)) || play_sound(sound)
!pause
else
pause
end

state = draw(%{state | camera: camera, pause: pause})

(window_should_close() && close_window()) || game_loop(state)
end

defp draw(%{camera: camera} = state) do
begin_drawing()
clear_background(@color_white)

# --- 3D ---
begin_mode_3d(camera)

draw_grid(10, 1.0)

end_mode_3d()
# --- 3D ---

draw_text("PRESS SPACE TO PAUSE/RESUME MUSIC", 200, 10, 20, @color_darkgray)
draw_fps(10, 10)

end_drawing()

state
end
end

Test.run()
3 changes: 3 additions & 0 deletions examples/resources/audio/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
| resource | author | licence | notes |
| :------------------- | :---------: | :------ | :---- |
| country.mp3 | [@emegeme](https://github.com/emegeme) | [CC0](https://creativecommons.org/publicdomain/zero/1.0/) | Originally created for "DART that TARGET" game |
Binary file added examples/resources/audio/country.mp3
Binary file not shown.
1 change: 0 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
];
buildInputs = with pkgs; [
raylib
glibc
];
shellHook = ''
export CPATH="$(erl -eval 'io:format("~s", [lists:concat([code:root_dir(), "/erts-", erlang:system_info(version), "/include"])])' -s init stop -noshell):`pwd`/deps/unifex/c_src/unifex/nif"
Expand Down
31 changes: 31 additions & 0 deletions lib/rayex/audio.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,43 @@ defmodule Rayex.Audio do
Audio related functions
"""

alias Rayex.Structs, as: S
alias Rayex.Unifex.Raylib

# Audio device management functions

@doc "Initialize audio device and context"
@spec init_audio_device() :: :ok
defdelegate init_audio_device(), to: Raylib

@doc "Check if audio device has been initialized successfully"
@spec is_audio_device_ready?() :: boolean()
defdelegate is_audio_device_ready?(), to: Raylib, as: :is_audio_device_ready

@doc "Set master volume (listener)"
@spec set_master_volume(float()) :: :ok
defdelegate set_master_volume(volume), to: Raylib

# Wave/Sound loading/unloading functions

@doc "Load sound from file"
@spec load_sound(String.t()) :: S.Sound.t()
defdelegate load_sound(fileName), to: Raylib

@doc "Checks if a sound is ready"
@spec is_sound_ready?(S.Sound.t()) :: boolean()
defdelegate is_sound_ready?(sound), to: Raylib, as: :is_sound_ready

# Wave/Sound management functions

@doc "Play a sound"
@spec play_sound(S.Sound.t()) :: :ok
defdelegate play_sound(sound), to: Raylib

@doc "Stop a sound"
@spec stop_sound(S.Sound.t()) :: :ok
defdelegate stop_sound(sound), to: Raylib

# Music management functions

# AudioStream management functions
Expand Down
60 changes: 29 additions & 31 deletions lib/rayex/structs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -315,14 +315,14 @@ end

defmodule Rayex.Structs.Model do
@moduledoc "Model"
@enforce_keys ~w[transform mesh_count material_count mashes materials mesh_material bone_count bones bind_pose]a
defstruct ~w[transform mesh_count material_count mashes materials mesh_material bone_count bones bind_pose]a
@enforce_keys ~w[transform mesh_count material_count meshes materials mesh_material bone_count bones bind_pose]a
defstruct ~w[transform mesh_count material_count meshes materials mesh_material bone_count bones bind_pose]a

@type t :: %__MODULE__{
transform: Rayex.Structs.Matrix.t(),
mesh_count: integer(),
material_count: integer(),
mashes: [Rayex.Structs.Mesh.t()],
meshes: [Rayex.Structs.Mesh.t()],
materials: [Rayex.Structs.Material.t()],
mesh_material: [integer()],
bone_count: integer(),
Expand Down Expand Up @@ -394,37 +394,35 @@ defmodule Rayex.Structs.Wave do
}
end

# XXX: ? https://github.com/raysan5/raylib/blob/master/src/raylib.h#L428
defmodule Rayex.Structs.RAudioBuffer do
@moduledoc "RAudioBuffer"
@enforce_keys ~w[]a
defstruct ~w[]a

@type t :: %__MODULE__{}
end

defmodule Rayex.Structs.AudioStream do
@moduledoc "AudioStream"
@enforce_keys ~w[buffer sample_rate sample_size channels]a
defstruct ~w[buffer sample_rate sample_size channels]a

@type t :: %__MODULE__{
buffer: [Rayex.Structs.RAudioBuffer.t()],
sample_rate: non_neg_integer(),
sample_size: non_neg_integer(),
channels: non_neg_integer()
}
end
# defmodule Rayex.Structs.AudioStream do
# @moduledoc "AudioStream"
# @enforce_keys ~w[buffer processor sample_rate sample_size channels]a
# defstruct ~w[buffer processor sample_rate sample_size channels]a
#
# @type t :: %__MODULE__{
# buffer: unifex_payload :: binary(),
# processor: unifex_payload :: binary(),
# sample_rate: non_neg_integer(),
# sample_size: non_neg_integer(),
# channels: non_neg_integer()
# }
# end
#
# defmodule Rayex.Structs.Sound do
# @moduledoc "Sound"
# @enforce_keys ~w[stream frame_count]a
# defstruct ~w[stream frame_count]a
#
# @type t :: %__MODULE__{
# stream: Rayex.Structs.AudioStream.t(),
# frame_count: non_neg_integer()
# }
# end

defmodule Rayex.Structs.Sound do
@moduledoc "Sound"
@enforce_keys ~w[stream frame_count]a
defstruct ~w[stream frame_count]a

@type t :: %__MODULE__{
stream: Rayex.Structs.AudioStream.t(),
frame_count: non_neg_integer()
}
@type t :: binary()
end

defmodule Rayex.Structs.Music do
Expand All @@ -433,7 +431,7 @@ defmodule Rayex.Structs.Music do
defstruct ~w[stream frame_count looping ctx_type ctx_data]a

@type t :: %__MODULE__{
stream: Rayex.Structs.AudioStream.t(),
stream: binary(),
frame_count: non_neg_integer(),
looping: boolean(),
ctx_type: integer(),
Expand Down

0 comments on commit ee44d7c

Please sign in to comment.