mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-30 07:20: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:
@@ -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:")
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user