mirror of
https://github.com/ondra-novak/gates_of_skeldal.git
synced 2025-07-12 17:32:30 -04:00
sound, music, mixer
This commit is contained in:
parent
42087c926c
commit
f8a1501289
42 changed files with 1345 additions and 157 deletions
|
@ -12,7 +12,7 @@ static uint16_t screen_pitch = 640;
|
|||
|
||||
char game_display_init(const INI_CONFIG_SECTION *display_section, const char *title) {
|
||||
|
||||
SDLContext::Config cfg = {};
|
||||
SDLContext::VideoConfig cfg = {};
|
||||
const char *aspect_str;
|
||||
|
||||
aspect_str = ini_get_string(display_section, "aspect_ratio", "4:3");
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
add_subdirectory(tests)
|
||||
|
||||
add_library(skeldal_sdl sdl_context.cpp BGraph2.cpp input.cpp sound.cpp)
|
||||
add_library(skeldal_sdl
|
||||
sdl_context.cpp
|
||||
BGraph2.cpp
|
||||
input.cpp
|
||||
sound.cpp)
|
||||
set_property(TARGET skeldal_sdl PROPERTY CXX_STANDARD 20)
|
||||
|
|
|
@ -24,6 +24,11 @@ void SDLContext::SDL_Deleter::operator ()(SDL_Texture* texture) {
|
|||
SDL_DestroyTexture(texture);
|
||||
}
|
||||
|
||||
void SDLContext::SDL_Audio_Deleter::operator()(void *x) {
|
||||
SDL_AudioDeviceID id = reinterpret_cast<std::uintptr_t>(x);
|
||||
SDL_CloseAudioDevice(id);
|
||||
}
|
||||
|
||||
struct SDL_INIT_Context {
|
||||
|
||||
SDL_INIT_Context() {
|
||||
|
@ -80,7 +85,7 @@ void SDLContext::generateCRTTexture(SDL_Renderer* renderer, SDL_Texture** textur
|
|||
|
||||
|
||||
|
||||
void SDLContext::init_video(const Config &config, const char *title) {
|
||||
void SDLContext::init_video(const VideoConfig &config, const char *title) {
|
||||
char buff[256];
|
||||
static Uint32 update_request_event = SDL_RegisterEvents(1);
|
||||
_update_request_event = update_request_event;
|
||||
|
@ -502,3 +507,32 @@ SDL_Rect SDLContext::to_window_rect(const SDL_Rect &winrc, const SDL_Rect &sourc
|
|||
void SDLContext::set_quit_callback(std::function<void()> fn) {
|
||||
_quit_callback = std::move(fn);
|
||||
}
|
||||
|
||||
SDLContext::AudioInfo SDLContext::init_audio(const AudioConfig &config, SDL_AudioCallback cb, void *cb_ctx) {
|
||||
char buff[256];
|
||||
_audio.reset();
|
||||
SDL_AudioSpec aspec = {};
|
||||
aspec.callback = cb;
|
||||
aspec.userdata = cb_ctx;
|
||||
aspec.channels = 2;
|
||||
aspec.format = AUDIO_F32;
|
||||
aspec.freq = 44100;
|
||||
aspec.samples = 1024;
|
||||
SDL_AudioSpec obtaied = {};
|
||||
auto id = SDL_OpenAudioDevice(config.audioDevice, 0, &aspec, &obtaied, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE|SDL_AUDIO_ALLOW_SAMPLES_CHANGE);
|
||||
if (id < 1) {
|
||||
snprintf(buff, sizeof(buff), "SDL Error create audio: %s\n", SDL_GetError());
|
||||
throw std::runtime_error(buff);
|
||||
}
|
||||
_audio.reset(reinterpret_cast<void *>(id));
|
||||
|
||||
return {obtaied.freq};
|
||||
}
|
||||
void SDLContext::pause_audio(bool pause) {
|
||||
SDL_AudioDeviceID id = reinterpret_cast<std::intptr_t>(_audio.get());
|
||||
SDL_PauseAudioDevice(id, pause?1:0);
|
||||
}
|
||||
|
||||
void SDLContext::close_audio() {
|
||||
_audio.reset();
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ public:
|
|||
|
||||
SDLContext();
|
||||
|
||||
struct Config {
|
||||
struct VideoConfig {
|
||||
int window_width;
|
||||
int window_height;
|
||||
bool crt_filter;
|
||||
|
@ -26,11 +26,22 @@ public:
|
|||
int aspect_y;
|
||||
};
|
||||
|
||||
struct AudioConfig {
|
||||
const char *audioDevice;
|
||||
};
|
||||
|
||||
void init_video(const Config &config, const char *title);
|
||||
struct AudioInfo {
|
||||
int freq;
|
||||
};
|
||||
|
||||
void init_video(const VideoConfig &config, const char *title);
|
||||
|
||||
void close_video();
|
||||
|
||||
AudioInfo init_audio(const AudioConfig &config, SDL_AudioCallback cb, void *cb_ctx);
|
||||
void pause_audio(bool pause);
|
||||
void close_audio();
|
||||
|
||||
|
||||
void present_rect(uint16_t *pixels, unsigned int pitch, unsigned int x, unsigned int y, unsigned int xs,unsigned ys);
|
||||
void swap_render_buffers();
|
||||
|
@ -99,6 +110,12 @@ protected:
|
|||
slide_transition
|
||||
};
|
||||
|
||||
struct SDL_Audio_Deleter {
|
||||
void operator()(void *x);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
MS_EVENT ms_event;
|
||||
mutable std::mutex _mx;
|
||||
|
@ -112,6 +129,7 @@ protected:
|
|||
std::unique_ptr<SDL_Texture, SDL_Deleter> _texture;
|
||||
std::unique_ptr<SDL_Texture, SDL_Deleter> _texture2;
|
||||
std::unique_ptr<SDL_Texture, SDL_Deleter> _crt_effect;
|
||||
std::unique_ptr<void, SDL_Audio_Deleter> _audio;
|
||||
SDL_Texture *_visible_texture;
|
||||
SDL_Texture *_hidden_texture;
|
||||
|
||||
|
@ -125,6 +143,7 @@ protected:
|
|||
std::atomic<bool> _quit_requested = false;
|
||||
|
||||
|
||||
|
||||
std::vector<char> _display_update_queue;
|
||||
using QueueIter = const char *;
|
||||
std::queue<uint16_t> _keyboard_queue;
|
||||
|
|
|
@ -1,36 +1,250 @@
|
|||
#include "../platform.h"
|
||||
#include <libs/zvuk.h>
|
||||
#include "../sound.h"
|
||||
#include <libs/logfile.h>
|
||||
#include "global_context.h"
|
||||
|
||||
#include "sound_mixer.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#include <thread>
|
||||
void set_mixing_device(int mix_dev,int mix_freq,...) {
|
||||
|
||||
#define countof(array) (sizeof(array)/sizeof(array[0]))
|
||||
static void SDLCALL mixing_callback(void *userdata, Uint8 * stream, int len);
|
||||
static SoundMixer<2> sound_mixer;
|
||||
|
||||
static float master_volume = 1.0;
|
||||
static float sound_effect_volume = 1.0;
|
||||
static float music_volume = 1.0;
|
||||
static float base_freq;
|
||||
bool swap_channels = false;
|
||||
static void empty_deleter(const void *) {}
|
||||
|
||||
constexpr int BACK_BUFF_SIZE = 0x40000;
|
||||
static uint8_t *music_buffer = {};
|
||||
std::shared_ptr<WaveSource> music_source;
|
||||
static std::unique_ptr<IMusicStream> current_music = NULL;
|
||||
|
||||
struct VolumePreset {
|
||||
float left = 1.0;
|
||||
float right = 1.0;
|
||||
};
|
||||
|
||||
static std::unordered_map<int, VolumePreset > channel_volume_presset;
|
||||
|
||||
void game_sound_init_device(const INI_CONFIG_SECTION *audio_section) {
|
||||
|
||||
const char *dev = ini_get_string(audio_section, "device", "");
|
||||
if (!dev[0]) dev = nullptr;
|
||||
|
||||
SDLContext::AudioConfig cfg;
|
||||
cfg.audioDevice = dev;
|
||||
|
||||
auto r = get_sdl_global_context().init_audio(cfg, mixing_callback, nullptr);
|
||||
base_freq = r.freq;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
static void SDLCALL mixing_callback(void *userdata, Uint8 * stream, int len) {
|
||||
float *s = reinterpret_cast<float *>(stream);
|
||||
int samples = len/8;
|
||||
std::fill(s, s+2*samples, 0.0f);
|
||||
sound_mixer.mix_to_buffer(s, samples);
|
||||
}
|
||||
|
||||
char start_mixing() {
|
||||
return 0;
|
||||
get_sdl_global_context().pause_audio(false);
|
||||
return 0;
|
||||
}
|
||||
void stop_mixing() {
|
||||
get_sdl_global_context().pause_audio(true);
|
||||
|
||||
}
|
||||
|
||||
|
||||
static std::array<float,2> make_channel_volume(float left, float right) {
|
||||
if (swap_channels) std::swap(left, right);
|
||||
return {
|
||||
left * master_volume * sound_effect_volume,
|
||||
right * master_volume * sound_effect_volume,
|
||||
};
|
||||
}
|
||||
|
||||
void play_sample(int channel,const void *sample,int32_t size,int32_t lstart,int32_t sfreq,int type) {
|
||||
float speed = sfreq/base_freq;
|
||||
int step = type==1?1:2;
|
||||
auto samples = size/step;
|
||||
auto start = lstart/step;
|
||||
WaveSource::Format fmt = step==1?WaveSource::Format::uint8:WaveSource::Format::int16;
|
||||
auto src = std::make_shared<WaveSource>(WaveSource::WavePtr(sample,&empty_deleter),fmt,speed,samples,start);
|
||||
|
||||
VolumePreset &vp = channel_volume_presset[channel];
|
||||
|
||||
|
||||
|
||||
|
||||
sound_mixer.set_track(channel, WaveMixer<2>(src, make_channel_volume(vp.left, vp.right), 1.0));
|
||||
|
||||
}
|
||||
void set_channel_volume(int channel,int left,int right) {
|
||||
|
||||
float l = left / 32768.0;
|
||||
float r = right / 32768.0;
|
||||
VolumePreset &vp = channel_volume_presset[channel];
|
||||
vp.left = l;
|
||||
vp.right = r;
|
||||
sound_mixer.visit_track(channel, [&](WaveMixer<2> &mx){
|
||||
mx.set_channel_volumes_fade(make_channel_volume(vp.left, vp.right), {2e-4,2e-4});
|
||||
});
|
||||
}
|
||||
void set_end_of_song_callback(const char * (*cb)(void *), void *ctx) {
|
||||
|
||||
static size_t music_buffer_write_pos = 0;
|
||||
|
||||
|
||||
size_t copy_to_music_buffer(const void *data, size_t data_size) {
|
||||
size_t rdpos = music_source->get_output_position_bytes();
|
||||
if (static_cast<std::make_signed_t<std::size_t> >(rdpos) < 0) return 0;
|
||||
size_t l = music_source->length_bytes();
|
||||
size_t space = rdpos < music_buffer_write_pos?l - music_buffer_write_pos: rdpos - music_buffer_write_pos;
|
||||
if (space == 0) return 0;
|
||||
data_size = std::min(space,data_size);
|
||||
std::memcpy(music_buffer+music_buffer_write_pos, data, data_size);
|
||||
music_buffer_write_pos += data_size;
|
||||
if (music_buffer_write_pos >= l) {
|
||||
music_buffer_write_pos = 0;
|
||||
}
|
||||
return data_size;
|
||||
}
|
||||
|
||||
|
||||
static const char * (*end_of_song_callback)(void *ctx) = NULL;
|
||||
static void *end_of_song_callback_ctx = NULL;
|
||||
|
||||
void set_end_of_song_callback(const char * (*cb)(void *), void *ctx) {
|
||||
end_of_song_callback = cb;
|
||||
end_of_song_callback_ctx = ctx;
|
||||
}
|
||||
|
||||
void fade_music() {
|
||||
std::lock_guard _(music_source->get_lock());
|
||||
size_t rdpos = music_source->get_output_position_samples();
|
||||
WaveSource::StereoInt16 *rdptr = reinterpret_cast<WaveSource::StereoInt16 *>(music_buffer);
|
||||
size_t wrsamples = music_buffer_write_pos / sizeof(WaveSource::StereoInt16);
|
||||
size_t l =music_source->length();
|
||||
float_t size = (rdpos < wrsamples?0:l) + wrsamples - rdpos;
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
float fact = (size - i)/size;
|
||||
rdptr[rdpos].left = rdptr[rdpos].left * fact;
|
||||
rdptr[rdpos].right = rdptr[rdpos].right * fact;
|
||||
++rdpos;
|
||||
if (rdpos >=l) rdpos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr int music_track_id_base = 0x1000;
|
||||
|
||||
static void music_buffer_deleter(const void *ptr) {
|
||||
uint8_t *p = reinterpret_cast<uint8_t *>(const_cast<void *>(ptr));
|
||||
delete [] p;
|
||||
}
|
||||
|
||||
static void stop_music() {
|
||||
if (music_source) {
|
||||
std::lock_guard _(music_source->get_lock());
|
||||
size_t l =music_source->length_bytes();
|
||||
size_t rdpos = music_source->get_output_position_bytes();
|
||||
size_t size = (rdpos < music_buffer_write_pos?0:l) + music_buffer_write_pos - rdpos;
|
||||
music_source->stop_after_bytes(size);
|
||||
}
|
||||
music_source.reset();
|
||||
}
|
||||
|
||||
void create_new_music_track(int freq) {
|
||||
stop_music();
|
||||
static int track_id = music_track_id_base;
|
||||
double basef = base_freq;
|
||||
double rel_speed = freq/basef;
|
||||
music_buffer = new uint8_t[BACK_BUFF_SIZE];
|
||||
std::memset(music_buffer, 0, BACK_BUFF_SIZE);
|
||||
music_source = std::make_shared<WaveSource>(
|
||||
WaveSource::WavePtr(music_buffer,music_buffer_deleter),
|
||||
WaveSource::Format::stereo_int16, rel_speed,
|
||||
BACK_BUFF_SIZE/sizeof(WaveSource::StereoInt16),0);
|
||||
|
||||
sound_mixer.set_track(track_id, WaveMixer<2>(music_source,{music_volume,music_volume},1.0));
|
||||
track_id = track_id ^ 1;
|
||||
music_buffer_write_pos = BACK_BUFF_SIZE/2;
|
||||
}
|
||||
|
||||
static std::unique_ptr<IMusicStream> load_music_ex(const char *name) {
|
||||
if (name) {
|
||||
TMUSIC_STREAM *stream = music_open(name);
|
||||
if (stream) {
|
||||
TMUSIC_STREAM_INFO nfo = music_get_info(stream);
|
||||
if (nfo.channels != 2) {
|
||||
SEND_LOG("(MUSIC) Reject music - only stereo is supported");
|
||||
} else if (nfo.format != 2){
|
||||
SEND_LOG("(MUSIC) Reject music - only 16bit is supported");
|
||||
} else {
|
||||
return std::unique_ptr<IMusicStream>(static_cast<IMusicStream *>(stream));
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
|
||||
}
|
||||
int mix_back_sound(int synchro) {
|
||||
return 0;
|
||||
|
||||
static void handle_end_of_song() {
|
||||
current_music.reset();
|
||||
if (end_of_song_callback != NULL) {
|
||||
const char *new_music = end_of_song_callback(end_of_song_callback_ctx);
|
||||
current_music = load_music_ex(new_music);
|
||||
if (current_music) {
|
||||
auto nfo = current_music->get_info();
|
||||
create_new_music_track(nfo.freq);
|
||||
mix_back_sound(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int mix_back_sound(int _) {
|
||||
|
||||
if (current_music==NULL) {
|
||||
stop_music();
|
||||
} else {
|
||||
auto data = current_music->read();
|
||||
while (!data.empty()) {
|
||||
auto sz = copy_to_music_buffer(data.data(), data.size());
|
||||
if (sz == 0) {
|
||||
current_music->put_back(data);
|
||||
break;
|
||||
}
|
||||
data = data.substr(sz);
|
||||
if (data.empty()) data = current_music->read();
|
||||
}
|
||||
if (data.empty()) {
|
||||
handle_end_of_song();
|
||||
return mix_back_sound(_);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
|
||||
}
|
||||
|
||||
//int open_backsound(char *filename);
|
||||
void change_music(const char *filename) {
|
||||
|
||||
if (music_source) {
|
||||
fade_music();
|
||||
stop_music();
|
||||
}
|
||||
current_music = load_music_ex(filename);
|
||||
if (current_music) {
|
||||
auto nfo = current_music->get_info();
|
||||
create_new_music_track(nfo.freq);
|
||||
mix_back_sound(0);
|
||||
}
|
||||
}
|
||||
|
||||
//char *device_name(int device);
|
||||
|
@ -39,29 +253,67 @@ void change_music(const char *filename) {
|
|||
//void set_backsnd_freq(int freq);
|
||||
|
||||
char get_channel_state(int channel) {
|
||||
return 0;
|
||||
char c = 0;
|
||||
sound_mixer.visit_track(channel, [&](auto){c = 1;});
|
||||
return c;
|
||||
}
|
||||
void get_channel_volume(int channel,int *left,int *right) {
|
||||
|
||||
VolumePreset &vp = channel_volume_presset[channel];
|
||||
*left = static_cast<int>(vp.left*32768.0);
|
||||
*right = static_cast<int>(vp.right*32768.0);
|
||||
}
|
||||
void mute_channel(int channel) {
|
||||
|
||||
sound_mixer.visit_track(channel, [&](WaveMixer<2> &mx){
|
||||
mx.get_source()->stop();
|
||||
});
|
||||
}
|
||||
void chan_break_loop(int channel) {
|
||||
sound_mixer.visit_track(channel, [&](WaveMixer<2> &mx){
|
||||
mx.get_source()->break_loop();
|
||||
});
|
||||
|
||||
}
|
||||
void chan_break_ext(int channel,const void *org_sample,int32_t size_sample) {
|
||||
sound_mixer.visit_track(channel, [&](WaveMixer<2> &mx){
|
||||
auto src = mx.get_source();
|
||||
src->break_loop(src->length_in_samples(src->get_format(), size_sample));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
char set_snd_effect(int funct,int data) {
|
||||
|
||||
static void update_music_volume(){
|
||||
float v = music_volume * master_volume;
|
||||
for (int i = 0; i <2; i++)
|
||||
sound_mixer.visit_track(music_track_id_base+i,[&](WaveMixer<2> &m){
|
||||
m.set_channel_volume(v, v);
|
||||
});
|
||||
}
|
||||
|
||||
char set_snd_effect(AUDIO_PROPERTY funct,int data) {
|
||||
switch (funct) {
|
||||
case SND_PING: break;
|
||||
case SND_GFX: sound_effect_volume = data/256.0;break;
|
||||
case SND_MUSIC: music_volume = data/128.0;update_music_volume();break;
|
||||
case SND_GVOLUME: master_volume = data/256.0;update_music_volume();break;
|
||||
case SND_SWAP: swap_channels = !!data;break;
|
||||
default: return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
char check_snd_effect(AUDIO_PROPERTY funct) {
|
||||
if (funct == SND_PING|| funct == SND_GFX || funct == SND_MUSIC || funct == SND_GVOLUME) return 1;
|
||||
return 0;
|
||||
}
|
||||
char check_snd_effect(int funct) {
|
||||
return 0;
|
||||
}
|
||||
int get_snd_effect(int funct) {
|
||||
return 0;
|
||||
int get_snd_effect(AUDIO_PROPERTY funct) {
|
||||
switch (funct) {
|
||||
case SND_PING: return 1;
|
||||
case SND_GFX: return static_cast<int>(sound_effect_volume * 256.0);break;
|
||||
case SND_MUSIC: return static_cast<int>(music_volume * 128.0);break;
|
||||
case SND_GVOLUME: return static_cast<int>(master_volume * 256.0);break;
|
||||
case SND_SWAP:return swap_channels?1:0;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void *PrepareVideoSound(int mixfreq, int buffsize) {
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* sound.h
|
||||
*
|
||||
* Created on: 26. 1. 2025
|
||||
* Author: ondra
|
||||
*/
|
||||
|
||||
#ifndef PLATFORM_SDL_SOUND_H_
|
||||
#define PLATFORM_SDL_SOUND_H_
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#endif /* PLATFORM_SDL_SOUND_H_ */
|
104
platform/sdl/sound_mixer.h
Normal file
104
platform/sdl/sound_mixer.h
Normal file
|
@ -0,0 +1,104 @@
|
|||
#pragma once
|
||||
#include "wave_mixer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
template<int channels>
|
||||
class SoundMixer {
|
||||
public:
|
||||
|
||||
|
||||
void mix_to_buffer(float *buffer, std::size_t samples_len) {
|
||||
std::lock_guard _(_mx);
|
||||
for (std::size_t i = 0; i < _tracks.size(); ++i) {
|
||||
Track &t = _tracks[i];
|
||||
bool playing = t.wave.output_mix(buffer, samples_len);
|
||||
if (!playing) {
|
||||
Track &b = _tracks.back();
|
||||
if (&b != &t) std::swap(b,t);
|
||||
_tracks.pop_back();
|
||||
--i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<std::invocable<WaveMixer<channels> &> Fn>
|
||||
bool visit_track(int trackID, Fn &&fn) {
|
||||
std::lock_guard _(_mx);
|
||||
Track *t = find_track_by_id(trackID);
|
||||
if (t) {
|
||||
fn(t->wave);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
template<std::invocable<const WaveMixer<channels> &> Fn>
|
||||
bool visit_track(int trackID, Fn &&fn) const {
|
||||
std::lock_guard _(_mx);
|
||||
const Track *t = find_track_by_id(trackID);
|
||||
if (t) {
|
||||
fn(t->wave);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void set_track(int trackID, WaveMixer<channels> wave) {
|
||||
std::lock_guard _(_mx);
|
||||
Track *t = find_track_by_id(trackID);
|
||||
if (t) t->wave = std::move(wave);
|
||||
else _tracks.push_back({trackID, std::move(wave)});
|
||||
}
|
||||
|
||||
struct Track {
|
||||
int trackID;
|
||||
WaveMixer<channels> wave;
|
||||
};
|
||||
|
||||
|
||||
void set_tracks(Track *tracks, int count) {
|
||||
std::lock_guard _(_mx);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Track *t = find_track_by_id(tracks[i].trackID);
|
||||
if (t) t->wave = std::move(tracks[i].wave);
|
||||
else _tracks.push_back(std::move(tracks[i]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected:
|
||||
|
||||
Track *find_track_by_id(int track) {
|
||||
auto iter = std::find_if(_tracks.begin(), _tracks.end(), [&](const Track &t){
|
||||
return t.trackID == track;
|
||||
});
|
||||
if (iter == _tracks.end()) {
|
||||
return nullptr;
|
||||
} else {
|
||||
return &(*iter);
|
||||
}
|
||||
}
|
||||
|
||||
const Track *find_track_by_id(int track) const {
|
||||
auto iter = std::find_if(_tracks.begin(), _tracks.end(), [&](const Track &t){
|
||||
return t.trackID == track;
|
||||
});
|
||||
if (iter == _tracks.end()) {
|
||||
return nullptr;
|
||||
} else {
|
||||
return &(*iter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::mutex _mx;
|
||||
std::vector<Track> _tracks;
|
||||
|
||||
|
||||
};
|
145
platform/sdl/wave_mixer.h
Normal file
145
platform/sdl/wave_mixer.h
Normal file
|
@ -0,0 +1,145 @@
|
|||
#pragma once
|
||||
#include "wave_source.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
|
||||
template<typename X>
|
||||
class NoCopyObject: public X {
|
||||
public:
|
||||
using X::X;
|
||||
|
||||
NoCopyObject(const NoCopyObject &other):X() {}
|
||||
NoCopyObject &operator=(const NoCopyObject &other) {
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
template<int channels>
|
||||
class WaveMixer {
|
||||
public:
|
||||
|
||||
enum State {
|
||||
playing,
|
||||
done
|
||||
};
|
||||
|
||||
WaveMixer(std::shared_ptr<WaveSource> source)
|
||||
:_src(std::move(source)) {}
|
||||
WaveMixer(std::shared_ptr<WaveSource> source, const std::array<float,channels> &volumes, float speed = 1.0)
|
||||
:_src(std::move(source))
|
||||
,_volume{volumes}
|
||||
,_speed(speed) {}
|
||||
|
||||
|
||||
|
||||
static_assert(channels > 0);
|
||||
|
||||
|
||||
///output to buffer by adding values (mixing)
|
||||
/**
|
||||
* @param buffer sound buffer - add values here
|
||||
* @param samples_count count of samples in buffer (so samples * channels for count of floats)
|
||||
* @retval true still playing
|
||||
* @retval false done playing
|
||||
*/
|
||||
bool output_mix(float *buffer, std::size_t samples_count) {
|
||||
std::array<float, channels> vol;
|
||||
float speed;
|
||||
{
|
||||
std::lock_guard _(_mx);
|
||||
if (_state == done) {
|
||||
return 0;
|
||||
}
|
||||
vol = _volume;
|
||||
speed = _speed;
|
||||
}
|
||||
|
||||
bool ch = false;
|
||||
|
||||
auto do_volume_fade = [this,&ch](int c){
|
||||
float d = _target_volume[c] - _volume[c];
|
||||
if (d > 0) {
|
||||
d = std::min(d, _volume_change[c]);
|
||||
ch = true;
|
||||
}
|
||||
else if (d < 0) {
|
||||
d = std::max(d, -_volume_change[c]);
|
||||
ch = true;
|
||||
}
|
||||
_volume[c] += d;
|
||||
};
|
||||
|
||||
auto iter = buffer;
|
||||
bool playing = _src->output([&](const auto &value){
|
||||
using T = std::decay_t<decltype(value)>;
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
for (int c = 0; c < channels; ++c) {
|
||||
if (_volume_fade) do_volume_fade(c);
|
||||
float v = value * vol[c];
|
||||
v = v + (*iter);
|
||||
(*iter) = v;
|
||||
++iter;
|
||||
}
|
||||
} else {
|
||||
//TODO: handle 1 channel audio
|
||||
static_assert(std::is_same_v<T, WaveSource::StereoFloat>);
|
||||
if (_volume_fade) for (int c =0; c< channels; ++c) do_volume_fade(c);
|
||||
if constexpr(channels == 1) {
|
||||
float v = (value.left + value.right) * 0.5;
|
||||
(*iter) += v * vol[0];
|
||||
} else {
|
||||
(*iter) += value.left * vol[0];
|
||||
++iter;
|
||||
(*iter) += value.right * vol[0];
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
|
||||
}, samples_count, speed);
|
||||
_volume_fade = ch;
|
||||
return playing;
|
||||
}
|
||||
|
||||
void set_channel_volumes(const std::array<float,channels> &volumes) {
|
||||
std::lock_guard _(_mx);
|
||||
_volume = volumes;
|
||||
}
|
||||
|
||||
void set_channel_volumes_fade(const std::array<float,channels> &volumes, const std::array<float,channels> &change_speed) {
|
||||
std::lock_guard _(_mx);
|
||||
_volume_fade = true;
|
||||
_target_volume = volumes;
|
||||
_volume_change = change_speed;
|
||||
}
|
||||
|
||||
void set_channel_volume(int channel, float volume) {
|
||||
std::lock_guard _(_mx);
|
||||
if (channel >= 0 && channel < channels) {
|
||||
_volume[channel] = volume;
|
||||
}
|
||||
}
|
||||
|
||||
void set_speed(float speed) {
|
||||
std::lock_guard _(_mx);
|
||||
_speed = speed;
|
||||
}
|
||||
|
||||
std::shared_ptr<WaveSource> get_source() const {return _src;}
|
||||
|
||||
protected:
|
||||
|
||||
|
||||
|
||||
std::shared_ptr<WaveSource> _src;
|
||||
mutable NoCopyObject<std::mutex> _mx;
|
||||
std::array<float, channels> _volume = {};
|
||||
std::array<float, channels> _target_volume = {};
|
||||
std::array<float, channels> _volume_change = {};
|
||||
bool _volume_fade = false;
|
||||
float _speed = 1.0;
|
||||
State _state = playing;
|
||||
|
||||
|
||||
|
||||
};
|
248
platform/sdl/wave_source.h
Normal file
248
platform/sdl/wave_source.h
Normal file
|
@ -0,0 +1,248 @@
|
|||
#pragma once
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
|
||||
class WaveSource {
|
||||
public:
|
||||
using WaveDeleter = void (*)(const void *);
|
||||
using WavePtr = std::unique_ptr<const void, WaveDeleter>;
|
||||
|
||||
struct StereoInt16 {
|
||||
std::int16_t left;
|
||||
std::int16_t right;
|
||||
};
|
||||
|
||||
struct StereoFloat {
|
||||
float left;
|
||||
float right;
|
||||
};
|
||||
|
||||
enum class Format {
|
||||
uint8,
|
||||
int8,
|
||||
uint16,
|
||||
int16,
|
||||
stereo_int16
|
||||
};
|
||||
|
||||
constexpr static std::size_t length_in_samples(Format f, std::size_t bytes) {
|
||||
if (f == Format::stereo_int16) return bytes>>2;
|
||||
if (f == Format::uint16 || f == Format::int16) return bytes>>1;
|
||||
else return bytes;
|
||||
}
|
||||
constexpr static std::size_t length_in_bytes(Format f, std::size_t samples) {
|
||||
if (f == Format::stereo_int16) return samples<<2;
|
||||
if (f == Format::uint16 || f == Format::int16) return samples<<1;
|
||||
else return samples;
|
||||
|
||||
}
|
||||
Format get_format() const {return _format;}
|
||||
|
||||
///retrieve overall play time in samples of source wave
|
||||
/**
|
||||
* @return play time in samples - this value can be non-integer - when pointer
|
||||
* can point between two samples
|
||||
*
|
||||
*/
|
||||
double get_play_time() const {
|
||||
return _position;
|
||||
}
|
||||
///retrieves output position in samples.
|
||||
/** useful for cycle buffer returns sample index in wave buffer which has been
|
||||
* recently sent to the wave device, so it is save to overwrite it. As the
|
||||
* @return output position in samples
|
||||
*/
|
||||
std::size_t get_output_position_samples() const {
|
||||
std::lock_guard _(_mx);
|
||||
return static_cast<std::size_t>(std::round(wrap_pos(_position - 1.0)));
|
||||
}
|
||||
|
||||
///retrieves output position in samples.
|
||||
/** useful for cycle buffer returns byte offset in wave buffer which has been
|
||||
* recently sent to the wave device, so it is save to overwrite it. As the
|
||||
* @return output position in bytes
|
||||
*/
|
||||
std::size_t get_output_position_bytes() const {
|
||||
return length_in_bytes(_format,get_output_position_samples());
|
||||
}
|
||||
|
||||
///break loop - so wave finishes current cycle and ends
|
||||
void break_loop() {
|
||||
std::lock_guard _(_mx);
|
||||
break_loop_finish_at(_length);
|
||||
}
|
||||
|
||||
///break loop by extending play beyond the loop - must be already defined in wave buffer
|
||||
void break_loop(std::size_t final_len) {
|
||||
std::lock_guard _(_mx);
|
||||
break_loop_finish_at(final_len);
|
||||
}
|
||||
|
||||
///stop playing now
|
||||
void stop() {
|
||||
std::lock_guard _(_mx);
|
||||
_wrap_offset = _position - _length;
|
||||
_loop_pos = _length;
|
||||
}
|
||||
|
||||
///stops playing after given samples played
|
||||
void stop_after_samples(std::size_t pos) {
|
||||
std::lock_guard _(_mx);
|
||||
_stop_at = _position + pos;
|
||||
}
|
||||
///stops playing after given bytes played
|
||||
void stop_after_bytes(std::size_t pos) {
|
||||
stop_after_samples(length_in_samples(_format, pos));
|
||||
}
|
||||
std::size_t length() const {return _length;}
|
||||
std::size_t length_bytes() const {return length_in_bytes(_format, _length);}
|
||||
|
||||
|
||||
///create wave source
|
||||
/**
|
||||
* @param data pointer to data, can be allocated or static, depend on deleter
|
||||
* @param format format - one of enum
|
||||
* @param speed frequency in ratio to base frequency. For instance if base frequency is 44100
|
||||
* and wave frequency is 11025, then speed is 0.25
|
||||
* @param len_samples count of samples
|
||||
* @param loop_pos
|
||||
*/
|
||||
WaveSource(WavePtr data, Format format, double speed, std::size_t len_samples, std::size_t loop_pos)
|
||||
:_wave(std::move(data))
|
||||
,_format(format)
|
||||
,_speed(speed)
|
||||
,_length(len_samples)
|
||||
,_loop_pos(std::min(loop_pos, len_samples))
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
template<std::invocable<float> Fn>
|
||||
bool output(Fn &&fn, std::size_t samples, double speed = 1.0) {
|
||||
std::lock_guard _(_mx);
|
||||
switch (_format) {
|
||||
case Format::int8: return do_output(std::forward<Fn>(fn),
|
||||
reinterpret_cast<const std::int8_t *>(_wave.get()),
|
||||
samples, speed) > 0;
|
||||
case Format::int16: return do_output(std::forward<Fn>(fn),
|
||||
reinterpret_cast<const std::int16_t *>(_wave.get()),
|
||||
samples, speed) > 0;
|
||||
case Format::uint8: return do_output(std::forward<Fn>(fn),
|
||||
reinterpret_cast<const std::uint8_t *>(_wave.get()),
|
||||
samples, speed) > 0;
|
||||
case Format::uint16: return do_output(std::forward<Fn>(fn),
|
||||
reinterpret_cast<const std::uint16_t *>(_wave.get()),
|
||||
samples, speed) > 0;
|
||||
case Format::stereo_int16: return do_output(std::forward<Fn>(fn),
|
||||
reinterpret_cast<const StereoInt16 *>(_wave.get()),
|
||||
samples, speed) > 0;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
///for locking buffer - but probably not needed if you use memory barriers
|
||||
std::recursive_mutex &get_lock() {return _mx;}
|
||||
|
||||
protected:
|
||||
mutable std::recursive_mutex _mx;
|
||||
WavePtr _wave;
|
||||
Format _format;
|
||||
double _speed;
|
||||
double _length;
|
||||
double _loop_pos;
|
||||
double _position = 0;
|
||||
double _wrap_offset = 0;
|
||||
double _interpolation_step = 1.0;
|
||||
double _stop_at = std::numeric_limits<double>::max();
|
||||
|
||||
|
||||
|
||||
constexpr float interpolate(float s1, float s2, float frac) {
|
||||
return s1 + (s2 - s1) * frac;
|
||||
}
|
||||
|
||||
constexpr float interpolate(uint8_t s1, uint8_t s2, float frac) {
|
||||
float sampl1 = static_cast<float>(s1)/static_cast<float>(128)- 1.0f;
|
||||
float sampl2 = static_cast<float>(s2)/static_cast<float>(128)- 1.0f;
|
||||
return interpolate(sampl1, sampl2, frac);
|
||||
}
|
||||
constexpr float interpolate(uint16_t s1, uint16_t s2, float frac) {
|
||||
float sampl1 = static_cast<float>(s1)/static_cast<float>(32768) - 1.0f;
|
||||
float sampl2 = static_cast<float>(s2)/static_cast<float>(32768) - 1.0f;
|
||||
return interpolate(sampl1, sampl2, frac);
|
||||
}
|
||||
|
||||
constexpr float interpolate(int16_t s1, int16_t s2, float frac) {
|
||||
float sampl1 = static_cast<float>(s1)/static_cast<float>(32768);
|
||||
float sampl2 = static_cast<float>(s2)/static_cast<float>(32768);
|
||||
return interpolate(sampl1, sampl2, frac);
|
||||
}
|
||||
constexpr StereoFloat interpolate(const StereoInt16 &s1, const StereoInt16 &s2, float frac) {
|
||||
return {
|
||||
interpolate(s1.left, s1.left, frac),
|
||||
interpolate(s2.right, s2.right, frac)
|
||||
};
|
||||
}
|
||||
constexpr float interpolate(int8_t s1, int8_t s2, float frac) {
|
||||
float sampl1 = static_cast<float>(s1)/static_cast<float>(128);
|
||||
float sampl2 = static_cast<float>(s2)/static_cast<float>(128);
|
||||
return interpolate(sampl1, sampl2, frac);
|
||||
}
|
||||
|
||||
|
||||
template<typename T, typename Fn >
|
||||
std::size_t do_output(Fn &&fn, const T *wave, std::size_t samples, double speed) {
|
||||
float spd = speed * _speed;
|
||||
for (std::size_t i = 0; i < samples; ++i) {
|
||||
if (_position>= _stop_at) {
|
||||
return i;
|
||||
}
|
||||
double p = wrap_pos(_position - _wrap_offset);
|
||||
double sp;
|
||||
double fp = std::modf(p,&sp);
|
||||
std::size_t s1 = static_cast<std::size_t>(wrap_pos(sp));
|
||||
std::size_t s2 = static_cast<std::size_t>(wrap_pos(sp+1));
|
||||
if (s1 == s2) return i;
|
||||
fn(interpolate(wave[s1], wave[s2], fp));
|
||||
_position = _position + spd;
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
|
||||
double wrap_pos(double position) const {
|
||||
if (position >= _length) {
|
||||
if (_loop_pos == _length) {
|
||||
position = _length;
|
||||
} else {
|
||||
position = std::fmod((position - _length),(_length - _loop_pos)) + _loop_pos;
|
||||
}
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
|
||||
void break_loop_finish_at(double position) {
|
||||
double adj = wrap_pos(_position);
|
||||
_wrap_offset = _position - adj;
|
||||
_loop_pos = _length = position;
|
||||
}
|
||||
|
||||
};
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue