17
0
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:
IwanXXI
2026-10-10 21:22:39 +02:00
committed by Jano211
parent c7f9796387
commit b4b1e6c32f
17 changed files with 7509 additions and 0 deletions

View File

@@ -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:")
{