mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-28 13:27:30 +01:00
Broadcast radio receiver in the cab
Plays internet radio streams (http, mp3) through a positional emitter in the cab, with analog tuning, inter-station noise and two speaker profiles. Stations are listed in fmradio.txt, cab controls are defined with fmradio entries in the .mmd file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -154,6 +154,8 @@ set(SOURCES
|
||||
"model/vertex.cpp"
|
||||
"audio/audio.cpp"
|
||||
"audio/audiorenderer.cpp"
|
||||
"audio/radiostream.cpp"
|
||||
"audio/fmradio.cpp"
|
||||
"utilities/motiontelemetry.cpp"
|
||||
"application/uitranscripts.cpp"
|
||||
"world/station.cpp"
|
||||
|
||||
@@ -274,6 +274,8 @@ openal_renderer::~openal_renderer() {
|
||||
|
||||
if( m_alcEventCallbackSOFT != nullptr ) { m_alcEventCallbackSOFT( nullptr, nullptr ); } // stop callbacks before teardown
|
||||
|
||||
if( m_context != nullptr ) { m_fmradio.release(); } // the receiver owns implementation-side resources, which have to go before the context does
|
||||
|
||||
::alcMakeContextCurrent( nullptr );
|
||||
|
||||
if( m_context != nullptr ) { ::alcDestroyContext( m_context ); }
|
||||
@@ -487,6 +489,11 @@ openal_renderer::update( double const Deltatime ) {
|
||||
}
|
||||
}
|
||||
|
||||
// update streamed sound
|
||||
if( m_ready ) {
|
||||
m_fmradio.update( Deltatime, cached_camerapos );
|
||||
}
|
||||
|
||||
// reset potentially used volume change flag
|
||||
audio::event_volume_change = false;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
#pragma once
|
||||
|
||||
#include "audio/audio.h"
|
||||
#include "audio/fmradio.h"
|
||||
#include "model/ResourceManager.h"
|
||||
#include "application/uitranscripts.h"
|
||||
|
||||
@@ -121,6 +122,8 @@ public:
|
||||
|
||||
void erase( sound_source const *Controller );
|
||||
void update( double const Deltatime );
|
||||
// provides access to the broadcast radio receiver
|
||||
audio::fm_radio &fmradio() { return m_fmradio; }
|
||||
|
||||
glm::dvec3 cached_camerapos;
|
||||
|
||||
@@ -142,6 +145,7 @@ private:
|
||||
buffer_manager m_buffers;
|
||||
source_list m_sources;
|
||||
source_sequence m_sourcespares;
|
||||
audio::fm_radio m_fmradio; // streamed sound, maintained separately from regular emitters
|
||||
|
||||
// OpenAL Soft extension entry points, resolved at runtime via al(c)GetProcAddress
|
||||
LPALDEFERUPDATESSOFT m_alDeferUpdatesSOFT { nullptr };
|
||||
|
||||
640
audio/fmradio.cpp
Normal file
640
audio/fmradio.cpp
Normal file
@@ -0,0 +1,640 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "audio/fmradio.h"
|
||||
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/Logs.h"
|
||||
|
||||
namespace audio
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
auto const EU07_FMRADIO_BUFFERCOUNT{8}; // number of sound buffers owned by the receiver
|
||||
auto const EU07_FMRADIO_QUEUELENGTH{6}; // max number of sound buffers waiting for playback. kept short, so the sound follows the knobs without noticeable delay
|
||||
auto const EU07_FMRADIO_BUFFERSPERSECOND{40}; // inverse of the length of a single sound buffer
|
||||
// sample rate of generated audio. it has to stay the same for all buffers queued in the emitter, so station audio is converted to it if needed
|
||||
unsigned int const EU07_FMRADIO_SAMPLERATE{44100};
|
||||
auto const EU07_FMRADIO_PREBUFFERLENGTH{1.0}; // seconds of station audio required to let it through
|
||||
auto const EU07_FMRADIO_UNATTENDEDLIMIT{1.0}; // seconds without contact from the owner after which the receiver turns itself off
|
||||
auto const EU07_FMRADIO_CAPTURERANGE{0.15f}; // MHz, max distance from the station frequency at which the station can be heard
|
||||
auto const EU07_FMRADIO_CLEANRANGE{0.03f}; // MHz, max distance from the station frequency at which the reception is undisturbed
|
||||
auto const EU07_FMRADIO_SETTLETIME{0.1}; // seconds the tuning knob has to spend at a station before the receiver connects with its stream
|
||||
// seconds the connection with a station is kept after the tuning knob moves away from it.
|
||||
// allows to pass a station, hear where it was and come back to it without waiting for another connection
|
||||
auto const EU07_FMRADIO_LINGERTIME{8.0};
|
||||
auto const EU07_FMRADIO_CARRIERNOISE{0.02f}; // fraction of the noise left when the receiver is locked on a station
|
||||
// the receiver makes a short electric crackle when it locks on a station or lets go of it
|
||||
auto const EU07_FMRADIO_CRACKLELEVEL{0.25f};
|
||||
auto const EU07_FMRADIO_CRACKLEDENSITY{0.12f}; // fraction of samples which carry a spike
|
||||
auto const EU07_FMRADIO_CRACKLEDECAY{0.008f}; // seconds
|
||||
auto const EU07_FMRADIO_CRACKLELENGTH{0.04f}; // seconds
|
||||
auto const EU07_FMRADIO_NOISELEVEL{0.3f}; // amplitude of the noise heard between the stations
|
||||
// level of the receiver signal at the input of the simulated amplifier. modern stations broadcast at full scale most of the time,
|
||||
// so without some headroom the amplifier and the filters boosting parts of the spectrum would be driven into heavy distortion
|
||||
auto const EU07_FMRADIO_HEADROOM{0.5f};
|
||||
auto const EU07_FMRADIO_LIMITERKNEE{0.9f}; // output level above which the peaks are rounded off
|
||||
// gain of the emitter with the volume knob turned all the way up. regular cab sounds are short and loud,
|
||||
// while a broadcast played at nominal gain gets lost in the noise of a running vehicle
|
||||
auto const EU07_FMRADIO_LOUDNESS{3.f};
|
||||
auto const EU07_FMRADIO_PI{3.14159265358979f};
|
||||
|
||||
std::string trim(std::string const &Text)
|
||||
{
|
||||
auto const first{Text.find_first_not_of(" \t\r\n")};
|
||||
if (first == std::string::npos)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
auto const last{Text.find_last_not_of(" \t\r\n")};
|
||||
return Text.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// filter designs follow the audio eq cookbook by Robert Bristow-Johnson
|
||||
fm_radio::biquad fm_radio::biquad::highpass(float const Rate, float const Frequency, float const Q)
|
||||
{
|
||||
auto const omega{2.f * EU07_FMRADIO_PI * std::min(Frequency, Rate * 0.45f) / Rate};
|
||||
auto const cosine{std::cos(omega)};
|
||||
auto const alpha{std::sin(omega) / (2.f * Q)};
|
||||
auto const a0{1.f + alpha};
|
||||
biquad filter;
|
||||
filter.b0 = (1.f + cosine) * 0.5f / a0;
|
||||
filter.b1 = -(1.f + cosine) / a0;
|
||||
filter.b2 = filter.b0;
|
||||
filter.a1 = -2.f * cosine / a0;
|
||||
filter.a2 = (1.f - alpha) / a0;
|
||||
return filter;
|
||||
}
|
||||
|
||||
fm_radio::biquad fm_radio::biquad::lowpass(float const Rate, float const Frequency, float const Q)
|
||||
{
|
||||
auto const omega{2.f * EU07_FMRADIO_PI * std::min(Frequency, Rate * 0.45f) / Rate};
|
||||
auto const cosine{std::cos(omega)};
|
||||
auto const alpha{std::sin(omega) / (2.f * Q)};
|
||||
auto const a0{1.f + alpha};
|
||||
biquad filter;
|
||||
filter.b0 = (1.f - cosine) * 0.5f / a0;
|
||||
filter.b1 = (1.f - cosine) / a0;
|
||||
filter.b2 = filter.b0;
|
||||
filter.a1 = -2.f * cosine / a0;
|
||||
filter.a2 = (1.f - alpha) / a0;
|
||||
return filter;
|
||||
}
|
||||
|
||||
// Gain is in decibels
|
||||
fm_radio::biquad fm_radio::biquad::peak(float const Rate, float const Frequency, float const Q, float const Gain)
|
||||
{
|
||||
auto const amplitude{std::pow(10.f, Gain / 40.f)};
|
||||
auto const omega{2.f * EU07_FMRADIO_PI * std::min(Frequency, Rate * 0.45f) / Rate};
|
||||
auto const cosine{std::cos(omega)};
|
||||
auto const alpha{std::sin(omega) / (2.f * Q)};
|
||||
auto const a0{1.f + alpha / amplitude};
|
||||
biquad filter;
|
||||
filter.b0 = (1.f + alpha * amplitude) / a0;
|
||||
filter.b1 = -2.f * cosine / a0;
|
||||
filter.b2 = (1.f - alpha * amplitude) / a0;
|
||||
filter.a1 = filter.b1;
|
||||
filter.a2 = (1.f - alpha / amplitude) / a0;
|
||||
return filter;
|
||||
}
|
||||
|
||||
void fm_radio::power(bool const State)
|
||||
{
|
||||
std::scoped_lock lock{m_controlmutex};
|
||||
m_control.power = State;
|
||||
m_control.unattended = 0.0;
|
||||
}
|
||||
|
||||
// sets frequency the receiver is tuned to, in MHz
|
||||
void fm_radio::tune(float const Frequency)
|
||||
{
|
||||
std::scoped_lock lock{m_controlmutex};
|
||||
m_control.frequency = std::clamp(Frequency, EU07_FMRADIO_BANDSTART, EU07_FMRADIO_BANDEND);
|
||||
}
|
||||
|
||||
// sets position of the volume knob, in range 0-1
|
||||
void fm_radio::volume(float const Volume)
|
||||
{
|
||||
std::scoped_lock lock{m_controlmutex};
|
||||
m_control.volume = std::clamp(Volume, 0.f, 1.f);
|
||||
}
|
||||
|
||||
void fm_radio::type(fmradio_type const Type)
|
||||
{
|
||||
std::scoped_lock lock{m_controlmutex};
|
||||
m_control.type = Type;
|
||||
}
|
||||
|
||||
// sets location of the receiver in the simulation region space, and audibility parameters at the current position of the listener
|
||||
void fm_radio::placement(glm::dvec3 const &Location, float const Soundproofing, float const Range)
|
||||
{
|
||||
std::scoped_lock lock{m_controlmutex};
|
||||
m_control.location = Location;
|
||||
m_control.soundproofing = Soundproofing;
|
||||
m_control.range = std::max(1.f, Range);
|
||||
}
|
||||
|
||||
// returns frequency of the first station on the list, or middle of the band if there's none
|
||||
float fm_radio::default_frequency()
|
||||
{
|
||||
load_stations();
|
||||
return m_stations.empty() ? (EU07_FMRADIO_BANDSTART + EU07_FMRADIO_BANDEND) * 0.5f : m_stations.front().frequency;
|
||||
}
|
||||
|
||||
// imports the list of stations. each line of the file holds name of a station, address of its stream and its frequency, separated with semicolons
|
||||
void fm_radio::load_stations()
|
||||
{
|
||||
std::scoped_lock lock{m_stationsmutex};
|
||||
if (m_stationsloaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_stationsloaded = true;
|
||||
|
||||
std::ifstream input{Global.fmradio_stations};
|
||||
if (false == input.is_open())
|
||||
{
|
||||
// NOTE: the list isn't a part of the simulator and it's up to the user to provide one, so its absence isn't an error
|
||||
WriteLog("fm radio: no station list found at \"" + Global.fmradio_stations + "\", the receiver will pick up only noise", logtype::sound);
|
||||
return;
|
||||
}
|
||||
std::string line;
|
||||
auto linenumber{0};
|
||||
while (std::getline(input, line))
|
||||
{
|
||||
++linenumber;
|
||||
if (linenumber == 1 && line.rfind("\xEF\xBB\xBF", 0) == 0)
|
||||
{
|
||||
line.erase(0, 3); // byte order mark
|
||||
}
|
||||
line = trim(line);
|
||||
if (line.empty() || line.front() == '#' || line.rfind("//", 0) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// NOTE: the address can potentially contain semicolons, so we take the outermost ones as separators
|
||||
auto const nameend{line.find(';')};
|
||||
auto const frequencystart{line.rfind(';')};
|
||||
if (nameend == std::string::npos || frequencystart == nameend)
|
||||
{
|
||||
ErrorLog("fm radio: malformed entry in line " + std::to_string(linenumber) + " of the station list, expected \"name; address; frequency\"", logtype::sound);
|
||||
continue;
|
||||
}
|
||||
station entry;
|
||||
entry.name = trim(line.substr(0, nameend));
|
||||
entry.url = trim(line.substr(nameend + 1, frequencystart - nameend - 1));
|
||||
auto frequency{trim(line.substr(frequencystart + 1))};
|
||||
std::replace(std::begin(frequency), std::end(frequency), ',', '.');
|
||||
entry.frequency = static_cast<float>(std::atof(frequency.c_str()));
|
||||
if (entry.url.empty() || entry.frequency < EU07_FMRADIO_BANDSTART || entry.frequency > EU07_FMRADIO_BANDEND)
|
||||
{
|
||||
ErrorLog("fm radio: station \"" + entry.name + "\" in line " + std::to_string(linenumber) + " of the station list has no address or its frequency is outside of 87.5-108 MHz range", logtype::sound);
|
||||
continue;
|
||||
}
|
||||
m_stations.emplace_back(entry);
|
||||
}
|
||||
WriteLog(m_stations.empty() ? "fm radio: the station list is empty, the receiver will pick up only noise" : "fm radio: " + std::to_string(m_stations.size()) + " station(s) on the list", logtype::sound);
|
||||
}
|
||||
|
||||
// returns index of the station which can be received at specified frequency, or -1
|
||||
int fm_radio::find_station(float const Frequency) const
|
||||
{
|
||||
auto result{-1};
|
||||
auto distance{EU07_FMRADIO_CAPTURERANGE};
|
||||
for (std::size_t index{0}; index < m_stations.size(); ++index)
|
||||
{
|
||||
auto const stationdistance{std::abs(m_stations[index].frequency - Frequency)};
|
||||
if (stationdistance < distance)
|
||||
{
|
||||
distance = stationdistance;
|
||||
result = static_cast<int>(index);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// selects the station to receive, taking into account movement of the tuning knob
|
||||
void fm_radio::update_tuning(control_block const &Control, double const Deltatime)
|
||||
{
|
||||
auto const candidate{find_station(Control.frequency)};
|
||||
if (candidate != m_candidate)
|
||||
{
|
||||
m_candidate = candidate;
|
||||
m_candidatetime = 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_candidatetime += Deltatime;
|
||||
}
|
||||
// NOTE: connection is made after the knob stays at the station for a moment, to skip stations passed at full speed
|
||||
if (candidate != -1 && candidate != m_station && m_candidatetime >= EU07_FMRADIO_SETTLETIME)
|
||||
{
|
||||
m_station = candidate;
|
||||
WriteLog("fm radio: tuned to \"" + m_stations[m_station].name + "\", " + std::to_string(m_stations[m_station].frequency) + " MHz", logtype::sound);
|
||||
m_stream.open(m_stations[m_station].url);
|
||||
m_buffering = true;
|
||||
m_inputphase = 0.0;
|
||||
m_inputsamples = {0, 0};
|
||||
}
|
||||
if (m_station != -1)
|
||||
{
|
||||
if (m_station == candidate)
|
||||
{
|
||||
m_lingertime = 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// the knob has left the station we were connected with. the connection is kept for a while in case the knob comes back
|
||||
m_lingertime += Deltatime;
|
||||
if (m_lingertime > EU07_FMRADIO_LINGERTIME)
|
||||
{
|
||||
m_stream.close();
|
||||
m_station = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prepares speaker simulation for specified receiver
|
||||
void fm_radio::setup_speaker(fmradio_type const Type)
|
||||
{
|
||||
m_speakertype = Type;
|
||||
m_speaker.clear();
|
||||
auto const rate{static_cast<float>(EU07_FMRADIO_SAMPLERATE)};
|
||||
switch (Type)
|
||||
{
|
||||
case fmradio_type::cheap:
|
||||
{
|
||||
// tiny speaker in a thin plastic case: no bass to speak of, harsh midrange, overdriven amplifier
|
||||
m_speaker.emplace_back(biquad::highpass(rate, 420.f, 0.8f));
|
||||
m_speaker.emplace_back(biquad::highpass(rate, 300.f, 0.6f));
|
||||
m_speaker.emplace_back(biquad::peak(rate, 1900.f, 1.4f, 7.f));
|
||||
m_speaker.emplace_back(biquad::lowpass(rate, 3400.f, 0.9f));
|
||||
m_speakerdrive = 2.2f;
|
||||
m_speakergain = 0.75f;
|
||||
break;
|
||||
}
|
||||
case fmradio_type::wooden:
|
||||
{
|
||||
// large speaker in a wooden cabinet: full, slightly boomy lows and soft, rolled off highs
|
||||
m_speaker.emplace_back(biquad::highpass(rate, 65.f, 0.7f));
|
||||
m_speaker.emplace_back(biquad::peak(rate, 130.f, 0.9f, 5.f));
|
||||
m_speaker.emplace_back(biquad::peak(rate, 2400.f, 0.8f, -2.5f));
|
||||
m_speaker.emplace_back(biquad::lowpass(rate, 4200.f, 0.6f));
|
||||
m_speakerdrive = 1.f;
|
||||
m_speakergain = 1.1f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fills the staging area with next piece of the receiver output
|
||||
void fm_radio::generate_chunk(control_block const &Control, std::size_t const Samplecount)
|
||||
{
|
||||
m_chunk.assign(Samplecount, 0);
|
||||
m_mix.resize(Samplecount);
|
||||
|
||||
// strength of the station signal at the current position of the tuning knob. it drops with the distance from the station frequency
|
||||
auto carrier{0.f};
|
||||
if (m_candidate != -1)
|
||||
{
|
||||
auto const distance{std::abs(m_stations[m_candidate].frequency - Control.frequency)};
|
||||
auto const detune{std::clamp((distance - EU07_FMRADIO_CLEANRANGE) / (EU07_FMRADIO_CAPTURERANGE - EU07_FMRADIO_CLEANRANGE), 0.f, 1.f)};
|
||||
carrier = 1.f - detune * detune * (3.f - 2.f * detune);
|
||||
}
|
||||
// the moment the receiver locks on a station or loses it is marked with a crackle.
|
||||
// together with the drop of the noise it tells there's a station, even if it's passed too quickly to make out anything else
|
||||
// NOTE: the thresholds are set apart, so the receiver doesn't keep thumping when the knob is left right at the edge
|
||||
auto const locked{m_locked ? carrier > 0.4f : carrier >= 0.6f};
|
||||
if (locked != m_locked)
|
||||
{
|
||||
m_locked = locked;
|
||||
m_thumpposition = 0;
|
||||
}
|
||||
|
||||
// station audio
|
||||
// NOTE: it's available only if we're connected with the station at the knob, the connection may be still in progress or kept with another station
|
||||
auto reception{0.f};
|
||||
if (m_station != -1 && m_station == m_candidate)
|
||||
{
|
||||
auto const streamrate{m_stream.rate()};
|
||||
if (m_buffering && streamrate > 0 && m_stream.available() >= static_cast<std::size_t>(streamrate * EU07_FMRADIO_PREBUFFERLENGTH))
|
||||
{
|
||||
m_buffering = false;
|
||||
}
|
||||
if (false == m_buffering)
|
||||
{
|
||||
// station audio is converted to our sample rate with linear interpolation, which is plenty for a radio speaker
|
||||
auto const step{static_cast<double>(streamrate) / EU07_FMRADIO_SAMPLERATE};
|
||||
m_input.assign(static_cast<std::size_t>(m_inputphase + step * Samplecount), 0);
|
||||
if (m_stream.read(m_input.data(), m_input.size()) < m_input.size())
|
||||
{
|
||||
// we ran out of data. the missing part of the chunk is already silent, and the station is put on hold until the reserve is rebuilt
|
||||
m_buffering = true;
|
||||
}
|
||||
std::size_t inputindex{0};
|
||||
for (std::size_t index{0}; index < Samplecount; ++index)
|
||||
{
|
||||
m_inputphase += step;
|
||||
while (m_inputphase >= 1.0)
|
||||
{
|
||||
m_inputsamples[0] = m_inputsamples[1];
|
||||
m_inputsamples[1] = (inputindex < m_input.size() ? m_input[inputindex++] : 0);
|
||||
m_inputphase -= 1.0;
|
||||
}
|
||||
m_chunk[index] = static_cast<std::int16_t>(m_inputsamples[0] + (m_inputsamples[1] - m_inputsamples[0]) * m_inputphase);
|
||||
}
|
||||
reception = carrier;
|
||||
}
|
||||
}
|
||||
|
||||
// mix of the station, the noise and the crackle
|
||||
auto const receptionstep{(reception - m_reception) / static_cast<float>(Samplecount)};
|
||||
auto const carrierstep{(carrier - m_carrier) / static_cast<float>(Samplecount)};
|
||||
auto const cracklelength{static_cast<int>(EU07_FMRADIO_CRACKLELENGTH * EU07_FMRADIO_SAMPLERATE)};
|
||||
for (std::size_t index{0}; index < Samplecount; ++index)
|
||||
{
|
||||
// NOTE: the clamps matter, accumulated rounding errors can push the values slightly below zero and the square root further down wouldn't like it
|
||||
m_reception = std::clamp(m_reception + receptionstep, 0.f, 1.f);
|
||||
m_carrier = std::clamp(m_carrier + carrierstep, 0.f, 1.f);
|
||||
// white noise from a xorshift generator
|
||||
m_noisestate ^= m_noisestate << 13;
|
||||
m_noisestate ^= m_noisestate >> 17;
|
||||
m_noisestate ^= m_noisestate << 5;
|
||||
auto const noise{static_cast<float>(m_noisestate) * (2.f / 4294967296.f) - 1.f};
|
||||
auto signal{m_chunk[index] * (1.f / 32768.f)};
|
||||
if (m_reception < 0.999f)
|
||||
{
|
||||
// an fm receiver which is off the station frequency cuts off peaks of the signal, the further the stronger
|
||||
auto const ceiling{0.05f + 0.95f * m_reception};
|
||||
signal = std::clamp(signal * (1.f + 2.f * (1.f - m_reception)), -ceiling, ceiling);
|
||||
}
|
||||
// the noise is pushed down by the station signal itself, whether there's anything to hear on it or not
|
||||
auto const noisegain{std::max((1.f - m_carrier) * (1.f - m_carrier), EU07_FMRADIO_CARRIERNOISE)};
|
||||
auto sample{signal * std::sqrt(m_reception) + noise * noisegain * EU07_FMRADIO_NOISELEVEL};
|
||||
if (m_thumpposition >= 0)
|
||||
{
|
||||
// sparse spikes of random polarity, fading out quickly
|
||||
auto const time{static_cast<float>(m_thumpposition) / EU07_FMRADIO_SAMPLERATE};
|
||||
if (std::abs(noise) > 1.f - EU07_FMRADIO_CRACKLEDENSITY)
|
||||
{
|
||||
sample += (noise < 0.f ? -EU07_FMRADIO_CRACKLELEVEL : EU07_FMRADIO_CRACKLELEVEL) * std::exp(-time / EU07_FMRADIO_CRACKLEDECAY);
|
||||
}
|
||||
if (++m_thumpposition >= cracklelength)
|
||||
{
|
||||
m_thumpposition = -1;
|
||||
}
|
||||
}
|
||||
m_mix[index] = sample;
|
||||
}
|
||||
m_reception = reception;
|
||||
m_carrier = carrier;
|
||||
|
||||
// speaker
|
||||
if (false == Global.fmradio_cleansound)
|
||||
{
|
||||
if (m_speaker.empty() || m_speakertype != Control.type)
|
||||
{
|
||||
setup_speaker(Control.type);
|
||||
}
|
||||
for (auto &sample : m_mix)
|
||||
{
|
||||
// amplifier, with soft saturation
|
||||
sample = std::tanh(sample * EU07_FMRADIO_HEADROOM * m_speakerdrive);
|
||||
for (auto &filter : m_speaker)
|
||||
{
|
||||
sample = filter.process(sample);
|
||||
}
|
||||
sample *= m_speakergain;
|
||||
}
|
||||
// safety net, an invalid value stuck in a filter would silence the receiver for good
|
||||
for (auto &filter : m_speaker)
|
||||
{
|
||||
if (false == std::isfinite(filter.z1) || false == std::isfinite(filter.z2))
|
||||
{
|
||||
filter.z1 = 0.f;
|
||||
filter.z2 = 0.f;
|
||||
std::fill(std::begin(m_mix), std::end(m_mix), 0.f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index{0}; index < Samplecount; ++index)
|
||||
{
|
||||
// peaks which still go over the top are rounded off, as hard clipping sounds much worse
|
||||
auto sample{m_mix[index]};
|
||||
auto const level{std::abs(sample)};
|
||||
if (level > EU07_FMRADIO_LIMITERKNEE)
|
||||
{
|
||||
auto const limited{EU07_FMRADIO_LIMITERKNEE + (1.f - EU07_FMRADIO_LIMITERKNEE) * std::tanh((level - EU07_FMRADIO_LIMITERKNEE) / (1.f - EU07_FMRADIO_LIMITERKNEE))};
|
||||
sample = (sample < 0.f ? -limited : limited);
|
||||
}
|
||||
m_chunk[index] = static_cast<std::int16_t>(sample * 32767.f);
|
||||
}
|
||||
}
|
||||
|
||||
// updates state of the receiver. expects active audio context
|
||||
void fm_radio::update(double const Deltatime, glm::dvec3 const &Listener)
|
||||
{
|
||||
control_block control;
|
||||
{
|
||||
std::scoped_lock lock{m_controlmutex};
|
||||
if (m_control.unattended > EU07_FMRADIO_UNATTENDEDLIMIT)
|
||||
{
|
||||
// the owner is gone, or has more important things to do
|
||||
m_control.power = false;
|
||||
}
|
||||
// NOTE: the time is added after the check, so a single long frame doesn't count as a lack of contact
|
||||
m_control.unattended += Deltatime;
|
||||
control = m_control;
|
||||
}
|
||||
|
||||
// pass messages from the stream worker to the log
|
||||
radio_stream::message message;
|
||||
while (m_stream.fetch_message(message))
|
||||
{
|
||||
if (message.second)
|
||||
{
|
||||
ErrorLog(message.first, logtype::sound);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteLog(message.first, logtype::sound);
|
||||
}
|
||||
}
|
||||
|
||||
if (false == control.power)
|
||||
{
|
||||
if (m_active)
|
||||
{
|
||||
m_stream.close();
|
||||
reset_emitter();
|
||||
m_station = -1;
|
||||
m_candidate = -1;
|
||||
m_active = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (false == m_active)
|
||||
{
|
||||
// switched on
|
||||
load_stations();
|
||||
m_reception = 0.f;
|
||||
m_carrier = 0.f;
|
||||
m_locked = false;
|
||||
m_thumpposition = -1;
|
||||
m_active = true;
|
||||
}
|
||||
update_tuning(control, Deltatime);
|
||||
|
||||
if (m_source == audio::null_resource && false == create_emitter())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// feed the emitter with generated audio
|
||||
auto queuedcount{reclaim_buffers()};
|
||||
auto const chunksize{static_cast<std::size_t>(EU07_FMRADIO_SAMPLERATE / EU07_FMRADIO_BUFFERSPERSECOND)};
|
||||
while (queuedcount < EU07_FMRADIO_QUEUELENGTH && false == m_sparebuffers.empty())
|
||||
{
|
||||
generate_chunk(control, chunksize);
|
||||
auto const buffer{m_sparebuffers.back()};
|
||||
m_sparebuffers.pop_back();
|
||||
::alBufferData(buffer, AL_FORMAT_MONO16, m_chunk.data(), static_cast<ALsizei>(m_chunk.size() * sizeof(std::int16_t)), static_cast<ALsizei>(EU07_FMRADIO_SAMPLERATE));
|
||||
::alSourceQueueBuffers(m_source, 1, &buffer);
|
||||
++queuedcount;
|
||||
}
|
||||
|
||||
// location
|
||||
auto const offset{glm::vec3{control.location - Listener}};
|
||||
::alSourcefv(m_source, AL_POSITION, glm::value_ptr(offset));
|
||||
// gain
|
||||
// NOTE: the knob position is squared to approximate the response of an audio taper potentiometer
|
||||
auto gain{EU07_FMRADIO_LOUDNESS * control.volume * control.volume * control.soundproofing * Global.VehicleVolume};
|
||||
auto const rangesquared{control.range * control.range};
|
||||
auto const distancesquared{glm::length2(offset)};
|
||||
if (distancesquared > rangesquared)
|
||||
{
|
||||
// outside of nominal hearing range the volume fades out, same way as it does for regular sounds
|
||||
auto const fadedistance{control.range * 0.75f};
|
||||
gain *= std::lerp(1.f, 0.f, std::clamp((distancesquared - rangesquared) / (fadedistance * fadedistance), 0.f, 1.f));
|
||||
}
|
||||
::alSourcef(m_source, AL_GAIN, gain);
|
||||
::alSourcef(m_source, AL_REFERENCE_DISTANCE, control.range * (1.f / 16.f) * control.soundproofing);
|
||||
|
||||
// playback. the emitter stops on its own when it runs out of queued data, so it may need a restart
|
||||
ALint state{AL_INITIAL};
|
||||
::alGetSourcei(m_source, AL_SOURCE_STATE, &state);
|
||||
if (state != AL_PLAYING && queuedcount > 0)
|
||||
{
|
||||
::alSourcePlay(m_source);
|
||||
}
|
||||
}
|
||||
|
||||
// stops the reception and frees implementation-side resources. expects active audio context
|
||||
void fm_radio::release()
|
||||
{
|
||||
m_stream.close();
|
||||
m_station = -1;
|
||||
m_candidate = -1;
|
||||
m_active = false;
|
||||
if (m_source != audio::null_resource)
|
||||
{
|
||||
reset_emitter();
|
||||
::alDeleteSources(1, &m_source);
|
||||
m_source = audio::null_resource;
|
||||
}
|
||||
if (false == m_buffers.empty())
|
||||
{
|
||||
::alDeleteBuffers(static_cast<ALsizei>(m_buffers.size()), m_buffers.data());
|
||||
m_buffers.clear();
|
||||
m_sparebuffers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
bool fm_radio::create_emitter()
|
||||
{
|
||||
::alGetError(); // reset the error state, so we don't get blamed for past sins of others
|
||||
|
||||
::alGenSources(1, &m_source);
|
||||
if (::alGetError() != AL_NO_ERROR)
|
||||
{
|
||||
m_source = audio::null_resource;
|
||||
if (false == m_emitterfailed)
|
||||
{
|
||||
ErrorLog("fm radio: failed to create sound emitter", logtype::sound);
|
||||
m_emitterfailed = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (m_buffers.empty())
|
||||
{
|
||||
m_buffers.resize(EU07_FMRADIO_BUFFERCOUNT);
|
||||
::alGenBuffers(static_cast<ALsizei>(m_buffers.size()), m_buffers.data());
|
||||
if (::alGetError() != AL_NO_ERROR)
|
||||
{
|
||||
m_buffers.clear();
|
||||
::alDeleteSources(1, &m_source);
|
||||
m_source = audio::null_resource;
|
||||
if (false == m_emitterfailed)
|
||||
{
|
||||
ErrorLog("fm radio: failed to create sound buffers", logtype::sound);
|
||||
m_emitterfailed = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
m_sparebuffers = m_buffers;
|
||||
m_emitterfailed = false;
|
||||
|
||||
::alSourcef(m_source, AL_PITCH, 1.f);
|
||||
::alSourcef(m_source, AL_MAX_GAIN, EU07_FMRADIO_LOUDNESS); // by default the gain is capped at 1
|
||||
::alSourcef(m_source, AL_ROLLOFF_FACTOR, 1.75f); // matches the setup of regular sounds
|
||||
::alSourcei(m_source, AL_LOOPING, AL_FALSE);
|
||||
glm::vec3 const zero{0.f};
|
||||
::alSourcefv(m_source, AL_VELOCITY, glm::value_ptr(zero));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// stops the playback and takes back all sound buffers
|
||||
void fm_radio::reset_emitter()
|
||||
{
|
||||
if (m_source == audio::null_resource)
|
||||
{
|
||||
return;
|
||||
}
|
||||
::alSourceStop(m_source);
|
||||
::alSourcei(m_source, AL_BUFFER, 0); // detaches all queued buffers
|
||||
m_sparebuffers = m_buffers;
|
||||
}
|
||||
|
||||
// collects buffers which were played back. returns: number of buffers still waiting in the queue
|
||||
int fm_radio::reclaim_buffers()
|
||||
{
|
||||
ALint processedcount{0};
|
||||
::alGetSourcei(m_source, AL_BUFFERS_PROCESSED, &processedcount);
|
||||
while (processedcount-- > 0)
|
||||
{
|
||||
ALuint buffer{0};
|
||||
::alSourceUnqueueBuffers(m_source, 1, &buffer);
|
||||
if (buffer != 0 && m_sparebuffers.size() < m_buffers.size())
|
||||
{
|
||||
m_sparebuffers.emplace_back(buffer);
|
||||
}
|
||||
}
|
||||
return static_cast<int>(m_buffers.size() - m_sparebuffers.size());
|
||||
}
|
||||
|
||||
} // namespace audio
|
||||
141
audio/fmradio.h
Normal file
141
audio/fmradio.h
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "audio/audio.h"
|
||||
#include "audio/radiostream.h"
|
||||
|
||||
namespace audio
|
||||
{
|
||||
|
||||
float const EU07_FMRADIO_BANDSTART{87.5f}; // MHz
|
||||
float const EU07_FMRADIO_BANDEND{108.f}; // MHz
|
||||
|
||||
// kind of the receiver, determines character of its sound
|
||||
enum class fmradio_type
|
||||
{
|
||||
cheap, // pocket-sized plastic receiver with a tiny speaker
|
||||
wooden // old table receiver with a large speaker in a wooden cabinet
|
||||
};
|
||||
|
||||
// broadcast radio receiver installed in a vehicle cab.
|
||||
// the stations are internet radio streams assigned to frequencies of the fm band in a text file.
|
||||
// the sound is generated piece by piece from the stream and synthesized noise, then played through a positional emitter
|
||||
// owned by the receiver, as this doesn't fit the regular buffer-based sound pipeline
|
||||
class fm_radio
|
||||
{
|
||||
|
||||
public:
|
||||
// constructors
|
||||
fm_radio() = default;
|
||||
// methods
|
||||
// owner-side interface, to be called by the vehicle the receiver is installed in.
|
||||
// NOTE: the owner is expected to call power() on regular basis, a receiver left without supervision turns itself off
|
||||
void power(bool const State);
|
||||
// sets frequency the receiver is tuned to, in MHz
|
||||
void tune(float const Frequency);
|
||||
// sets position of the volume knob, in range 0-1
|
||||
void volume(float const Volume);
|
||||
void type(fmradio_type const Type);
|
||||
// sets location of the receiver in the simulation region space, and audibility parameters at the current position of the listener
|
||||
void placement(glm::dvec3 const &Location, float const Soundproofing, float const Range);
|
||||
// returns frequency of the first station on the list, or middle of the band if there's none
|
||||
float default_frequency();
|
||||
// renderer-side interface
|
||||
// updates state of the receiver. expects active audio context
|
||||
void update(double const Deltatime, glm::dvec3 const &Listener);
|
||||
// stops the reception and frees implementation-side resources. expects active audio context
|
||||
void release();
|
||||
|
||||
private:
|
||||
// types
|
||||
struct control_block
|
||||
{
|
||||
bool power{false};
|
||||
float frequency{EU07_FMRADIO_BANDSTART};
|
||||
float volume{0.f};
|
||||
fmradio_type type{fmradio_type::cheap};
|
||||
glm::dvec3 location{0.0};
|
||||
float soundproofing{1.f};
|
||||
float range{15.f};
|
||||
double unattended{0.0}; // time since the last call from the owner
|
||||
};
|
||||
struct station
|
||||
{
|
||||
std::string name;
|
||||
std::string url;
|
||||
float frequency{0.f}; // MHz
|
||||
};
|
||||
// second order filter section
|
||||
struct biquad
|
||||
{
|
||||
float b0{1.f}, b1{0.f}, b2{0.f}, a1{0.f}, a2{0.f};
|
||||
float z1{0.f}, z2{0.f};
|
||||
|
||||
float process(float const Input)
|
||||
{
|
||||
auto const output{b0 * Input + z1};
|
||||
z1 = b1 * Input - a1 * output + z2;
|
||||
z2 = b2 * Input - a2 * output;
|
||||
return output;
|
||||
}
|
||||
static biquad highpass(float const Rate, float const Frequency, float const Q);
|
||||
static biquad lowpass(float const Rate, float const Frequency, float const Q);
|
||||
static biquad peak(float const Rate, float const Frequency, float const Q, float const Gain);
|
||||
};
|
||||
// methods
|
||||
void load_stations();
|
||||
// returns index of the station which can be received at specified frequency, or -1
|
||||
int find_station(float const Frequency) const;
|
||||
// selects the station to receive, taking into account movement of the tuning knob
|
||||
void update_tuning(control_block const &Control, double const Deltatime);
|
||||
// prepares speaker simulation for specified receiver
|
||||
void setup_speaker(fmradio_type const Type);
|
||||
// fills the staging area with next piece of the receiver output
|
||||
void generate_chunk(control_block const &Control, std::size_t const Samplecount);
|
||||
bool create_emitter();
|
||||
// stops the playback and takes back all sound buffers
|
||||
void reset_emitter();
|
||||
// collects buffers which were played back. returns: number of buffers still waiting in the queue
|
||||
int reclaim_buffers();
|
||||
// members
|
||||
std::mutex m_controlmutex;
|
||||
control_block m_control; // settings requested by the owner
|
||||
std::mutex m_stationsmutex;
|
||||
std::vector<station> m_stations;
|
||||
bool m_stationsloaded{false};
|
||||
radio_stream m_stream;
|
||||
bool m_active{false}; // the receiver is switched on
|
||||
int m_station{-1}; // station the stream is connected with
|
||||
int m_candidate{-1}; // station at the current position of the tuning knob
|
||||
double m_candidatetime{0.0}; // time the knob has spent at the candidate station
|
||||
double m_lingertime{0.0}; // time since the knob has left the station the stream is connected with
|
||||
bool m_buffering{true}; // station audio is held until there's enough data to keep it going
|
||||
float m_reception{0.f}; // current quality of the station audio, 0-1
|
||||
float m_carrier{0.f}; // current strength of the station signal, 0-1
|
||||
bool m_locked{false}; // the receiver is locked on a station signal
|
||||
int m_thumpposition{-1}; // progress of the sound made when the receiver locks on a station or loses it, in samples. negative if the sound isn't playing
|
||||
double m_inputphase{0.0}; // sample rate conversion, position between two most recent samples of station audio
|
||||
std::array<std::int16_t, 2> m_inputsamples{0, 0}; // sample rate conversion, two most recent samples of station audio
|
||||
fmradio_type m_speakertype{fmradio_type::cheap};
|
||||
std::vector<biquad> m_speaker; // filters simulating the speaker and the case
|
||||
float m_speakerdrive{1.f}; // signal level at the input of the simulated amplifier
|
||||
float m_speakergain{1.f}; // make-up gain
|
||||
std::uint32_t m_noisestate{0x2545f491}; // state of the noise generator
|
||||
bool m_emitterfailed{false}; // helper, prevents flooding the log with the same error
|
||||
ALuint m_source{audio::null_resource};
|
||||
std::vector<ALuint> m_buffers; // all sound buffers owned by the receiver
|
||||
std::vector<ALuint> m_sparebuffers; // sound buffers which aren't queued for playback
|
||||
std::vector<std::int16_t> m_chunk; // helper, staging area for audio data
|
||||
std::vector<std::int16_t> m_input; // helper, staging area for audio data
|
||||
std::vector<float> m_mix; // helper, staging area for audio data
|
||||
};
|
||||
|
||||
} // namespace audio
|
||||
910
audio/radiostream.cpp
Normal file
910
audio/radiostream.cpp
Normal file
@@ -0,0 +1,910 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "audio/radiostream.h"
|
||||
|
||||
#ifdef DBG_NEW
|
||||
#undef new
|
||||
#endif
|
||||
|
||||
#define ASIO_DISABLE_VISIBILITY
|
||||
#include <asio.hpp>
|
||||
|
||||
#define DR_MP3_IMPLEMENTATION
|
||||
#define DR_MP3_NO_STDIO
|
||||
#include "ref/dr_libs/dr_mp3.h"
|
||||
|
||||
namespace audio
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
auto const EU07_RADIOSTREAM_BUFFERLENGTH{8}; // seconds of decoded audio kept for the owner
|
||||
auto const EU07_RADIOSTREAM_CONNECTTIMEOUT{std::chrono::seconds{10}};
|
||||
auto const EU07_RADIOSTREAM_STALLTIMEOUT{std::chrono::seconds{10}}; // max time without any incoming data
|
||||
auto const EU07_RADIOSTREAM_MAXREDIRECTS{5};
|
||||
std::size_t const EU07_RADIOSTREAM_MAXHEADERSIZE{32 * 1024};
|
||||
std::size_t const EU07_RADIOSTREAM_MAXPLAYLISTSIZE{64 * 1024};
|
||||
// the decoder needs to see the beginning of the next frame to stay in sync, so it's only fed when there's more data than a frame can take.
|
||||
// we start with the size safe for the largest frames, then scale it down to the frames of the stream,
|
||||
// otherwise audio of low bitrate streams would be delivered in pieces up to a second long
|
||||
std::size_t const EU07_RADIOSTREAM_DECODETHRESHOLD{4096};
|
||||
std::size_t const EU07_RADIOSTREAM_MINDECODETHRESHOLD{512};
|
||||
// amount of data without a single valid frame after which the stream is deemed not to be mp3
|
||||
std::size_t const EU07_RADIOSTREAM_MAXJUNKSIZE{512 * 1024};
|
||||
|
||||
std::string lowercase(std::string Text)
|
||||
{
|
||||
std::transform(std::begin(Text), std::end(Text), std::begin(Text), [](unsigned char const c) { return static_cast<char>(std::tolower(c)); });
|
||||
return Text;
|
||||
}
|
||||
|
||||
std::string trim(std::string const &Text)
|
||||
{
|
||||
auto const first{Text.find_first_not_of(" \t\r\n")};
|
||||
if (first == std::string::npos)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
auto const last{Text.find_last_not_of(" \t\r\n")};
|
||||
return Text.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
bool starts_with_nocase(std::string const &Text, std::string const &Prefix)
|
||||
{
|
||||
return Text.size() >= Prefix.size() && lowercase(Text.substr(0, Prefix.size())) == Prefix;
|
||||
}
|
||||
|
||||
struct stream_address
|
||||
{
|
||||
std::string host;
|
||||
std::string port{"80"};
|
||||
std::string target{"/"};
|
||||
|
||||
// host with optional port, as expected by http host header
|
||||
std::string authority() const
|
||||
{
|
||||
auto const name{host.find(':') != std::string::npos ? "[" + host + "]" : host};
|
||||
return port == "80" ? name : name + ":" + port;
|
||||
}
|
||||
};
|
||||
|
||||
// splits provided address into components. returns: empty string on success, or description of the problem
|
||||
std::string parse_address(std::string const &Url, stream_address &Address)
|
||||
{
|
||||
auto url{trim(Url)};
|
||||
if (starts_with_nocase(url, "https://"))
|
||||
{
|
||||
return "encrypted (https) streams aren't supported";
|
||||
}
|
||||
if (starts_with_nocase(url, "http://"))
|
||||
{
|
||||
url.erase(0, 7);
|
||||
}
|
||||
else if (url.find("://") != std::string::npos)
|
||||
{
|
||||
return "unsupported protocol";
|
||||
}
|
||||
|
||||
Address = stream_address{};
|
||||
auto const hostend{url.find_first_of("/?")};
|
||||
auto authority{url.substr(0, hostend)};
|
||||
if (hostend != std::string::npos)
|
||||
{
|
||||
Address.target = url.substr(hostend);
|
||||
if (Address.target.front() == '?')
|
||||
{
|
||||
Address.target.insert(0, "/");
|
||||
}
|
||||
}
|
||||
// credentials aren't supported, but shouldn't prevent the connection
|
||||
auto const credentialsend{authority.rfind('@')};
|
||||
if (credentialsend != std::string::npos)
|
||||
{
|
||||
authority.erase(0, credentialsend + 1);
|
||||
}
|
||||
if (false == authority.empty() && authority.front() == '[')
|
||||
{
|
||||
// ipv6 literal
|
||||
auto const literalend{authority.find(']')};
|
||||
if (literalend == std::string::npos)
|
||||
{
|
||||
return "malformed address";
|
||||
}
|
||||
Address.host = authority.substr(1, literalend - 1);
|
||||
if (literalend + 2 < authority.size() && authority[literalend + 1] == ':')
|
||||
{
|
||||
Address.port = authority.substr(literalend + 2);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const portstart{authority.rfind(':')};
|
||||
Address.host = authority.substr(0, portstart);
|
||||
if (portstart != std::string::npos && portstart + 1 < authority.size())
|
||||
{
|
||||
Address.port = authority.substr(portstart + 1);
|
||||
}
|
||||
}
|
||||
if (Address.host.empty())
|
||||
{
|
||||
return "malformed address";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// combines address received in a redirect with the address of the redirecting resource
|
||||
std::string resolve_redirect(std::string const &Location, stream_address const &Origin)
|
||||
{
|
||||
if (Location.find("://") != std::string::npos)
|
||||
{
|
||||
return Location;
|
||||
}
|
||||
if (Location.rfind("//", 0) == 0)
|
||||
{
|
||||
return "http:" + Location;
|
||||
}
|
||||
auto const root{"http://" + Origin.authority()};
|
||||
if (false == Location.empty() && Location.front() == '/')
|
||||
{
|
||||
return root + Location;
|
||||
}
|
||||
auto directory{Origin.target.substr(0, Origin.target.find('?'))};
|
||||
directory.erase(directory.rfind('/') + 1);
|
||||
return root + directory + Location;
|
||||
}
|
||||
|
||||
// extracts address of the first stream listed in provided m3u or pls playlist. returns: the address, or empty string
|
||||
std::string parse_playlist(std::string const &Playlist)
|
||||
{
|
||||
std::istringstream lines{Playlist};
|
||||
std::string line;
|
||||
while (std::getline(lines, line))
|
||||
{
|
||||
line = trim(line);
|
||||
if (line.empty() || line.front() == '#' || line.front() == '[')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// pls entries have form of FileN=address, m3u ones are plain addresses
|
||||
if (starts_with_nocase(line, "file"))
|
||||
{
|
||||
auto const separator{line.find('=')};
|
||||
if (separator != std::string::npos)
|
||||
{
|
||||
line = trim(line.substr(separator + 1));
|
||||
}
|
||||
}
|
||||
if (starts_with_nocase(line, "http://") || starts_with_nocase(line, "https://"))
|
||||
{
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
struct http_response
|
||||
{
|
||||
int status{0};
|
||||
std::map<std::string, std::string> headers; // names are stored in lower case
|
||||
std::string body; // part of the content which arrived together with the header
|
||||
|
||||
std::string header(std::string const &Name) const
|
||||
{
|
||||
auto const lookup{headers.find(Name)};
|
||||
return lookup != std::end(headers) ? lookup->second : std::string{};
|
||||
}
|
||||
};
|
||||
|
||||
// parses provided response header. returns: true on success
|
||||
bool parse_response(std::string const &Header, http_response &Response)
|
||||
{
|
||||
std::istringstream lines{Header};
|
||||
std::string line;
|
||||
if (false == static_cast<bool>(std::getline(lines, line)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// status line, either regular 'HTTP/1.x 200 OK' or 'ICY 200 OK' sent by shoutcast servers
|
||||
std::istringstream statusline{line};
|
||||
std::string protocol;
|
||||
statusline >> protocol >> Response.status;
|
||||
if (statusline.fail() || (false == starts_with_nocase(protocol, "http/") && lowercase(protocol) != "icy"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
while (std::getline(lines, line))
|
||||
{
|
||||
auto const separator{line.find(':')};
|
||||
if (separator == std::string::npos)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Response.headers[lowercase(trim(line.substr(0, separator)))] = trim(line.substr(separator + 1));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// removes http chunked transfer framing from received data, if there's any
|
||||
class content_decoder
|
||||
{
|
||||
|
||||
public:
|
||||
explicit content_decoder(bool const Chunked) : m_chunked{Chunked} {}
|
||||
// appends payload of provided data to the output. returns: false if the data is malformed
|
||||
bool feed(std::uint8_t const *Data, std::size_t const Size, std::vector<std::uint8_t> &Output)
|
||||
{
|
||||
if (false == m_chunked)
|
||||
{
|
||||
Output.insert(std::end(Output), Data, Data + Size);
|
||||
return true;
|
||||
}
|
||||
std::size_t index{0};
|
||||
while (index < Size)
|
||||
{
|
||||
switch (m_stage)
|
||||
{
|
||||
case stage::size:
|
||||
{
|
||||
auto const character{static_cast<char>(Data[index++])};
|
||||
if (character == '\n')
|
||||
{
|
||||
// chunk size is a hex number, optionally followed by extensions we don't care about
|
||||
char *numberend{nullptr};
|
||||
m_remaining = std::strtoul(m_line.c_str(), &numberend, 16);
|
||||
if (numberend == m_line.c_str())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_line.clear();
|
||||
m_stage = (m_remaining > 0 ? stage::payload : stage::done);
|
||||
}
|
||||
else if (character != '\r')
|
||||
{
|
||||
m_line += character;
|
||||
if (m_line.size() > 256)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case stage::payload:
|
||||
{
|
||||
auto const count{std::min<std::size_t>(m_remaining, Size - index)};
|
||||
Output.insert(std::end(Output), Data + index, Data + index + count);
|
||||
index += count;
|
||||
m_remaining -= count;
|
||||
if (m_remaining == 0)
|
||||
{
|
||||
m_stage = stage::separator;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case stage::separator:
|
||||
{
|
||||
// payload of each chunk is followed by a line break
|
||||
if (Data[index++] == '\n')
|
||||
{
|
||||
m_stage = stage::size;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case stage::done:
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool done() const
|
||||
{
|
||||
return m_stage == stage::done;
|
||||
}
|
||||
|
||||
private:
|
||||
enum class stage
|
||||
{
|
||||
size,
|
||||
payload,
|
||||
separator,
|
||||
done
|
||||
};
|
||||
bool m_chunked;
|
||||
stage m_stage{stage::size};
|
||||
std::string m_line;
|
||||
unsigned long m_remaining{0};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// single connection attempt of the radio stream worker
|
||||
class radio_session
|
||||
{
|
||||
|
||||
public:
|
||||
enum class result
|
||||
{
|
||||
stopped, // the owner lost interest in the stream
|
||||
failed, // connection didn't work out or was lost, it's worth to try again
|
||||
unsupported // there's nothing we can do with this stream
|
||||
};
|
||||
radio_session(radio_stream &Owner, std::stop_token Stoptoken, std::uint32_t const Generation) : m_owner{Owner}, m_stoptoken{std::move(Stoptoken)}, m_generation{Generation}, m_resolver{m_io}, m_socket{m_io} {}
|
||||
~radio_session()
|
||||
{
|
||||
asio::error_code ignored;
|
||||
m_socket.close(ignored);
|
||||
}
|
||||
// connects with specified stream and feeds the owner with decoded audio until the connection ends
|
||||
result play(std::string Url);
|
||||
// returns true if the session delivered any audio
|
||||
bool delivered() const
|
||||
{
|
||||
return m_delivered;
|
||||
}
|
||||
|
||||
private:
|
||||
// runs pending network operation until specified flag is set by its completion handler. returns: false if the operation was aborted
|
||||
bool wait(bool const &Done, std::chrono::seconds const Timeout);
|
||||
bool connect(stream_address const &Address);
|
||||
bool send(std::string const &Data);
|
||||
// reads some data from the socket. returns: number of received bytes, or 0 if the connection is done for
|
||||
std::size_t receive(std::uint8_t *Data, std::size_t const Size);
|
||||
bool receive_response(http_response &Response);
|
||||
// retrieves the playlist and replaces provided address with the one found in it. returns: false if it wasn't possible
|
||||
bool receive_playlist(http_response const &Response, std::string &Url, result &Failure);
|
||||
result receive_audio(http_response const &Response);
|
||||
result fail(std::string const &Reason, result const Result = result::failed);
|
||||
|
||||
radio_stream &m_owner;
|
||||
std::stop_token m_stoptoken;
|
||||
std::uint32_t m_generation;
|
||||
asio::io_context m_io;
|
||||
asio::ip::tcp::resolver m_resolver;
|
||||
asio::ip::tcp::socket m_socket;
|
||||
std::string m_error; // description of the most recent network problem
|
||||
bool m_delivered{false};
|
||||
};
|
||||
|
||||
bool radio_session::wait(bool const &Done, std::chrono::seconds const Timeout)
|
||||
{
|
||||
auto const deadline{std::chrono::steady_clock::now() + Timeout};
|
||||
while (false == Done)
|
||||
{
|
||||
auto const timedout{std::chrono::steady_clock::now() >= deadline};
|
||||
if (timedout || m_stoptoken.stop_requested())
|
||||
{
|
||||
m_error = (timedout ? "operation timed out" : "");
|
||||
// abort the operation, then let its completion handler run as it refers to data of our caller
|
||||
asio::error_code ignored;
|
||||
m_resolver.cancel();
|
||||
m_socket.close(ignored);
|
||||
m_io.restart();
|
||||
m_io.run();
|
||||
return false;
|
||||
}
|
||||
if (m_io.stopped())
|
||||
{
|
||||
m_io.restart();
|
||||
}
|
||||
m_io.run_one_for(std::chrono::milliseconds{100});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool radio_session::connect(stream_address const &Address)
|
||||
{
|
||||
asio::error_code error;
|
||||
asio::ip::tcp::resolver::results_type endpoints;
|
||||
bool done{false};
|
||||
|
||||
m_resolver.async_resolve(Address.host, Address.port, [&](asio::error_code const &Error, asio::ip::tcp::resolver::results_type Results) {
|
||||
error = Error;
|
||||
endpoints = std::move(Results);
|
||||
done = true;
|
||||
});
|
||||
if (false == wait(done, EU07_RADIOSTREAM_CONNECTTIMEOUT))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (error)
|
||||
{
|
||||
m_error = "can't find the server (" + error.message() + ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
done = false;
|
||||
asio::async_connect(m_socket, endpoints, [&](asio::error_code const &Error, asio::ip::tcp::endpoint const &) {
|
||||
error = Error;
|
||||
done = true;
|
||||
});
|
||||
if (false == wait(done, EU07_RADIOSTREAM_CONNECTTIMEOUT))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (error)
|
||||
{
|
||||
m_error = "can't connect with the server (" + error.message() + ")";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool radio_session::send(std::string const &Data)
|
||||
{
|
||||
asio::error_code error;
|
||||
bool done{false};
|
||||
|
||||
asio::async_write(m_socket, asio::buffer(Data), [&](asio::error_code const &Error, std::size_t) {
|
||||
error = Error;
|
||||
done = true;
|
||||
});
|
||||
if (false == wait(done, EU07_RADIOSTREAM_CONNECTTIMEOUT))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (error)
|
||||
{
|
||||
m_error = error.message();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t radio_session::receive(std::uint8_t *Data, std::size_t const Size)
|
||||
{
|
||||
asio::error_code error;
|
||||
std::size_t received{0};
|
||||
bool done{false};
|
||||
|
||||
m_socket.async_read_some(asio::buffer(Data, Size), [&](asio::error_code const &Error, std::size_t const Count) {
|
||||
error = Error;
|
||||
received = Count;
|
||||
done = true;
|
||||
});
|
||||
if (false == wait(done, EU07_RADIOSTREAM_STALLTIMEOUT))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (error && received == 0)
|
||||
{
|
||||
m_error = (error == asio::error::eof ? "connection closed by the server" : error.message());
|
||||
}
|
||||
return received;
|
||||
}
|
||||
|
||||
bool radio_session::receive_response(http_response &Response)
|
||||
{
|
||||
std::string data;
|
||||
std::array<std::uint8_t, 2048> chunk;
|
||||
while (true)
|
||||
{
|
||||
// some servers end lines with a bare line feed, so we look for both variants
|
||||
auto headerend{data.find("\r\n\r\n")};
|
||||
auto headerendsize{std::size_t{4}};
|
||||
if (headerend == std::string::npos)
|
||||
{
|
||||
headerend = data.find("\n\n");
|
||||
headerendsize = 2;
|
||||
}
|
||||
if (headerend != std::string::npos)
|
||||
{
|
||||
Response = http_response{};
|
||||
if (false == parse_response(data.substr(0, headerend), Response))
|
||||
{
|
||||
m_error = "unrecognized reply from the server";
|
||||
return false;
|
||||
}
|
||||
Response.body = data.substr(headerend + headerendsize);
|
||||
return true;
|
||||
}
|
||||
if (data.size() > EU07_RADIOSTREAM_MAXHEADERSIZE)
|
||||
{
|
||||
m_error = "unrecognized reply from the server";
|
||||
return false;
|
||||
}
|
||||
auto const received{receive(chunk.data(), chunk.size())};
|
||||
if (received == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
data.append(reinterpret_cast<char const *>(chunk.data()), received);
|
||||
}
|
||||
}
|
||||
|
||||
radio_session::result radio_session::fail(std::string const &Reason, result const Result)
|
||||
{
|
||||
if (m_stoptoken.stop_requested())
|
||||
{
|
||||
return result::stopped;
|
||||
}
|
||||
if (false == Reason.empty())
|
||||
{
|
||||
m_owner.post("radio stream: " + Reason, true);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
radio_session::result radio_session::play(std::string Url)
|
||||
{
|
||||
for (auto hop{0}; hop <= EU07_RADIOSTREAM_MAXREDIRECTS; ++hop)
|
||||
{
|
||||
stream_address address;
|
||||
auto const addresserror{parse_address(Url, address)};
|
||||
if (false == addresserror.empty())
|
||||
{
|
||||
return fail(addresserror + " \"" + Url + "\"", result::unsupported);
|
||||
}
|
||||
|
||||
m_owner.status(radio_stream::state::connecting, m_generation);
|
||||
m_owner.post("radio stream: connecting with \"" + Url + "\"");
|
||||
{
|
||||
// each hop needs a fresh connection
|
||||
asio::error_code ignored;
|
||||
m_socket.close(ignored);
|
||||
}
|
||||
if (false == connect(address))
|
||||
{
|
||||
return fail(m_error);
|
||||
}
|
||||
// NOTE: we don't ask for the in-band metadata, so the content is pure audio
|
||||
auto const request{"GET " + address.target + " HTTP/1.1\r\nHost: " + address.authority() + "\r\nUser-Agent: MaSzyna\r\nAccept: */*\r\nConnection: close\r\n\r\n"};
|
||||
http_response response;
|
||||
if (false == send(request) || false == receive_response(response))
|
||||
{
|
||||
return fail(m_error);
|
||||
}
|
||||
|
||||
if (response.status >= 300 && response.status < 400)
|
||||
{
|
||||
auto const location{response.header("location")};
|
||||
if (location.empty())
|
||||
{
|
||||
return fail("server sent a redirect without the target");
|
||||
}
|
||||
Url = resolve_redirect(location, address);
|
||||
continue;
|
||||
}
|
||||
if (response.status != 200)
|
||||
{
|
||||
return fail("server replied with status " + std::to_string(response.status));
|
||||
}
|
||||
|
||||
auto const contenttype{lowercase(response.header("content-type"))};
|
||||
auto const path{lowercase(address.target.substr(0, address.target.find('?')))};
|
||||
auto const isplaylist{contenttype.find("mpegurl") != std::string::npos || contenttype.find("scpls") != std::string::npos || path.ends_with(".m3u") || path.ends_with(".pls")};
|
||||
if (isplaylist)
|
||||
{
|
||||
auto failure{result::failed};
|
||||
if (false == receive_playlist(response, Url, failure))
|
||||
{
|
||||
return failure;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (auto const *format : {"aac", "ogg", "opus", "flac"})
|
||||
{
|
||||
if (contenttype.find(format) != std::string::npos)
|
||||
{
|
||||
return fail("unsupported audio format \"" + contenttype + "\", only mp3 streams can be played", result::unsupported);
|
||||
}
|
||||
}
|
||||
|
||||
auto const name{response.header("icy-name")};
|
||||
auto const bitrate{response.header("icy-br")};
|
||||
m_owner.post("radio stream: connected" + (name.empty() ? "" : ", station \"" + name + "\"") + (bitrate.empty() ? "" : ", " + bitrate + " kbps"));
|
||||
return receive_audio(response);
|
||||
}
|
||||
return fail("too many redirects");
|
||||
}
|
||||
|
||||
bool radio_session::receive_playlist(http_response const &Response, std::string &Url, result &Failure)
|
||||
{
|
||||
content_decoder content{lowercase(Response.header("transfer-encoding")).find("chunked") != std::string::npos};
|
||||
std::vector<std::uint8_t> playlist;
|
||||
if (false == content.feed(reinterpret_cast<std::uint8_t const *>(Response.body.data()), Response.body.size(), playlist))
|
||||
{
|
||||
Failure = fail("malformed reply from the server");
|
||||
return false;
|
||||
}
|
||||
std::array<std::uint8_t, 2048> chunk;
|
||||
while (false == content.done() && playlist.size() < EU07_RADIOSTREAM_MAXPLAYLISTSIZE)
|
||||
{
|
||||
// the playlist is complete when the server closes the connection
|
||||
auto const received{receive(chunk.data(), chunk.size())};
|
||||
if (received == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (false == content.feed(chunk.data(), received, playlist))
|
||||
{
|
||||
Failure = fail("malformed reply from the server");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (m_stoptoken.stop_requested())
|
||||
{
|
||||
Failure = result::stopped;
|
||||
return false;
|
||||
}
|
||||
auto const address{parse_playlist(std::string{std::begin(playlist), std::end(playlist)})};
|
||||
if (address.empty())
|
||||
{
|
||||
Failure = fail("playlist \"" + Url + "\" doesn't contain any stream address", result::unsupported);
|
||||
return false;
|
||||
}
|
||||
Url = address;
|
||||
return true;
|
||||
}
|
||||
|
||||
radio_session::result radio_session::receive_audio(http_response const &Response)
|
||||
{
|
||||
content_decoder content{lowercase(Response.header("transfer-encoding")).find("chunked") != std::string::npos};
|
||||
std::vector<std::uint8_t> input;
|
||||
input.reserve(4 * EU07_RADIOSTREAM_DECODETHRESHOLD);
|
||||
if (false == content.feed(reinterpret_cast<std::uint8_t const *>(Response.body.data()), Response.body.size(), input))
|
||||
{
|
||||
return fail("malformed reply from the server");
|
||||
}
|
||||
|
||||
auto decoder{std::make_unique<drmp3dec>()};
|
||||
::drmp3dec_init(decoder.get());
|
||||
std::vector<std::int16_t> frame(DRMP3_MAX_SAMPLES_PER_FRAME);
|
||||
std::array<std::uint8_t, 4096> chunk;
|
||||
std::size_t junksize{0};
|
||||
auto threshold{EU07_RADIOSTREAM_DECODETHRESHOLD};
|
||||
|
||||
m_owner.status(radio_stream::state::receiving, m_generation);
|
||||
|
||||
while (true)
|
||||
{
|
||||
// decode what we have...
|
||||
std::size_t position{0};
|
||||
while (input.size() - position >= threshold)
|
||||
{
|
||||
drmp3dec_frame_info info{};
|
||||
auto const samplecount{::drmp3dec_decode_frame(decoder.get(), input.data() + position, static_cast<int>(input.size() - position), frame.data(), &info)};
|
||||
if (samplecount > 0)
|
||||
{
|
||||
if (info.channels > 1)
|
||||
{
|
||||
// mix the channels down, the sound is going to be emitted from a single point in the cab
|
||||
for (auto index{0}; index < samplecount; ++index)
|
||||
{
|
||||
auto sum{0};
|
||||
for (auto channel{0}; channel < info.channels; ++channel)
|
||||
{
|
||||
sum += frame[index * info.channels + channel];
|
||||
}
|
||||
frame[index] = static_cast<std::int16_t>(sum / info.channels);
|
||||
}
|
||||
}
|
||||
m_owner.store(frame.data(), static_cast<std::size_t>(samplecount), static_cast<unsigned int>(info.sample_rate), m_generation);
|
||||
m_delivered = true;
|
||||
junksize = 0;
|
||||
threshold = std::clamp(static_cast<std::size_t>(info.frame_bytes) * 3, EU07_RADIOSTREAM_MINDECODETHRESHOLD, EU07_RADIOSTREAM_DECODETHRESHOLD);
|
||||
}
|
||||
else if (info.frame_bytes > 0)
|
||||
{
|
||||
// data which isn't part of any frame. the decoder is looking for the stream again, which takes more data
|
||||
junksize += static_cast<std::size_t>(info.frame_bytes);
|
||||
threshold = EU07_RADIOSTREAM_DECODETHRESHOLD;
|
||||
}
|
||||
else
|
||||
{
|
||||
// the decoder wants more data
|
||||
break;
|
||||
}
|
||||
position += static_cast<std::size_t>(info.frame_bytes);
|
||||
}
|
||||
input.erase(std::begin(input), std::begin(input) + static_cast<std::ptrdiff_t>(position));
|
||||
if (junksize > EU07_RADIOSTREAM_MAXJUNKSIZE || input.size() > EU07_RADIOSTREAM_MAXJUNKSIZE)
|
||||
{
|
||||
return fail(m_delivered ? "lost track of audio data in the stream" : "no mp3 audio found in the stream, only mp3 streams can be played", m_delivered ? result::failed : result::unsupported);
|
||||
}
|
||||
// ...then get some more
|
||||
if (content.done())
|
||||
{
|
||||
return fail("stream ended");
|
||||
}
|
||||
auto const received{receive(chunk.data(), chunk.size())};
|
||||
if (received == 0)
|
||||
{
|
||||
return fail(m_error);
|
||||
}
|
||||
if (false == content.feed(chunk.data(), received, input))
|
||||
{
|
||||
return fail("malformed data from the server");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
radio_stream::~radio_stream()
|
||||
{
|
||||
close();
|
||||
// NOTE: destructors of retired workers wait for their threads to finish
|
||||
}
|
||||
|
||||
// starts reception of specified stream, dropping current one if there's any
|
||||
void radio_stream::open(std::string const &Url)
|
||||
{
|
||||
close();
|
||||
if (Url.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_url = Url;
|
||||
std::uint32_t generation;
|
||||
{
|
||||
std::scoped_lock lock{m_mutex};
|
||||
generation = m_generation;
|
||||
m_state = state::connecting;
|
||||
}
|
||||
m_worker.finished = std::make_shared<std::atomic<bool>>(false);
|
||||
m_worker.thread = std::jthread{[this](std::stop_token Stoptoken, std::string Address, std::uint32_t const Workergeneration, std::shared_ptr<std::atomic<bool>> Finished) { run(std::move(Stoptoken), std::move(Address), Workergeneration, std::move(Finished)); },
|
||||
Url, generation, m_worker.finished};
|
||||
}
|
||||
|
||||
// terminates reception of the current stream
|
||||
void radio_stream::close()
|
||||
{
|
||||
{
|
||||
std::scoped_lock lock{m_mutex};
|
||||
// from now on the current worker can't deliver anything
|
||||
++m_generation;
|
||||
m_samplesstart = 0;
|
||||
m_samplescount = 0;
|
||||
m_state = state::idle;
|
||||
m_rate = 0;
|
||||
}
|
||||
m_url.clear();
|
||||
if (m_worker.thread.joinable())
|
||||
{
|
||||
// the worker may need a moment to shut down its connection. we don't wait for it here, as we're likely called from the main thread
|
||||
m_worker.thread.request_stop();
|
||||
m_retired.emplace_back(std::move(m_worker));
|
||||
m_worker = worker{};
|
||||
}
|
||||
reap();
|
||||
}
|
||||
|
||||
// disposes of retired workers which are done with their cleanup
|
||||
void radio_stream::reap()
|
||||
{
|
||||
std::erase_if(m_retired, [](worker const &Worker) { return Worker.finished->load(); });
|
||||
}
|
||||
|
||||
// returns number of decoded samples waiting to be collected
|
||||
std::size_t radio_stream::available() const
|
||||
{
|
||||
std::scoped_lock lock{m_mutex};
|
||||
return m_samplescount;
|
||||
}
|
||||
|
||||
// moves up to specified number of decoded samples to provided buffer. returns: number of samples moved
|
||||
std::size_t radio_stream::read(std::int16_t *Output, std::size_t const Count)
|
||||
{
|
||||
std::scoped_lock lock{m_mutex};
|
||||
auto const count{std::min(Count, m_samplescount)};
|
||||
for (std::size_t index{0}; index < count; ++index)
|
||||
{
|
||||
Output[index] = m_samples[(m_samplesstart + index) % m_samples.size()];
|
||||
}
|
||||
if (count > 0)
|
||||
{
|
||||
m_samplesstart = (m_samplesstart + count) % m_samples.size();
|
||||
m_samplescount -= count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// retrieves next diagnostic message posted by the worker. returns: false if there's no message waiting
|
||||
bool radio_stream::fetch_message(message &Message)
|
||||
{
|
||||
std::scoped_lock lock{m_messagesmutex};
|
||||
if (m_messages.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Message = std::move(m_messages.front());
|
||||
m_messages.pop_front();
|
||||
return true;
|
||||
}
|
||||
|
||||
void radio_stream::run(std::stop_token Stoptoken, std::string Url, std::uint32_t const Generation, std::shared_ptr<std::atomic<bool>> Finished)
|
||||
{
|
||||
auto failures{0};
|
||||
while (false == Stoptoken.stop_requested())
|
||||
{
|
||||
auto result{radio_session::result::failed};
|
||||
auto delivered{false};
|
||||
{
|
||||
radio_session session{*this, Stoptoken, Generation};
|
||||
result = session.play(Url);
|
||||
delivered = session.delivered();
|
||||
}
|
||||
if (result == radio_session::result::stopped || Stoptoken.stop_requested())
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (result == radio_session::result::unsupported)
|
||||
{
|
||||
status(state::failed, Generation);
|
||||
break;
|
||||
}
|
||||
// try again after a pause, which gets longer if the server keeps refusing to cooperate
|
||||
failures = (delivered ? 0 : failures + 1);
|
||||
auto const pause{std::chrono::seconds{std::min(15, 1 << std::min(failures, 4))}};
|
||||
status(state::waiting, Generation);
|
||||
post("radio stream: next connection attempt in " + std::to_string(pause.count()) + " s");
|
||||
auto const deadline{std::chrono::steady_clock::now() + pause};
|
||||
while (false == Stoptoken.stop_requested() && std::chrono::steady_clock::now() < deadline)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{50});
|
||||
}
|
||||
}
|
||||
Finished->store(true);
|
||||
}
|
||||
|
||||
// stores decoded samples, discarding oldest data if there's no room left. data from outdated workers is ignored
|
||||
void radio_stream::store(std::int16_t const *Samples, std::size_t const Count, unsigned int const Rate, std::uint32_t const Generation)
|
||||
{
|
||||
std::scoped_lock lock{m_mutex};
|
||||
if (Generation != m_generation || Rate == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Rate != m_rate)
|
||||
{
|
||||
// (re)initialize the buffer for the new format
|
||||
m_samples.assign(static_cast<std::size_t>(Rate) * EU07_RADIOSTREAM_BUFFERLENGTH, 0);
|
||||
m_samplesstart = 0;
|
||||
m_samplescount = 0;
|
||||
m_rate = Rate;
|
||||
}
|
||||
auto const capacity{m_samples.size()};
|
||||
for (std::size_t index{0}; index < Count; ++index)
|
||||
{
|
||||
m_samples[(m_samplesstart + m_samplescount) % capacity] = Samples[index];
|
||||
if (m_samplescount < capacity)
|
||||
{
|
||||
++m_samplescount;
|
||||
}
|
||||
else
|
||||
{
|
||||
// the buffer is full, the oldest sample was just overwritten
|
||||
m_samplesstart = (m_samplesstart + 1) % capacity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void radio_stream::status(state const State, std::uint32_t const Generation)
|
||||
{
|
||||
std::scoped_lock lock{m_mutex};
|
||||
if (Generation == m_generation)
|
||||
{
|
||||
m_state = State;
|
||||
}
|
||||
}
|
||||
|
||||
void radio_stream::post(std::string Text, bool const Error)
|
||||
{
|
||||
std::scoped_lock lock{m_messagesmutex};
|
||||
m_messages.emplace_back(std::move(Text), Error);
|
||||
// if nobody collects the messages there's no point in hoarding them
|
||||
while (m_messages.size() > 64)
|
||||
{
|
||||
m_messages.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace audio
|
||||
108
audio/radiostream.h
Normal file
108
audio/radiostream.h
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace audio
|
||||
{
|
||||
|
||||
// receiver of an internet radio stream (plain http, mp3 payload).
|
||||
// owns a worker thread which maintains the connection, decodes received data
|
||||
// and stores the result as 16-bit mono samples for the owner to collect.
|
||||
// the worker doesn't touch anything outside of the object, and the owner is never made to wait for the network
|
||||
class radio_stream
|
||||
{
|
||||
|
||||
friend class radio_session;
|
||||
|
||||
public:
|
||||
// types
|
||||
enum class state
|
||||
{
|
||||
idle, // no stream requested
|
||||
connecting, // establishing connection with the server
|
||||
receiving, // audio data is coming in
|
||||
waiting, // connection was lost or refused, waiting to make another attempt
|
||||
failed // stream can't be played, no further attempts will be made
|
||||
};
|
||||
using message = std::pair<std::string, bool>; // text, is error
|
||||
// constructors
|
||||
radio_stream() = default;
|
||||
radio_stream(radio_stream const &) = delete;
|
||||
radio_stream &operator=(radio_stream const &) = delete;
|
||||
// destructor
|
||||
~radio_stream();
|
||||
// methods
|
||||
// starts reception of specified stream, dropping current one if there's any
|
||||
void open(std::string const &Url);
|
||||
// terminates reception of the current stream
|
||||
void close();
|
||||
// returns address of currently requested stream, or empty string
|
||||
std::string const &url() const
|
||||
{
|
||||
return m_url;
|
||||
}
|
||||
state status() const
|
||||
{
|
||||
return m_state.load();
|
||||
}
|
||||
// returns number of decoded samples waiting to be collected
|
||||
std::size_t available() const;
|
||||
// returns sample rate of decoded audio, or 0 if nothing was decoded yet
|
||||
unsigned int rate() const
|
||||
{
|
||||
return m_rate.load();
|
||||
}
|
||||
// moves up to specified number of decoded samples to provided buffer. returns: number of samples moved
|
||||
std::size_t read(std::int16_t *Output, std::size_t const Count);
|
||||
// retrieves next diagnostic message posted by the worker. returns: false if there's no message waiting
|
||||
bool fetch_message(message &Message);
|
||||
|
||||
private:
|
||||
// types
|
||||
struct worker
|
||||
{
|
||||
std::jthread thread;
|
||||
std::shared_ptr<std::atomic<bool>> finished;
|
||||
};
|
||||
// methods
|
||||
void run(std::stop_token Stoptoken, std::string Url, std::uint32_t const Generation, std::shared_ptr<std::atomic<bool>> Finished);
|
||||
// stores decoded samples, discarding oldest data if there's no room left. data from outdated workers is ignored
|
||||
void store(std::int16_t const *Samples, std::size_t const Count, unsigned int const Rate, std::uint32_t const Generation);
|
||||
void status(state const State, std::uint32_t const Generation);
|
||||
void post(std::string Text, bool const Error = false);
|
||||
// disposes of retired workers which are done with their cleanup
|
||||
void reap();
|
||||
// members
|
||||
std::string m_url;
|
||||
worker m_worker;
|
||||
std::vector<worker> m_retired; // workers which were told to stop but may still be busy closing their connection
|
||||
std::atomic<state> m_state{state::idle};
|
||||
std::atomic<unsigned int> m_rate{0};
|
||||
mutable std::mutex m_mutex; // guards the sample buffer and the generation counter
|
||||
std::uint32_t m_generation{0}; // identifies the worker allowed to deliver data
|
||||
std::vector<std::int16_t> m_samples; // ring buffer
|
||||
std::size_t m_samplesstart{0};
|
||||
std::size_t m_samplescount{0};
|
||||
std::mutex m_messagesmutex;
|
||||
std::deque<message> m_messages;
|
||||
};
|
||||
|
||||
} // namespace audio
|
||||
@@ -914,6 +914,18 @@ sound_source::location() const {
|
||||
+ m_owner->VectorFront() * (double)m_offset.z };
|
||||
}
|
||||
|
||||
// returns location-based gain factor of the emitter for the current position of the listener
|
||||
float
|
||||
sound_source::soundproofing_factor() {
|
||||
|
||||
// NOTE: the calculation needs the listener to be assigned to a vehicle or in one of the outside views.
|
||||
// this may not be the case yet when we're called from vehicle update, as the global camera data is refreshed later in the frame
|
||||
if( FreeFlyModeFlag || Global.CabWindowOpen || Global.pCamera.m_owner != nullptr ) {
|
||||
update_soundproofing();
|
||||
}
|
||||
return m_properties.soundproofing;
|
||||
}
|
||||
|
||||
void
|
||||
sound_source::range( float const Range ) {
|
||||
|
||||
|
||||
@@ -139,6 +139,9 @@ public:
|
||||
// returns location of the sound source in simulation region space
|
||||
glm::dvec3
|
||||
location() const;
|
||||
// returns location-based gain factor of the emitter for the current position of the listener
|
||||
float
|
||||
soundproofing_factor();
|
||||
// returns defined range of the sound
|
||||
void
|
||||
range( float const Range );
|
||||
|
||||
@@ -405,6 +405,13 @@ commanddescription_sequence Commands_descriptions = {
|
||||
{"wiperswitchincrease", command_target::vehicle, command_mode::oneoff},
|
||||
{"wiperswitchdecrease", command_target::vehicle, command_mode::oneoff},
|
||||
{"lightsset", command_target::vehicle, command_mode::oneoff},
|
||||
{"fmradiotoggle", command_target::vehicle, command_mode::oneoff},
|
||||
{"fmradiovolumeincrease", command_target::vehicle, command_mode::oneoff},
|
||||
{"fmradiovolumedecrease", command_target::vehicle, command_mode::oneoff},
|
||||
{"fmradiovolumeset", command_target::vehicle, command_mode::oneoff},
|
||||
{"fmradiotuneincrease", command_target::vehicle, command_mode::oneoff},
|
||||
{"fmradiotunedecrease", command_target::vehicle, command_mode::oneoff},
|
||||
{"fmradiotuneset", command_target::vehicle, command_mode::oneoff},
|
||||
};
|
||||
|
||||
// Maps of command and coresponding strings
|
||||
@@ -789,6 +796,13 @@ std::unordered_map<std::string, user_command> const commandMap = {
|
||||
{"wiperswitchincrease", user_command::wiperswitchincrease},
|
||||
{"wiperswitchdecrease", user_command::wiperswitchdecrease},
|
||||
{"lightsset", user_command::lightsset},
|
||||
{"fmradiotoggle", user_command::fmradiotoggle},
|
||||
{"fmradiovolumeincrease", user_command::fmradiovolumeincrease},
|
||||
{"fmradiovolumedecrease", user_command::fmradiovolumedecrease},
|
||||
{"fmradiovolumeset", user_command::fmradiovolumeset},
|
||||
{"fmradiotuneincrease", user_command::fmradiotuneincrease},
|
||||
{"fmradiotunedecrease", user_command::fmradiotunedecrease},
|
||||
{"fmradiotuneset", user_command::fmradiotuneset},
|
||||
{"none", user_command::none}};
|
||||
|
||||
} // namespace simulation
|
||||
|
||||
@@ -403,6 +403,13 @@ enum class user_command
|
||||
wiperswitchincrease,
|
||||
wiperswitchdecrease,
|
||||
lightsset,
|
||||
fmradiotoggle,
|
||||
fmradiovolumeincrease,
|
||||
fmradiovolumedecrease,
|
||||
fmradiovolumeset,
|
||||
fmradiotuneincrease,
|
||||
fmradiotunedecrease,
|
||||
fmradiotuneset,
|
||||
none = -1
|
||||
};
|
||||
|
||||
|
||||
@@ -816,6 +816,15 @@ drivermouse_input::default_bindings() {
|
||||
{ "radiovolumenext_sw:",{
|
||||
radiovolumeincrease,
|
||||
none } },
|
||||
{ "fmradio_sw:",{
|
||||
fmradiotoggle,
|
||||
none } },
|
||||
{ "fmradiovolume_sw:",{
|
||||
fmradiovolumeincrease,
|
||||
fmradiovolumedecrease } },
|
||||
{ "fmradiotune_sw:",{
|
||||
fmradiotuneincrease,
|
||||
fmradiotunedecrease } },
|
||||
{ "pantfront_sw:", {
|
||||
pantographtogglefront,
|
||||
none } },
|
||||
|
||||
5430
ref/dr_libs/dr_mp3.h
Normal file
5430
ref/dr_libs/dr_mp3.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -193,6 +193,24 @@ bool global_settings::ConfigParseAudio(cParser& Parser, const std::string& token
|
||||
return true;
|
||||
}
|
||||
|
||||
if (token == "fmradio.enabled")
|
||||
{
|
||||
ParseOne(Parser, fmradio_enabled);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (token == "fmradio.cleansound")
|
||||
{
|
||||
ParseOne(Parser, fmradio_cleansound);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (token == "fmradio.stations")
|
||||
{
|
||||
fmradio_stations = Parser.getToken<std::string>(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (token == "sound.volume.vehicle")
|
||||
{
|
||||
ParseOneClamped(Parser, VehicleVolume, 0.f, 1.f);
|
||||
@@ -1555,6 +1573,9 @@ global_settings::export_as_text( std::ostream &Output ) const {
|
||||
export_as_text( Output, "sound.volume.positional", EnvironmentPositionalVolume );
|
||||
export_as_text( Output, "sound.volume.ambient", EnvironmentAmbientVolume );
|
||||
export_as_text( Output, "sound.asyncload", AudioAsyncLoad );
|
||||
export_as_text( Output, "fmradio.enabled", fmradio_enabled );
|
||||
export_as_text( Output, "fmradio.cleansound", fmradio_cleansound );
|
||||
export_as_text( Output, "fmradio.stations", fmradio_stations );
|
||||
export_as_text( Output, "physicslog", WriteLogFlag );
|
||||
export_as_text( Output, "fullphysics", FullPhysics );
|
||||
export_as_text( Output, "debuglog", iWriteLogEnabled );
|
||||
|
||||
@@ -201,6 +201,10 @@ struct global_settings {
|
||||
int audio_max_sources = 30;
|
||||
std::string AudioRenderer;
|
||||
bool AudioAsyncLoad{ false }; // sound files are decoded by worker threads
|
||||
// broadcast radio receiver
|
||||
bool fmradio_enabled{ true }; // allows cab radio receivers to play internet radio streams
|
||||
bool fmradio_cleansound{ false }; // disables simulation of the receiver speaker
|
||||
std::string fmradio_stations{ "fmradio.txt" }; // list of stations available to the receivers
|
||||
// input
|
||||
float fMouseXScale{ 1.5f };
|
||||
float fMouseYScale{ 0.2f };
|
||||
|
||||
@@ -483,6 +483,13 @@ TTrain::commandhandler_map const TTrain::m_commandhandlers = {
|
||||
{user_command::radiovolumeincrease, &TTrain::OnCommand_radiovolumeincrease},
|
||||
{user_command::radiovolumedecrease, &TTrain::OnCommand_radiovolumedecrease},
|
||||
{user_command::radiovolumeset, &TTrain::OnCommand_radiovolumeset},
|
||||
{user_command::fmradiotoggle, &TTrain::OnCommand_fmradiotoggle},
|
||||
{user_command::fmradiovolumeincrease, &TTrain::OnCommand_fmradiovolumeincrease},
|
||||
{user_command::fmradiovolumedecrease, &TTrain::OnCommand_fmradiovolumedecrease},
|
||||
{user_command::fmradiovolumeset, &TTrain::OnCommand_fmradiovolumeset},
|
||||
{user_command::fmradiotuneincrease, &TTrain::OnCommand_fmradiotuneincrease},
|
||||
{user_command::fmradiotunedecrease, &TTrain::OnCommand_fmradiotunedecrease},
|
||||
{user_command::fmradiotuneset, &TTrain::OnCommand_fmradiotuneset},
|
||||
{user_command::cabchangeforward, &TTrain::OnCommand_cabchangeforward},
|
||||
{user_command::cabchangebackward, &TTrain::OnCommand_cabchangebackward},
|
||||
{user_command::generictoggle0, &TTrain::OnCommand_generictoggle},
|
||||
@@ -8186,6 +8193,58 @@ void TTrain::OnCommand_radiovolumeset(TTrain *Train, command_data const &Command
|
||||
}
|
||||
}
|
||||
|
||||
void TTrain::OnCommand_fmradiotoggle(TTrain *Train, command_data const &Command)
|
||||
{
|
||||
if (Command.action == GLFW_PRESS && Train->m_fmradiopresent)
|
||||
{
|
||||
Train->m_fmradiopower = !Train->m_fmradiopower;
|
||||
}
|
||||
}
|
||||
|
||||
void TTrain::OnCommand_fmradiovolumeincrease(TTrain *Train, command_data const &Command)
|
||||
{
|
||||
// the knob keeps turning for as long as the control is held, the actual movement takes place in the update
|
||||
Train->m_fmradiovolumechange = (Command.action != GLFW_RELEASE && Train->m_fmradiopresent ? 1 : 0);
|
||||
}
|
||||
|
||||
void TTrain::OnCommand_fmradiovolumedecrease(TTrain *Train, command_data const &Command)
|
||||
{
|
||||
Train->m_fmradiovolumechange = (Command.action != GLFW_RELEASE && Train->m_fmradiopresent ? -1 : 0);
|
||||
}
|
||||
|
||||
void TTrain::OnCommand_fmradiovolumeset(TTrain *Train, command_data const &Command)
|
||||
{
|
||||
if (Command.action != GLFW_RELEASE && Train->m_fmradiopresent)
|
||||
{
|
||||
Train->m_fmradiovolume = std::clamp(static_cast<float>(Command.param1), 0.f, 1.f);
|
||||
if (false == Train->m_controlmapper.contains("fmradio_sw:"))
|
||||
{
|
||||
// receivers without dedicated switch are switched with the volume knob
|
||||
Train->m_fmradiopower = Train->m_fmradiovolume > 0.f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TTrain::OnCommand_fmradiotuneincrease(TTrain *Train, command_data const &Command)
|
||||
{
|
||||
Train->m_fmradiotunechange = (Command.action != GLFW_RELEASE && Train->m_fmradiopresent ? 1 : 0);
|
||||
}
|
||||
|
||||
void TTrain::OnCommand_fmradiotunedecrease(TTrain *Train, command_data const &Command)
|
||||
{
|
||||
Train->m_fmradiotunechange = (Command.action != GLFW_RELEASE && Train->m_fmradiopresent ? -1 : 0);
|
||||
}
|
||||
|
||||
// sets the receiver to frequency specified in MHz
|
||||
void TTrain::OnCommand_fmradiotuneset(TTrain *Train, command_data const &Command)
|
||||
{
|
||||
if (Command.action != GLFW_RELEASE && Train->m_fmradiopresent)
|
||||
{
|
||||
Train->m_fmradiodial = std::clamp(100.f * (static_cast<float>(Command.param1) - audio::EU07_FMRADIO_BANDSTART) / (audio::EU07_FMRADIO_BANDEND - audio::EU07_FMRADIO_BANDSTART), 0.f, 100.f);
|
||||
Train->m_fmradiotuned = true;
|
||||
}
|
||||
}
|
||||
|
||||
void TTrain::OnCommand_cabchangeforward(TTrain *Train, command_data const &Command)
|
||||
{
|
||||
|
||||
@@ -10040,6 +10099,7 @@ void TTrain::update_sounds(double const Deltatime)
|
||||
}
|
||||
|
||||
update_sounds_radio();
|
||||
update_sounds_fmradio(Deltatime);
|
||||
}
|
||||
|
||||
void TTrain::update_sounds_resonancenoise(sound_source &Sound) const
|
||||
@@ -10132,6 +10192,52 @@ void TTrain::update_sounds_radio()
|
||||
}
|
||||
}
|
||||
|
||||
// operates the broadcast radio receiver
|
||||
void TTrain::update_sounds_fmradio(double const Deltatime)
|
||||
{
|
||||
// NOTE: there's a single receiver in the simulation, it plays in the cab occupied by the user
|
||||
if (simulation::Train != this || false == m_fmradiopresent || false == Global.fmradio_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto &fmradio{audio::renderer.fmradio()};
|
||||
auto const bandwidth{audio::EU07_FMRADIO_BANDEND - audio::EU07_FMRADIO_BANDSTART};
|
||||
if (false == m_fmradiotuned)
|
||||
{
|
||||
// initially the receiver is tuned to the first station on the list
|
||||
m_fmradiodial = 100.f * (fmradio.default_frequency() - audio::EU07_FMRADIO_BANDSTART) / bandwidth;
|
||||
m_fmradiotuned = true;
|
||||
}
|
||||
// knobs
|
||||
if (m_fmradiovolumechange != 0)
|
||||
{
|
||||
m_fmradiovolume = std::clamp(m_fmradiovolume + m_fmradiovolumechange * 0.5f * static_cast<float>(Deltatime), 0.f, 1.f);
|
||||
if (false == m_controlmapper.contains("fmradio_sw:"))
|
||||
{
|
||||
// receivers without dedicated switch are switched with the volume knob
|
||||
m_fmradiopower = m_fmradiovolume > 0.f;
|
||||
}
|
||||
}
|
||||
if (m_fmradiotunechange != 0)
|
||||
{
|
||||
// the knob moves slowly at first to allow fine tuning, then speeds up
|
||||
m_fmradiotunetime += static_cast<float>(Deltatime);
|
||||
auto const tunerate{std::lerp(0.15f, 2.5f, std::clamp(m_fmradiotunetime * 0.5f, 0.f, 1.f))}; // MHz per second
|
||||
m_fmradiodial = std::clamp(m_fmradiodial + 100.f * m_fmradiotunechange * tunerate * static_cast<float>(Deltatime) / bandwidth, 0.f, 100.f);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_fmradiotunetime = 0.f;
|
||||
}
|
||||
|
||||
fmradio.type(m_fmradiotype);
|
||||
fmradio.tune(audio::EU07_FMRADIO_BANDSTART + m_fmradiodial * 0.01f * bandwidth);
|
||||
fmradio.volume(m_fmradiovolume);
|
||||
fmradio.placement(m_fmradiosound.location(), m_fmradiosound.soundproofing_factor(), m_fmradiosound.range());
|
||||
// NOTE: the receiver runs on batteries of its own, so it doesn't care about the state of the vehicle
|
||||
fmradio.power(m_fmradiopower);
|
||||
}
|
||||
|
||||
void TTrain::update_screens(double dt)
|
||||
{
|
||||
for (auto &screen : m_screens)
|
||||
@@ -10234,6 +10340,7 @@ bool TTrain::LoadMMediaFile(std::string const &asFileName)
|
||||
}
|
||||
// NOTE: since radiosound is an incomplete template not using std::optional it gets a special treatment
|
||||
m_radiosound.owner(DynamicObject);
|
||||
m_fmradiosound.owner(DynamicObject);
|
||||
CabSoundLocations.clear();
|
||||
|
||||
cParser parser(asFileName, cParser::buffer_FILE, DynamicObject->asBaseDir, true, std::vector<std::string>(), true);
|
||||
@@ -10342,6 +10449,8 @@ bool TTrain::InitializeCab(int NewCabNo, std::string const &asFileName)
|
||||
}
|
||||
}
|
||||
m_radiosound.offset(nullvector);
|
||||
m_fmradiosound.offset(nullvector);
|
||||
m_fmradiopresent = false;
|
||||
for (auto const &[soundsource, soundoffset] : CabSoundLocations)
|
||||
{
|
||||
if (soundsource.get() && soundsource.get()->offset() == nullvector)
|
||||
@@ -10567,6 +10676,39 @@ bool TTrain::InitializeCab(int NewCabNo, std::string const &asFileName)
|
||||
parser.getTokens();
|
||||
parser >> ScreenUpdateRate;
|
||||
}
|
||||
else if (token == "fmradio:")
|
||||
{
|
||||
// broadcast radio receiver, the parameter selects character of its sound
|
||||
std::string fmradiotype;
|
||||
parser.getTokens();
|
||||
parser >> fmradiotype;
|
||||
m_fmradiotype = (fmradiotype == "wooden" ? audio::fmradio_type::wooden : audio::fmradio_type::cheap);
|
||||
m_fmradiopresent = true;
|
||||
}
|
||||
else if (token == "fmradiospeaker:")
|
||||
{
|
||||
// submodel marking location of the receiver speaker, the sound is emitted from there
|
||||
std::string speakername;
|
||||
parser.getTokens();
|
||||
parser >> speakername;
|
||||
TSubModel *speaker{nullptr};
|
||||
for (auto const *source : {DynamicObject->mdKabina, DynamicObject->mdLowPolyInt})
|
||||
{
|
||||
if (source != nullptr && (speaker = source->GetFromName(speakername)) != nullptr)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (speaker != nullptr)
|
||||
{
|
||||
m_fmradiosound.offset(speaker->offset(1.f));
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorLog("Bad config: failed to locate fm radio speaker submodel \"" + speakername + "\"", logtype::model);
|
||||
}
|
||||
m_fmradiopresent = true;
|
||||
}
|
||||
// btLampkaUnknown`"unknown",mdKabina,false);
|
||||
} while (token != ""
|
||||
// TODO: enable full per-cab deserialization when/if .mmd files get proper per-cab switch configuration
|
||||
@@ -10608,6 +10750,15 @@ bool TTrain::InitializeCab(int NewCabNo, std::string const &asFileName)
|
||||
{
|
||||
m_radiosound.offset(caboffset);
|
||||
}
|
||||
if (m_fmradiosound.offset() == nullvector)
|
||||
{
|
||||
m_fmradiosound.offset(caboffset);
|
||||
}
|
||||
if (m_fmradiopresent && false == m_fmradiopower && m_controlmapper.contains("fmradiovolume_sw:") && false == m_controlmapper.contains("fmradio_sw:"))
|
||||
{
|
||||
// receivers without dedicated switch are switched with the volume knob, so it has to start at the off position
|
||||
m_fmradiovolume = 0.f;
|
||||
}
|
||||
std::vector<std::pair<std::reference_wrapper<std::optional<sound_source>>, glm::vec3>> soundlocations = {
|
||||
{dsbReverserKey, ggDirKey.model_offset()},
|
||||
{dsbNastawnikJazdy, ggJointCtrl.model_offset()},
|
||||
@@ -11947,6 +12098,33 @@ bool TTrain::initialize_gauge(cParser &Parser, std::string const &Label, int con
|
||||
}
|
||||
}
|
||||
|
||||
// broadcast radio receiver
|
||||
if (Label == "fmradio_sw:" || Label == "fmradiovolume_sw:" || Label == "fmradiotune_sw:" || Label == "fmradioscale:")
|
||||
{
|
||||
auto &gauge = Cabine[Cabindex].Gauge(-1); // pierwsza wolna gałka
|
||||
gauge.Load(Parser, DynamicObject);
|
||||
if (Label == "fmradio_sw:")
|
||||
{
|
||||
gauge.AssignBool(&m_fmradiopower);
|
||||
}
|
||||
else
|
||||
{
|
||||
gauge.AssignFloat(Label == "fmradiovolume_sw:" ? &m_fmradiovolume : &m_fmradiodial);
|
||||
}
|
||||
if (Label != "fmradioscale:")
|
||||
{
|
||||
// the scale pointer is an indicator, the rest are controls
|
||||
m_controlmapper.insert(gauge, Label);
|
||||
}
|
||||
m_fmradiopresent = true;
|
||||
// unless the speaker location is defined explicitly, the sound comes from the first defined element of the receiver
|
||||
if (m_fmradiosound.offset() == glm::vec3())
|
||||
{
|
||||
m_fmradiosound.offset(gauge.model_offset());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ABu 090305: uniwersalne przyciski lub inne rzeczy
|
||||
if (Label == "mainctrlact:")
|
||||
{
|
||||
|
||||
@@ -216,6 +216,7 @@ class TTrain {
|
||||
void update_sounds_runningnoise( sound_source &Sound );
|
||||
void update_sounds_resonancenoise( sound_source &Sound ) const;
|
||||
void update_sounds_radio();
|
||||
void update_sounds_fmradio( double const Deltatime );
|
||||
inline
|
||||
end cab_to_end( int const End ) const {
|
||||
return End == 2 ? end::rear : end::front; }
|
||||
@@ -485,6 +486,13 @@ class TTrain {
|
||||
static void OnCommand_radiovolumeincrease(TTrain *Train, command_data const &Command);
|
||||
static void OnCommand_radiovolumedecrease(TTrain *Train, command_data const &Command);
|
||||
static void OnCommand_radiovolumeset(TTrain *Train, command_data const &Command);
|
||||
static void OnCommand_fmradiotoggle(TTrain *Train, command_data const &Command);
|
||||
static void OnCommand_fmradiovolumeincrease(TTrain *Train, command_data const &Command);
|
||||
static void OnCommand_fmradiovolumedecrease(TTrain *Train, command_data const &Command);
|
||||
static void OnCommand_fmradiovolumeset(TTrain *Train, command_data const &Command);
|
||||
static void OnCommand_fmradiotuneincrease(TTrain *Train, command_data const &Command);
|
||||
static void OnCommand_fmradiotunedecrease(TTrain *Train, command_data const &Command);
|
||||
static void OnCommand_fmradiotuneset(TTrain *Train, command_data const &Command);
|
||||
static void OnCommand_cabchangeforward( TTrain *Train, command_data const &Command );
|
||||
static void OnCommand_cabchangebackward( TTrain *Train, command_data const &Command );
|
||||
static void OnCommand_generictoggle( TTrain *Train, command_data const &Command );
|
||||
@@ -919,6 +927,17 @@ private:
|
||||
bool m_couplingdisconnect { false };
|
||||
bool m_couplingdisconnectback { false };
|
||||
float m_radiovolume { Global.DefaultRadioVolume };
|
||||
// broadcast radio receiver
|
||||
bool m_fmradiopresent { false }; // the cab is equipped with the receiver
|
||||
audio::fmradio_type m_fmradiotype { audio::fmradio_type::cheap };
|
||||
bool m_fmradiopower { false };
|
||||
bool m_fmradiotuned { false }; // helper, the tuning knob was set to its initial position
|
||||
float m_fmradiovolume { 0.5f }; // position of the volume knob, in range 0-1
|
||||
float m_fmradiodial { 50.f }; // position of the tuning knob, in range 0-100 covering the whole band. NOTE: gauges ignore changes of bound value smaller than 0.01, hence the wide range
|
||||
int m_fmradiovolumechange { 0 }; // direction in which the volume knob is being turned
|
||||
int m_fmradiotunechange { 0 }; // direction in which the tuning knob is being turned
|
||||
float m_fmradiotunetime { 0.f }; // how long the tuning knob is being turned, it gets faster with time
|
||||
sound_source m_fmradiosound { sound_placement::internal, 2 * EU07_SOUND_CABCONTROLSCUTOFFRANGE }; // provides location and soundproofing of the receiver
|
||||
|
||||
public:
|
||||
float fPress[20][7]; // cisnienia dla wszystkich czlonow
|
||||
|
||||
Reference in New Issue
Block a user