Compare commits

...
11 Commits
Author SHA1 Message Date
Paul Dino Jones 522a64b0f3 Update to latest simapi 2026-03-11 22:40:01 -04:00
Paul Dino Jones d465db0055 Remove more debug code 2026-03-04 01:09:45 -05:00
Paul Dino Jones 2eb48907a0 fix compilation error 2026-03-03 23:42:18 -05:00
Paul Dino Jones 479c77d251 clean up debug code in pulseaudio mainloop start 2026-03-03 23:37:45 -05:00
Paul Dino Jones e32cac5666 Allow to automatically spawn pulseaudio daemon 2026-03-03 23:08:00 -05:00
Paul Dino Jones 93a967ec15 Update to latest simapi for RaceRoom support 2026-02-28 16:04:31 -05:00
Paul Dino Jones db6b5b027c Add link to guide for reverse engineering 2026-02-14 13:45:02 -05:00
dafteran4 76b7d4dafc Noise for haptics (#31) 2026-02-14 12:32:14 -05:00
dafteran4 f7df9ab90e Low latency (#32) 2026-02-13 11:13:33 -05:00
dafteran4 5781518f44 sound: Fix frequency handling and add RPM rev test (#29)
- Change curr_frequency from uint32_t to double for precise frequency
  control
- Remove get_closest_frequency() and change phase calculation in engine
  sound to ensure continuous sine curve
- Add RPM revving test
2026-02-12 14:29:10 -05:00
Paul Dino Jones ce05f0d78b Update to latest simapi for Forza support 2026-02-11 10:52:13 -05:00
12 changed files with 124 additions and 58 deletions
+4
View File
@@ -21,6 +21,10 @@ Cross Platform device manager for driving and flight simulators, for use with co
- Convincing shaker effects for noise tranducers for wheel slip, wheel lock, and abs, as well as engine rpm and gear shifts.
- Support for many [wheels and pedals](https://spacefreak18.github.io/simapi/thirdpartydevices) including Clubsport Elite V3, [Logitech G29](https://spacefreak18.github.io/simapi/logitechg29), and Moza R5.
## Adding More Devices
- If a device isn't already supported, feel free to assist in the reverse engineering process by submitting a pcap or even better working code in a pull request!
https://santeri.pikarinen.com/pages/usb_hid_reverse_engineering/
## Quick Install
**One-Line Installation:**
+1
View File
@@ -26,6 +26,7 @@ configs = (
modulation = "frequency";
frequency = 17;
frequencyMax = 37;
noise = 10; // additive noise in Hz
},
{
device = "Sound";
+2 -5
View File
@@ -19,10 +19,8 @@ int setupsound()
slogi("connecting pulseaudio...");
// Get a mainloop and its context
mainloop = pa_threaded_mainloop_new();
assert(mainloop);
mainloop_api = pa_threaded_mainloop_get_api(mainloop);
context = pa_context_new(mainloop_api, "Monocoque");
assert(context);
pa_context_set_state_callback(context, &context_state_cb, mainloop);
@@ -30,13 +28,12 @@ int setupsound()
pa_threaded_mainloop_lock(mainloop);
// Start the mainloop
assert(pa_threaded_mainloop_start(mainloop) == 0);
assert(pa_context_connect(context, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL) == 0);
pa_threaded_mainloop_start(mainloop);
pa_context_connect(context, NULL, 0, NULL);
// Wait for the context to be ready
for(;;) {
pa_context_state_t context_state = pa_context_get_state(context);
assert(PA_CONTEXT_IS_GOOD(context_state));
if (context_state == PA_CONTEXT_READY) break;
pa_threaded_mainloop_wait(mainloop);
}
@@ -80,7 +80,7 @@ int patestCallbackGearShift(const void* inputBuffer,
data->curr_duration += 1.0 / SAMPLE_RATE;
if (data->curr_duration >= data->duration) {
data->curr_duration = 0.0;
data->curr_frequency = 0;
data->curr_frequency = 0.0;
}
*out++ = sample;
@@ -16,6 +16,15 @@
#define M_PI (3.14159265)
#endif
static double apply_noise(double base_freq, double noise_amount) {
if (noise_amount > 0.0) {
double r = (double) rand() / RAND_MAX * 2.0 - 1.0;
return base_freq + r * noise_amount;
} else {
return base_freq;
}
}
void gear_sound_stream(pa_stream *s, size_t length, void *userdata) {
SoundData* data = (SoundData*)userdata;
@@ -24,59 +33,75 @@ void gear_sound_stream(pa_stream *s, size_t length, void *userdata) {
for (size_t i = 0; i < num_samples; i++) {
static double t = 0.0;
double sample = 0;
if (data->frequency>0)
{
sample = ((double)data->curr_amplitude/100) * 32767.0 * sin(2.0 * M_PI * data->curr_frequency * data->curr_duration);
}
double sample = 0;
if (data->curr_frequency>0.0)
{
double t = data->phase;
double a = (double)data->curr_amplitude/100;
// fade-out 5ms to prevent discontinuity
static double fade_out_duration = 0.005;
double remaining = data->duration - data->curr_duration;
if (remaining < fade_out_duration) {
a *= remaining / fade_out_duration;
}
sample = a * 32767.0 * sin(2.0 * M_PI * t);
double f = apply_noise(data->curr_frequency, data->noise);
t += f / SAMPLE_RATE;
if (t >= 1.0) {
t -= floor(t);
}
data->phase = t;
data->curr_duration += 1.0 / SAMPLE_RATE;
if (data->curr_duration >= data->duration) {
data->curr_duration = 0.0;
data->curr_frequency = 0.0;
data->phase = 0.0;
}
}
buffer[i] = (int16_t)sample;
data->curr_duration += 1.0 / SAMPLE_RATE;
if (data->curr_duration >= data->duration) {
data->curr_duration = 0.0;
data->curr_frequency = 0;
}
}
pa_stream_write(s, buffer, length, NULL, 0LL, PA_SEEK_RELATIVE);
}
double get_closest_frequency(size_t num_samples, double input_frequency) {
double samples_per_cycle = SAMPLE_RATE / input_frequency;
double adjusted_frequency = SAMPLE_RATE / (num_samples / floor(num_samples/samples_per_cycle) );
return adjusted_frequency;
}
void engine_sound_stream(pa_stream *s, size_t length, void *userdata) {
SoundData* data = (SoundData*)userdata;
double freq = data->curr_frequency;
size_t num_samples = length / sizeof(int16_t);
freq = get_closest_frequency( num_samples, freq );
//size_t num_samples = (size_t) (DURATION * SAMPLE_RATE);
int16_t buffer[num_samples];
//double t = 0;
for (size_t i = 0; i < num_samples; i++) {
double t = data->phase;
double sample = ((double)data->curr_amplitude/100) * 32767.0 * sin( 2.0 * M_PI * freq * t );
double sample = ((double)data->curr_amplitude/100) * 32767.0 * sin( 2.0 * M_PI * t );
buffer[i] = (int16_t)sample;
t += 1.0 / SAMPLE_RATE;
if (t >= SAMPLE_RATE) {
t = 0;
double f = apply_noise(data->curr_frequency, data->noise);
t += f / SAMPLE_RATE;
if (t >= 1.0) {
t -= floor(t);
}
data->phase = t;
}
data->phase = round(data->phase);
pa_stream_write(s, buffer, length, NULL, 0LL, PA_SEEK_RELATIVE);
}
@@ -166,7 +191,7 @@ int usb_generic_shaker_init(SoundDevice* sounddevice, pa_threaded_mainloop* main
// recommended settings, i.e. server uses sensible values
pa_buffer_attr buffer_attr;
buffer_attr.maxlength = (uint32_t) 32767;
buffer_attr.tlength = (uint32_t) -1;
buffer_attr.tlength = (uint32_t) 2048;
buffer_attr.prebuf = (uint32_t) -1;
buffer_attr.minreq = (uint32_t) -1;
@@ -182,14 +207,14 @@ int usb_generic_shaker_init(SoundDevice* sounddevice, pa_threaded_mainloop* main
// for now i'm only supporting playing on one specified channel which is the concept you should build your setups around
cv.values[pan] = channel_volume;
assert(pa_stream_connect_playback(stream, devname, &buffer_attr, stream_flags, &cv, NULL) == 0);
pa_stream_connect_playback(stream, devname, &buffer_attr, stream_flags, &cv, NULL);
//pa_stream_connect_playback(stream, devname, &buffer_attr, stream_flags, &cv, NULL);
// Wait for the stream to be ready
for(;;)
{
pa_stream_state_t stream_state = pa_stream_get_state(stream);
assert(PA_STREAM_IS_GOOD(stream_state));
PA_STREAM_IS_GOOD(stream_state);
//PA_STREAM_IS_GOOD(stream_state);
if (stream_state == PA_STREAM_READY) break;
pa_threaded_mainloop_wait(mainloop);
+10 -8
View File
@@ -26,7 +26,7 @@ int gear_sound_set(SoundDevice* sounddevice, SimData* simdata)
}
sounddevice->sounddata.last_gear = simdata->gear;
slogt("set gear frequency to %i", sounddevice->sounddata.curr_frequency);
slogt("set gear frequency to %f", sounddevice->sounddata.curr_frequency);
}
@@ -37,9 +37,9 @@ double modulate(SoundDevice* sounddevice, double raw_effect, SoundEffectModulati
{
case SOUND_EFFECT_MODULATION_FREQUENCY:
modulated_effect = ((sounddevice->sounddata.frequencyMax - sounddevice->sounddata.frequency) * raw_effect) + sounddevice->sounddata.frequency;
sounddevice->sounddata.curr_frequency = trunc(modulated_effect);
sounddevice->sounddata.curr_frequency = modulated_effect;
sounddevice->sounddata.curr_amplitude = sounddevice->sounddata.amplitude;
slogt("set curr frequency to %i from raw effect %f and base frequency %i", sounddevice->sounddata.curr_frequency, raw_effect, sounddevice->sounddata.frequency);
slogt("set curr frequency to %f from raw effect %f and base frequency %i", sounddevice->sounddata.curr_frequency, raw_effect, sounddevice->sounddata.frequency);
break;
case SOUND_EFFECT_MODULATION_AMPLIFY:
modulated_effect = ((sounddevice->sounddata.amplitudeMax - sounddevice->sounddata.amplitude) * raw_effect) + sounddevice->sounddata.amplitude;
@@ -80,7 +80,7 @@ int sounddev_tyreslip_update(SimDevice* this, SimData* simdata)
}
else
{
sounddevice->sounddata.curr_frequency = 0;
sounddevice->sounddata.curr_frequency = 0.0;
sounddevice->sounddata.curr_amplitude = 0;
sounddevice->sounddata.curr_duration = 0;
}
@@ -100,7 +100,7 @@ int sounddev_tyrelock_update(SimDevice* this, SimData* simdata)
}
else
{
sounddevice->sounddata.curr_frequency = 0;
sounddevice->sounddata.curr_frequency = 0.0;
sounddevice->sounddata.curr_amplitude = 0;
sounddevice->sounddata.curr_duration = 0;
}
@@ -118,7 +118,7 @@ int sounddev_absbrakes_update(SimDevice* this, SimData* simdata)
}
else
{
sounddevice->sounddata.curr_frequency = 0;
sounddevice->sounddata.curr_frequency = 0.0;
sounddevice->sounddata.curr_amplitude = 0;
sounddevice->sounddata.curr_duration = 0;
}
@@ -138,7 +138,7 @@ int sounddev_suspension_update(SimDevice* this, SimData* simdata)
}
else
{
sounddevice->sounddata.curr_frequency = 0;
sounddevice->sounddata.curr_frequency = 0.0;
sounddevice->sounddata.curr_amplitude = 0;
sounddevice->sounddata.curr_duration = 0;
}
@@ -177,6 +177,7 @@ int sounddev_init(SoundDevice* sounddevice, const char* devname, MonocoqueTyreId
slogi("pan is: %i", sds.pan);
slogi("channels is: %i", sds.channels);
slogi("duration is: %f", sds.duration);
slogi("noise is: %i", sds.noise);
sounddevice->modulationType = sds.modulation;
@@ -184,12 +185,13 @@ int sounddev_init(SoundDevice* sounddevice, const char* devname, MonocoqueTyreId
sounddevice->sounddata.frequencyMax = sds.frequencyMax;
sounddevice->sounddata.amplitude = sds.amplitude;
sounddevice->sounddata.amplitudeMax = sds.amplitudeMax;
sounddevice->sounddata.noise = (double)sds.noise;
sounddevice->sounddata.curr_duration = 0;
sounddevice->sounddata.phase = 0;
sounddevice->sounddata.curr_amplitude = 0;
sounddevice->sounddata.curr_frequency = 0;
sounddevice->sounddata.curr_frequency = 0.0;
const char* streamname= "Engine";
+2 -1
View File
@@ -26,10 +26,11 @@ typedef struct
uint32_t amplitude;
uint32_t amplitudeMax;
double duration;
uint32_t curr_frequency;
double curr_frequency;
uint32_t curr_amplitude;
double curr_duration;
double phase;
double noise;
}
SoundData;
+39 -12
View File
@@ -416,7 +416,7 @@ void releaseloop(loop_data* f, SimData* simdata, SimMap* simmap)
}
free(devices);
int r = simfree(simdata, simmap, f->map);
int r = simapi_sim_clear(simdata, simmap);
slogd("simfree returned %i", r);
f->numdevices = 0;
slogi("stopped mapping data, press q again to quit");
@@ -446,7 +446,7 @@ void shmdatamapcallback(uv_timer_t* handle)
//appstate = 2;
if (appstate == 2)
{
simdatamap(simdata, simmap, NULL, f->map, false, NULL);
simapi_datamap(simdata, simmap, f->map, false, NULL);
looprun(ms, f, simdata);
}
@@ -483,7 +483,7 @@ static void on_udp_recv(uv_udp_t* handle, ssize_t nread, const uv_buf_t* rcvbuf,
if (appstate == 2)
{
simdatamap(simdata, simmap, NULL, f->map, true, a);
simapi_datamap(simdata, simmap, f->map, true, a);
looprun(ms, f, simdata);
}
@@ -511,7 +511,7 @@ void udpstart(MonocoqueSettings* sms, loop_data* f, SimData* simdata, SimMap* si
{
if (appstate == 2)
{
simdatamap(simdata, simmap, NULL, f->sim, true, NULL);
simapi_datamap(simdata, simmap, f->sim, true, NULL);
if (doui == true)
{
looprun(sms, f, simdata);
@@ -528,7 +528,7 @@ void datacheckcallback(uv_timer_t* handle)
if ( appstate == 1 )
{
SimInfo si = getSim(simdata, simmap, f->ms->force_udp_mode, startudp, false);
SimInfo si = simapi_get_sim(simdata, simmap, f->ms->force_udp_mode, startudp, false);
//TODO: move all this to a siminfo struct in loop_data
f->simstate = si.isSimOn;
f->sim = si.simulatorapi;
@@ -569,7 +569,7 @@ void datacheckcallback(uv_timer_t* handle)
}
if(appstate == 2)
{
SimInfo si = getSim(simdata, simmap, f->ms->force_udp_mode, NULL, false);
SimInfo si = simapi_get_sim(simdata, simmap, f->ms->force_udp_mode, NULL, false);
if(si.isSimOn == false)
{
appstate = 1;
@@ -620,7 +620,7 @@ int monocoque_mainloop(MonocoqueSettings* ms)
{
SimData* simdata = malloc(sizeof(SimData));
SimMap* simmap = createSimMap();
SimMap* simmap = simapi_simmap_create();
struct termios newsettings, canonicalmode;
tcgetattr(0, &canonicalmode);
@@ -644,9 +644,9 @@ int monocoque_mainloop(MonocoqueSettings* ms)
baton->sim = 0;
baton->req.data = (void*) baton;
set_simapi_log_info(simapilib_loginfo);
set_simapi_log_debug(simapilib_logdebug);
set_simapi_log_trace(simapilib_logtrace);
simapi_set_log_info(simapilib_loginfo);
simapi_set_log_debug(simapilib_logdebug);
simapi_set_log_trace(simapilib_logtrace);
if (0 != uv_poll_init(uv_default_loop(), poll, 0))
{
@@ -686,7 +686,6 @@ int monocoque_mainloop(MonocoqueSettings* ms)
free(baton);
free(simdata);
freesimmap(simmap, false);
return 0;
}
@@ -726,7 +725,35 @@ int tester(SimDevice* devices, int numdevices)
simdata->Yvelocity = 100;
simdata->Zvelocity = 0;
sleep(3);
sleep(1);
fprintf(stdout, "Revving rpm from 1000 to 8000 and back\n");
for (int r = 0; r < 8000; r += 3)
{
simdata->rpms = 1000 + r;
for (int x = 0; x < numdevices; x++)
{
if (devices[x].initialized == true)
{
devices[x].update(&devices[x], simdata);
}
}
usleep(1000);
}
for (int r = 0; r < 8000; r += 3)
{
simdata->rpms = 9000 - r;
for (int x = 0; x < numdevices; x++)
{
if (devices[x].initialized == true)
{
devices[x].update(&devices[x], simdata);
}
}
usleep(1000);
}
fprintf(stdout, "Setting rpms to 1000\n");
simdata->rpms = 1000;
+2
View File
@@ -747,6 +747,7 @@ int devsetup(const char* device_type, const char* device_subtype, const char* co
ds->sounddevsettings.pan = 0;
ds->sounddevsettings.channels = 1;
ds->sounddevsettings.duration = 2.0;
ds->sounddevsettings.noise = 0;
if (ds->effect_type == EFFECT_GEARSHIFT)
{
ds->sounddevsettings.duration = .125;
@@ -762,6 +763,7 @@ int devsetup(const char* device_type, const char* device_subtype, const char* co
config_setting_lookup_int(device_settings, "pan", &ds->sounddevsettings.pan);
config_setting_lookup_int(device_settings, "channels", &ds->sounddevsettings.channels);
config_setting_lookup_float(device_settings, "duration", &ds->sounddevsettings.duration);
config_setting_lookup_int(device_settings, "noise", &ds->sounddevsettings.noise);
const char* temp = NULL;
int found = 0;
+1
View File
@@ -185,6 +185,7 @@ typedef struct
double duration;
char* dev;
SoundEffectModulationType modulation;
int noise;
}
SoundDeviceSettings;
@@ -11,7 +11,9 @@ set(simulatorapi_source_files
simapi/include/outgauge.h
simapi/include/f12018.h
simapi/include/rbrdata.h
simapi/include/r3e.h
simapi/include/wreckfest2data.h
simapi/include/forza.h
simapi/simmap/basicmap.h
simapi/simmap/mapacdata.c
simapi/simmap/mapacdata.h
@@ -24,8 +26,11 @@ set(simulatorapi_source_files
simapi/simapi/wreckfest2.h
simapi/simapi/f1.h
simapi/simapi/rbr.h
simapi/simapi/forzadef.h
simapi/simapi/r3edef.h
simapi/simapi/mapping/acmapper.c
simapi/simapi/mapping/rf2mapper.c
simapi/simapi/mapping/r3emapper.c
simapi/simapi/mapping/pcars2mapper.c
simapi/simapi/mapping/dirt2mapper.c
simapi/simapi/mapping/scs2mapper.c
@@ -33,6 +38,7 @@ set(simulatorapi_source_files
simapi/simapi/mapping/f12018mapper.c
simapi/simapi/mapping/rbrmapper.c
simapi/simapi/mapping/wreckfest2mapper.c
simapi/simapi/mapping/forzamapper.c
simapi/simapi/getpid.c
simapi/simapi/getpid.h
)